目录
- 进程的概念
- 进程的特性
- 子进程,父进程
- 进程状态
- 进程优先级
- 进程切换
- 进程的虚拟地址空间
- 进程退出
- 进程等待
-
- 进程等待的现象
- 进程等待的接口
-
- [wait 和 waitpid 中的 status 参数](#wait 和 waitpid 中的 status 参数)
- 阻塞等待和非阻塞等待
- 进程的程序替换
进程的概念
进程包括了可执行程序的代码和数据,进程控制块 (PCB)
当一个可执行程序被执行后,内存中就会出现一个进程,如果多个可执行程序被执行,内存中就会出现多个进程,对于这些进程,OS 要进行管理,管理时遵循先描述后组织的方案,先使用结构体task_struct对进程的属性进行描述和保存,再用链表将task_struct组织起来,接下来 OS 对进程的管理就变成了对链表的增删查改了
在 Linux 中,PCB 实际上就是 task_struct 结构体,它用来描述进程的属性

进程的特性
- 竞争性:系统中可能存在很多进程,但是
CPU只有一个,进程会对CPU进行争抢,得到在上面运行的资格 - 独立性:进程之前互相不会干扰
- 并发性:不同的进程之间按照顺序执行。
CPU在一个进程完成任务时,可以通过进程切换的方式换另一个进程执行 - 并行性:不同的进程可以同时在
CPU上执行 - 进程是动态的,因为它是在运行的,程序是静态的,因为它是没有在运行的
- 一个被执行的程序可能包含多个进程
子进程,父进程
子进程是由父进程创建出来的进程,父进程如果要创建子进程,就要使用 fork 系统调用
cpp
pid_t fork(void);
fork 的功能是给当前的父进程创建一个子进程,如果成功创建,给子进程返回 0,给父进程返回子进程的 id,也就是 pid。如果创建失败,给父进程返回 -1
子进程创建成功后,子进程会和父进程使用内存中的同一份代码和数据,子进程会执行 fork 函数之后的代码。在子进程或父进程要修改数据时,OS 会将内存中的数据拷贝一份,放在新的位置,让子进程或父进程进行修改,也就是写时拷贝
验证
cpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
using namespace std;
int main()
{
int val = 1;
pid_t pid = fork();
// 子进程
if (pid == 0)
{
val = 100;
printf("我是子进程, pid = %d, 父进程的pid = %d, val = %d\n", getpid(), getppid(), val);
exit(1);
}
else if (pid == -1)
{
perror("fork");
exit(2);
}
// 父进程
sleep(10);
printf("我是父进程, pid = %d, 子进程的pid = %d, val = %d\n", getpid(), pid, val);
return 0;
}
结果
cpp
我是子进程, pid = 3478489, 父进程的pid = 3478488, val = 100
我是父进程, pid = 3478488, 子进程的pid = 3478489, val = 1
父进程在子进程修改完 val 之后,才输出 val,但是 val 仍然是 1,说明了写时拷贝的现象
对于一个父进程而言,它的父进程是 bash,也就是命令行解释器
验证
cpp
int main()
{
int val = 1;
pid_t pid = fork();
// 子进程
if (pid == 0)
{
val = 100;
printf("我是子进程, pid = %d, 父进程的pid = %d\n", getpid(), getppid());
exit(1);
}
else if (pid == -1)
{
perror("fork");
exit(2);
}
// 父进程
sleep(10);
printf("我是父进程, pid = %d, 我的父进程的pid = %d, 子进程的pid = %d\n", getpid(), getppid(), pid);
return 0;
}
结果
cpp
我是子进程, pid = 3482892, 父进程的pid = 3482891
我是父进程, pid = 3482891, 我的父进程的pid = 3459766, 子进程的pid = 3482892
查看 pid 为 3459766 的进程:

进程状态
状态一览
Linux 中,进程的状态由下方的这个数组来决定:
cpp
static const char *const task_state_array[] = {
"R (running)",
"S (sleeping)",
"D (disk sleep)",
"T (stopped)",
"t (tracing stop)",
"X (dead)",
"Z (zombie)",
};
R:运行状态,进程在CPU上运行时所处的状态S:浅睡眠状态,是可中断休眠,进程在等待某种资源时就会处于这个状态D:深度睡眠状态,是不可中断休眠,进程在进行某些IO操作,比如向磁盘写数据的时候会处于这个状态T:暂停状态,由用户使用ctrl + z或SIGSTOP信号强制暂停的进程都会处于这个状态t:调试的时候,进程被断点暂停时会处于这个状态X:结束状态Z:僵尸状态,当子进程运行结束时,子进程不会立马退出,而是会等待父进程获取自己的退出信息,此时子进程就会处于僵尸状态。处于僵尸状态时,进程不会被kill命令杀死,只要父进程没有回收退出信息,子进程就会一直待在内存中,耗费内存资源
防止僵尸状态的措施
验证僵尸状态
cpp
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;
int main()
{
int pid = fork();
// 子进程
if (pid == 0)
{
cout << "我是子进程,pid = " << getpid();
exit(1);
}
// 父进程
cout << "我是父进程,pid = %d" << getpid();
while (true)
;
}
cpp
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
4165291 4165292 4165291 4152775 pts/4 4165291 Z+ 1000 0:00 [test] <defunct>
可以看到,子进程的状态是 Z+,Z 表示是僵尸状态,+ 表示是前台进程
防止这个现象的出现,可以使用 wait 或者 waitpid 接口
wait
cpp
pid_t wait(int *status);
作用
让父进程等待若干个子进程退出。父进程调用 wait 时,如果子进程还没退出,就会阻塞在调用处
参数
status:输出型参数,用来获取进程的退出状态,不需要时可以设置为 NULL
返回值
子进程如果退出了,会返回子进程的 pid,如果没有子进程或者子进程无效会返回 -1
使用例
cpp
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int pid = fork();
// 子进程
if (pid == 0)
{
cout << "我是子进程,pid = " << getpid() << endl;
exit(1);
}
// 父进程
cout << "我是父进程,pid = " << getpid() << endl;
sleep(10);
int ret = wait(NULL);
if (ret < 0)
{
perror("wait");
exit(2);
}
cout << "子进程退出成功" << endl;
}
运行结果
cpp
我是父进程,pid = 4170564
我是子进程,pid = 4170565
子进程退出成功
子进程退出,父进程还未回收时
cpp
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
4152775 4170564 4170564 4152775 pts/4 4170564 S+ 1000 0:00 ./test
4170564 4170565 4170564 4152775 pts/4 4170564 Z+ 1000 0:00 [test] <defunct>
父进程回收后
cpp
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
4152775 4170564 4170564 4152775 pts/4 4170564 S+ 1000 0:00 ./test
waitpid
cpp
pid_t waitpid(pid_t pid, int *status, int options);
作用
让父进程等待指定的子进程或全部子进程退出
参数
pid :子进程的 pid,取 -1 表示等待所有子进程退出,取大于 0 的值表示等待指定的子进程退出
status:输出型参数,用来获取子进程的退出状态,不需要可以设置为 NULL
options:默认为 0,表示子进程未退出时,父进程会在调用处阻塞并等待。取 WNOHANG 时,不管子进程是否退出,父进程都不会阻塞在调用处
返回值
子进程正常退出,调用会返回 pid。如果 options 为 WNOHANG 并且调用时没有已退出的子进程,返回 0。不存在子进程或子进程无效会返回 -1
使用例
cpp
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/wait.h>
using namespace std;
int main()
{
int pid = fork();
// 子进程
if (pid == 0)
{
cout << "我是子进程,pid = " << getpid() << endl;
exit(1);
}
// 父进程
cout << "我是父进程,pid = " << getpid() << endl;
sleep(10);
int ret = waitpid(pid, NULL, 0);
if (ret < 0)
{
perror("wait");
exit(2);
}
cout << "子进程退出成功" << endl;
}
运行结果
cpp
我是父进程,pid = 1410
我是子进程,pid = 1411
子进程退出成功
子进程运行结束,父进程未回收
cpp
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
4152775 1410 1410 4152775 pts/4 1410 S+ 1000 0:00 ./test
1410 1411 1410 4152775 pts/4 1410 Z+ 1000 0:00 [test] <defunct>
父进程回收子进程
cpp
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
4152775 1410 1410 4152775 pts/4 1410 S+ 1000 0:00 ./test
孤儿进程
孤儿进程的概念
当子进程运行未结束,父进程先结束时,子进程就失去了自己的父进程,此时的子进程会被 1 号进程领养,它就变成了孤儿进程,此时的子进程是后台进程,把子进程领养的 1 号进程可以看成 OS 或 OS 的一部分
验证孤儿进程
cpp
int main()
{
int pid = fork();
// 子进程
if (pid == 0)
{
int cnt = 0;
while (cnt < 10)
{
printf("我是子进程, pid = %d, 我的父进程pid = %d\n", getpid(), getppid());
sleep(1);
cnt++;
}
exit(1);
}
// 父进程
printf("我是父进程, pid = %d, 我的父进程pid = %d\n", getpid(), getppid());
printf("父进程退出\n");
return 0;
}
运行结果
cpp
我是子进程, pid = 7167, 我的父进程pid = 7166
我是父进程, pid = 7166, 我的父进程pid = 4152775
父进程退出
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
我是子进程, pid = 7167, 我的父进程pid = 1
1 号进程
cpp
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 0:33 /usr/lib/systemd/systemd --system --deserialize=48 showopts
孤儿进程存在的意义
一个子进程在运行结束的时候会进入僵尸状态,如果父进程早就退出了,子进程又不被其他进程领养,那子进程的退出信息永远都不会被回收,它永远都不会结束,造成资源泄露,所以需要 1 号进程进行领养,防止出现这种情况
对于一个父进程,它有自己的父进程 bash,bash 一般是不会退出的,所以父进程的退出信息一定会被回收,父进程不会进入僵尸状态或变成孤儿进程
进程优先级
系统的资源是有限的,所以需要让进程按照顺序来获取资源,优先级就是用来决定该顺序
的。在 Linux 中,优先级被保存在 task_struct 中,它的值越大,表明优先级越低,值越小,优先级越高
cpp
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 609610 606473 0 80 0 - 1854 hrtime pts/0 00:00:00 test
进程的属性中,PRI 表示进程的优先级,默认是 80,取值范围是 [60, 99]。NI 是一个用来对优先级进行修正的属性,取值范围是 [-20, 19],进程的优先级等于 80 + NI,80 就是 PRI 的默认值
如果要调整进程的优先级,可以使用 top, nice, renice 指令
这些指令在修改优先级的时候,如果是普通用户,能输入 [0, 19] 之间的数,大于 19 就会直接设置为 19,小于 0 就会拒绝请求。如果是管理员,能输入 [-20, 19] 之间的数,大于 19 就会设置为 19,小于 -20 就会设置为 -20。设置这些限制,是因为如果某一个用户一直调整同一个进程的优先级,将它的优先级调整得非常高,就会导致其它进程一直无法获得资源,进程进入饥饿状态
top
top 指令可以查看当前所有进程的属性,在 top 的界面下,输入 r,再输入进程的 pid,就可以输入一个值来修改指定进程的优先级
cpp
#include <unistd.h>
int main()
{
printf("我的pid = %d\n", getpid());
sleep(10);
printf("进程退出\n");
return 0;
}
运行结果
cpp
我的pid = 617582
进程退出
未修改时进程的优先级
cpp
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 618393 606473 0 80 0 - 1854 hrtime pts/0 00:00:00 test
用 top 修改后进程的优先级
cpp
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 618393 606473 0 99 19 - 1854 hrtime pts/0 00:00:00 test
nice
nice 可以修改程序刚启动起来时的优先级,用法为:
cpp
nice -n [优先级的值] [执行程序]
cpp
#include <unistd.h>
int main()
{
printf("我的pid = %d\n", getpid());
sleep(10);
printf("进程退出\n");
return 0;
}
运行结果
cpp
我的pid = 617582
进程退出
指令及启动时的优先级
cpp
nice -n 19 ./test
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 627122 606473 0 99 19 - 1854 hrtime pts/0 00:00:00 test
renice
renice 可以修改已在运行中的进程的优先级,用法为:
cpp
renice [优先级的值] [pid]
cpp
#include <unistd.h>
int main()
{
printf("我的pid = %d\n", getpid());
sleep(10);
printf("进程退出\n");
return 0;
}
运行结果
cpp
我的pid = 628918
进程退出
未修改时的优先级
cpp
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 628918 606473 0 80 0 - 1854 hrtime pts/0 00:00:00 test
指令及修改后的优先级
cpp
renice 19 628918
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 628918 606473 0 99 19 - 1854 hrtime pts/0 00:00:00 test
进程切换
进程 A 在运行的时候,会产生上下文数据,这些上下文数据会被保存在 CPU 的寄存器中,当要切换至进程 B 运行时,A 产生的上下文数据会被保存回它的 task_struct 中的 TSS 任务状态段内,保证下次运行可以恢复数据,接下来恢复进程 B 的上下文数据到 CPU 的寄存器内,进程 B 就可以接着运行了

在 Linux 中,有一个结构体叫做 runqueue,它是一个和进程切换关系非常大的结构体

说明 runqueue 之前,先说明一下 rqueue_elem 结构体,在 rqueue_elem 结构体中:
- nr_active:表示当前活动进程的数量
- bitmap :位图,有 5 个 int 型数据,就有了 160 个比特位,可以表示 160 个队列的状态,
1表示 queue 中的某一个队列有进程,0表示没有 - queue :数组,保存了 140 个队列,每个队列都是用来存放相同优先级进程的
在 runqueue 中,有一个可以保存两个 rqueue_elem 类型元素的数组 prio_array,指向 prio_array[0] 的指针 active 和指向 prio_array[1] 的指针 expired,在这其中,prio_array[0] 用来保存就绪进程,prio_array[1] 用来保存时间片用完的进程
创建新进程的时候,如果这个进程是一个实时进程,它的优先级是 [0, 99],会根据优先级被直接插入到 queue 中 [0, 99] 下标所对应的队列中。如果这个进程是普通进程,os 会根据它的优先级来计算要插入的位置,具体方法是 100 + nice + 20,nice 的范围是 [-20, 19],因此它会被插入到 [100, 139] 下标对应的队列中
在 CPU 进行调度的时候,会先根据 active 指针找到 prio_array[0],查看 nr_active 是否大于 0,大于 0 说明有就绪的进程,此时会根据 bitmap 中的二进制位查看 queue 中哪个队列不是空的,找到这个非空队列,拿出队头的第一个进程,将它调度到 CPU 上执行。在进程执行完毕后,根据 expired 指针,将进程放到 prio_array[1] 的 queue 中,然后继续刚才的步骤进行调度,当最后没有就绪进程时,交换 active 和 expired,重新进行调度
根据这些,如果一个用户直接去修改进程的优先级 PRI,那么进程就会在队列中重新寻找插入位置,在时间上会有消耗,因此 Linux 设计了 NI 让用户进行修改,当用户修改了 NI 时,进程在队列中的位置不会马上发生改变,而是在进程运行完毕后,再计算新的插入位置进行插入
进程的虚拟地址空间
虚拟地址空间的概念
os 为每个进程都分配了虚拟地址空间,虚拟地址就是其中的一个地址,每个进程都拥有自己的虚拟地址空间。一个地址对应 1B,在 32 位机器上,一个进程的虚拟地址空间一共有 232 个地址,也就是 232B,等于 4GB。在 64 位机器上,一个进程的虚拟地址空间一共有 264 个地址,也就是 264B,等于 234GB
物理地址是内存中的地址
平常在写程序的时候接触到的地址实际上都是虚拟地址,而不是真正的物理地址,一个程序员是看不见物理地址的
虚拟地址空间的样子大致为:

页表
每个进程都会有自己的页表,页表中保存了虚拟地址和物理地址的映射关系,如果某个进程想要访问内存中的代码和数据,就可以用虚拟地址在页表中查找对应的物理地址,转到内存中去访问代码和数据

在一个程序被双击,但是还没有完全启动时(被加载到内存之前),此时 os 会根据程序的代码段,数据段的大小在虚拟地址空间中开辟相同大小的代码段和数据段,再把程序的代码和数据拷贝到物理内存中,最后在页表中建立虚拟地址和物理地址的映射关系

虚拟地址空间的原理
在进程的 PCB,也就是 task_struct 中,有一个 struct mm_struct* 类型的指针 mm,它指向了 mm_struct 类型的结构体,在这个结构体中又有一个变量 mmap,它是 struct vm_area_struct* 类型的指针,指向了一个链表或红黑树,在这个链表或红黑树中,保存了 vm_area_struct 类型的结构体,它的内部有两个长整形变量 vm_start 和 vm_end,这两个变量划分出了虚拟地址空间中的栈,堆,共享区等区域,如果要修改区域的大小,就是修改这两个变量
根据这些,可以说虚拟地址空间本质就是一个结构体 mm_struct,而栈,堆,共享区本质就是 vm_area_struct 结构体中的两个变量

写时拷贝
父进程在未创建出子进程的时候,页表中记录的代码段的权限为只读,数据段的权限为可读可写,在父进程刚创建出子进程的时候,会将页表中数据段的权限修改为只读,然后 os 创建子进程的虚拟地址空间和页表,建立虚拟地址到物理地址的映射关系,此时子进程所用的代码和数据与父进程是一样的

当子进程要修改数据的时候,会通过虚拟地址查找页表,找到页表项后发现数据的权限是只读,于是就会引发错误,os 通过错误知道子进程要修改数据,就会触发写时拷贝机制,把要修改的数据在内存中拷贝一份,再修改页表,让子进程去修改拷贝出来的数据

虚拟地址空间的意义
- 可执行程序的代码和数据在内存中可能不是连续存放的,但是在引入了虚拟地址空间后,可执行程序的代码和数据在虚拟地址空间中是连续存放的,这样就可以将原本在物理内存中无序的代码和数据转化为有序的,更方便找到代码和数据
- 由于页表的存在,当进程要做一些非法操作时,就会被
os拦截,这能够保护物理内存。比如说,动态申请的空间在被释放的时候,会让物理内存中开辟的空间,页表项以及虚拟地址空间内堆的那块空间都被释放,之后进程还想通过虚拟地址访问,就无法在页表中查找到映射关系,会被os拦截 - 可以让进程管理和内存管理进行解耦。比如进程在用虚拟地址查找页表时,如果页表项中没有保存物理地址,说明对应的数据或代码还没有加载到内存中,此时
os会引发缺页中断,停止进程的执行,将数据或代码加载到内存中,在页表中记录物理地址,然后进程就可以继续访问数据或代码,缺页中断的过程进程不用关心,它只需要拿到最后的物理地址
进程退出
进程退出的概念
进程退出指进程停止运行,一般来说,在三种情况下进程会退出:
- 进程运行完毕,结果无错误
- 进程运行完毕,结果有错误
- 进程遇到异常
进程在退出的时候,会设置自己的退出码,退出码一般和 main 函数的返回值是一致的,如果程序员没有给定,默认返回 0 ,表示运行完毕无错误,返回非 0 表示运行完毕且遇到了错误。使用指令 echo $? 可以查看当前进程的退出码,使用 strerror 函数则可以查看所有的退出码以及它们的含义
验证
cpp
#include <stdio.h>
#include <string.h>
int main()
{
for (int i = 0; i <= 200; ++i)
{
printf("%d : %s\n", i, strerror(i));
}
return 0;
}
结果
cpp
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
//....
当前进程的退出码
cpp
0
如果进程在运行的过程中遇到了异常,比如除 0,野指针问题,那这个进程会被信号杀死,使用 kill -l 可以查看所有的信号
验证
cpp
int main()
{
int x = 1 / 0;
return 0;
}
结果
cpp
Floating point exception (core dumped) //8号信号
所有的信号
cpp
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) SIGRTMAX
设置退出码
设置进程的退出码主要有两种方式,第一种是调用 exit 或 _exit 接口,第二种是在 main 函数中使用 return 语句
exit / _exit
cpp
void exit(int status);
作用
终止调用自己的进程,将进程的退出码设置为 status
参数
status:用户给定的退出码
使用例
cpp
#include <stdlib.h>
int main()
{
exit(10);
return 0;
}
运行并查看退出码
cpp
10
cpp
void _exit(int status);
作用
终止调用自己的进程,将进程的退出码设置为 status
参数
status:用户给定的退出码
使用例
cpp
#include <unistd.h>
int main()
{
_exit(10);
return 0;
}
运行并查看退出码
cpp
20
exit 和 _exit 的区别
exit 是由 C 语言提供的函数,内部封装了 _exit,在执行后会设置进程的退出码并刷新用户级缓冲区,将用户级缓冲区中的数据进行输出,最后终止进程。_exit 是系统调用,在执行后只会设置进程的退出码,将进程终止,不会刷新用户级缓冲区
验证
cpp
int main()
{
printf("hello world");
_exit(10);
}
int main()
{
printf("hello world");
exit(20);
}
分别将两份代码运行,会发现上面的代码没有输出 hello world,下面的代码输出了 hello world,这是因为 printf 执行时会把 hello world 放到用户级缓冲区中,exit 会刷新缓冲区,_exit 不会刷新
return
在 main 函数中使用 return n 进行返回的时候,n 会被设置为进程的退出码
验证
cpp
int main()
{
return 10;
}
运行并查看退出码
cpp
10
实际上在 return 返回时就是调用了 exit 函数,传入的参数就是 n
进程等待
进程等待的现象
根据前面的信息,子进程在运行完毕后,只会停止运行,不会立即消失,此时子进程在等待父进程回收自己的退出信息,进入了僵尸状态,产生了进程等待,当父进程回收了子进程的退出信息后,子进程就会消失
为了防止子进程进入僵尸状态,父进程就需要用 wait 或 waitpid 接口来等待子进程,当子进程运行完毕的时候,第一时间将它回收,在父进程调用 wait 或 waitpid 等待子进程的时候,也属于进程等待
进程等待的接口
进程等待的接口包括 wait 和 waitpid 两个,在僵尸进程处已经介绍过,在这里只说明一些其他的问题
wait 和 waitpid 中的 status 参数
wait 和 waitpid 接口都有一个输出型参数 status,它是一个有 16 个比特位的数,前 8 位用作进程的退出码,后 7 位用作信号的编号,从右向左第 8 位作为 core dump 标志,暂时不重要

如果进程正常运行完毕,没有被信号杀死,那么后 8 位就是全 0,前 8 位会被设置成指定的退出码,通过 (status >> 8) & 0xff 的方式可以得到这个退出码,这个计算方式也是宏WEXITSTATUS(status) 的实现方式,给这个宏传入 status 也能获得进程的退出码
如果进程被信号杀死,那么前 8 位就是全 0,后 7 位会被设置为信号的编号,通过 status & 0x7F 的方式可以得到信号的编号,这个计算方式也是宏 WIFEXITED(status) 的实现方式,WIFEXITED 用于查看后 7 位是否为全 0 判断进程是否正常退出
子进程正常退出时
cpp
#include <sys/wait.h>
#include <unistd.h>
#include <cstdio>
int main()
{
pid_t pid = fork();
// 子进程
if (pid == 0)
{
sleep(2);
exit(1);
}
int status = 0;
waitpid(pid, &status, 0);
printf("子进程的退出码:%d, 信号编号:%d\n", (status >> 8) & 0xff, status & 0x7f);
}
运行结果
cpp
子进程的退出码:1, 信号编号:0
子进程被信号杀死时
cpp
int main()
{
pid_t pid = fork();
// 子进程
if (pid == 0)
{
sleep(2);
int x = 1 / 0;
}
int status = 0;
waitpid(pid, &status, 0);
printf("子进程的退出码:%d, 信号编号:%d\n", (status >> 8) & 0xff, status & 0x7f);
}
运行结果
cpp
子进程的退出码:0, 信号编号:8
阻塞等待和非阻塞等待
waitpid 的第三个参数 option 取 0 时,父进程在调用 waitpid 时,会被阻塞在调用处,直到等待的子进程运行完毕,这叫做阻塞等待
验证
cpp
#include <sys/wait.h>
#include <unistd.h>
#include <cstdio>
int main()
{
pid_t pid = fork();
// 子进程
if (pid == 0)
{
sleep(2);
exit(0);
}
int status = 0;
printf("阻塞等待\n");
waitpid(pid, &status, 0);
printf("子进程退出\n");
}
结果
cpp
阻塞等待
子进程退出
可以看到,只输出了一句子进程退出,说明父进程在子进程退出前,被阻塞在了 waitpid 调用处
waitpid 的第三个参数取 WNOHANG 时,如果子进程未运行完毕,父进程调用 waitpid 时,不会在 waitpid 的调用处被阻塞,而是去继续执行后面的代码,这叫做非阻塞等待
验证
cpp
int main()
{
pid_t pid = fork();
// 子进程
if (pid == 0)
{
sleep(5);
exit(0);
}
int status = 0;
while (true)
{
int ret = waitpid(pid, &status, WNOHANG);
sleep(1);
if (ret == 0)
{
printf("子进程未退出\n");
}
else
{
printf("子进程退出\n");
break;
}
}
}
结果
cpp
子进程未退出
子进程未退出
子进程未退出
子进程未退出
子进程未退出
子进程退出
进程的程序替换
程序替换的概念
进程的程序替换指将当前进程在内存中的数据段和代码段进行替换,当程序替换成功时,进程就会去执行新的代码,不会执行原先的代码。这个过程中,不会发生进程的切换

父进程创建出子进程,子进程会和父进程共用一样的代码和数据,当子进程发生程序替换的时候,需要修改代码段和数据段,也会发生写时拷贝,将代码段和数据段拷贝到内存新的空间中,再进行替换
程序替换的接口
程序替换的接口一般都是 exec 开头的,后面接的字母代表了它的使用方式:
如果带有 l,说明这个函数有可变参数,需要传入多个参数,这些参数以 NULL 结尾
如果带有 v,说明这个函数接收一个指针数组,指针数组内是多个指定的参数,这些参数也以 NULL 结尾
如果带有 p,说明这个函数传路径时只需要给程序名,它会自动去环境变量 PATH 所知名的目录中查找程序
如果带有 e,
这些函数在失败时都返回 -1,在成功时都不会有返回值,因为成功时就发生了程序替换,原来的代码就不再向下执行了,返回值无意义
execl
cpp
`int execl(const char* path, const char* arg, ...);`
参数
path:要执行的程序所在的路径
arg:可变参数列表,传入的参数会被作为命令行参数,根据要执行的程序传参,比如要执行命令 ls -l,那么就传三个参数 ls,-a,NULL,其中 NULL 表示传参结束
使用例
cpp
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main()
{
int pid = fork();
if (pid == 0)
{
printf("子进程进行程序替换\n");
sleep(1);
execl("/usr/bin/ls", "ls", "-l", NULL);
printf("测试程序替换\n");
}
int ret = waitpid(pid, NULL, 0);
if (ret < 0)
{
printf("waitpid产生错误\n");
}
printf("子进程退出成功\n");
return 0;
}
运行结果
cpp
子进程进行程序替换
total 28
-rw-rw-r-- 1 ubuntu ubuntu 91 Mar 20 17:27 makefile
-rwxrwxr-x 1 ubuntu ubuntu 16128 Mar 20 17:34 test
-rw-rw-r-- 1 ubuntu ubuntu 467 Mar 20 17:34 test.cc
-rw-rw-r-- 1 ubuntu ubuntu 2176 Mar 20 17:34 test.o
子进程退出成功
execlp
cpp
int execlp(const char* file, const char* arg, ...);
参数
file:要执行的程序的名称,执行时会自动去环境变量 PATH 指定的路径中查找程序
arg:可变参数列表,与 execl 相同的用法
使用例
父进程创建子进程,子进程进行程序替换并执行 ls -a 命令
cpp
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main()
{
int pid = fork();
if (pid == 0)
{
printf("子进程进行程序替换\n");
sleep(1);
execlp("ls", "ls", "-l", NULL);
printf("测试程序替换\n");
}
int ret = waitpid(pid, NULL, 0);
if (ret < 0)
{
printf("waitpid产生错误\n");
}
printf("子进程退出成功\n");
return 0;
}
运行结果
cpp
子进程进行程序替换
total 28
-rw-rw-r-- 1 ubuntu ubuntu 91 Mar 20 17:27 makefile
-rwxrwxr-x 1 ubuntu ubuntu 16128 Mar 20 19:00 test
-rw-rw-r-- 1 ubuntu ubuntu 899 Mar 20 19:00 test.cc
-rw-rw-r-- 1 ubuntu ubuntu 2160 Mar 20 19:00 test.o
子进程退出成功
execv
cpp
int execv(const char* path, char* const argv[]);
参数
path:要执行的程序所在的路径
argv:命令行参数表,这是一个指针数组,相当于把命令行参数都放在了一个指针数组内
使用例
父进程创建子进程,子进程进行程序替换并执行 ls -a 命令
cpp
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main()
{
int pid = fork();
char *argv[3] = {(char *)"ls", (char *)"-a", NULL};
if (pid == 0)
{
printf("子进程进行程序替换\n");
sleep(1);
execv("/usr/bin/ls", argv);
printf("测试程序替换\n");
}
int ret = waitpid(pid, NULL, 0);
if (ret < 0)
{
printf("waitpid产生错误\n");
}
printf("子进程退出成功\n");
return 0;
}
运行结果
cpp
子进程进行程序替换
. .. makefile test test.cc test.o
子进程退出成功
execvp
cpp
int execvp(const char* file, char* const argv[]);
参数
file:程序的名称,调用时会去环境变量 PATH 给定的路径中查找
argv:命令行参数列表
使用例
父进程创建子进程,子进程进行程序替换并执行 ls -a 命令
cpp
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>
int main()
{
int pid = fork();
char *argv[3] = {(char *)"ls", (char *)"-a", NULL};
if (pid == 0)
{
printf("子进程进行程序替换\n");
sleep(1);
execvp("ls", argv);
printf("测试程序替换\n");
}
int ret = waitpid(pid, NULL, 0);
if (ret < 0)
{
printf("waitpid产生错误\n");
}
printf("子进程退出成功\n");
return 0;
}
运行结果
cpp
子进程进行程序替换
. .. makefile test test.cc test.o
子进程退出成功
execvpe
cpp
int execvpe(const char* file, char* const argv[], char* const envp[]);
参数
file:要执行的程序的名称
argv:命令行参数列表
envp:环境变量表
使用例
父进程创建子进程,子进程进行程序替换,执行别的程序并输出它的命令行参数和环境变量
cpp
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t pid = fork();
char *argv[3] = {(char *)"-a", (char *)"-l", NULL};
char *env[3] = {(char *const)"x=1", (char *const)"y=2", NULL};
// 将新的环境变量添加到环境变量表中
for (int i = 0; env[i]; ++i)
{
putenv(env[i]);
}
// 子进程
if (pid == 0)
{
printf("执行程序替换\n");
execvpe("./process", argv, environ);
printf("程序替换失败\n");
exit(1);
}
int ret = waitpid(pid, nullptr, 0);
if (ret < 0)
{
printf("waitpid error");
exit(2);
}
printf("子进程退出成功\n");
return 0;
}
// process.cc:
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
printf("命令行参数:\n");
for (int i = 0; i < argc; ++i)
{
printf("argv[%d] = %s\n", i, argv[i]);
}
printf("环境变量:\n");
for (int i = 0; env[i]; ++i)
{
printf("env[%d] = %s\n", i, env[i]);
}
return 0;
}
运行结果
cpp
执行程序替换
命令行参数:
argv[0] = -a
argv[1] = -l
环境变量:
env[0] = SHELL=/bin/bash
env[1] = COLORTERM=truecolor
env[2] = TERM_PROGRAM_VERSION=1.109.5
env[3] = TST_HACK_BASH_SESSION_ID=2707072416459380
env[4] = PWD=/home/ubuntu/linux-lesson/lesson-review/lesson-5
env[5] = LOGNAME=ubuntu
env[6] = XDG_SESSION_TYPE=tty
env[7] = VSCODE_GIT_ASKPASS_NODE=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/node
env[8] = HOME=/home/ubuntu
env[9] = LANG=en_US.UTF-8
env[10] = LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=00:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.avif=01;35:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.webp=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:*~=00;90:*#=00;90:*.bak=00;90:*.crdownload=00;90:*.dpkg-dist=00;90:*.dpkg-new=00;90:*.dpkg-old=00;90:*.dpkg-tmp=00;90:*.old=00;90:*.orig=00;90:*.part=00;90:*.rej=00;90:*.rpmnew=00;90:*.rpmorig=00;90:*.rpmsave=00;90:*.swp=00;90:*.tmp=00;90:*.ucf-dist=00;90:*.ucf-new=00;90:*.ucf-old=00;90:
env[11] = SSL_CERT_DIR=/usr/lib/ssl/certs
env[12] = GIT_ASKPASS=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/extensions/git/dist/askpass.sh
env[13] = PROMPT_COMMAND=__vsc_prompt_cmd_original
env[14] = SSH_CONNECTION=113.214.198.203 57365 10.0.4.2 22
env[15] = VSCODE_GIT_ASKPASS_EXTRA_ARGS=
env[16] = VSCODE_PYTHON_AUTOACTIVATE_GUARD=1
env[17] = LESSCLOSE=/usr/bin/lesspipe %s %s
env[18] = XDG_SESSION_CLASS=user
env[19] = TERM=xterm-256color
env[20] = LESSOPEN=| /usr/bin/lesspipe %s
env[21] = USER=ubuntu
env[22] = VSCODE_GIT_IPC_HANDLE=/run/user/1000/vscode-git-477b474b57.sock
env[23] = GOPROXY=https://mirrors.tencent.com/go/
env[24] = SHLVL=1
env[25] = XDG_SESSION_ID=51346
env[26] = XDG_RUNTIME_DIR=/run/user/1000
env[27] = SSL_CERT_FILE=/usr/lib/ssl/cert.pem
env[28] = SSH_CLIENT=113.214.198.203 57365 22
env[29] = DEBUGINFOD_URLS=https://debuginfod.ubuntu.com
env[30] = VSCODE_GIT_ASKPASS_MAIN=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/extensions/git/dist/askpass-main.js
env[31] = XDG_DATA_DIRS=/usr/local/share:/usr/share:/var/lib/snapd/desktop
env[32] = BROWSER=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/bin/helpers/browser.sh
env[33] = PATH=/home/ubuntu/.vscode-server/cli/servers/Stable-072586267e68ece9a47aa43f8c108e0dcbf44622/server/bin/remote-cli:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
env[34] = DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus
env[35] = TERM_PROGRAM=vscode
env[36] = VSCODE_IPC_HOOK_CLI=/run/user/1000/vscode-ipc-06d342a3-e90b-4b7d-a9e8-51c443b0c470.sock
env[37] = _=./test
env[38] = OLDPWD=/home/ubuntu/linux-lesson/lesson-review
env[39] = x=1
env[40] = y=2
子进程退出成功
execle
cpp
int execle(const char* path, const char* arg, ..., char* const envp[]);
参数
path:要执行的程序的路径
arg:可变参数列表,传入的参数会被作为命令行参数
envp:环境变量表
使用例
父进程创建子进程,子进程执行新的程序,输出它的命令行参数和环境变量
cpp
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
using namespace std;
int main()
{
pid_t pid = fork();
char *env[3] = {(char *)"x=1", (char *)"y=2", NULL};
if (pid == 0)
{
printf("进行程序替换\n");
execle("./process", "-a", "-l", NULL, env);
printf("程序替换失败\n");
exit(1);
}
int ret = waitpid(pid, nullptr, 0);
if (ret < 0)
{
printf("waitpid error\n");
exit(2);
}
printf("子进程退出\n");
return 0;
}
// process.cc:
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
printf("命令行参数:\n");
for (int i = 0; i < argc; ++i)
{
printf("argv[%d] = %s\n", i, argv[i]);
}
printf("环境变量:\n");
for (int i = 0; env[i]; ++i)
{
printf("env[%d] = %s\n", i, env[i]);
}
return 0;
}
运行结果
cpp
进行程序替换
命令行参数:
argv[0] = -a
argv[1] = -l
环境变量:
env[0] = x=1
env[1] = y=2
子进程退出