目录
进程创建
fork函数初识
fork函数返回值
写时拷贝
fork常规用法
fork调用失败的原因
进程终止
进程退出场景
进程退出码
进程常见退出方法
exit函数
_exit函数
return退出
return、exit和_exit之间的区别与联系
进程异常退出
进程等待
进程等待的必要性
获取子进程status
进程等待的方法
wait方法
waitpid方法
多进程创建以及等待的代码模型
非阻塞轮询
进程创建
fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
返回值:
子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做: 分配新的内存块和内核数据结构给子进程 将父进程部分数据结构内容拷贝至子进程 添加子进程到系统进程列表当中 fork返回,开始调度器调度
fork之后,父子进程代码共享
运行之后:
我们可以观察到fork之前的代码执行了一次,而fork之后的代码执行了两次,其中Before是由父进程打印的,而调用fork函数之后打印的两个After,则分别由父进程和子进程两个进程执行。也就是说,fork之前父进程独立执行,而fork之后父子进程两个执行流分别执行
注意:fork之后,父进程和子进程谁先执行完全是由调度器决定
fork函数返回值
fork函数为什么要给子进程返回0,给父进程返回子进程的pid?
一个父进程可以创建很多个子进程,而一个子进程只能有一个父进程。因此对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的pid才能很好的对子进程指派任务
为什么fork有两个返回值?
父进程调用fork函数后,为了创建子进程,fork函数内部将会进行一系列操作,包括创建子进程的进程控制块(PCB - task_struct),创建子进程的进程地址空间,创建子进程对应的页表等等。子进程创建完毕之后,操作系统还需要将子进程的进程控制块添加到系统进程列表当中,此时子进程便创建完毕了
也就是说,在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句不仅父进程需要执行,子进程也同样需要执行,这就是fork有两个返回值的原因
写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
1.为什么数据要进行写时拷贝?
进程具有独立性。多进程运行,需要独享各种资源,多进程运行期间互不干扰,不能让子进程的修改影响到父进程
2.为什么不创建子进程的时候就进行数据的拷贝?
子进程不一定会使用父进程的所有数据,并且在子进程不对数据进行写入的情况下,没有必要对数据进行拷贝,我们应该按需分配,在需要修改数据的时候再分配(延迟分配),这样可以高效的使用内存空间
3.代码会不会进行写时拷贝?
90%的情况下不会的,但着并不代表代码不能进行写时拷贝,例如再进行进程替换的时候,则需要进行代码的写时拷贝
fork常规用法
1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子 进程来处理请求。
2.一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
fork函数创建子进程也可能会失败,有以下两种情况:
1.系统中有太多的进程
2.实际用户的进程数超过了限制
进程终止
进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
进程退出码
进程退出码
我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2022当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的。
既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,我们一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。
当我们的代码运行起来就变成了进程,当进程结束后main函数的返回值实际上就是该进程的进程退出码,我们可以使用echo $?命令查看最近一次进程退出的退出码信息。
例如,对于下面这个简单的代码:
该进程结束之后我们可以查看该进程的退出码:
使用echo $?指令
这时,我们就可以确定以上代码顺序执行
为什么以0表示代码执行成功,以非0表示代码执行错误?
因为成功只有一种情况,成功了就算成功了,而失败有很多种情况,例如:野指针问题,除0错误,栈溢出,越界访问,内存空间不足等原因
c语言中的strerror函数可以通过错误码来获取该错误码对应的错误信息:
运行之后,我们就可以得到错误码所对应的错误信息
实际上再Linux中的 pwd ,ls指令都是可执行程序,在其执行完毕之后也会有退出码
顺序运行之后退出码为0
但是,如果我们使用的是错误的指令,它会返回非0的错误码
进程常见退出方法
exit函数
1. 执行用户通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写入(刷新缓冲区)
3. 调用_exit终止进程
exit在退出进程时,会先将缓冲区的数据输出,在终止进程
运行之后,我们可以看到,缓冲区的数据输入到了缓冲区上
_exit函数
尽量不要去使用这个接口,它可以在程序的任何地方使用,使用时会直接终止掉程序,并不会再终止程序前做任何收尾工作
以下代码在使用_exit终止程序,缓冲区的数据不会被刷新出来
代码运行之后:
通过观察可以发现缓冲区的数据并没有刷新出来
return退出
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返 回值当做 exit的参数。
return、exit和_exit之间的区别与联系
return、exit和_exit之间的区别
只有在main函数中return 才能起到退出进程的作用,在其它子函数中只会退出该函数并不会退出进程,exit和_exit可以在代码中任何地方使用,都有退出进程的作用
使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。
return、exit和_exit之间的联系
执行return n等同于执行exit(n),因为在main函数运行结束之后,return的返回值会当作exit函数的参数,来调用exit函数
使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程。
进程异常退出
情况一:向进程发出信号导致进程异常退出
例如:对一个进程使用 kill -9 pid 或者 使用ctrl+C使进程异常退出
情况二:代码错误导致进程运行时异常退出
例如:代码执行时遇到野指针或者遇到除0错误时,进程异常退出
进程等待
进程等待的必要性
1.子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
2.进程一旦变成僵尸状态,连kill -9 也无法杀掉该进程,因为谁也没有办法杀死一个已经死去的进程。
3.父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对, 或者是否正常退出。
4.父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
获取子进程status
下面进程等待所使用的两个函数wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统进行填充
如果对status这个参数传入NULL,则表示不关心子进程的退出状态信息,否则操作系统会通过该参数将子进程的退出信息反馈给父进程
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
在status低十六位中,高八位表示进程的退出状态(退出码),
进程若是被信号所杀,则低7位表示终止信号,而第八位比特位是core dump 标记
我们可以通过一系列位操作,就可以根据status得到进程的退出码和退出信息
exitCode = (status >> 8) & 0xff; //退出码
exitSignal = status & 0x7f; //退出信号
对于次系统还提供了两个宏来获取退出码和退出信号
WIFEXITED(status):用于查看进程是否正常退出,本质是检查是否收到信号
WEXITSTATUS(status):用于获取进程的退出码
exitNormal = WIFEXITED(status); //是否正常退出
exitCode = WEXITSTATUS(status); //获取退出码
注意:当一个进程非正常退出时,说明该进程是被信号所杀死,那么该进程的退出码也就没意义了
进程等待的方法
wait方法
函数原型: pid_t wait(int* status);
作用:等待任意子进程
返回值:等待成功返回被等待进程的pid,等待失败返回-1
参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL
例如:创建子进程之后,父进程可使用wait函数一直等待子进程,直到子进程退出后读取子进程的退出信息
1 #include <stdio.h>2 #include <stdlib.h>3 #include <unistd.h>4 #include <sys/types.h>5 #include <sys/wait.h>6 7 int main()8 {9 pid_t id = fork();10 if(id == 0) //child11 {12 int cnt = 10;13 while(cnt)14 {15 printf("I am child!. pid: %d, ppid: %d\n", getpid(), getppid()) ;16 sleep(1);17 cnt--;18 }19 exit(0);20 }21 22 //parent23 int status = 0;24 pid_t ret = wait(&status);25 26 if(ret > 0)27 {28 printf("wait child success...\n");29 if(WIFEXITED(status)) //exit normal30 { 31 printf("exit code: %d", WEXITSTATUS(status));32 }33 }34 35 sleep(3);36 return 0;37 }
我们可以使用监控脚本来对本进程进行实时监控:
while :; do ps ajx | head -1 && ps ajx | grep myproc | grep -v grep; echo "###############"; sleep 1; done
这时我们可以看到,当子进程退出之后,父进程读取了子进程的退出信息,子进程也就不会变成僵尸进程了
waitpid方法
函数原型: pid_ t waitpid(pid_t pid, int *status, int options);
作用:等待指定子进程或任意子进程
返回值:
1.等待成功返回被等待进程的pid
2.如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
3.如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在
参数:
1.pid:待等待子进程的pid,若设置为-1,则等待任意子进程
2.status:输出型参数,获取子进程的退出状态,不关心可设置为NULL
3.option:当设置WNOHANG|时,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待,若正常结束,则返回该子进程的pid
例如:创建子进程之后,父进程可以使用waitpid函数一直等待子进程(此时将waitpid的第三个参数设置为0),直到子进程退出后读取子进程的退出信息
1 #include <stdio.h>2 #include <stdlib.h>3 #include <unistd.h>4 #include <sys/types.h>5 #include <sys/wait.h>6 7 int main()8 {9 pid_t id = fork();10 if(id == 0) //child11 {12 int count = 10;13 while(count)14 {15 printf("I am child..., pid: %d, ppid: %d\n", getpid(), getppid());16 sleep(1);17 count--;18 }19 exit(0);20 }21 22 //parent23 int status = 0;24 pid_t ret = waitpid(id, &status, 0);25 if(ret > 0)26 {27 printf("Wait success....\n");28 //wait success29 if(WIFEXITED(status))30 {31 //exit normal32 printf("exit code: %d\n", WEXITSTATUS(status));33 }34 else35 {36 //signal kill37 printf("signal kill: %d\n", status&0x7F);38 }39 }40 41 sleep(3);42 43 return 0;44 }
在父进程运行的过程中,我们可以尝试使用kill -9命令将子进程杀死,这时父进程也能等待子进程成功
注意:被信号杀死而退出的进程,其退出码将没有意义
多进程创建以及等待的代码模型
实际上我们还可以同时创建多个子进程,然后让父进程依次等待子进程退出,这叫做多进程创建以及等待的代码模型
例如以下代码中同时创建了10个子进程,同时将子进程这些子进程的pid放入到数组中
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{ int ids[10] = {0};for(int i = 0; i < 10; ++i){pid_t id = fork();if(id == 0) //child{printf("child create success...:%d pid: %d, ppid: %d\n", i, getpid(), getppid());//sleep(1);exit(i);}//parentsleep(1);ids[i] = id;}int status = 0;for(int i = 0; i < 10; ++i){pid_t ret = waitpid(ids[i], &status, 0);if(ret >= 0){printf("wait success...\n");if(WIFEXITED(status)){//exit normalprintf("exit code: %d\n", WEXITSTATUS(status));sleep(1);}else{//signal killprintf("exit signal: %d\n", status&0x7F);sleep(1);}}}return 0;
}
运行如下:
非阻塞轮询
在上述的例子中,当子进程未退出时,父进程都在一直等待子进程的退出,在等待期间,在等待期间父进程没有做任何事情,这种父进程处于阻塞状态下的等待叫做阻塞等待
实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未推出时父进程苦于做一些自己的事情,当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即:非阻塞等待
向waitpid函数的第三个参数potions传入WNOHANG,这样一来,等待的子进程若是没结束,那么waitpid函数将直接返回0,不予以等待。而等待的子进程若是正常结束,则返回子进程的pid
例如,父进程可以隔一段时间调用一次waitpid函数,若是等待的子进程尚未退出,则父进程可以去做一些其他事,过一段时间在调用waitpid函数读取子进程的退出信息
int main()
{pid_t id = fork();//childif(id == 0){int cnt = 3;while(cnt--){printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());sleep(3);}exit(0);}//parentwhile(1){int status = 0;pid_t ret = waitpid(id, &status, WNOHANG);if(ret > 0){printf("wait child success...\n");printf("exit code:%d\n", WEXITSTATUS(status));break;}else if(ret == 0){printf("parent do other things...\n");sleep(1);}else{printf("waitpid error...\n");break;}}return 0;
}
运行结果:父进程每隔一段时间进去查看子进程是否退出,若为退出则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出之后读取子进程的退出信息