文章目录
- 🦖 前言
- 🦖 fork()函数调用失败原因
- 🦖 进程终止
- 💥 进程退出码
- 💥 进程正常退出
- 🦖 进程等待
- 💥 僵尸进程
- 💥 如何解决僵尸进程的内存泄漏问题
- 💥 wait( )/waitpid( )函数
- 🌟 进程退出信息
- 💥 非阻塞式等待
- 💥 父进程如何获取子进程的退出信息
- 🦖 进程替换
- 💥 进程替换的原理
🦖 前言
进程控制是一种在操作系统上对进程进行管理和调度的一个过程;
这包括创建进程,终止进程,等待进程,暂停和恢复进程,进程间的通信和调度进程等待;
在之前关于Linux的内容中谈论了大量的关于进程的内容;
本文将重点对于基础进程控制进行一定的讲解;
🦖 fork()函数调用失败原因
在『 Linux 』使用fork函数创建进程与进程状态的查看-CSDN博客中提到了使用fork()
函数对进程进行创建等操作;
fork()
函数本质上就是在一个已经存在的进程当中创建出该进程的子进程,在此不再进行赘述;
知道了fork()
函数的大致原理,那么有一个问题:
fork()
函数调用失败的原因是什么?
fork()
函数调用失败的原因本质上分为两种:
-
系统中存在大量进程
当内存当中存在大量进程时,由于进程需要维护对应的
PCB
结构体与对应的内存数据;当出现大量的进程时将会极度占用内存资源;
OS
为了防止崩溃的情况,当内存吃紧或是当前进程数过多的情况将会驳回创建进程的请求从而导致子进程创建失败; -
实际用户的进程数超过了限制
一般情况下,操作系统会限制每个用户可以拥有的进程数量,以确保系统资源的合理分配和管理;
故当实际用户的进程数超过了限制时,操作系统也将驳回创建进程的请求;
🦖 进程终止
进程终止即字面意思理解;
一个正在运行的进程运行结束并释放对应的内存资源;
一般进程终止存在以下几种状态:
💥 进程退出码
在上文中提到的三种状态其中两种为程序正常终止的状态;
分别为:
- 代码运行完毕且结果正确
- 代码运行完毕但结果错误
在这两种情况下,程序(进程)的代码数据已经被执行完毕,只是对应的结果是错误,这种进程终止方式统称为进程的正常终止;
以我平时写代码的习惯而言:
int main(){//代码数据return 0;
}
在这段代码当中,或许有些人并不理解为什么在main()
函数当中需要返回一个0
值;
可能从某些编译器的源代码当中向下进行追述可以明白这个函数返回值最终将会传给操作系统;
实际上这个return 0
所返回的0
值被称为一个进程的退出码;
在c/C++中可以通过strerror()
打印退出码对应的退出信息;
#include <cstring>/#include <string.h>
char* strerror(int errnum); //声明
-
存在一个程序
#include<cstring> int main() {for (int i = 0; i < 150;++i){printf("strerror(%d) : %s \n", i, strerror(i));}return 0; }
即为打印出
150
以内的退出码;运行该进程结果为:
$ ./myproc strerror(0) : Success strerror(1) : Operation not permitted strerror(2) : No such file or directory strerror(3) : No such process strerror(4) : Interrupted system call strerror(5) : Input/output error strerror(6) : No such device or address strerror(7) : Argument list too long strerror(8) : Exec format error strerror(9) : Bad file descriptor strerror(10) : No child processes strerror(11) : Resource temporarily unavailable strerror(12) : Cannot allocate memory strerror(13) : Permission denied strerror(14) : Bad address strerror(15) : Block device required strerror(16) : Device or resource busy strerror(17) : File exists strerror(18) : Invalid cross-device link strerror(19) : No such device strerror(20) : Not a directory strerror(21) : Is a directory strerror(22) : Invalid argument strerror(23) : Too many open files in system strerror(24) : Too many open files strerror(25) : Inappropriate ioctl for device strerror(26) : Text file busy strerror(27) : File too large strerror(28) : No space left on device strerror(29) : Illegal seek strerror(30) : Read-only file system strerror(31) : Too many links strerror(32) : Broken pipe strerror(33) : Numerical argument out of domain strerror(34) : Numerical result out of range strerror(35) : Resource deadlock avoided strerror(36) : File name too long strerror(37) : No locks available strerror(38) : Function not implemented strerror(39) : Directory not empty strerror(40) : Too many levels of symbolic links strerror(41) : Unknown error 41 strerror(42) : No message of desired type strerror(43) : Identifier removed strerror(44) : Channel number out of range strerror(45) : Level 2 not synchronized strerror(46) : Level 3 halted strerror(47) : Level 3 reset strerror(48) : Link number out of range strerror(49) : Protocol driver not attached strerror(50) : No CSI structure available strerror(51) : Level 2 halted strerror(52) : Invalid exchange strerror(53) : Invalid request descriptor strerror(54) : Exchange full strerror(55) : No anode strerror(56) : Invalid request code strerror(57) : Invalid slot strerror(58) : Unknown error 58 strerror(59) : Bad font file format strerror(60) : Device not a stream strerror(61) : No data available strerror(62) : Timer expired strerror(63) : Out of streams resources strerror(64) : Machine is not on the network strerror(65) : Package not installed strerror(66) : Object is remote strerror(67) : Link has been severed strerror(68) : Advertise error strerror(69) : Srmount error strerror(70) : Communication error on send strerror(71) : Protocol error strerror(72) : Multihop attempted strerror(73) : RFS specific error strerror(74) : Bad message strerror(75) : Value too large for defined data type strerror(76) : Name not unique on network strerror(77) : File descriptor in bad state strerror(78) : Remote address changed strerror(79) : Can not access a needed shared library strerror(80) : Accessing a corrupted shared library strerror(81) : .lib section in a.out corrupted strerror(82) : Attempting to link in too many shared libraries strerror(83) : Cannot exec a shared library directly strerror(84) : Invalid or incomplete multibyte or wide character strerror(85) : Interrupted system call should be restarted strerror(86) : Streams pipe error strerror(87) : Too many users strerror(88) : Socket operation on non-socket strerror(89) : Destination address required strerror(90) : Message too long strerror(91) : Protocol wrong type for socket strerror(92) : Protocol not available strerror(93) : Protocol not supported strerror(94) : Socket type not supported strerror(95) : Operation not supported strerror(96) : Protocol family not supported strerror(97) : Address family not supported by protocol strerror(98) : Address already in use strerror(99) : Cannot assign requested address strerror(100) : Network is down strerror(101) : Network is unreachable strerror(102) : Network dropped connection on reset strerror(103) : Software caused connection abort strerror(104) : Connection reset by peer strerror(105) : No buffer space available strerror(106) : Transport endpoint is already connected strerror(107) : Transport endpoint is not connected strerror(108) : Cannot send after transport endpoint shutdown strerror(109) : Too many references: cannot splice strerror(110) : Connection timed out strerror(111) : Connection refused strerror(112) : Host is down strerror(113) : No route to host strerror(114) : Operation already in progress strerror(115) : Operation now in progress strerror(116) : Stale file handle strerror(117) : Structure needs cleaning strerror(118) : Not a XENIX named type file strerror(119) : No XENIX semaphores available strerror(120) : Is a named type file strerror(121) : Remote I/O error strerror(122) : Disk quota exceeded strerror(123) : No medium found strerror(124) : Wrong medium type strerror(125) : Operation canceled strerror(126) : Required key not available strerror(127) : Key has expired strerror(128) : Key has been revoked strerror(129) : Key was rejected by service strerror(130) : Owner died strerror(131) : State not recoverable strerror(132) : Operation not possible due to RF-kill strerror(133) : Memory page has hardware error strerror(134) : Unknown error 134 ......
其中
return 0
时表示程序正常退出且 结果正确 ;
当一个进程结束后,可以对应的使用echo
打印出上一个进程结束时的退出码;
echo $?
同样以上一个程序为例,此时将退出信息return 0
改为return 111
;
#include<cstring>
int main() {for (int i = 0; i < 150;++i){printf("strerror(%d) : %s \n", i, strerror(i));}return 111;//此处进行修改
}
将该程序运行后再使用echo $?
打印出上一个进程运行结束后的退出码;
$ ./myproc
strerror(0) : Success
strerror(1) : Operation not permitted
strerror(2) : No such file or directory
......
strerror(149) : Unknown error 149 $ echo $?
111
$ echo $?
0
$ echo $?
0
从该处的结果可以得出结论;
- 为什么此处的
echo $?
只打印出了一次111
;
在之前的文章中提到,在Linux
当中,命令也属于文件,也需要被执行;
既然要被执行那么就会变成进程,对应的也有属于自身的退出码;
故当一个命令在被执行过后再使用echo $?
打印退出码时将会打印出该命令的退出码;
💥 进程正常退出
在上文当中谈到了进程退出的三种情况其中两种情况都为正常退出的情况;
同时在上文当中提到了 进程退出码 的概念;
对于进程退出码而言,只有当进程正常退出的情况退出码才有意义;
进程退出时除了return
可以对进程进行结束以外还有对应的exit()
,_exit()
;
#include<stdlib.h>/<cstdlib>
exit();#include<unistd.h>
_exit();
-
存在一段程序
#include<iostream> using namespace std; int func(){return 10; } int main(){cout<<func()<<endl;return 0; }
在这段程序当中出现了两个
return
,分别为main()
函数与普通函数func()
函数;
那么在上段代码中的两个return
的意义都是什么;
这可以将return
分为两种情况;
-
当
return
存在于main()
函数当中当
return
存在main()
函数中,其意义表示为程序(进程)的退出; -
当
return
存在于普通函数当中当
return
存在普通函数中,其意义表示为当前函数的返回;
与return
不同,exit()
与_exit()
无论处于哪都表示进程的退出;
-
存在一段代码
int func() { // return 10;exit(10);//由于_exit 与 exit在此处所展示的效果相同故不进行演示 } int main() {func();return 111; }
当运行这段代码后再使用
echo $?
打印出进程对应的退出码;$ ./myproc $ echo $? 10
对应的退出码变成了
10
;由此也可以验证对于
exit()
与_exit()
而言,无论是普通函数还是main()
函数,都充当着结束进程的功能;
那么对应的这两个函数的功能是否完全相同?
int main()
{cout<<"hello world\n";exit(-1);
}
已知当printf()
或者cout
输出字符串并包含\n
时,输出流将会被刷新并将缓冲区的内容写入到标准输出设备;
故这个程序的结果为:
hello world
那么将该处的\n
进行删除并再次运行程序;
int main()
{cout<<"hello world";exit(-1);
}
hello world
对应的结果还是不变;
此时将exit()
换成_exit()
重新编译后再次运行程序;
int main() {cout << "hello world";_exit(-1);
}
$ ./myproc
$
从该段当中可以看出,当exit()
被换成_exit()
时,将不再刷新缓冲区进行输出;
- 为什么使用
exit()
会进行打印而_exit()
不会? exit()
与_exit()
之间的区别是什么?
实际上,_exit()
属于系统调用,当程序调用_exit()
对进程进行结束时将通过系统调用直接结束进程;
而对于exit()
而言,其本质是一个对_exit()
封装后的C标准库函数;
🦖 进程等待
在操作系统当中, 进程等待(Process Waiting) 指一个进程暂时停止执行,知道某个特定事件发生或者某个特定条件得到满足后再继续执行;
💥 僵尸进程
在『 Linux 』僵尸进程与孤儿进程-CSDN博客中提到了对于僵尸进程的概念;
一个进程的创建与资源回收是由其父进程或者是OS(操作系统)进行的;
而僵尸进程的概念即为,当进程退出时其并不被允许进行资源回收而回处于僵尸状态(Z);
本质上一个进程在结束之后其对应的内存资源将会被释放,但是若是该进程的父进程并未读取到该进程退出的返回状态时该僵尸进程虽然对应的内存资源被释放但仍有一部分的PCB结构体未被释放;
过多的僵尸进程将在操作系统当中存在过多的PCB结构体使得大量的占用内存,属于是一种内存泄漏的问题;
那么在这里存在一个问题:
- 子进程由父进程进行创建并且子进程将会帮助父进程完成对应的工作,那么父进程是否需要关心子进程完成工作的完成情况?且若是父进程需要关注又该如何得知?父进程若是不需要又该如何处理?
💥 如何解决僵尸进程的内存泄漏问题
在上文当中提到了对于僵尸进程的内存泄露问题;
那么如何解决僵尸进程的内存泄露问题?
在POSIX
标准库中存在着这样的两个函数,分别为wait()
与waitpid()
两个函数;
#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
这两个函数实际上是一个用于进程等待的函数;
这两个函数的函数名简而言之即为等待子进程的状态发生变化,当父进程等待到子进程的状态变为Z
(Zombie)时,父进程将回收子进程并读取其对应的退出信息,从而结局僵尸问题;
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main() {pid_t id = fork();if (id == 0) {// 子进程int cnt = 2;while (cnt--) {cout << "pid : " << getpid() << " ppid : " << getppid() << endl;sleep(1);}} else if (id > 0) {// 父进程sleep(4);wait(NULL);//wait(NULL) 与 waitpid(-1,NULL,0) 效果相等,在此不演示waitpid();} else {// 创建进程失败exit(-1);}return 0;
}
如该段代码所示;
该段代码即在一个程序中创建一个子进程,且子进程"存活"2s
,父进程存活4s
;
父进程将在子进程结束处于僵尸进程时将子进程进行回收;
运行该程序并且试用Shell
对进程进行观察;
while :; do ps axj | head -1 && ps axj | grep myproc | grep -v grep ; sleep 1 ; echo "-----------------------------------" ; done
当运行程序时shell
语句对应显示的信息为:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND9338 9877 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc9877 9878 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc
------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND9338 9877 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc9877 9878 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc
------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND9338 9877 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc9877 9878 9877 9338 pts/2 9877 Z+ 1002 0:00 [myproc] <defunct>
------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND9338 9877 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc9877 9878 9877 9338 pts/2 9877 Z+ 1002 0:00 [myproc] <defunct>
------------------------------------PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
------------------------------------
子进程运行了2s
后状态称为了Z
即僵尸进程;
父进程在4s
过后执行了wait(NULL)
将子进程进行回收,对上述内容进行了验证;
💥 wait( )/waitpid( )函数
上文当中提到了一个问题,简而言之即为父进程如何去管理其子进程;
对于该问题的解答,首先为父进程需不需要关心子进程的工作完成情况,答案是肯定的;
若是父进程未对子进程的工作完成情况进行管理则不能很好的根据子进程的工作完成情况而做出其他处理;
那么父进程该如何得知子进程的工作完成情况?
在上文当中可以得知,当一个进程退出时,即代表它的工作完成(可能);
那么一个进程的退出情况无非分为三种:
- 代码运行完毕且结果正确
- 代码运行完毕但结果错误
- 代码未运行完毕异常终止
其中第一种与第二种都属于进程正常结束,第三种属于进程的异常终止,而一般情况下父进程都是采用wait()/waitpid()
的方式获取子进程的退出信息从而对后序操作进行处理;
上文当中演示了使用wait()/waitpid()
解决僵尸进程的内存泄露问题;
对应的两个函数的分别为:
#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *status);
pid_t waitpid(pid_t pid, int *status, int options);
-
返回值
对于两个函数的返回值都是一样的返回值,且返回值分别有几种情况:
-
return value > 0
当返回值大于
0
时则表示进程等待成功且回收成功,并返回处理的子进程的PID
; -
return value == 0
当返回值等于
0
时则表示当前不存在已终止的进程,即没有已终止的子进程可等待; -
return value == -1
当返回值为
-1
时则表示等待过程当中出现了错误,这通常发生在传递给waitpid()
函数的参数不合法或者出现了系统错误的情况;
-
-
参数
-
pid
对于
waitpid()
中的参数pid
表示需要等待的子进程的ID
;pid > 0
表示等待进程ID
为pid
的子进程;pid == -1
表示等待任意子进程;pid == 0
表示等待和调用进程属于同一个进程组的任何进程(不作过多描述);pid < -1
表示等待进程组ID
为pid
的任意子进程(不作过多描述); -
status
这是一个输出型参数,用于存储子进程的退出状态信息,当子进程终止时,它会将退出状态信息存储在这个指针所指向的位置;
一般用法为:
int status = 0; waitpid(-1,&status,0);
当进程结束后对应的进程信息将会填入
status
当中; -
options
这是一个标志位用来制定等待子进程时的一些选项;
options = 0
时表示默认等待为阻塞等待子进程结束;options = WNOHANG
表示非阻塞式等待(WNOHANG
为对魔术数字的#define
的重命名``);
-
而对于上文中提到的问题实质性是根据参数当中的status
进行处理,当父进程成功等待对应的子进程并对子进程进行处理时,status
将会获取子进程对应的退出信息;
🌟 进程退出信息
对于进程的退出信息分别有两种:
-
进程退出码
进程正常退出(代码跑完结果正确或是代码跑完结果错误);
-
退出信号状态
进程异常退出(被信号杀死)时对应的退出信号状态;
存在一段代码:
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>int main() {pid_t id = fork();if (id == 0) {// 子进程int cnt = 2;while (cnt--) {cout << "pid : " << getpid() << " ppid : " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {// 父进程sleep(4);int status = 0;pid_t id = waitpid(-1,&status,0);cout << id << " : " << status < < < < endl;} else {// 创建进程失败exit(-1);}return 0;
}
在这段程序中,waitpid()
将结束子进程的僵尸状态并且使用status
获取进程中对应的退出信息并进行打印,其子进程的退出码为111
;
运行后结果为:
$ ./myproc
pid : 10914 ppid : 10913
pid : 10914 ppid : 10913
pid = 10914 , status = 28416
从该段答案当中发现实际上打印的子进程的退出信息并不为代码中的退出码;
而实际上退出信息与提出码并不相同,退出码是退出信息中的其中一个部分;
退出信息一般由 退出码 与 退出终止信号以二进制的方式组成;
-
正常退出
当进程正常退出时,其次低八位代表该进程退出时的退出码;
对应的可以采用位运算计算出对应的退出码;
以该段代码为例:
#include <iostream> #include <sys/types.h> #include <sys/wait.h> using namespace std;int main() {pid_t id = fork();if (id == 0) {// 子进程int cnt = 2;while (cnt--) {cout << "pid : " << getpid() << " ppid : " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {// 父进程sleep(4);int status = 0;pid_t id = waitpid(-1, &status, 0);printf("pid = %d , 退出码 = %d \n", id, (status>>8)&0xff);} else {// 创建进程失败exit(-1);}return 0; }
采用了位运算,即将退出信息 右移八位 后再使用按位与
&
上0xFF
最终得到对应的进程退出码;运行程序后结果为:
$ ./myproc pid : 11219 ppid : 11218 pid : 11219 ppid : 11218 pid = 11219 , 退出码 = 111
-
异常终止
当进程异常终止时期对应的退出码即无意义;
但是按照对应的位运算也可以得出进程异常终止的信号状态;
稍微将代码进行改动:
#include <iostream> #include <sys/types.h> #include <sys/wait.h> using namespace std;int main() {pid_t id = fork();if (id == 0) {// 子进程int cnt = 2;while (cnt) {cout << "pid : " << getpid() << " ppid : " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {// 父进程sleep(4);int status = 0;pid_t id = waitpid(-1, &status, 0);printf("pid = %d , 信号状态 = %d \n", id, status&0x7f);} else {// 创建进程失败exit(-1);}return 0; }
此处将代码改为了一个子进程中无限循环的状态;
且由于进程退出信息中的低七位为进程异常终止时的进程信号状态,此时直接使用按位与
&
上0x7F
即可;在运行该段代码后采用
9
号信号将进程杀死;kill -9 xxxxx(表示子进程的pid)
$ ./myproc pid : 11702 ppid : 11701 pid : 11702 ppid : 11701 pid = 11702 , 信号状态 = 9
从该处可以看出最终的结果显示出了对应的信号状态;
在Linux当中可以使用
kill -l
来查看所有的信号状态从而对进程的结束状态进行了解(再次不进行过多描述);$ kill -l1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
-
core dump
在上图中出现除了 进程退出码 , 信号状态 以外还存在着一个为
core dump
的标志;core dump
一般指进程在异常终止时产生的核心转储文件;核心转储文件包括了进程在异常终止时的内存映像,也便于后序的调试分析;
当
core dump
为1
时表示生成了对应的core dump
文件;当
core dump
为0
时表示未生成对应的core dump
文件;当然
core dump
需要进行配置;
当然对应的进程退出码也不一定需要使用对应的位运算进行;
在POSIX
中存在两个宏分别为WIFEXITED()
与WEXITSTATUS()
;
其对应的声名分别为:
#include <sys/wait.h>int WIFEXITED(int status);
int WEXITSTATUS(int status);
-
WIFEXITED(int status)
这个宏将判断进程的退出信息判断其是否为正常退出;
若是正常退出将返回
1
,若是异常终止则返回0
; -
WEXITSTATUS(int status)
该宏可以在退出信息中提取对应的进程退出码;
一般的使用情况为利用这两个宏来判断子进程是否为正常退出从而对后序进行处理;
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;int main() {pid_t id = fork();if (id == 0) {// 子进程int cnt = 2;while (cnt--) {cout << "pid : " << getpid() << " ppid : " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {// 父进程sleep(4);int status = 0;waitpid(-1, &status, 0);if(WIFEXITED(status)){ //利用WIFEXITESD判断进程是否正常退出printf("进程退出码为: %d\n", WEXITSTATUS(status));// 利用WEXITSTATUS获取进程退出信息中的退出码}else{ cout << "进程异常终止" << endl;}} else {// 创建进程失败exit(-1);}return 0;
}
💥 非阻塞式等待
在上文当中,对于wait()
与waitpid()
中提到了一个对应的进程等待问题;
且在上文当中的进程等待属于阻塞式等待;
既然存在阻塞式等待那么必定也会存在非阻塞式等待;
在waitpid()
函数当中,其中的options
参数若是为0
时则默认为阻塞等待;
除了0
以外还有一个特殊的宏为WNOHANG
;
这个宏为对一个 魔术数字 1
的重命名;
#define WNOHANG 1
当waitpid()
函数中第三个参数为WNOHANG
时则表示非阻塞式等待;
- 那么如何理解阻塞式等待与非阻塞式等待?
当父进程进行阻塞式等待时操作系统将会把父进程对应的数据放置于阻塞队列当中,当任意一个子进程变为僵尸状态时该父进程将会复苏,并获取子进程对应的退出信息再执行父进程后序的代码;
当等待成功后EIP
将会继续读取父进程的下一行指令并在对应位置使CPU继续执行父进程的代码;
当父进程进行非阻塞式等待时,父进程将会直接判断是否存在需要等待的子进程,即是否存在状态发生变化(僵尸状态)的进程,其对应的返回值如下进行比较:
-
return value > 0
当返回值大于
0
时则表示进程等待成功且回收成功,并返回处理的子进程的PID
; -
return value == 0
当返回值等于
0
时则表示当前不存在已终止的进程,即没有已终止的子进程可等待; -
return value == -1
当返回值为
-1
时则表示等待过程当中出现了错误,这通常发生在传递给waitpid()
函数的参数不合法或者出现了系统错误的情况;
一般情况下,非阻塞式等待需要配合循环进行使用,若是父进程不为一个循环且为一个非阻塞式等待(WNOHANG)
时,若是父进程在调用waitpid()
时其子进程并未结束;
父进程则将错失获取子进程退出信息的机会;
当然非阻塞式等待也使得多进程的状态下能够提高程序整体的效率,当子进程在进行处理时父进程进行非阻塞式等待并处理与子进程不同的工作使得整体的效率增加;
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <vector>using namespace std;typedef void (*FunPoint)();vector<FunPoint> P_FunV;void Func1() { cout << "Func1()" << endl; }
void Func2() { cout << "Func2()" << endl; }void Load() {P_FunV.push_back(Func1);P_FunV.push_back(Func2);
}int main() {pid_t id = fork();if (id == 0) {// 子进程int cnt = 5;while (cnt--) {cout << "子进程 : "<< "pid = " << getpid() << " ppid = " << getppid() << endl;sleep(1);}exit(111);} else if (id > 0) {// 父进程bool quite = false;while (!quite) {cout << "父进程 : "<< "pid = " << getpid() << " ppid = " << getppid() << endl;sleep(1);int status = 0;pid_t res = waitpid(-1, &status, WNOHANG);if (res > 0) {printf("等待成功 进程执行完毕,退出码:%d\n", WEXITSTATUS(status));quite = true;} else if (res == 0){//在该条件当中 父进程并未识别到已经结束的任意子进程且在等待时进行的是非阻塞式等待,故父进程可以在等待期间通过循环参与其他工作(cout << "不存在已经结束的子进程" << endl);if(P_FunV.empty())Load();else{for(auto iter:P_FunV){iter();}}} else {perror("子进程等待失败");quite = true;}}} else {// 创建子进程失败}return 0;
}
以该段代码为例
该段代码在父进程当中fork()
出了一个子进程,并设置了一个函数指针的vector
;
在子进程并未结束时父进程循环边进行非阻塞式等待,边将函数加载至vector
当中进行其余操作;
💥 父进程如何获取子进程的退出信息
上文当中讲了许多关于父进程调用wait()/waitpid()
从而获取子进程的退出信息,此时子进程已经死亡,对应的资源也已经释放,那么父进程是如何做到的,wait()/waitpid()
做了什么?
当一个进程终止时,内核会保留其PCB
结构体,并将其标记为僵尸状态,以便父进程可以查询其退出状态;
此时,进程的内存资源,包括堆栈,全局变量等,通常会被释放;
但是其PCB
结构体中仍然会保留进程的一些信息,例如退出状态码,资源使用情况等;
而wait()/waitpid()
则将在子进程的PCB
结构体当中找到对应的退出信息并返回;
-
那么既然可以使用
wait()/waitpid()
来获取子进程的退出信息,那么是否可以使用全局变量使得父进程在不使用wait()/waitpid()
的情况下取得子进程的退出结果?这个答案显然为否,在上文当中提到了进程具有独立性,当父子进程中的其中一个进程试图去 写入/修改 另一个进程的数据时将会发生写时拷贝从而保证进程的独立性;
-
那么既然进程具有独立性,且
PCB
结构体为内核数据结构,父进程是否有权利获取子进程的退出信息,又是如何获取子进程的退出信息的?实际上但以权限而言,单以进程的权限而言,其父进程不具有获取内核数据结构数据的权限;
但实际上在调用
wait()/waitpid()
调用的是系统调用,故父进程在使用该函数的情况下有权力获取子进程的退出信息;
🦖 进程替换
在上文中提到,进程通过fork()
创建出子进程,并使得子进程可以完成对应的工作;
而实际上,子进程不仅可以执行与父进程相同的代码,同时子进程还可以单独调用执行另一个程序;
在<unistd.h>
头文件中存在一个系列的函数为exec
系列函数;
其功能可以将一个进程替换为另一个进程,并执行另一份代码数据,且其对应的PID
也不会发生变化;
此处主要围绕execl()
函数进行讲解;
int execl(const char *path, const char *arg0, ... /* (char *) NULL */);
-
参数
execl()
函数接受一个字符串参数path
,表示要执行的程序的路径,以及一个或多个以NULL
结尾的字符串参数;
#include <unistd.h>#include <iostream>using namespace std;int main() {cout << "hello world1" << endl;cout << "hello world1" << endl;cout << "hello world1" << endl;execl("/bin/ls", "ls", "-a", NULL);cout << "hello world2" << endl;cout << "hello world2" << endl;cout << "hello world2" << endl;return 0;
}
在这段代码当中,将会打印3个hello world1
与3个hello world2
;
其中在打印3个hello world1
的下一句为执行一个新的程序ls
,并传递-a
参数,且以NULL
作为结束;
当运行时其对应的结果为:
$ ./myproc
hello world1
hello world1
hello world1
. .. makefile myproc myproc.cpp
当执行完前三句打印后进程替换为了ls
程序;
-
那么是否其
PID
不会发生变化?将代码进行修改,且在该路径中再添加一个文件夹并分别使用
getpid()
观察其PID
情况;#include <unistd.h>#include <iostream>using namespace std;int main() {cout << "hello world1" << endl;cout << "hello world1" << endl;cout << "hello world1" << endl;printf("当前程序为myproc 且PID为:%d \n", getpid());execl("./test_/mytest", "mytest", NULL);cout << "hello world2" << endl;cout << "hello world2" << endl;cout << "hello world2" << endl;return 0; }
该路径下存在一个名为
test_
的目录且目录中存在一个可执行文件为mytest
;且对应的代码为如下:
#include <iostream> #include<unistd.h>using namespace std;int main() {printf("当前程序为mytest 且PID为:%d\n", getpid());return 0;}
运行后最终的结果为:
$ ./myproc hello world1 hello world1 hello world1 当前程序为myproc 且PID为:14115 当前程序为mytest 且PID为:14115
证明实际上在进行进程替换的时候起PID并不会替换,即在进行进程替换的时候将对应的代码和数据载入内存当中并不会产生一个新的进程;
💥 进程替换的原理
进程替换是指一个进程将自己的内存映像替换为另一个程序的内存映像,并开始执行该程序的过程;
这种机制允许一个进程在不创建新的进程的情况下,动态地加载和执行其他程序,从而实现程序的动态更新资源回收等功能;
进程在进行进程替换时通常需要进行以下几个步骤:
-
加载新程序
即将要执行的新程序的可执行文件加载至对应的内存空间当中,并将其映射至当前进程的地址空间当中;
-
清理资源
在加载新程序之前,原始进程将是放一部分资源从而使得新程序在运行时不会受到原始进程状态的影响;
-
替换内存映像
当新程序的可执行文件被加载至内存当中后,原始进程将会覆盖自己的内存映像并将新程序的代码数据替换为自己的;
-
执行新程序
最后,原始进程将控制权转移到新的程序的入口点并开始执行新程序,此时原始进程将不再执行;
以该图为例;