目录
一,引言
在前几篇的讲解中,已经初步了解了进程的相关概念,进程是什么,进程的相关操作,以及命令行参数,环境变量等等。本节主要讲解,从开始创建一个进程,到结束一个进程 ,以及前几篇出现僵尸 进程的解决办法--进程等待 ,和最后进程替换等内容。本节详细讲解以上内容。
二,进程结束
在讲解进程终止之前,稍微说一下进程创建。在Linux中提供fork()函数用来创建子进程。fork函数的头文件为<unistd.h>中。其中fork函数的返回类型为pid_t 。这个返回类型可以当作整形来理解。fork函数在创建成功的前提下有两种返回值,子进程返回0;父进程返回子进程的pid。在创建子进程之后会发生写时拷贝的概念:下面详细了解一下写时拷贝。
1,写时拷贝
当fork函数创建了子进程之后。就触发了写时拷贝。也就是子进程也会拷贝一份和父进程几乎一致的PCB,虚拟地址空间,页表等等如图:

当创建一个子进程之后,子进程会创建task_struct,以及虚拟地址,页表等等,此时完成的是实时拷贝的初始化阶段,也就是子进程并没有修改数据。
当子进程修改数据时,操作系统会发现原本的页表项为只读状态,此时就会触发写时拷贝的拷贝功能,将修改部分的页进行拷贝,如下黄色的区域,子进程页表指向新的物理物理内存页。

上图中,就是修改后的父子进程。两者task_struct独立,虚拟空间也是独立,在不修改时,物理内是共享的。一旦数据修改。物理空间就也是独立的。这进而表现出进程独立性的特性。
2,进程终止
在了解的父子进程的拷贝逻辑之后。通常来说一个程序会出现三种情况:
1,代码运行结束并且运行结果正确。
2,代码运行结束但是运行结果并不正确
3,代码没有正常运行结束
这时候如何得知一个代码运行的情况呢,就引出了进程的退出码。
退出码:可以告诉我们最后一次执行命令的状态。可以根据这个退出码来得知这个指令是正确结束,还是错误结束。常见的退出码如下:

在Linux操作系统中:使用命令 echo $?来查看最后一次执行命令的退出码。如图:

在一个进程结束时--也就是代码运行结束。子进程会返回子进程的退出码给父进程。通常来说这个这个退出码会写入在PCB中。在讲解僵尸进程时讲到。子进程退出并不会立即销毁。只有当父进程接收到子进程的退出信息之后。子进程的PCB才会回收。之后父进程会将退出码写入环境变量之中。这也就是为什么echo命令可以查看最近一个指令的退出码。
3,exit和_exit
上文中讲解到退出码的概念,除了系统给出的退出码。在自己写的程序中,比如当编写子进程代码时,遇到某种情况,就会引起进程退出等等。因此引入exit和_exit的概念。
cpp
#include <unistd.h>
void exit(int status);
cpp
#include <unistd.h>
void _exit(int status);
在Linux中有这两种结束进程并且返回退出码的函数。这两者的差异最后说明。首先先讲解一下函数用法:
exit 函数执行进程退出工作。当代码运行到这个函数时。该进程就会结束。其中输入的status参数就是退出码 该变量虽然是整形,但是父进程获取子进程的退出码时,只会截取该变量的前八个比特位。因此范围[0,255] 。例如:
cpp
int main()
7 {
8 pid_t id = fork();
9 if(id == 0)
10 {
11 //chlid
12 printf("我是一个子进程\n");
13 exit(10);
14 printf("我的id是******\n");
15 }
16 int i = 0;
W> 17 pid_t add = wait(&i);
18 printf("子进程的退出码%d",WEXITSTATUS(i));
19 return 0;
20 }
运行结果如下:

此时会发现子进程的第二个printf并没有执行。表明exit结束该子进程。
上文中讲到,退出码要截取前八位bit位。WEXITSTATUS宏函数就是实现这个功能。
最后讲一下:_exit和exit的区别
exit本质来说是c语言提供的库函数,_exit是系统调用。它们两者是上下级的关系。也就是说exit内部封装了_exit。在功能上来说exit在执行时不仅会结束进程,在结束进程之前会清理所有的缓冲区,确保数据都被写入。_exit则不会清理缓冲区。
举个例子:C语言的\n不仅 有换行的作用还要清理缓冲区的作用。因此当代码没有\n时,_exit系统调用并不会讲数据打印出来。大家可以去尝试一下。
三,进程等待
在进程状态时讲到,有一种进程状态叫做僵尸进程,这种进程不仅仅会浪费空间,还会造成内存泄漏。自此,一个进程的结束,往往来说父进程来知道子进程的运行状态--是否正常的结束该进程。因此,引入进程等待的概念。
进程等待就是解决僵尸进程的问题(回收子进程) ,并且获取子进程所需要的信息。
1,进程等待方法
在进行进程等待方法讲解之前,先介绍一部分宏函数
1,WIFEXITED(status)--表示判断子进程是否正常退出
2,WEXITSTATUS(status)--表示若1中判断为真,也就是正常退出。这个结果为退出码
3,WIFSIGNALED(status)-- 表示判断子进程是否被信号终止
4,WTERMSIG(status)--表示若3为真,结果为信号编号
wait方法
cpp
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
等待子进程成功返回子进程pid,失败返回-1 .下面还有一个方法,两者的status参数含义一致。
waitpid方法
cpp
pid_ t waitpid(pid_t pid, int *status, int options);
waitpid和wait中status的参数功能是一致的。所有这里讲解waitpid。
第一个参数pid:可以指定回收某个进程,当这个pid为-1,就相当于wait()。当这个pid为0表示回收同组进程。当小于-1,则对该值取绝对值。
第二个参数status:这个为输出型参数,通过宏函数进行修改,来得出不同的结果。
第三个参数options:这个表示该方法时候阻塞等待。当该值为0时,阻塞等待,一直等待子进程结束。当该值为WNOHANG。这时是非阻塞等待,直接返回零。
2,阻塞等待和非阻塞等待
在上文中讲解了相关函数的方法,在这一节就实际的例子来了解一下这两者的区别:
阻塞等待:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid1 = fork();
pid_t pid2 = fork(); // 创建两个子进程
// 子进程1:退出码 10
if (pid1 == 0 && pid2 > 0) {
printf("子进程1(PID:%d)退出,退出码10\n", getpid());
exit(10);
}
// 子进程2:退出码 20
if (pid2 == 0 && pid1 > 0) {
printf("子进程2(PID:%d)退出,退出码20\n", getpid());
exit(20);
}
// 父进程:只等待子进程1(假设pid1是第一个子进程的PID)
if (pid1 > 0 && pid2 > 0) {
int status;
// 精准等待 PID 为 pid1 的子进程,阻塞模式
pid_t ret = waitpid(pid1, &status, 0);
if (ret > 0 && WIFEXITED(status)) {
printf("回收子进程%d,退出码:%d\n", ret, WEXITSTATUS(status));
}
}
return 0;
}
非阻塞等待:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程睡眠5秒后退出
sleep(5);
exit(30);
}
// 父进程:非阻塞等待子进程
int status;
while (1) {
pid_t ret = waitpid(pid, &status, WNOHANG);
if (ret == 0) {
// 子进程还未退出,父进程可做其他事
printf("子进程还在运行,父进程继续执行其他逻辑...\n");
sleep(1);
} else if (ret > 0) {
// 成功回收子进程
printf("子进程%d退出,退出码:%d\n", ret, WEXITSTATUS(status));
break;
} else {
// 等待失败
perror("waitpid 失败");
break;
}
}
return 0;
}
相较于阻塞等待,非阻塞等待,可以等子进程执行的区间,父进程完成其余的代码。只需要循环访查看子进程是否结束,来保证子进程不会出现僵尸进程的问题。因此非阻塞等待可以提高运行效率。
四,进程程序替换
在讲述用fork函数来创建子进程时,有时子进程往往运行并不是自己的程序,当子进程想要执行其他程序时,就需要用到进程替换。
通常来说,当进程执行程序替换函数之后,进程所对应的代码数据部分会被完全覆盖,相对于的PCB部分关于内存方面的也会被重构。但像一些pid等待并不会改变,也就是说当执行exec类函数时并不创建新进程,而是替代该进程执行所替代的函数。
程序替换的函数一共有6种:
cpp
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
本质上来说,是否传入绝对路径,命令行参数表,环境变量表等选项。
第一个参数代码传入的程序的路径,第一个参数在前三个都是第一个参数后跟的选项,第三个参数是环境变量,通常来说继承父进程的,但是也可以传入自己设定的环境变量。