本篇文章主要讲述的是进程的退出和进程等待。希望本篇文章的内容会对你有所帮助。
🙋♂️ 作者:@Ggggggtm 🙋♂️
👀 专栏:Linux从入门到精通 👀
💥 标题:进程控制💥
** ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 ❣️**
一、fork创建子进程
我们知道fork是创建子进程的。创建的子进程,在我们不做任何操作的情况下,子进程有数据和代码吗?操作系统都做了哪些事情呢?
1、1 在创建子进程中操作系统的作用
我们先想一下,创建一个子进程本质上在是干啥?是不是系统中多了一个进程呢!进程是由所对应的代码和数据,再加上内核数据结构构成的。
那一切都就很容易理解了。创建子进程,就是给子进程分配自己的内核数据结构。同时,把部分所需代码和数据加载到进程当中。我们父进程的代码和数据一般都是从磁盘加载(我们自己所写的代码)过来的。子进程的代码和数据哪里来的呢?
因为在加载的过程中,子进程并没有自己的代码和数据,所以子进程只能使用父进程的代码和数据了!也就是子进程和父进程共享一份数据!
我们知道,每个进程都是具有独立性的,进程之间不会相互影响。那子进程和父进程共享一份代码和数据,不管是子进程还是父进程去修改数据,那不就影响到了另一个进程吗?因为代码和数据是共享的。但是,事实并不是不会相互影响。呢么做到的不相互影响呢?
我们为了保持进程的独立性,就采用了写时拷贝技术。那么我们接下来先了解一下写时拷贝技术。
1、2 写时拷贝
我们知道子进程和父进程共享一份代码和数据,那么只要有任何一进程对其数据进行修改,就会对该数据进行深拷贝,再进行修改。这就很好的解决了相互影响的问题。这就是写时拷贝,只有在对其数据进行修改时,才对数据开空间,进行深拷贝。
写时拷贝就是一种拖延技术,是在浅拷贝的基础之上增加了引用计数的方式来实现的。
** 引用计数**:用来记录资源使用者的个数。在构造时,将资源的计数给成1,每增加一个对象使用该资源,就给计数增加1,当某个对象被销毁时,先给该计数减1,然后再检查是否需要释放资源,如果计数为1,说明该对象时资源的最后一个使用者,将该资源释放;否则就不能释放,因为还有其他对象在使用该资源。
那么这里有一个问题:为什么在创建子进程时,不直接对数据进行深拷贝,而是采用与父进程共享一份代码和数据呢?
首先,我们所创建的子进程可能就不会用到数据空间,即使用到了,也可能是只读,并不会对其进行修改而影响到进程的独立性。其次,操作系统也无法预知那些空间会被写入。所以操作系统选择写时拷贝技术,会有以下好处:
- 可以很好的分离父子进程的数据,保证进程的独立性;
- 用时才分配空间,合理充分利用了内存,是高效使用内存的一种表现。
二、进程终止
2、1 常见的进程退出
我们常见的进程退出有如下三种:
代码运行完毕,结果正确;
代码运行完毕,结果不正确;
代码异常终止。
关键是我们怎么知道进程运行结果正确还是不正确呢?这时候就需要了解一下
进程的退出码了。
2、2 进程的退出码
进程的退出码(Exit Code)是一个整数值,用于表示进程在终止时返回给操作系统的状态信息。退出码提供了有关进程是否成功执行和执行结果的信息。如果出错,同时也方便我们地位错误的原因。
2、2、1 运行结果正确实例
我们平常写代码中也一直在用到退出码——return 0。在main函数中,return 的返回值就算是进程的退出码。我们可在linux下查看一下。linux下查看最近进程的退出码的命令是:**echo $?**。实例代码如下:
#include<iostream> using namespace std; int main() { cout<<"正常运行"<<endl; return 0; }
我们再来查看一下改进程的退出码,结果如下:
那要是我们设置 return 的返回值为100呢?我们再来看看结果:
2、2、2 运行结果不正确实例
我们可通过一个判断来完成结果是否正确。代码如下:
int main() { const int N=100; int ret=0; for(int i=0;i<N;i++) { ret+=i; } if(ret!=5050) return 1; return 0; }
我们知道,1到100的和为5050。我们在这里故意把答案写错来判断一下,结果如下:
运行结果不正确的原因有很多,我们可通过sterror函数将它们打印出来,结果如下:
我们发现,一共是由133种运行错误的结果。我们不妨来验证一下:
退出码是2,是不是与我们刚刚打印出的对上了!
2、2、3 代码运行异常
我们先看如下代码:
int main() { cout<<"hello linux"<<endl; cout<<"hello linux"<<endl; cout<<"hello linux"<<endl; int* p=NULL; *p=1; cout<<"hello linux"<<endl; cout<<"hello linux"<<endl; cout<<"hello linux"<<endl; return 0; }
上述代码是对空指针进行了访问并且修改,必然会导致程序崩溃。结果如下:
我们看到:Segmentation fault。我们再看退出码是139。当程序崩溃时,退出码毫无意义,并且一般情况下 return 语句都不会别执行!
2、3 常见的进程退出方式
我们平常再写代码的时候,如何用代码终止一个进程呢?其实在main() 函数中的return语句,就是终止进程的。return 的值就是退出码,返回给操作系统。那还有其他的方式吗?答案是有的。我们接着往下看。
可能我们大家在平常中也会用到exit()函数。exit在代码的任何地方调用,都是表示终止进程。那我们来简单测试一下exit()函数。代码如下:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { printf("hello linux"); sleep(3); exit(111); return 0; }
我们上面打印的字符串,并没有选择通过 \n 或者fflush 将其在缓冲区刷新出来,而是休眠了3秒。那当exit 终止程序时,会将其刷新出来吗?我们看如下运行结果:
通过上述发现,在exit 终止程序是,会刷新缓冲区。退出码也是我们所设置的 111 。
其实还有一个函数,_exit() 也是用来终止程序的。我们不妨也来简单测试一下 _exit() 函数,看是否与 exit() 函数相同。代码如下:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { printf("hello linux"); sleep(3); _exit(111); return 0; }
运行结果如下:
我们发现,_exit() 函数并没有刷新缓冲区!
exit() 函数时C语言的库函数,_exit() 是系统的接口。其实exit() 底层也是调用的_exit(),但是在调用_exit() 之前,还做了一些其他的工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入。
- 调用_exit。
那么提问:printf 所打印的数据并不是直接向输出设备输出,而是保存在了缓冲区。那么这个缓冲区是在哪里呢?是由谁来维护的呢?
我们先看如下图片:
_exit() 函数直接调的是操作系统內部的函数。如果上述的缓冲区是在操作系统內部的话,_exit() 函数也会把数据刷新出来。但是是并没有。从这点也可以说明,上述的缓冲区并不是在操作系统內部。显而易见,缓冲区是由C语言标准库函数来维护的。
这里对exit() 和 _exit()函数进行总结。exit()和_exit()都是用于退出程序的函数,但在使用方式和功能上存在一些区别。
exit()是C语言标准库中的函数,在C++中也可以使用。它执行以下操作:
- 调用终止处理程序(atexit函数注册的函数)。
- 刷新并关闭stdin、stdout和stderr流。
- 调用各个注册函数进行清理工作。
- 通过调用C库实现的_exit()系统调用来终止进程。
_exit()是一个系统调用,用于直接终止进程,不会进行任何清理工作。它执行以下操作:
- 立即终止进程,并不执行后续的清理工作。
- 不会刷新缓冲区或关闭文件描述符,也不会执行终止处理程序。
- 可以带有一个整数参数,表示退出状态,将该值传递给操作系统。
因此,exit()更安全,它可以确保执行所有的清理工作,并且允许某些回调函数得到执行的机会。_exit()则是一种粗暴的退出方式,立即终止进程,没有机会执行任何清理工作。
三、进程等待
3、1 进程等待的引入
在进程的状态中讲到中讲到,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是不对, 或者是否正常退出。那么父进程怎么获取这些信息呢?
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。这也是父进程等待的原因!那父进程是如何等待的呢?
3、2 等待的方法
3、2、1 wait 方法
进程等待函数**
wait()
是在操作系统中用于等待子进程结束并获取子进程的状态信息的函数。
wait()
函数用于等待任意一个子进程终止,并可以获取子进程的终止状态**。我们再来看一下wait的使用方法:
上图我们可到使用wait需要引入的头文件。同时wait的返回值: 成功返回被等待进程pid,失败返回-1;参数: 输出型参数,获取子进程退出状态,不关心则可以设置成为****NULL。
** **在下面讲述waitpid()函数时,会讲到status。我们再来看wait()的使用方法。代码如下:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<sys/types.h> #include<sys/wait.h> int main() { pid_t id = fork(); if(id < 0) { perror("fork"); exit(1); //标识进程运行完毕,结果不正确 } else if(id == 0) { //子进程 int cnt = 5; while(cnt) { printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt--, getpid(), getppid()); sleep(1); } exit(0); } else { //父进程 printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid()); pid_t ret=wait(NULL); if(ret>0) { printf("等待成功!,%d\n",ret); } } return 0; }
运行结果如下:
观察上图发现,父进程打印出第一句话后,就不再往后执行。没错,wait()就是阻塞式等待。就是当子进程退出时,wait()回收子进程的资源和退出信息。当子进程一直在运行不退出,wait()就一直处于阻塞状态进行等待。
我们不妨来验证一下,wait()到底回收子进程的资源了吗。验证的代码如上述验证wait()的代码几乎一样,只不过是当wait()等待成功后,不让父进程退出,接着打印。我们看如下运行结果:
通过上图,我们看到当子进程推出后,父进程仍然在打印,并没有退出。如果父进程不对子进程进行回收,子进程就会进入僵尸状态。我们在查看的时候,子进程并没有进入僵尸状态,而是被回收了。 我们接下来再看一下waitpid的使用方法。
3、2、2 waitpid 方法
waitpid()
函数用于等待指定进程ID的子进程终止,也可等待任意子进程的终止。我们先来看看怎么使用waitpid。如下图:
返回值: 当正常返回的时候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。0表示阻塞式等待。
获取子进程status的方式:
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)
我们结合代码一起理解一下waitpid()和 获取子进程status。代码如下:
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int code = 0; int main() { pid_t id = fork(); if(id < 0) { perror("fork"); exit(1); //标识进程运行完毕,结果不正确 } else if(id == 0) { //子进程 int cnt = 5; while(cnt--) { printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid()); sleep(1); } exit(0); } else { //父进程 printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid()); int status = 0; pid_t ret = waitpid(id, &status, 0); //阻塞式的等待! if(ret > 0) { // 0x7F -> 0000.000 111 1111 printf("等待子进程成功, ret: %d, 子进程收到的信号编号: %d,子进程退出码: %d\n",\ ret, status & 0x7F ,(status >> 8)&0xFF); //0xff --> 0000...000 1111 1111 } } }
运行结果如下:
父进程也可通过如下方式看子进程是否正常终止,和获取子进程的退出码:
if(WIFEXITED(status)) //是否正常终止子进程 { //子进程是正常退出的 printf("子进程执行完毕,子进程的退出码: %d\n", WEXITSTATUS(status)); //获取退出码 } else{ printf("子进程异常退出: %d\n", WIFEXITED(status)); }
我们再来看一下waitpid()进行非阻塞等待。 第三个参数就是:WNOHANG。我们知道Linux使用C语言写的,waitpid()是系统调用接口,也就是操作系统调用自己内部的函数。这个函数就是用C语言写的函数,全部是大写的WNOHANG是什么呢?宏定义!!!****WNOHANG的值就是1,为什么不直接写1呢?因为可能过一段时间,我们就不知道1在这里是什么意思了,这类数字也被称为魔鬼数字/魔术数字。WNOHANG是Wait No HANG。也就是没有夯住。"夯住"通常指的是一个进程或者应用程序无法继续正常执行,似乎被卡住了或者不再响应用户的输入或命令。这种情况也被称为"进程僵死"或"进程挂起"。
我们通过如下代码测试一下非阻塞等待:
#include<sys/wait.h> #include<sys/types.h> #include<stdlib.h> #include<unistd.h> #include<stdio.h> typedef void (*handler_t)(); //函数指针类型 std::vector<handler_t> handlers; //函数指针数组 void fun_one() { printf("这是一个临时任务1\n"); } void fun_two() { printf("这是一个临时任务2\n"); } // 设置对应的方法回调 // 以后想让父进程闲了执行任何方法的时候,只要向Load里面注册,就可以让父进程执行对应的方法喽! void Load() { handlers.push_back(fun_one); handlers.push_back(fun_two); } int main() { pid_t id = fork(); if(id == 0) { // 子进程 int cnt = 5; while(cnt) { printf("我是子进程: %d\n", cnt--); sleep(1); } exit(11); // 11 仅仅用来测试 } else { int quit = 0; while(!quit) { int status = 0; pid_t res = waitpid(-1, &status, WNOHANG); //以非阻塞方式等待 if(res > 0) { //等待成功 && 子进程退出 printf("等待子进程退出成功, 退出码: %d\n", WEXITSTATUS(status)); quit = 1; } else if( res == 0 ) { //等待成功 && 但子进程并未退出 printf("子进程还在运行中,暂时还没有退出,父进程可以在等一等, 处理一下其他事情??\n"); if(handlers.empty()) Load(); for(auto iter : handlers) { //执行处理其他任务 iter(); } } else { //等待失败 printf("wait失败!\n"); quit = 1; } sleep(1); } } return 0; }
waitpid函数不管子进程是否运行结束,都会有一个返回值。返回值大于0:等待成功 且 子进程退出。返回值等于0:等待成功 且 子进程没有退出。返回值小于0:等待失败。当我们知道子进程并没投退出时,父进程还可做一些其他事情,直到子进程退出。我们看运行结果:
从上述的运行结果中可看出,在子进程没有退出的情况下,父进程也可做一些其他事情。退出码为11是我们自己设置的。
四、总结
子进程的退出状态信息返回给了操作系统中的进程控制块内,为什么wait和waitpid能够拿到子进程的退出状态呢?wait和waitpid是系统调用接口, 就是操作系统內部的函数!!!当然可以拿到了。设置全局变量行吗?答案是不行的。一旦对全局变量修改,就会发生写时拷贝。同时信号也无法处理。
由于内容较多,分为两篇文章来整理。本篇文章主要讲述的是进程的退出和进程等待,下篇文章会讲到进程的替换。感谢阅读ovo~
版权归原作者 Ggggggtm 所有, 如有侵权,请联系我们删除。