创建子进程需要用fork函数,以下是fork函数的声明及相关描述:
fork函数的返回值:
调用成功:给父进程返回子进程的pid,给子进程返回0。
调用失败:创建子进程失败,向父进程返回-1。
一般情况下,fork函数不会调用失败,而失败的情况有以下两种:
1.系统中有太多的进程
2.OS会限制一个用户所能创建的进程数量,避免OS自己都挂掉
int main()
{
pid_t id=fork();
//子进程
if(id==0)
{
cout<<"我是子进程,fork的返回值是:"<
运行截图:
为什么同一个fork函数却会给父子进程针对同一个变量但返回的却是两个不同的值?
fork创建子进程之后,子进程和父进程共享代码和数据,只有某个进程单独对数据进行修改的时候,才会发生写时拷贝,即另外开辟一块内存来存储改变后的数据,而原数据仍保留一份(即fork的返回值在内存中存在两份,一份属于父进程,一份属于子进程,这两个值不相同),这也是os为了避免内存浪费或者不高效行为的一种机制。
至于为什么父子进程fork返回值的地址却是一样的,这个在会在后面进程虚拟地址部分具体讲解。
而不只是数据能做修改,当程序替换时,也能修改写时拷贝代码。
进程终止的情况分类:
1.进程正常执行完成后退出:创建子进程的目的就是为了帮用户完成任务,但计算结果到底是否正确就需要我们自行判断,可以根据退出码来帮助判断结果是否正确。
2.进程执行中异常导致进程终止
进程异常退出是进程在运行过程中被意外终止,从而导致进程本来应该继续执行的任务无法完成。即该完成的任务没有完成,因为异常导致进程提前终止。
#include
#include
#include
using namespace std;
int main()
{
for(int i=0;i<255;i++)
{
cout<<"退出码:"<
运行截图:
C语言strerror对进程退出码进行了解释,但进程的退出码和相关的描述可以自定义,不一定是按照C语言规定的来设置的。
在命令行使用echo $?会显示最近一次执行的进程的退出码
进程退出的方式:
1.main函数return使进程退出
2.exit和_exit都是使进程退出,但是exit会刷新缓冲区,执行清理函数等,而_exit是直接退出,不做其他任何操作。从此处也能看出缓冲区不存在内核,而存在用户层。
进程等待:父进程等待子进程退出,如果父进程对子进程不管不顾,那么子进程就变为僵尸进程,kill -9命令也无法将僵尸进程杀死,因为它已经死亡了,而父进程同时也要知道子进程完成任务的情况,所以也要接收子进程的退出码及信号。
总结:
1.避免内存泄漏
2.获取子进程执行的结果
只要接收到信号,那就说明进程是异常退出的,此时的退出码就失去了意义;
没有接收到信号,那么进程就是正常运行的,此时的退出码可以用来评判结果是否正确。
进程等待的方法有两种:
1.wait(int* status)
返回值:成功返回被等待进程的pid,失败返回-1。
参数:status:输出型参数
获取子进程退出状态,不关心可以设置为NULL。即不需要获取子进程的退出码和信号
2. waitpid(pid_t pid, int *status, int options)
参数:pid:如果pid>0,表示等待指定的进程。
如果pid=-1,表示等待任意一个子进程,和wait是相同效果。
status:同wait函数中的status,获取进程的退出码和信号的。
options:waitpid函数执行的选项,例如,将其设置为WNOHANG,可进行非阻塞轮询。
一般都是等待成功,除非是等待一个pid不存在的进程。
将status看作一个位图:不要当成一个完整的整数,而是其中特定的比特位来分别表示信号和退出码
前十六个比特位不用,低八位表示退出状态即退出码,倒数七位表示信号。
获取退出码和信号的方式(status右移八位,但status本身不变)
父进程如何获取子进程的退出信息?
子进程的pcb中有信号和退出码,父进程调用waitpid通过pid找到子进程的pcb,再将其中退出码和信号写进waitpid函数中的status,父进程就可以获取子进程的退出码和信号了。
父进程在wait的时候,如果子进程没有退出,那么父进程在干嘛?
父进程处于阻塞等待的状态,那么父进程就不在运行队列当中,而是在子进程的队列当中,即子进程的pcb中含有一个父进程的指针,当os检测到子进程结束后,通过该指针找到父进程,再执行父进程的操作。
如果不想让父进程一直等待子进程退出,而是在等待子进程退出的过程中去做一些其他事情,那么就可以采用非阻塞轮询(将waitpid中的option设置为WNOHANG)的方式,即时不时检测一下子进程是否退出,如果没有退出就继续做自己的事情,如果退出了那就回收,如果出错了,也能及时得到反馈。
非阻塞轮询的返回值有三种:
1.子进程还在运行当中没有退出
2.子进程运行过程中出错
3.子进程正常运行后退出
WNOHANG:父进程采用非阻塞轮询的方式。
创建子进程的目的是为了让子进程帮我执行特定的任务
1.让子进程执行父进程的一部分代码
2.如果子进程指向一个全新的程序代码,就叫做程序替换。
程序替换函数:
以execl函数为例来理解这些函数的用法:
#include
#include
#include
using namespace std;
int main()
{
pid_t id=fork();
if(id==0)
{
execl("/bin/ls","ls","-l","-a",NULL);
exit(0);
}
return 0;
}
参数:
path:代表执行的程序所在的路径,在一系列程序替换函数中,函数名中不带p的替换函数,都要传入执行程序的路径。
args:代表如何执行程序,在命令行上输入的指令都要传入进来,而且必须用NULL结尾。
函数名中带l:在传参处必须带上完整的指令,并且还要以NULL结尾。
函数名中带p:不需要传入程序的路径,只需要传入程序的名称,调用时,会自动去环境变量对应的路径下去寻找改程序。
函数名中带v:可以将执行程序的指令放在一个vetcor容器里面,在传参时传入该vector,不需要在传参处带上所有指令。
函数名中带e:可以传入环境变量,可以是系统自带的环境变量,也可以是自己自定义的环境变量。但注意,自定义环境变量是覆盖式传入,自己定义的环境变量会覆盖系统的环境变量,如果不想覆盖式传入,可以在传入系统环境变量的基础上,追加自己的环境变量。
而上面的一系列函数都是对execve函数的封装,只有该函数是真正的系统调用。
追加环境变量代码:
#include
#include
#include
using namespace std;
extern char** environ; //系统的环境变量
int main()
{
pid_t id=fork(); //创建子进程
//让子进程去完成任务
if(id==0)
{
putenv("MYENY=youcanseeme"); // 追加自定义环境变量到系统的环境变量表当中
execle("../myotherproc/test1","test1",NULL,environ); //执行另一个目录的可执行程序
exit(0);
}
return 0;
}
在shell命令行当中直接使用export指令导入自定义环境变量,也可以做到传递环境变量。因为我们在bash中执行test程序,test变成bash的子进程,而test因为创建子进程并且程序替换去调用test1,那么test1就成了test的子进程,而子进程是继承父进程的环境变量表的,所以在shell命令行导入的环境变量,最终也能被test1这个“孙子进程”所接受到。
程序替换可以做到调用其他语言写的可执行程序,只要这个程序可以转化为Linux操作系统下进程的程序,都可以被替换执行。
程序替换实质上OS先创建了进程相关的数据结构(进程创建时,先有进程数据结构,再将代码和数据加载到内存),然后把进程的数据段和代码段用存在于磁盘上的程序进行了替换,把磁盘上的程序加载到了内存,进而执行替换后的程序,所以excel函数也可以被称为加载器。
所以如果一个程序中有execl函数,那么os先创建进程数据结构,然后通过execl对代码和数据进行替换。
注意:
1.程序替换是整体替换,不能局部替换。
2.程序替换只影响调用程序替换的进程,因为进程具有独立性。
3.因为父进程和子进程共享代码和数据,即页表映射的是同一块物理内存,而当子进程进行程序替换时,加载了新的代码,那么此时会发生写时拷贝,即写时拷贝也能发生在代码区。
关于execl函数的返回值问题:
execl如果没有替换程序成功就会有返回值,因为如果一旦替换成功了,就算接收了返回值,那么这个返回值在execl之后的任何一处都没有作用,因为后面的代码都被替换了,所以程序替换成功之后就不会有返回值的存在,而一旦有返回值,就是替换失败,并且会执行execl后面的代码,所以不用对返回值进行判断,execl就是替换新的程序,后面紧跟的就是没有替换成功的操作。