【Linux进程控制】

进程创建

fork函数

fork函数可以从已存在进程中创建一个新进程,新进程为子进程,原进程为父进程。

返回值:在子进程中返回0,父进程中返回子进程的PID,子进程创建失败则返回-1.

进程调用fork,当控制转移到内核中的fork代码后,内核做:
1.分配新的内存和内核数据结构给子进程。
2.将父进程的部分数据内容拷贝给子进程。
3.添加子进程到系统进程列表中。
4.fork返回,开始调度器调度。

注意:fork之后,父进程和子进程谁先执行完全由调度器决定

fork函数为什么要给子进程返回0,给父进程返回子进程的PID?
一个父进程可以创建多个子进程,但一个子进程只有一个父进程。因此,对于子进程来说,父进程是不需要被标识的;而对于父进程来说,子进程是需要被标识的,因为父进程创建子进程的目的是让其执行任务的,父进程只有知道了子进程的PID才能很好的对该子进程指派任务。

为什么fork函数有两个返回值?
父进程调用fork后,为了创建子进程,fork函数内部会进行一系列操作,包括创建子进程的进程的进程控制块,创建子进程的进程地址空间,创建子进程的页表等等,子进程创建完毕后,操作系统还要将子进程的进程控制块添加到系统进程列表中,此时子进程便创建完毕了。
也就是说,在fork函数内部执行return语句之前,子进程就已经创建完毕了,那么之后的return语句不仅父进程需要执行,子进程也需要执行,这就是fork函数有两个返回值得原因。

写时拷贝

当子进程刚被创建时,子进程和父进程的数据和代码是共享的,即父子进程的代码和数据通过各自的页表映射到物理内存的同一块空间。只有当父进程或子进程需要修改数据时,才将父或子进程的数据在物理内存中拷贝一份,然后在进行修改。
这种需要需要在数据修改时再进行拷贝的技术称为写时拷贝。

为什么数据要进行写时拷贝?
进程具有独立性,多进程运行需要独享各种资源,多进程运行期间互不干扰,不能让一个进程的修改而影响到其他进程。

为什么不在创建子进程时就进行数据拷贝?
子进程不一定会使用父进程的所有数据,并且在子进程没有对数据进行写入的情况下,没有必要对数据进行拷贝,应该按需分配,在需要修改数据的时候再分配,这样可以最大限度的利用内存空间。

代码会不会进行写时拷贝?
大部分情况下是不会的,但这并不代表代码不能进行写时拷贝,例如在进程替换的时候,则需要进行代码的写时拷贝。

fork常规用法

1.一个进程希望复制自己,使子进程同时执行不同的代码段。例如父进程等待客户端请求,生成子进程来处理请求。
2.一个进程需要执行一个不同的程序,例如调用exec函数完成进程替换。

fork调用失败的原因

大部分情况下,fork都不会失败,但也有两种失败的情况
1.系统中有太多的进程,内存空间不足。
2.实际用户的进程数超过了限制。

进程终止

进程退出场景

进程退出只有三种情况:
1.代码运行完毕,结果正确。
2.代码运行完毕,结果不正确。
3.代码异常终止。

进程退出码

我们都知道main函数是代码的入口,但实际上main函数只是用户级别代码的入口,main函数也是被其他函数调用的,例如在VS2013当中main函数就是被一个名为__tmainCRTStartup的函数所调用,而__tmainCRTStartup函数又是通过加载器被操作系统所调用的,也就是说main函数是间接性被操作系统所调用的。

既然main函数是间接性被操作系统所调用的,那么当main函数调用结束后就应该给操作系统返回相应的退出信息,而这个所谓的退出信息就是以退出码的形式作为main函数的返回值返回,一般以0表示代码成功执行完毕,以非0表示代码执行过程中出现错误,这就是为什么我们都在main函数的最后返回0的原因。

echo $? 命令查看最近一次进程退出的退出码信息

为什么以0表示代码执行成功,以非0表示代码执行错误?
因为代码执行成功只有一种情况,成功了就是成功了,而代码执行错误却有多种原因,例如内存空间不足、非法访问以及栈溢出等等,我们就可以用这些非0的数字分别表示代码执行错误的原因。

strerror函数可以通过错误码,获取该错误码当中对应的错误信息。
注意: 退出码都有对应的字符串含义,帮助用户确认执行失败的原因,而这些退出码具体代表什么含义是人为规定的,不同环境下相同的退出码的字符串含义可能不同。

进程正常退出

main函数中return退出

在main函数中使用return退出进程是我们常用的方法。

exit函数

使用exit函数退出进程也是我们常用的方法,exit函数可以在代码中的任何地方退出进程,并且exit函数在退出进程前会做一系列工作:
1.执行用户通过atexit或on_exit定义的清理函数。
2.关闭所有打开的流,所有的缓存数据均被写入。
3.调用_exit函数终止进程。

_exit函数

使用_exit函数退出进程的方法我们并不经常使用,_exit函数也可以在代码中的任何地方退出进程,但是_exit函数会直接终止进程,并不会在退出进程前会做任何收尾工作。

return、exit和_exit之间的区别与联系

区别:只有在main函数当中的return才能起到退出进程的作用,子函数当中return不能退出进程,而exit函数和_exit函数在代码中的任何地方使用都可以起到退出进程的作用。

使用exit函数退出进程前,exit函数会执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再终止进程,而_exit函数会直接终止进程,不会做任何收尾工作。

联系:执行return num等同于执行exit(num),因为调用main函数运行结束后,会将main函数的返回值当做exit的参数来调用exit函数。

使用exit函数退出进程前,exit函数会先执行用户定义的清理函数、冲刷缓冲,关闭流等操作,然后再调用_exit函数终止进程

进程异常退出

1.在进程运行过程中向进程发生kill -9信号使得进程异常退出,或是使用Ctrl+C使得进程异常退出等。

2.代码错误导致进程运行时异常退出。例如,代码当中存在野指针问题使得进程运行时异常退出,或是出现除0的情况使得进程运行时异常退出等。

进程等待

进程等待的必要性

1.子进程退出,父进程如果不读取子进程的退出信息,子进程就会变成僵尸进程,进而造成内存泄漏。
2.进程一旦变成僵尸进程,那么就算是kill -9命令也无法将其杀死,因为谁也无法杀死一个已经死去的进程
3.对于一个进程来说,最关心自己的就是其父进程,因为父进程需要知道自己派给子进程的任务完成的如何。
3.父进程需要通过进程等待的方式获取子进程的退出状态,获取子进程的退出信息,从而让操作系统回收子进程的进程控制块

获取子进程的status

下面进程等待所使用的两个函数wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统进行填充。
如果对status参数传入NULL,表示不关心子进程的退出状态信息。否则,操作系统会将子进程的退出信息填充到status中。

status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,在status的低16比特位当中,高8位表示进程的退出状态,即退出码进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。
在这里插入图片描述
我们通过位操作,根据status得到进程的退出码和退出信号。

exitCode = (status >> 8) & 0xFF; //正常退出时的退出码,当异常退出时退出码是没有意义的
exitSignal = status & 0x7F;      //退出信号

对于此,系统当中提供了两个宏来获取退出码和退出信号。

WIFEXITED(status):用于查看进程是否是正常退出,本质是检查是否收到信号,正常退出,即为真。
WEXITSTATUS(status):用于获取进程的退出码

需要注意的是,当一个进程非正常退出时,说明该进程是被信号所杀,那么该进程的退出码也就没有意义了。

进程等待方法

wait方法

函数原型: pid_t wait(int* status)

作用:等待任意子进程退出

返回值:等待成功返回被等待进程的PID,等待失败返回-1.

参数:输出型参数,获取子进程的退出状态,不关心可设置为NULL。

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、options:当设置为WNOHANG时,代表非阻塞式等待,若等待的子进程没有结束,则waitpid函数直接返回0,不予以等待。若正常结束,则返回该子进程的pid。

进程替换

替换原理

用fork创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支),若想让子进程执行另一个程序,往往需要调用一种exec函数。

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,并从新程序的启动例程开始执行。
在这里插入图片描述
当进行进程程序替换时,有没有创建新的进程?
进程替换之后,该进程对应的进程控制块PCB,进程地址空间以及页表等数据结构都没有发生改变,只是进程在物理内存中的数据和代码发生了改变,所以并没有创建新的进程,而且进程替换前后该进程的PID都没有发生改变。

子进程进行进程程序替换后,会影响父进程的代码和数据吗?
子进程刚被创建时,与父进程共享代码和数据,但当子进程需要进行进程程序替换时,也就意味着子进程需要对其数据和代码进行写入操作,这时便需要将父子进程共享的代码和数据进行写时拷贝,此后父子进程的代码和数据也就分离了,因此子进程进行程序替换后不会影响父进程的代码和数据。

替换函数

替换函数有六种以exec开头的函数,它们统称为exec函数

一、int execl(const char *path, const char *arg, ...);

第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

例如,要执行的是ls程序。

execl("/usr/bin/ls", "ls", "-a", "-i", "-l", NULL);
二、int execlp(const char *file, const char *arg, ...);

第一个参数是要执行程序的名字,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾。

例如,要执行的是ls程序。

execlp("ls", "ls", "-a", "-i", "-l", NULL);
三、int execle(const char *path, const char *arg, ..., char *const envp[]);

第一个参数是要执行程序的路径,第二个参数是可变参数列表,表示你要如何执行这个程序,并以NULL结尾,第三个参数是你自己设置的环境变量。

四、int execv(const char *path, char *const argv[]);

第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。

例如,要执行的是ls程序。

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execv("/usr/bin/ls", myargv);
五、int execvp(const char *file, char *const argv[]);

第一个参数是要执行程序的名字,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾。

例如,要执行的是ls程序。

char* myargv[] = { "ls", "-a", "-i", "-l", NULL };
execvp("ls", myargv);
六、int execve(const char *path, char *const argv[], char *const envp[]);

第一个参数是要执行程序的路径,第二个参数是一个指针数组,数组当中的内容表示你要如何执行这个程序,数组以NULL结尾,第三个参数是你自己设置的环境变量。

函数解释

这些函数如果调用成功,则加载指定的程序并从启动代码开始执行,不再返回。
如果调用出错,则返回-1。

也就是说,exec系列函数只要返回了,就意味着调用失败。

命名理解

这六个exec系列函数的函数名都以exec开头,其后缀的含义如下:

l(list):表示参数采用列表的形式,逐个列出。
v(vector):表示参数采用数组的形式。
p(path):表示能自动搜索环境变量PATH,进行程序查找
e(env):表示可以传入自己设置的环境变量

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值