👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
- 一、进程创建fork
- 二、进程终止
- 2.1 退出码
- 2.2 进程常见退出方法之正常终止
- 2.2.1 return退出
- 2.2.2 exit函数
- 2.2.3 _exit函数(不建议使用)
- 2.2.4 return && exit && _exit的区别
- 2.2.5 进程异常
- 三、进程等待
- 3.1 进程等待的必要性(为什么要有进程等待)
- 3.2 进程等待的系统调用接口
- 3.2.1 wait()函数
- 3.2.2 waitpid()函数
- 3.3 非阻塞轮询WNOHANG
- 四、进程替换
- 4.1 什么是进程替换
- 4.2 介绍替换函数
- 4.2.1 execl函数
- 4.2.2 execlp函数
- 4.2.3 execv函数
- 4.2.4 execvp函数
- 4.2.5 execle函数
- 4.2.6 execvpe函数
- 4.2.7 execve函数
- 4.3 巧记函数
- 五、相关代码
一、进程创建fork
fork
函数是从已存在进程中创建一个新进程,这个新进程称为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
函数返回值:
fork()
创建成功:将子进程的PID
作为父进程的fork
函数的返回值,将0
作为子进程的fork
函数的返回值。fork()
创建失败:不创建子进程,将-1
返回给父进程。原因是:系统中有太多的进程等- 不同的返回值的目的是:为了让子进程和父进程做不同的事 ~
-
当程序执行到
fork()
函数时,操作系统会以父进程为模板,为子进程创建内核数据结构task_struct
,子进程会以父进程为模板初始化属性(字段);每个进程都需要一个进程地址空间,所以子进程也会复制父进程的进程地址空间;每个进程还需要一个独立的页表结构(用于将虚拟地址映射到物理地 址),也是通过复制父进程的页表,这也就是为什么子进程能和父进程共享代码和数据的原因。 -
由于每个进程具有独立性,当父子进程任意一方要修改数据,那么就会引发写时拷贝机制(只有修改共享的数据时才进行实际的拷贝。这样可以节省内存,并提高性能),并且这是由操作系统自动完成的。其原理是通过分页机制(以子进程的视角为例):操作系统会通过子进程的页表将对应需要修改变量的虚拟地址映射成物理地址,在物理内存中进行写时拷贝,即为这个变量开辟新的空间进行修改,那么对应的物理地址也要更新为新开辟空间的地址,而虚拟地址不发生变化。
二、进程终止
2.1 退出码
程序终止一定会有以下三种情况:
-
程序运行成功,返回正确结果
-
程序运行成功,返回错误结果
-
程序异常终止(除零错误、越界访问等)
不知道大家有没有注意到这样一个问题:为什么要在main
函数的最后写return 0
结尾?既然返回这个数字,又是返回给谁呢?
我们都知道main
函数是程序的入口,但实际上main
函数只是我们用户级别代码的入口,main
函数也是被其他函数调用的!
这里我以
VS2019
为例,教大家如何查看
- 在
main
函数的定义处设置一个断点。(选中main
函数所在行按F9
即可)
- 按
F5
进入调试模式。(有一个黄色的断点在断点里面代表成功)
3. 查看调用堆栈。打开 “调试” -> “窗口” -> “调用堆栈”
- 然后会弹出一个窗口
在VS2019
中,main
函数就是被一个名为mainCRTStartup
的函数所调用,而mainCRTStartup
函数又是被操作系统所调用的。所以main
函数调用结束后就应该告诉操作系统“我已经执行完毕,可以释放资源”。
C/C++
中的main
函数返回一个整数值,我们称之为退出码,它通常用来表示程序的退出状态。
-
返回
0
通常表示程序成功地执行完毕。 -
返回非
0
通常表示程序在执行过程中发生了某种错误。
当一个程序执行完成并终止时,其退出码会被传递给其父进程(父进程要对子进程负责)。我们可以通过以下命令查看一个程序的退出码
- 在
Linux
操作系统中,?
是一个特殊的变量,保存着最近一次程序的退出码。
echo $?
退出码有很多,单纯返回一个数字我们并不知道是什么意思,因此在Linux
系统中,通常可以使用strerror()
函数将错误码转换为对应的错误描述字符串。
我们先可以查看手册来获取strerror
函数的相关信息
man strerror
我们尝试打印1~200
错误码转换为对应的错误描述字符串
0 -> Success
1 -> Operation not permitted
2 -> No such file or directory
3 -> No such process
4 -> Interrupted system call
5 -> Input/output error
6 -> No such device or address
7 -> Argument list too long
8 -> Exec format error
9 -> Bad file descriptor
10 -> No child processes
11 -> Resource temporarily unavailable
12 -> Cannot allocate memory
13 -> Permission denied
14 -> Bad address
15 -> Block device required
16 -> Device or resource busy
17 -> File exists
18 -> Invalid cross-device link
19 -> No such device
20 -> Not a directory
21 -> Is a directory
22 -> Invalid argument
23 -> Too many open files in system
24 -> Too many open files
25 -> Inappropriate ioctl for device
26 -> Text file busy
27 -> File too large
28 -> No space left on device
29 -> Illegal seek
30 -> Read-only file system
31 -> Too many links
32 -> Broken pipe
33 -> Numerical argument out of domain
34 -> Numerical result out of range
35 -> Resource deadlock avoided
36 -> File name too long
37 -> No locks available
38 -> Function not implemented
39 -> Directory not empty
40 -> Too many levels of symbolic links
41 -> Unknown error 41
42 -> No message of desired type
43 -> Identifier removed
44 -> Channel number out of range
45 -> Level 2 not synchronized
46 -> Level 3 halted
47 -> Level 3 reset
48 -> Link number out of range
49 -> Protocol driver not attached
50 -> No CSI structure available
51 -> Level 2 halted
52 -> Invalid exchange
53 -> Invalid request descriptor
54 -> Exchange full
55 -> No anode
56 -> Invalid request code
57 -> Invalid slot
58 -> Unknown error 58
59 -> Bad font file format
60 -> Device not a stream
61 -> No data available
62 -> Timer expired
63 -> Out of streams resources
64 -> Machine is not on the network
65 -> Package not installed
66 -> Object is remote
67 -> Link has been severed
68 -> Advertise error
69 -> Srmount error
70 -> Communication error on send
71 -> Protocol error
72 -> Multihop attempted
73 -> RFS specific error
74 -> Bad message
75 -> Value too large for defined data type
76 -> Name not unique on network
77 -> File descriptor in bad state
78 -> Remote address changed
79 -> Can not access a needed shared library
80 -> Accessing a corrupted shared library
81 -> .lib section in a.out corrupted
82 -> Attempting to link in too many shared libraries
83 -> Cannot exec a shared library directly
84 -> Invalid or incomplete multibyte or wide character
85 -> Interrupted system call should be restarted
86 -> Streams pipe error
87 -> Too many users
88 -> Socket operation on non-socket
89 -> Destination address required
90 -> Message too long
91 -> Protocol wrong type for socket
92 -> Protocol not available
93 -> Protocol not supported
94 -> Socket type not supported
95 -> Operation not supported
96 -> Protocol family not supported
97 -> Address family not supported by protocol
98 -> Address already in use
99 -> Cannot assign requested address
100 -> Network is down
101 -> Network is unreachable
102 -> Network dropped connection on reset
103 -> Software caused connection abort
104 -> Connection reset by peer
105 -> No buffer space available
106 -> Transport endpoint is already connected
107 -> Transport endpoint is not connected
108 -> Cannot send after transport endpoint shutdown
109 -> Too many references: cannot splice
110 -> Connection timed out
111 -> Connection refused
112 -> Host is down
113 -> No route to host
114 -> Operation already in progress
115 -> Operation now in progress
116 -> Stale file handle
117 -> Structure needs cleaning
118 -> Not a XENIX named type file
119 -> No XENIX semaphores available
120 -> Is a named type file
121 -> Remote I/O error
122 -> Disk quota exceeded
123 -> No medium found
124 -> Wrong medium type
125 -> Operation canceled
126 -> Required key not available
127 -> Key has expired
128 -> Key has been revoked
129 -> Key was rejected by service
130 -> Owner died
131 -> State not recoverable
132 -> Operation not possible due to RF-kill
133 -> Memory page has hardware error
# 后面没有了 ~
实际上Linux
中的ls
、pwd
等命令都是可执行程序,当执行这些命令时,就是一个进程。因此使用这些命令后我们也可以查看其对应的退出码。
如果正常运行的话,其错误码就是0
2.2 进程常见退出方法之正常终止
2.2.1 return退出
在main
函数中使用return
退出进程是我们常用的方法。这里就不再过多赘述了 ~
2.2.2 exit函数
#include <unistd.h>
void exit(int status);
# status - 退出码
使用exit
函数退出进程也是我们常用的方法,可以在代码中的任何地方退出进程。
2.2.3 _exit函数(不建议使用)
_exit
是一个系统调用接口,它和exit
函数的用法一模一样,可以在代码中的任何地方退出进程。
【文档介绍】
2.2.4 return && exit && _exit的区别
-
return
在除main
函数以外使用代表当前函数结束,只有在main
函数中代表进程退出。 -
exit()
是一个库函数,位于<stdlib.h>
头文件中,在任意地方使用都代表进程退出。它和_exit
的区别是:exit()
函数会执行一系列的清理工作(如刷新缓冲区、关闭流等) -
_exit()
是一个系统调用接口,它位于<unistd.h>
头文件中,在任意地方使用都代表进程退出。它和exit()
的区别是:_exit()
函数不会执行任何的清理操作,它直接终止程序。因此,使用_exit()
可能会导致资源泄漏或未完成的操作,所以不建议使用。
或者可以这样理解_exit
和exit
:exit()
是对 _exit()
做的封装实现,_exit()
就只是单纯的退出程序,而 exit()
在退出之前还会做一些事,比如冲刷缓冲区,再调用 _exit()
2.2.5 进程异常
子进程异常终止通常是由接收到某种信号引起的。常见的信号包括SIGKILL
(强制终止)、 SIGSEGV
(段错误)等。当子进程收到这些信号时,它可能会以异常终止的方式结束。或者可以进程强制终止ctrl + c
在进程等待部分会做演示 ~
三、进程等待
3.1 进程等待的必要性(为什么要有进程等待)
在Linux
中,一个进程终止了不会立马进入死亡状态。而是会先进入僵尸状态。但如果父进程没有及时对子进程进行回收,这个进程就会变成僵尸进程,进而造成内存泄漏。(僵尸状态是指进程已经终止执行,但其相关的进程控制块PCB
和资源仍然保留在系统中,直到其父进程获取子进程终止状态,子进程才能释放资源,变为死亡状态)
注意:进程一旦变成僵尸状态,那就刀枪不入,就连“杀人不眨眼”的kill -9 PID
也无能为力,因为谁也没有办法杀死一个已经退出的进程。
因此,需要通过进程等待
-
解决僵尸进程!(必须解决)
-
获取父进程布置给子进程的任务完成的怎么样了! (可选)
3.2 进程等待的系统调用接口
我们可以通过 系统调用
wait()
或者waitpid()
来进行对子进程进行状态检测与回收的功能
3.2.1 wait()函数
我们可以通过man
手册来查询wait()
函数的相关信息
man 2 wait
# 2号手册是专门用来查系统调用接口的~
-
当子进程退出时,其退出状态、终止原因等信息将会通过系统调用接口
wait()
由操作系统来写入status
指向的变量中。在wait()
函数中我们主要演示如何回收僵尸进程,因此这里暂时不关心子进程状态,直接设置为NULL
即可。(waitpid()
详细介绍status
) -
wait
函数的返回值是要回收子进程的PID
。如果当前进程没有子进程,则wait()
会立即返回-1
。
下面我来演示让父进程调用wait
函数来回收僵尸进程。
以下代码一共sleep
了15
秒,其中前五秒父子进程一直在打印自己的消息,在后五秒中,父进程还在继续打印,子进程提前退出,此时为僵尸进程。为了更好观察结果,最后五秒父进程结束打印,此时处于阻塞状态,进行回收僵尸进程
执行进行命令进行动态监控进程状态
while :; do ps axj | head -1 && ps axj | grep proc | grep -v grep;echo "-----------------------------------------------------------";sleep 1;done
而常识告诉我们,进程不可能只有一个。因此,当有多个子进程时,wait
函数会等待任意一个子进程结束,并返回子进程的PID
。(例如以下代码样例)
注意:wait()
并不保证按照子进程创建的顺序进行回收,而是依赖于内核调度的具体实现。
如果任意一个子进程都不退出,调用 wait()
的父进程会一直是阻塞状态,我们称这现象为阻塞等待。换句话说,wait
函数会阻塞父进程,直到所有子进程结束为止。如果父进程没有子进程,或者所有子进程都已经结束,wait
函数会立即返回。
【总结】
wait()
可以回收僵尸进程如果任意一个子进程都不退出,调用
wait()
的父进程会一直是阻塞状态,我们称这现象为阻塞等待
3.2.2 waitpid()函数
-
waitpid()
函数中的第一个参数pid_t pid
可以指定回收的子进程;如果设置为-1
可以等待任一个子进程,与wait()
等效。 -
waitpid
函数中的第二个参数int* status
是输出型参数,可以获取子进程的退出状态。-
如果传递
NULL
,表示父进程不关心子进程的退出状态信息。 -
如果传递非
NULL
,则通过系统调用waitpid/wait
让操作系统从子进程PCB
对象获取退出信息(如退出码、退出信号)反馈给父进程(这些都由操作系统完成)。 -
由于一个进程退出的场景有三种(程序运行成功,结果错误;程序运行成功,结果正确;程序异常终止),那么父进程就需要关心:子进程为什么异常终止?没有异常,结果对吗?不对是因为什么?即退出码是什么?因此
status
以二进制划分为以下2
个部分
-
获取异常信号:
status & 0x7F
。其中7F
表示0111 1111
;或者可以使用系统自定义的宏WTERMSIG(status)
-
获取退出状态(退出码):
(status >> 8) & 0xFF
。其中FF
表示1111 1111
;或者可以使用系统自定义的宏WEXITSTATUS(status)
-
补充:
WIFEXITED(status)
:用来判断进程是正常退出还是异常退出,异常退出返回0
,没有异常返回一个非0
的值。
-
-
waitpid
函数中的第三个参数int options
是设置等待的方式。-
设置为
0
,表示waitpid
会阻塞父进程,直到指定的子进程退出才返回。 -
设置为
WNOHANG
,表示非阻塞等待。 点击跳转
-
-
返回值:若成功,返回结束子进程的
PID
(>0
);若出错(无效的子进程PID
、没有子进程等),返回-1
(<0
);还有一个等于0
的情况,在非阻塞轮询会提到。
在waitpid()
中重点演示:父进程获取子进程状态的演示
单纯一个退出码看着有点难受,我们可以将其转化对应的错误信息
那如何验证子进程是否出现进程异常呢? 举一个例子,假设子进程不小心写了一个非法访问
虽然我的源代码的退出状态设置为3
,但由于进程异常提前终止,并没有执行到exit(3)
,所以退出码默认为0
。接下来我们可以执行kill -l
来看看是什么型号导致的
或者可以使用kill
对子进程发送型号
3.3 非阻塞轮询WNOHANG
当使用 waitpid
函数时,如果子进程不退出,父进程就会一直处于阻塞状态,什么都干不了,直到其子进程退出。但是,有时候我们希望父进程在等待子进程退出时不阻塞,而是可以在等待子进程退出的同时执行其他任务,这就是非阻塞轮询的概念。
因此,可以通过waitpid
函数的第三个参数设置为 WNOHANG
(Linux
系统提供的宏) + 循环,来告诉内核如果子进程没有立即退出,就不要“傻傻”的等待了,而是一边等待子进程退出,一边执行其他任务。
四、进程替换
4.1 什么是进程替换
进程替换是指一个正在运行的进程被另一个进程所取代的过程。常见的进程替换方式是使用 exec
系列函数。
接下来举一个例子来带大家看看
【运行结果】
我们发现:子进程在运行的过程中确实被替换成了ls -al
的指令,但是为什么子进程中的最后一条打印语句没有执行?
这就和进程替换原理有关:
-
当替换函数成功调用后,替换函数会将新程序(代码和数据)加载到子进程的进程地址空间中,覆盖掉子进程原来的所有内容。由于一开始子进程共享父进程的代码和数据,必定会触发写时拷贝,确保父子进程之间的内存隔离。一旦新程序加载完成并且子进程地址空间和页表被更新,子进程会从新程序的入口点开始执行。
-
注意:虽然在调用替换函数之前会创建子进程,但是在整个替换过程中,并不会创建新的进程。
4.2 介绍替换函数
4.2.1 execl函数
函数原型如下:
#include <unistd.h>
int execl(const char* path, const char* arg, ...);
-
参数
path
是新程序的完整路径。 -
参数
arg0
到argn
是新程序的命令行参数。由于...
表示可变参数列表,因此可以传递多个命令行参数。最后一个参数必须是NULL
,表示参数列表的结束。 -
如果
execl
函数调用成功,它将不会返回,因为当前进程已被替换,而如果调用失败,它将返回-1
既然替换函数可以将原有的进程替换为系统命令,当然也可以替换为我们自己写的可执行程序,因为所有程序运行起来本质上都是一个进程。
- 比如替换
C++
程序
4.2.2 execlp函数
execlp
函数会在系统的环境变量PATH
中查找可执行文件,而不需要指定完整的路径。
函数原型如下:
#include <unistd.h>
int execlp(const char* file, const char* arg0, ...);
-
参数
file
是要执行的程序的文件名,而不是完整的路径名。前提是这个文件名的路径需要再环境变量PATH
中。 -
参数
arg0
到argn
是新程序的命令行参数。由于...
表示可变参数列表,因此可以传递多个命令行参数。最后一个参数必须是NULL
,表示参数列表的结束。 -
如果
execlp
函数调用成功,它将不会返回,因为当前进程已被替换,而如果调用失败,它将返回-1
4.2.3 execv函数
execv()
与execl()
和execlp()
函数的区别在于:它接受一个指向参数的字符串指针数组,而不用明确列出每个参数。
函数原型如下:
#include <unistd.h>
int execv(const char *path, char *const argv[]);
-
参数
path
是要执行的新程序的完整路径。 -
参数
argv[]
表示一个字符串数组,表示新进程的命令行参数。数组的第一个元素通常是执行的程序的名称,后续元素是命令行参数,最后一个元素必须是NULL
。 -
与
execl
和execlp
函数一样,如果execv
函数调用成功,它将不会返回,而如果调用失败,它将返回-1
4.2.4 execvp函数
execvp
函数与execv
函数类似,它可以在系统的环境变量PATH
中查找可执行文件,而不需要指定完整的路径。
函数原型如下:
#include <unistd.h>
int execvp(const char* file, char* const argv[]);
-
参数
file
是要执行的程序的文件名,而不是完整的路径名。前提是这个文件名的路径需要再环境变量PATH
中。 -
参数
argv[]
是一个字符串数组,表示新进程的命令行参数。数组的第一个元素通常是执行的程序的名称,后续元素是命令行参数,最后一个元素必须是NULL
。 -
如果
execvp
函数调用成功,它将不会返回,而如果调用失败,它将返回-1
4.2.5 execle函数
e
表示env
环境变量表,execle
函数允许你自定义新程序的环境变量,而无需继承父进程(bash
)的环境变量。
函数原型如下:
#include <unistd.h>
int execl(const char* path, const char* arg, ..., char* const envp[]);
-
参数
path
是要执行的新程序的完整路径。 -
参数
arg0
到argn
是新程序的命令行参数。注意最后一个元素必须是NULL
。 -
最后一个参数
envp[]
是一个指向新程序的环境变量的指针数组,其中每个元素都是以key=value
的形式表示一个环境变量,最后一个元素必须是NULL
指针,用于表示环境变量列表的结束。-
如果提供了环境变量数组
envp[]
,那么新程序将会使用这个环境变量数组,并且会覆盖掉原程序的环境变量。如果不提供环境变量数组,新程序将会继承原程序的环境变量。 -
补充:如果你想要子进程在父进程的环境变量的基础上增加环境变量,那么你可以使用
putenv
函数(自己查文档),注意:putenv
函数是针对当前进程环境变量的修改操作,不会直接影响父进程的环境变量。
-
-
如果
execle
函数调用成功,它将不会返回,而如果调用失败,它将返回-1
替换程序代码样例
进程替换部分
4.2.6 execvpe函数
execvpe
函数和execle
函数类似,只是将第二个参数封装成了字符串指针数组
函数原型如下:
#include <unistd.h>
int execvpe(const char *file, char *const argv[], char *const envp[]);
-
参数
file
是要执行的程序的文件名,而不是完整的路径名。前提是这个文件名的路径需要在环境变量PATH
中。 -
参数
argv[]
表示一个字符串数组,表示新进程的命令行参数。数组的第一个元素通常是执行的程序的名称,后续元素是命令行参数,最后一个元素必须是NULL
。 -
最后一个参数
envp[]
是一个指向新程序的环境变量的指针数组,其中每个元素都是以key=value
的形式表示一个环境变量,最后一个元素必须是NULL
指针,用于表示环境变量列表的结束。-
如果提供了环境变量数组
envp[]
,那么新程序将会使用这个环境变量数组,并且会覆盖掉原程序的环境变量。如果不提供环境变量数组,新程序将会继承原程序的环境变量。 -
补充:如果你想要子进程在父进程的环境变量的基础上增加环境变量,那么你可以使用
putenv
函数(自己查文档),注意:putenv
函数是针对当前进程环境变量的修改操作,不会直接影响父进程的环境变量。
-
-
如果
execvpe
函数调用成功,它将不会返回,而如果调用失败,它将返回-1
4.2.7 execve函数
事实上,只有execve
函数才是真正的系统调用,因此以上所介绍的函数其底层都调用了系统调用接口 execve()
来完成进程替换的功能!
函数原型如下:
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
-
参数
filename
是新程序的完整路径。 -
参数
argv[]
表示一个字符串数组,表示新进程的命令行参数。数组的第一个元素通常是执行的程序的名称,后续元素是命令行参数,最后一个元素必须是NULL
。 -
最后一个参数
envp[]
是一个指向新程序的环境变量的指针数组,其中每个元素都是以key=value
的形式表示一个环境变量,最后一个元素必须是NULL
指针,用于表示环境变量列表的结束。-
如果提供了环境变量数组
envp[]
,那么新程序将会使用这个环境变量数组,并且会覆盖掉原程序的环境变量。如果不提供环境变量数组,新程序将会继承原程序的环境变量。 -
补充:如果你想要子进程在父进程的环境变量的基础上增加环境变量,那么你可以使用
putenv
函数(自己查文档),注意:putenv
函数是针对当前进程环境变量的修改操作,不会直接影响父进程的环境变量。
-
-
如果
execvpe
函数调用成功,它将不会返回,而如果调用失败,它将返回-1
4.3 巧记函数
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
-
l(list)
: 表示参数采用列表 (一一列举) -
v(vector)
: 表示参数用数组 -
p(path)
: 有p
表示自动搜索环境变量PATH
里的路径 -
e(env)
: 表示可以自定义维护环境变量
五、相关代码
本篇博客相关代码:点击跳转