学习目标
首先进程控制分为四大部分:进程创建、进程退出、进程等待、进程替换;
第一步:学习如何来创建一个进程,一般我们会使用fork函数来创建子进程,创建子进程之后,就要去探索子进程与父进程的相关联系;
第二步:学习如何让一个进程退出,需要认识并熟练使用exit、_exit、return函数来完成进程退出,了解进程退出码,进程退出码的组成,如何获取进程退出码;
第三步:学习如何等待一个进程和进程等待的相关函数,通过函数来获取进程退出码;
第四步:学习如何进行进程替换,了解并熟练使用进程替换的接口exec*函数
一、进程创建
进程创建必备函数:fork函数的介绍:
在已知进程中创建一个新的进程,已知进程为父进程,新进程为子进程
fork函数声明:
pid_t fork(void);
头文件:
#include <unistd.h>
返回值:
返回值为0,表示为子进程 |
返回的为子进程的pid,但是此时是父进程 |
返回值为-1,创建子进程失败 |
说明:
- fork之后,子进程与父进程共享代码,如果某一进程要修改某一段代码,会发生写时拷贝
- 通常使用 if-else 语句让父子进程执行不同的代码
- fork通常会让子进程去执行别的程序,也就是程序替换
提问:
1. 为什么fork之后,对代码修改会发生写时拷贝?
首先fork之后,父子进程共享父进程的代码,当这段 被共享的数据 需要修改的时候,为了保证进程的独立性,操作系统会介入其中,发生写时拷贝,然后去修改拷贝后的数据,保证父子进程互不影响。(谁被修改,就把谁写时拷贝,其他的不变)
2. 为什么同一个变量可以有不同的值?
(1) 首先Linux支持同一个变量名表示不同的内存,因为变量最终都是存放在内存中的,而变量名是给人看的,计算机只看二进制,所有即使同一个变量名,但是内存空间的地址不同,就可以区分;
(2) 这里也发生了写时拷贝,上面的图是一个简单版本,下面有一个复杂版本;
(3) 页表、虚拟空间地址都相同,但是映射过去的物理地址空间不同;
二、进程退出
进程退出,我们就会想到之前讲过的僵尸进程,先复习一下僵尸进程,当子进程退出时,子进程会把大部分资源(代码段、数据段、页表、地址空间等)还给操作系统,唯独保留下该进程的PCB,保留PCB的原因是,PCB中记录了一个进程退出时的退出码,退出码会反馈进程退出的原因(执行完结果正确、执行完结果错、没执行完,异常退出)。而退出码是会被父进程读取的,这也是必须要读取的。若没有父进程读取子进程的退出码,那该子进程就会变成僵尸进程。
而读取进程退出码的原因是,我们必须知道子进程是因为什么退出!
说到这里,我们会清楚两点:
1. 进程退出时,必须提供进程的退出码,以供父进程读取
2. 父进程读取子进程的退出码的方式为:进程等待
所以,我们学习进程退出,要学会下面三个方面:
1. 进程退出的场景
2. 进程退出的方法
2. 进程退出码的解读
1. 进程退出的场景
这里我们应该深有体会,有时候进程在执行一部分代码的时候就直接终止,比如野指针的使用、除0操作;又或者执行结束,但是结果不正确;最常见的就是执行结束,也得到了想要的结果。所以我们将进程退出分为以下三个场景:
1. 程序执行结束,结果正确 |
2. 程序执行结束,结果不正确 |
3. 程序没执行结束,异常终止 |
2. 进程退出的方法
我们在学习C/C++时,通常会在main函数的末尾写上一个return 0,这表示main函数返回值为0,main函数返回成功。其实这就是我们程序的退出方法之一。
下面列举了进程退出的方法汇总和逐方法讲解:
return num | 必须在主函数中使用 在其他函数体使用,仅仅代表函数的结束 |
exit(num) | 可以在任意地方调用,代表直接退出进程 |
_exit(num) | 同上 |
ctrl + c kill -num pid | 在命令行中使用信号杀死进程,属于异常终止 |
注意:
- exit 和 _exit可以在任意地方调用,都是用来退出进程的;但是return必须在主函数中使用,在其他函数体使用,仅仅代表函数的结束
- exit支持刷新缓冲区,_exit不支持
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
// printf("exit()会刷新缓冲区");printf("_exit()不会刷新缓冲区");_exit(0);
// exit(0);
}
这里的结果显而易见,_exit不会刷新缓冲区,也就是根本不会打印任何东西;反之exit则会打印出来;但是在每个printf语句里末尾都加一个 '\n' ,他们都会打印出来
这是因为 ' \n '是有刷新缓冲区的作用。
- exit在底层封装了_exit,比_exit多了个刷新缓冲区
思考:这个缓冲区是操作系统内部的吗?
首先该缓冲区一定不是操作系统内部的,因为_exit 是一个系统调用接口,_exit被调用的时候,释放了资源,但是并没有刷新缓冲区,而exit函数封装之后才会刷新缓冲区,所以这个缓冲区一定不是操作系统的缓冲区,而是用户层的。
3. 进程退出码
进程退出码:记录进程退出时的情况,正常或异常,正确或错误。
进程退出码的组成:
进程退出码 = 退出码 + 核心转储标志 + 异常码
注意:
- 进程退出码以位图的方式呈现,我们只关心低16位
- 异常码为0到6比特位,表示进程出异常收到的异常编号
- 退出码为8到15比特位,表示进程正常退出的退出码
- 异常码 = 0,表示进程执行过程中无异常,看退出码
- 异常码 ≠ 0,表示进程执行出异常,退出码无意义
获取进程退出码的方法:
指令:echo $? |
进程等待函数wait、waitpid的输出型参数status |
三、进程等待
1. 为什么要进行进程等待?
1. 子进程退出时,父进程如果不进行进程等待,子进程会变成僵尸进程,造成内存泄漏
2. 进程一旦变成僵尸进程,无法使用kill命令来杀死
3. 子进程退出时,会将其退出情况存放到进程退出码中,这个退出码就放在PCB里,所以父进程进行进程等待,也可以获取子进程的退出情况
2. 进程等待必要性
回收子进程资源(必做),获取子进程退出码(选做)
3. 进程等待的方法
(1)wait函数
函数声明:
pid_t wait(int* status)
头文件:
#include<sys/types.h>
#include<sys/wait.h>
返回值:
成功:返回被等待进程的 pid ;
失败:返回 -1;
status:
输出型参数,获取子进程退出码,不获取则可以设置成为NULL
示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int id = fork(); //fork创建子进程if(id == 0) //子进程{printf("I am a child , pid = %d\n", getpid());exit(1);}int status = 0;if(wait(&status) == id) //等待成功,返回被等待进程pidprintf("status = %d\n", status);else //失败,返回-1printf("wait error\n");return 0;
}
(2)waitpid函数
函数声明:
pid_ t waitpid(pid_t pid, int* status, int options)
头文件:
#include <sys/types.h>
#include <sys/wait.h>
返回值:
正常返回:waitpid返回等待到的子进程pid;
设置选项WNOHANG:而调用中waitpid发现没有已退出的子进程可收集,则返回0;
调用中出错:则返回-1
status:
输出型参数,依旧是获取子进程的退出码,不获取可设置为NULL。
查看进程是否是正常退出:
WIFEXITED(status)
若为正常终止子进程返回的状态,则为真
异常退出为假,可查看进程异常退出信号
查看进程的退出码:
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。
options:
当options传入 WNOHANG 时 (一个宏,值为1)
代表当pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。
若正常结束,则返回该子进程pid
options传入的整型值,不是1,就会一直等待子进程的退出,父进程会阻塞等待;
为WNOHANG,则不会等待,父进程会执行后面的代码,是非阻塞等待
示例1:使用WIFEXITED和WEXITSTATUS获取进程退出情况
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int id = fork(); //fork创建子进程if(id == 0) //子进程{printf("I am a child, pid = %d\n", getpid());exit(11); //子进程退出}int status = 0;waitpid(id, &status, 0);if(WIFEXITED(status)) //当进程正常退出,返回真printf("exit code = %d\n", WEXITSTATUS(status)); //打印进程的退出码else //进程异常退出printf("exit signal = %d\n", WIFEXITED(status)); //打印进程的异常退出信号return 0;
}
示例2: 阻塞等待
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int id = fork(); //fork创建子进程if(id == 0) //子进程{int count = 5;while(count--){printf("I am a child, pid = %d\n", getpid());sleep(1);}exit(11); //子进程退出}int status = 0;while(1){int rid = waitpid(id, &status, 0);if(rid == 0)printf("child is running\n");else{printf("child exit success\n");return 0;}sleep(1);}
}
示例3:非阻塞等待
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{int id = fork(); //fork创建子进程if(id == 0) //子进程{int count = 5;while(count--){printf("I am a child, pid = %d\n", getpid());sleep(1);}exit(11); //子进程退出}int status = 0;while(1){int rid = waitpid(id, &status, WNOHANG);if(rid == 0)printf("child is running\n");else{printf("child exit success\n");return 0;}sleep(1);}
}
四、进程替换
进程替换就是将现在正在执行的进程换去执行另一个程序的代码
1. 进程替换的函数
(1)库函数
#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 execvpe(const char* file, char* const argv[]s, char* const envp[])
(2)系统调用函数
#include <unistd.h>
int execve(const char *path, char *const argv[], char *const envp[]);
名词解释:
path | 传程序的绝对路径 |
file | 传程序的文件名 |
arg | 可变参数列表,依次传使用规则传入字符串 但结尾必须为NULL |
argv | 将上面的可变参数放在指针数组里,传数组名 数组最后一个元素必须为NULL |
envp | 将自己写的环境变量放在指针数组里,传数组名 数组最后一个元素必须为NULL |
函数返回值:
这些函数如果调用成功,则替换后的程序从启动代码开始执行,不再返回;
如果调用出错则返回 -1;
所以exec函数只有出错的返回值而没有成功的返回值
快速记忆exec*函数的命名:
l -> list : 表示参数采用列表
v -> vector : 参数用数组
p -> path : 有p自动搜索环境变量PATH,只需要传程序名,自动搜索路径
e -> env : 表示需要自己传环境变量
2. 进程替换后的注意事项
- 进程替换不会产生新的进程,进程pid不变;
- 进程成功替换后,不会执行exec*函数后面的代码,因为被替换掉了
- exec*函数只有失败返回值,没有成功返回值
- 不论什么语言,只要是一个可执行程序,我们就可以替换
- 进程替换,只是把被替换的程序的代码段和数据段覆盖到替换之前到程序上,其他内核数据结构(PCB、页表、虚拟地址空间)不变,这也就解释了为什么没有产生新进程
3. exec函数示例
#include <unistd.h>
int main()
{char *const argv[] = {"ls", "-l", NULL};char *const envp[] = {"PATH=/usr/bin", NULL};// execl("/usr/bin/ls", "ls", "-l", NULL);// execlp("ls", "ls", "-l", NULL);// execle("ls", "ls", "-l", NULL, envp);// execv("/usr/bin/ls", argv);// execvp("ls", argv);// execve("/usr/bin/ls", argv, envp);return 0;
}
4. 进程替换的应用场景
通常会创建子进程,让子进程进行进程替换,利用该特性,我们可以实现一个自己的shell