• 进程的控制


    进程控制

    一、获取进程pid

    pid_t getpid(void)

    获取调用进程的pid

    pid_t getppid(void)

    获取调用进程的父进程pid

    它们返回一个类型为pid_t的整数值,在Linux系统上pid_t被定义为int。

    二、创建子进程

    pid_t fork(void)

    为调用进程创建子进程。

    RetVal:在父进程中,该函数的返回值为子进程pid;在子进程中,该函数的返回值为0;对于创建子进程失败的情况,返回-1给父进程。

    1、父子进程的关系

    • 父进程调用fork时,内核为子进程创建task_struct,同时为它创建一份与父进程的进程地址空间和页表几乎相同的副本
    • 子进程获得父进程打开的文件描述符的相同副本,因此可以读写任何父进程打开的文件。

    可以说,在子进程刚创建出来时,父子进程最大的区别就是pid不同。

    2、fork双返回值的原理剖析

    子进程创建成功后,被OS添加至任务队列等待调度。

    父进程继续执行fork的剩余代码,通过pid判断自己并不是刚刚创建出来的子进程,因此为父进程返回子进程的pid。

    子进程被调度后,从上次父进程创建完子进程之后的下一条指令继续执行。在返回时,通过pid判断自己是刚刚创建出来的子进程,因此返回0。

    三、进程退出

    所谓进程退出,就是进行“资源清理和进程善后”。比如:关闭进程打开的文件描述符、向父进程发送本进程退出的信号、“清理”进程相关的数据结构。注:清理的本质就是将这些数据结构归还到操作系统管理的数据结构内存池。

    当进程在正常执行完指令,或者执行时发生异常,都要进行退出。

    1、常见的退出方法

    • void exit(int status)

    在任意函数调用exit()都会终止整个进程。其中,status会以退出码的形式存储下来,等待父进程读取。

    注:该函数会刷新进程相关的IO缓冲区到对应文件

    • return n

    main函数中的return语句会终止整个进程,return n相当于exit(n),返回值即为进程的退出码。

    • void _exit(int status)

    与exit()基本相同,但是调用该函数退出时,进程相关IO缓冲区不会被刷新

    • 发送终止信号

    这些终止信号可以由用户自己发送,比如使用kill命令;也可以由系统发送,比如程序遇到访问野指针、除零等问题时。

    2、退出码

    退出码是标识进程退出状态的数据,通过退出码可以知道进程是否正常执行,执行结果是否正确。

    用户可以自定义退出码,也可以使用C库中提供的。

    使用echo $?查看最近一次命令的退出码

    image-20220801211017425

    对于需要建立子进程执行的命令(比如./xxx),命令退出码就是进程退出码;

    而对于shell内建命令(比如echo、clear),命令退出码是由shell制定的,其中0表示成功结束

    所谓内建命令,就是内置到命令行解释器bash里的函数。

    相对地,外部命令本质是外部程序代码,没有内置到命令行解释器中,因此需要bash创建子进程并进行程序替换去执行这些命令。

    四、进程等待

    当进程由于某种原因终止时,内核并不会立即将它清除,而是让它停留在"Z"状态(Zombie),这种Z状态的进程被称为僵尸进程。

    僵尸进程会一直占用系统资源,直到被父进程等待并回收,或者在父进程退出后由1号init进程领养并适时处理。

    最好的避免僵尸进程的方式就是调用系统提供的等待方法等待子进程退出。

    1、等待方法

    wait

    pid_t wait(int *status)

    其中,status是一个输出型参数,它的低16位用来存储有效信息:

    image-20220801214307740

    • 当子进程因为收到信号而退出时,子进程退出码没有任何意义。status低7位会存储信号的编号(大于0),可以通过status & 0x7f得到,或者通过宏函数:

    WIFEXITED(status):如果进程是正常通过exit或return退出的,则返回真;

    WEXITSTATUS(status):对于正常退出的进程,返回它的退出码;

    • 当子进程正常通过exit或return退出时,导致子进程退出的信号为0(即不存在信号使进程退出),退出码部分可以通过(status >> 8) & 0xff获得,或者通过宏函数:

    WIFSIGNALED(status):如果进程是由于收到信号而退出的,则返回真;

    WTERMSIG(status):对于收到信号退出的进程,返回信号编号。

    waitpid

    pid_t waitpid(pid_t pid, int *status, int options)

    waitpid是升级版的wait函数,它拥有更多的功能。

    1. pid:如果pid>0,表示当前进程等待ID为pid的子进程;如果pid=-1,表示当前进程等待所有子进程中的任意一个。

    2. status:与wait的status完全相同,是一个输出型参数;

    3. options:传0时,表示阻塞等待退出的进程;传WUNTRACED时,表示阻塞等待退出的进程或者收到信号停止运行的进程;传WCONTINUED时,表示阻塞等待退出的进程或者收到信号继续运行的进程;任意一个选项与WNOHANG按位或都表示以非阻塞方式调用waitpid;

    4. RetVal:如果对应options等待成功,则返回子进程pid;如果函数执行出错或父进程不存在没有退出的子进程,则返回-1;如果options有WNOHANG,即非阻塞等待,那么返回0表示本次没有等待到退出的进程。

    2、核心转储core dump

    status的第7位是core dump(核心转储)标志位。当进程因为异常而退出时,可以选择把进程的用户空间数据存储到硬盘上,产生一个名为core的文件,有助于之后的调试纠错

    如果当前操作系统允许发生核心转储,那么该标志位为1。

    但是,云服务器一般默认不允许产生core文件,因为core文件可能包含用户密码等信息,不安全;而且core文件一般较大,如果进程重复发生崩溃,那么可能会产生大量的文件。

    核心转储相关命令

    ulimit -a:查看当前系统的资源限制设置情况,该命令可查看当前core文件是否能生成,以及能够生成的最大core文件大小

    ulimit -c:修改core文件的最大生成大小

    image-20220801214639373

    3、系统如何获取退出状态

    进程的退出码和导致进程退出的信号保存在task_struct中:

    image-20220801214808988

    由于子进程退出时处于僵尸状态,资源没有被清理,所以task_struct中的内容可以被父进程调用wait函数读取到。

    4、阻塞等待与非阻塞等待

    当进程因为等待某个事件发生而停止执行其它指令,这种状态称为“阻塞状态”。

    阻塞状态下的进程如果放在“运行队列”,无疑是白白浪费时间片,因此操作系统维护了一个“等待队列”。

    • 当进程阻塞时,操作系统将它放入等待队列进行“休眠”。
    • 当进程等待的事件发生时,操作系统利用信号将进程“唤醒”并将它重新放入运行队列进行调度。

    如果进程调用方法等待某个事件,发现它没有发生便立即返回继续向下执行其它指令,这种称为“非阻塞等待”。

    非阻塞等待的优势就是可以在等待事件没有发生时继续完成其它工作。对应的,由于一次等待没有结果便返回,因此非阻塞等待往往需要“轮询”的方式频繁地进行等待方法的调用。

    五、进程替换

    所谓进程替换,就是利用内存映射,将当前进程的代码和数据替换成另一个可执行文件的代码和数据。

    1、execve

    int execve(const char *filename, char *const argv[], char *const envp[])

    1. filename:要替换的可执行文件的绝对路径或相对路径;
    2. argv:传给可执行文件的命令行参数,argv[0]是命令,比如./xxx,后面是选项,最后以NULL结尾;
    3. envp:传给可执行文件的环境变量,以key=value的形式,最后以NULL结尾
    4. RetVal:如果进程替换失败,例如根据路径找不到文件,则返回-1。

    2、execve原理分析

    1. 在当前进程执行到execve时,内核将进程地址空间的用户区内容全部清空,释放相关数据结构;
    2. 为用户区创建新的数据结构,将目标可执行文件的数据和代码映射到对应区域,同时将execve传来的命令行参数和环境变量添加至用户栈;
    3. 将进程PCB的程序计数器设置为代码的第一条指令。

    至此,目标可执行文件被完全加载到当前进程,execve在其中起到了加载器的作用。

    3、fork与execve

    根据进程替换的原理,execve并没有创建新的进程,而是在当前进程地址空间的基础上通过替换,使得新的程序在当前进程中运行。

    因此execve通常与fork搭配使用:

    父进程通过fork创建子进程,将子进程替换为另一个可执行文件(该可执行文件可以由任意语言编写而成)。

    4、其它进程替换函数

    Linux在系统调用execve的基础上又封装了一批进程替换的接口:

    1、int execl(const char *path, const char *arg, ...);

    2、int execlp(const char *file, const char *arg, ...);

    3、int execle(const char *path, const char *arg, ..., char * const envp[]);

    4、int execv(const char *path, char *const argv[]);

    5、int execvp(const char *file, char *const argv[]);

    6、int execvpe(const char *file, char *const argv[], char *const envp[]);

    函数名中带’l’表示以可变参数列表的形式传命令行参数;

    函数名中带’v’表示以数组的形式传命令行参数;

    函数名中带’p’表示如果不指明文件路径,则利用环境变量PATH中的默认路径查找;

    函数名中带’e’表示该接口允许传自定义的环境变量,如果没有’e’则默认传当前进程的环境变量。

  • 相关阅读:
    matlab RBF语音识别
    Linux 进程管理
    基于Rabbitmq和Redis的延迟消息实现
    混淆矩阵——AI产品经理给我使劲看
    vueRouter 重定向 高亮 传参 嵌套 简单示例
    进化:元宇宙明天的主题
    k8s从入门到精通
    【无人机协同】基于matlab无人飞行器协同车辆物资配送【含Matlab源码 1899期】
    UI/UX+前端架构:设计和开发高质量的用户界面和用户体验
    【数据结构】静态分配的顺序表插入元素
  • 原文地址:https://blog.csdn.net/Wyf_Fj/article/details/126111813