文章目录
- 🦖 前言
- 🦖 fork()函数调用失败原因
- 🦖 进程终止
- 💥 进程退出码
- 💥 进程正常退出
- 🦖 进程等待
- 💥 僵尸进程
- 💥 如何解决僵尸进程的内存泄漏问题
- 💥 wait( )/waitpid( )函数
- 🌟 进程退出信息
- 💥 非阻塞式等待
- 💥 父进程如何获取子进程的退出信息
- 🦖 进程替换
- 💥 进程替换的原理
🦖 前言

进程控制是一种在操作系统上对进程进行管理和调度的一个过程;
这包括创建进程,终止进程,等待进程,暂停和恢复进程,进程间的通信和调度进程等待;
在之前关于Linux的内容中谈论了大量的关于进程的内容;
本文将重点对于基础进程控制进行一定的讲解;
🦖 fork()函数调用失败原因

在『 Linux 』使用fork函数创建进程与进程状态的查看中提到了使用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) 指一个进程暂时停止执行,知道某个特定事件发生或者某个特定条件得到满足后再继续执行;
💥 僵尸进程

一个进程的创建与资源回收是由其父进程或者是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 COMMAND
9338 9877 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc
9877 9878 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc
------------------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
9338 9877 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc
9877 9878 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc
------------------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
9338 9877 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc
9877 9878 9877 9338 pts/2 9877 Z+ 1002 0:00 [myproc] <defunct>
------------------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
9338 9877 9877 9338 pts/2 9877 S+ 1002 0:00 ./myproc
9877 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 -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) 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) SIGRTMAXcore 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并不会替换,即在进行进程替换的时候将对应的代码和数据载入内存当中并不会产生一个新的进程;
💥 进程替换的原理

进程替换是指一个进程将自己的内存映像替换为另一个程序的内存映像,并开始执行该程序的过程;
这种机制允许一个进程在不创建新的进程的情况下,动态地加载和执行其他程序,从而实现程序的动态更新资源回收等功能;
进程在进行进程替换时通常需要进行以下几个步骤:
- 加载新程序
即将要执行的新程序的可执行文件加载至对应的内存空间当中,并将其映射至当前进程的地址空间当中; - 清理资源
在加载新程序之前,原始进程将是放一部分资源从而使得新程序在运行时不会受到原始进程状态的影响; - 替换内存映像
当新程序的可执行文件被加载至内存当中后,原始进程将会覆盖自己的内存映像并将新程序的代码数据替换为自己的; - 执行新程序
最后,原始进程将控制权转移到新的程序的入口点并开始执行新程序,此时原始进程将不再执行;

以该图为例;
















