本篇将较为全面的介绍的 Linux 中的进程控制,其中主要分为三个模块:fork 创建进程、进程终止以及进程的等待。其中关于进程的终止包含如何终止进程、进程退出的三种状态,以及进程的异常退出。最后讲解了进程的等待方法,其中包括阻塞等待和非阻塞等待。
目录如下:
目录
1. 进程创建 fork
2. 进程的终止
如何终止进程 —— 理解
进程的退出码
进程异常退出
进程退出的三种情况
为什么要维持进程的僵尸状态
如何终止进程 —— 方式
3. 进程的等待
进程等待的方法
1. 进程创建 fork
我们在进程中调用 fork 接口之后就会创建出一个新的进程(创建成功的时候 fork 在子进程中返回 0,对父进程返回子进程的 pid(方便父进程对子进程进行管理),若创建失败,返回给父进程 -1)。当我们调用 fork 的时候,转移到 fork 代码中之后,内核会做:
1. 分配新的内存块和内核管理数据结构(task_struct mm_struct 页表)给子进程。
2. 将父进程部分数据结构内容拷贝到子进程。
3. 添加子进程到系统进程列表中。
4. fork 返回,开始调度器调度。
关于父子进程之间的独立性,虽然说父子进程之间是共享代码和部分数据,但是这并不影响他们的独立性,即使共享代码,对于子进程也是只能访问部分的代码,其他的共享数据只是在读上共享,当我们想要写入更改的时候,系统会重新分配空间,进行写时拷贝。所以说进程之间的独立性更多体现的是在互不干扰,而不是完全独立。
关于 fork 的常规用法:
1. 父子进程之间执行不同的代码,使用 if else 对代码进行分流,让父子进行允许不同的代码。
2. 让子进程运行一个不同的进程,如调用 exec 函数(下文会介绍)。
2. 进程的终止
进程终止可能是由不同原因导致的,本段将主要围绕进程终止是在做什么,进程终止存在哪些情况,我们如何对进程进行终止。
如何终止进程 —— 理解
我们在创建一个进程的时候,需要创建一个内核数据结构、页表、地址空间还会在物理内容之中加载代码和相关数据,那么当我们终止进程的时候,无疑肯定是将内存中的代码和数据,还有内核管理数据结构给释放掉。
但是当将进程给释放的时候,我们也许还会将该数据的内核数据结构给保留下来,维护进程的推出信息,也就是进程的 Z 状态(僵尸进程)。
进程的退出码
在我们写的 main 函数中最末端的退出码,它代表什么含义呢?本段就来介绍关于进程的返回值问题,如下,我们分别观察我们调用的进程的返回值(其中查看最近一次的进程结束返回值的命令:echo $?)。
如上图所示,当我们在自己的进程中将返回值置为100的时候,得到的返回值就是100,当我们运行 ll 命令的时候,得到的返回值就是0。这里的返回值为退出码(一个子进程运行结束的时候,会将退出码返回给父进程),退出码表征着子进程的退出状态,当退出码为0的时候,表示该进程成功运行结束,当退出码为非0的时候,表示该进程并不是成功退出。
对于不同的退出码,表征着不同的退出状态,也就意味着不同的退出码中包含不同进程退出的信息,退出码会返回给父进程,我们的父进程就能得到该退出码,根据退出码中的含义,就可以得出进程退出的状态,当退出码为非0的时候,就可以将退出码对应的含义交给用户,我们可以使用 strerror 函数来查看不同的退出码信息,如下:
如上图所示,进程退出码一共有着130多个,其中每个都代表着不同的含义。
进程异常退出
上文中所提到的是有关进程的退出码的问题,进程的返回了它的退出码,最起码说明该进程运行成功结束。但是进程的异常退出,代表着进程在运行的过程中直接的退出,并未运行结束,关于异常退出有着很多情况,如访问野指针,除零等等,当我们访问野指针的时候,会报错 Segmentation fault,那我们在进程的退出码中查找对应的字符串是否会会存在 Segmentation fault 字符串,若没有,说明 Segmentation fault 不属于退出码中的信息,如下程序:
如上,我们并未在130多个字符串中找到 Segmentation fault 字符串,说明进程的异常退出代表着另一种的进程终止。
进程退出的三种情况
所以关于进程终止,其中一共包括三种情况:进程代码跑完,正常结束;进程代码跑完,不正常结束;进程代码跑完,出现异常。
关于进程中的 kill 指令包含的信号,我们会发现其中的11号指令也会引起 Segmentation fault 报错,如下:
如上,我们使用 kill -11 + pid 就可以使正在运行的进程报错为 Segmentation fault ,所以关于进程出异常,本质是因为进程收到了操作系统发给进程的信号。所以我们可以看进程退出时,退出信号是多少,就可以判断进程为什么异常了。
所以关于一个进程的终止,我们可以先查看进程是异常退出,然后在查看退出码,就可以知道进程退出的原因了。
为什么要维持进程的僵尸状态
结合上文中所说,关于进程的退出,我们会将进程的退出码和退出信号交给父进程,让我们知道进程是如何退出的,为什么会这样退出的。当我们将一个进程释放的时候,也需要将其的 task_struct 先维持起来,先不释放 task_struct 的内存,这是因为 task_struct 中存有有关退出码和退出信号i的数据,如下:
如何终止进程 —— 方式
关于进程的终止方法,其中一共包含三种方法,一:在 main 函数中使用 return;二:在程序的任何位置调用 exit 函数;三:在程序的任何位置调用 _exit 函数,关于 return 函数就不演示,如下演示 exit 和 _exit 函数:
如上我们会发现:exit 和 _exit 的作用十分相似,都是可以在程序员的任意位置直接将进程终止,但是我们仔细观察会发现调用 _exit 的进程并没有打印 hello linux,这是为什么呢?这是因为 _exit 函数并不会冲刷缓冲区,在打印函数中并没有加上 /n /r,需要打印的字符还在缓冲区内,而 exit 函数则会将其打印。_exit 函数为系统调用函数,exit 函数为 C 库函数,exit 函数是 _exit 函数的封装,并且还增加刷新缓冲区的功能。
3. 进程的等待
对于任何的进程,在退出的情况下,都必须被父进程进行等待。若父进程不管不顾,那么子进程就会变为僵尸进程(Z 状态),导致内存泄漏。
所以,关于进程的等待的作用为?
1. 父进程通过等待,解决子进程僵尸进程的问题,防止内存泄漏(必须);
2. 获取子进程的退出信息,知道子进程是因为什么原因退出的(不是必须的)。
进程等待的方法
等待进程可以通过调用 wait 或者 waitpid 函数来等待子进程的退出,如下:
我们先来观察 wait 函数的使用:
pid_t wait(int* status); 返回值:成功返回被等待进程的pid,失败返回-1。 参数:输出型参数,获取子进程的退出状态,不关心则设置为NULL
使用如下:
如上图所示,当我们使用代码测试和观测的时候,我们会发现当我们调用 wait 之前且子进程已经退出的时候,子进程呈现僵尸进程的状态。并且子进程没有退出,调用wait后父进程会一直等待子进程,这时候的父进程处于阻塞等待状态。
对于子进程本身而言,也是一个进程也是一个软件,当父进程调用 wait 等待的子进程的时候,会将父进程的 task_struct 链入到子进程中,并且置于 S 状态,当子进程退出的时候,操作系统按照运行顺序接着唤醒父进程,这时候父进程就拿到了子进程的 pid。
关于 waitpid 的使用,如下:
pid_ t waitpid(pid_t pid, int *status, int options); 返回值:当正常返回的时候waitpid返回收集到的子进程的进程ID;如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在; 参数:pid:Pid=-1,等待任一个子进程。与wait等效。Pid>0.等待其进程ID与pid相等的子进程。status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)options:WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
如下所示:
如上所示,当我们使用 waitpid 函数的时候,可以精准的等待对应的进程,当输入的进程 pid 错误的时候,等待的子进程也会错误,当我们将需要等待的进程 pid 改为正确的时候,就会等待成功。(在 waitpid 传参 wait(-1, NULL, 0) 就等价于 wait 函数)。
其中的 status 中代表的含义为:
对于 status 不能简单当作整型来看待,当作位图。(我们只研究 status 的16位)。
如上所示,当我们使用位操作打印出子进程退出的退出状态和终止信号。当我们正常退出的时候,我们在子进程的退出码设置的为3,打印的就为3,没有被信号所杀,所以终止信号为0.当我们使用信号杀死进程的时候,进程的终止信号就为9,因为没有正常退出,所以退出状态为0。
关于想要重 status 中获取关于退出信号和终止信号,可以使用宏:WIFEXITED(status) 获取终止信号、WEXITSTATUS(status) 退出码。、
非阻塞等待
以上等待的方法为阻塞等待(也就是父进程什么都不干,只能等到子进程退出之后哦,才能干自己的事情),那么当我们想要进行非阻塞等待(在等待的时候,可以进行其他的行为),该如何进行呢?
我们只需要在 waitpid 中的第三个参数传入宏:WNOHANG(传入0表示阻塞等待),关于使用 waitpid 进行非阻塞等待,我们需要结合 waitpid 的返回值结合进行使用,当返回值大于0的时候,表示等待成功,当返回值小于0的时候表示等待失败,当返回值等于0的时候,表示检测是成功的,但是子进程还未退出,需要下一次进行重复等待。
所以我们需要结合 waitpid 的返回值进行轮询等待,在轮询的时候,父进程可以做想做其他的事。如下:
如上所示,当我们的父进程在等待子进程的时候,可以进行非阻塞等待。(现实中大多情况下 我们进行的是阻塞等待)