Linux环境下的进程创建-fork函数的使用, 进程退出exit和_exit的区别,以及进程等待waitpid和status数据的提取方法
目录
一、进程创建
1.fork函数
1)进程调用fork函数是如何创建子进程的
2)代码示范
2.写时拷贝
二、进程退出
1.退出码
1)什么是退出码?
2)为什么要有退出码?
3)退出码是怎么做到的?
4)echo $?:查看最近一个进程的退出码
2.进程的三种退出状态
1)又可分为能运行完和没运行完
2)exit( )和_exit( )的区别
三、进程等待
1.进程等待是什么
2.为什么要进程等待
3.父进程是怎么等待的
1)wait和waitpid
2)写入型参数status的使用方式
①终止信号
②退出状态
③status相关宏定义
3)使用示范
4)阻塞等待与非阻塞等待的区别
总结
一、进程创建
1.fork函数
在linux种fork函数的作用是:从存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
返回值:子进程中返回0,父进程返回子进程id,出错返回-1。
这里的pid_t 可以看成是对int的封装
1)进程调用fork函数是如何创建子进程的
①通过分配新的内存块和内核数据结构给子进程
②将父进程部分数据结构内容拷贝至子进程
③添加子进程到系统进程列表当中 ,fork返回。
理解fork函数需要对进程的地址空间有着一定的理解,若对进程地址空间有疑惑的读者可以翻看笔者之前的“短”文,看最后的“进程地址空间详解”就可以了,这里给出链接:进程优先级介绍,详解环境变量,详解进程地址空间-CSDN博客
2)代码示范
这里利用fork对父子进程返回的id值的不同,使得父子进程运行不同得条件语句。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>int main()
{pid_t id=fork();if(id<0){printf("创建子进程失败\n");exit(1);}else if(id>0){//父进程while(1){printf("我是父进程,我的id是:%d,我的父进程id是:%d\n",getpid(),getppid());sleep(1);}}else{//子进程while(2){printf("我是子进程,我的id是:%d,我的父进程id是:%d\n",getpid(),getppid());sleep(2);}}return 0;
}
运行结果试图:(可以通过CTRL+C终止进程)
2.写时拷贝
在通过fork函数创建子进程时,父进程将部分数据结构内容拷贝至子进程,此时在内存中,操作系统为节省内存,父子进程的页表指向的是其实是同一块物理地址,换句话说,此时父子代码共享,数据也是共享的。
而写时拷贝的概念是:若当父子进程中有一方要修改某个变量时,操作系统会临时将该变量拷贝一份放在另一块内存中,并修改该进程的页表指向临时拷贝出来的变量,以确保进程的独立性。
二、进程退出
1.退出码
1)什么是退出码?
退出码就是main函数的而返回值,其中包括return返回或者exit返回。
2)为什么要有退出码?
父进程会通过检查子进程的退出码从而确定子进程执行情况,然后再将子进程的资源释放——删除残留的task_struct(PCB),以结束子进程的僵尸状态。
3)退出码是怎么做到的?
Linux环境下的退出码共有134种,可分为两种情况:
退出码方便计算机识别程序运行中出现的不同错误,但对人而言就不是很友好。
我们可以通过strerror()函数将退出码翻译为解释语言:
#include<stdio.h>
#include<string.h>int main()
{for(int i=0;i<150;++i)printf("%d:%s\n",i,strerror(i));return 0;
}
读者若感兴趣可自我尝试,这里截取一些常见的退出码:
如上,共134种错误。
4)echo $?:查看最近一个进程的退出码
正如上述,在Linux种有一个变量“?”记录着最近一个进程的退出码。
#include<stdio.h>
#include<stdlib.h>int main()
{exit(10);return 0;
}
值得注意的是如果连续两次使用echo $?指令,第二次打印出的总是0,有误echo也是个程序也有返回值。
2.进程的三种退出状态
1)又可分为能运行完和没运行完
2)exit( )和_exit( )的区别
除了通过return返回外,程序还可通过exit和_exit两个函数返回。
exit是“stdlib.h”中的函数,是基于_exit做的封装;
_exit是系统调用,在“unistd.h”中。
他们的作用都是“终止调用进程”,但执行起来有些微区别:
exit在结束进程时会刷新缓冲区,且会执行自定义的清理函数,而_exit不会。
#include<stdio.h>
#include<stdlib.h>
#inlcude<unistd.h>int main()
{printf("hello Linux");//注意没有换行符exit(0);//_exit(0);return 0;
}
exit执行情况:
_exit执行情况:
三、进程等待
1.进程等待是什么
每个子进程在运行结束后都会有一段僵尸进程状态,而进程等待是父进程通过系统调用等待子进程的一种方式。
僵尸进程是指 已终止但未被父进程回收(reap)的进程。
2.为什么要进程等待
①释放子进程的僵尸状态;②获取子进程的运行状态
3.父进程是怎么等待的
1)wait和waitpid
waitpid参数解释:
pid——要等待(回收)的子进程;
*status写入型参数——将读取到的子进程状态写入status中;
options为等待方式,传0则是阻塞式等待,传入WNOHANG则是非阻塞等待(若有疑问的读者可直接跳转至“阻塞等待与非阻塞等待的区别”查看)
2)写入型参数status的使用方式
写入型参数,顾名思义就是系统会向这个形参中写入数据,通过写入的数据便可以判别进程的运行状态。
一般status创建为整型初始化为0,而系统写入的数据一般在status变量的低十六位。
解释:4字节的整形status由32位二进制组成,而系统写入的数据则在低16位,也就是2^0-2^15之间,而就这16位二进制包含了两份数据——从0位到7位的二进制表示终止信号;从8位到15的二进制表示退出状态。
①终止信号
还记得上面讲述的进程的三种退出状态吗?
二者三种状态又可分为两种情况:
①情况一:程序无异常,代码能跑完,无退出码;
②情况二:程序出现异常,中途退出,无退出码。
终止信号数据用于记录第二种情况——进程没跑完,中途出现异常的情况。
若进程代码顺利跑完,则终止信号默认为0(status初始化值就是0)
获取终止信号的方式:
(status & 0X7F),0X7F是十六进制表示111 1111(其他全为0),&是位运算,(上下)同为1结果才是1。
Linux环境下的终止信号有个64个,可以通过kill -l指令查看:
②退出状态
status二进制的次八位,也就是退出码
获取退出状态(退出码)的方式:
(status>>8)& 0XFF,>>是位运算符,作用是将status的二进制右移动八位,前边补0.
Linux环境下的退出码有134种。
③status相关宏定义
若是觉得status中的两个数据使用过于麻烦,系统为我们提供了两个宏以便使用:
一个是WIFEXITED(status),他的作用是检查进程是否运行完毕,有无异常。若进程正常退出则WIFEXITED(status)值为非零,否则为0.
另一个是WEXITSTATUS(status),他的值是进程的退出码。
3)使用示范
值得注意的是waitpid的返回值就是等待进程的id
#include<stdio.h>
#include<string.h>
#include<stdlib.h>//exit
#include<sys/types.h>//waitpid
#include<sys/wait.h>//waitpid
#include<unistd.h>//forkint main()
{pid_t id=fork();if(id==0){//子进程int cnt=5;while(cnt--){printf("我的id:%d,父进程id:%d\n",getpid(),getppid());sleep(1);}exit(10);}//父进程int _status=0;int s_id=waitpid(id,&_status,0);printf("我的id:%d,子进程id:%d,sig number:%d,child exit code:%d",getpid(),s_id,(_status & 0X7F),(_status>>8&0XFF));sleep(1);return 0;
}
4)阻塞等待与非阻塞等待的区别
阻塞等待与非阻塞等待各有优劣,使用哪种等待方式全看当时情况:
比如,父进程只需等待单个子进程,且无其他任务需并行处理时就适合用阻塞等待,原因是可以将cpu资源让出来供其他进程使用。
总结
本文从进fork函数入手介绍了什么是进程创建,再通过strerror函数引出退出码以及exit和_exit的区别与联系,最后通过waitpid引出status的使用与相关数据的提取。本文还介绍了阻塞等待与非阻塞等待的区别。