当我们在了解了进程的基本概念之后,我们接下来需要对进程进行更深层次的学习,在这一章我们来认识一下进程的控制,理解进程底层相关的操作
进程的创建
fork函数初识
我们之前对于fork函数的认知仅仅停留在了在父进程中开启子进程的层面,今天我们来对fork进行更深层次地剖析
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>pid_t fork(void);返回值:自进程中返回 0 ,父进程返回子进程 id ,出错返回-1
分配新的内存块和内核数据结构给子进程(内存,PCB,页表等)将父进程部分数据结构内容拷贝至子进程(子进程是以父进程为模板进行拷贝)添加子进程到系统进程列表当中fork 返回,开始调度器调度
我们可以看到的是,父进程返回了子进程的pid,子进程返回了0
首先我们需要澄清一个概念:为什么fork会有两个返回值?
其实内核中fork()是一个函数,里面含有创建进程的代码,例如:
pid_t fork{
//创建进程代码//最后return
return pid;
}
而因为在返回前已经将子进程创建好了,子进程也会执行这条return语句,再加上原来父进程的return,所以就会有两个return
而我们给子进程返回0,父进程返回子进程的pid目的是为了让父进程方便找到子进程,因为子进程可以有多个,父进程只能有一个
现在我们再来看看我们的结果
我们可以看到,虽然有两个进程在执行,但是Now pid却只执行了一次,这是为什么呢?
其实原因是父进程先执行,打印了fork前的代码,当执行到fork时,才创建了一个子进程,此时两个进程共享代码,if else进行分流,分别打印自己的语句,最后走到return返回结束
所以我们可以看到,其实我们的子进程是从fork之后才以父进程为模板进行拷贝的,所以当我们的父进程运行到fork时,此时父进程的PCB中的程序计数器也在fork之后的位置,拷到子进程中不会改变,所以我们的子进程不会打印fork之前的语句
写时拷贝
我们可以看到,我们的数据段虽然是父子进程私有的,但其实开始时是子进程直接拷贝的父进程的数据,而后如果需要自己对数据进行重新写入,此时才重新改变页表与物理内存的对应关系,在物理内存中重新开辟一小段存储改变的数据,但在这个过程中虚拟内存不改变
上面的过程中我们可以分析两点:
1.写时拷贝:其实写时拷贝就是当父子进程需要对数据进行写入时,才进行一系列的开辟空间,改变页表对应关系,拷贝数据,写入数据,这个拷贝可以理解为即用即拷
2.为什么要有写时拷贝:本质上还是为了节约操作系统的空间与提高效率,因为我们创建进程时产生的的数据是很多的,有一些并不会使用,如果我们每次创建进程都提前将所有的数据进行拷贝,那么创建进程的效率会很低,浪费时间,站在节约空间与时间的角度,写时拷贝更高效
3.写时拷贝的数据:写时拷贝其实针对的是需要改变,进行写入的数据,而不会对原来不变的数据进行拷贝,我们写一小段数据,改一小段映射关系,这样在我们在用我们的虚拟地址进行只读操作时依然是连续的,且很高效
fork常规用法
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。一个进程要执行一个不同的程序。例如子进程从 fork 返回后,调用 exec 函数
fork调用失败的原因
系统中有太多的进程实际用户的进程数超过了限制
进程终止
进程退出场景
代码运行完毕,结果正确代码运行完毕,结果不正确代码异常终止
进程常见退出方法
正常终止(可以通过 echo $?查看进程退出码):
从main函数return返回
return n//n就代表了退出码
调用exit
#include<unistd.h>//头文件
void exit(int status)//status 退出码exit 最后也会调用 exit, 但在调用 exit 之前,还做了其他工作:1. 执行用户通过 atexit 或 on_exit 定义的清理函数。2. 关闭所有打开的流,所有的缓存数据均被写入3. 调用 _exit
调用_exit
#include<unistd.h>//头文件
void _exit(int status)//status 退出码说明:虽然 status 是 int ,但是仅有低 8 位可以被父进程所用。所以 _exit(-1) 时,在终端执行 $? 发现返回值是 255 。
![](https://img-blog.csdnimg.cn/20210928151048218.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5bmzICDnlJ8=,size_14,color_FFFFFF,t_70,g_se,x_16)
当我们直接用exit时,在exit之前的语句可以打印出来,进行了缓存刷新
我们使用_exit操作系统就不会对缓存进行刷新,直接退出了
注:我们在hello fork后面不能加\n,因为\n也会对缓存进行刷新,影响结果
ctrl + c ,信号终止
进程等待
进程等待必要性
之前了解过,子进程退出,父进程如果不管不顾,就可能造成 ‘ 僵尸进程 ’ 的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入, “ 杀人不眨眼 ” 的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
总的来说,我们必须让父进程对结束的子进程完成“收尸”工作,否则就可能引起内存泄漏,因为只有父进程可以对死掉的子进程进行信息采集与资源回收,所以我们就需要让父进程在子进程完成之后再完成,从而就出现了进程等待
进程等待的方法
wait方法
#include<sys/types.h>#include<sys/wait.h>pid_t wait(int*status);返回值:成功返回被等待进程 pid ,失败返回 -1 。参数:输出型参数,获取子进程退出状态 , 不关心则可以设置成为 NULL
我们可以看到,我们将子进程设置为3秒后退出,子进程变为了僵尸进程,而后父进程5秒后执行wait命令,对其进行了清理,最后只剩下了父进程,由此可见,wati命令可以回收子进程的资源,防止资源泄漏
我们再来看一段代码
我们这段代码其实就是设置wati让父进程等待,进入阻塞状态,等待子进程结束后再执行后面的代码
waitpid()
pid_ t waitpid(pid_t pid, int *status, int options);返回值:当正常返回的时候 waitpid 返回收集到的子进程的进程 ID ;如果设置了选项 WNOHANG, 而调用中 waitpid 发现没有已退出的子进程可收集 , 则返回 0 ;如果调用中出错 , 则返回 -1, 这时 errno 会被设置成相应的值以指示错误所在;参数:pid :Pid=-1, 等待任一个子进程。与 wait 等效。Pid>0. 等待其进程 ID 与 pid 相等的子进程。status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)options:WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。
如果子进程已经退出,调用 wait/waitpid 时, wait/waitpid 会立即返回,并且释放资源,获得子进程退出信息。如果在任意时刻调用 wait/waitpid ,子进程存在且正常运行,则进程可能阻塞。如果不存在该子进程,则立即出错返回。
获取子进程status
wait 和 waitpid ,都有一个 status 参数,该参数是一个输出型参数,由操作系统填充。如果传递 NULL ,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。status 不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究 status 低 16 比特位):
我们可以看到,当我们的进程退出时,,需要先判断是否正常退出,判断status第7位(去掉了core dump这一位)是否为0,(status&0x7f)
1.等于0则正常退出,再获取退出码((status>>8)&0xff)
退出码等于0,说明代码运行结果正常,不等于0则说明代码运行结果不正确,退出码的不同代表着不同的错误,看具体业务
2.不等于0,异常退出,获取终止信号(status & 0x7f)
异常退出返回8
正常退出返回0
正常退出返回10
两种可以直接获取退出信息的宏:
- WIFEXITED(status):若为正常终止子进程返回真。(查看进程是否正常退出)
- WEXITSTATUS(status):若 WIFEXITED(status)为正(正常退出),获取退出码。(查看进程退出码)
进程阻塞式等待实现
进程非阻塞式等待实现
这两种进程等待方式我们可以类比为,爸妈一直看着你写作业与爸妈隔一会看一下个一会看一下,总之,非阻塞式实现就是一直监控子进程,退出就进行回收资源,而阻塞式则是时间段式进行监控,这两种方式从资源消耗来看是阻塞式更节约资源,不过站在我们操作系统中,大部分的进程都是非阻塞式等待的,原因是这种方式较为简单
总结:
进程等待是什么?
父进程通过调用wait与waitpid系统接口,等待子进程状态的一种现象
为什么要等待进程?
1.防止子进程进入僵尸状态,致使内存泄漏
2.获取子进程的退出结果
怎么等待进程?
wait/waitpid
进程程序替换
替换原理
用 fork 创建子进程后执行的是和父进程相同的程序 ( 但有可能执行不同的代码分支 ), 子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种exec 函数时 , 该进程的用户空间代码和数据完全被新程序替换 , 从新程序的启动例程开始执行。调用exec 并不创建新进程 , 所以调用 exec 前后该进程的 id 并未改变
替换函数
#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[]);
我们替换函数的参数列表是有严格要求的,第一列参数永远是需要替换程序的绝对路径或者文件名,之后到envp环境变量前都是想要怎么执行它
函数解释
这些函数如果调用成功则加载新的程序从启动代码开始执行 , 不再返回。如果调用出错则返回 -1所以 exec 函数只有出错的返回值而没有成功的返回值。
注:最后envp之前要以NULL结尾
命名理解
l(list) : 表示参数采用列表v(vector) : 参数用数组p(path) : 有 p 自动搜索环境变量 PATHe(env) : 表示自己维护环境变量
其实只有我们的execve是真正的系统调用,其他最终调的都是execve
补充:一般的exec函数都是子进程去执行的,这样不会破坏父进程,因为进程具有独立性
模拟实现xshell
启动一个父进程,父进程从标准输入里读取用户输入的内容
父进程区分用户输入的内容当中那部分是命令,那部分是命令行参数
ls -a (ls 是命令 -a 是命令行参数)
父进程fork出子进程后,要让子进程进行进程程序替换,执行用户输入的命令。父进程进行进程等待,等到子进程结束
等到子进程退出之后,则进入循环获取用户输入
输入格式: ls -a(中间是空格)
#include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <string.h>
5 #include <fcntl.h>
6 #define MAX_CMD 1024
7 char command[MAX_CMD];
8 int do_face()
9 {
10 memset(command, 0x00, MAX_CMD);
11 printf("minishell$ ");
12 fflush(stdout);
13 if (scanf("%[^\n]%*c", command) == 0) {
14 getchar();
15 return -1;
16 }
17 return 0;
18 }
19 char **do_parse(char *buff)
20 {
21 int argc = 0;
22 static char *argv[32];
23 char *ptr = buff;
24 while(*ptr != '\0') {
E> 25 if (!isspace(*ptr)) {
26 argv[argc++] = ptr;
E> 27 while((!isspace(*ptr)) && (*ptr) != '\0') {
28 ptr++;
29 }
30 }else {
E> 31 while(isspace(*ptr)) {
32 *ptr = '\0';
33 ptr++;
34 }
35 }
36 }
37 argv[argc] = NULL;
38 return argv;
39 }
40 int do_exec(char *buff)
41 {
42 char **argv = {NULL};
43 int pid = fork();
44 if (pid == 0) {
45 argv = do_parse(buff);
46 if (argv[0] == NULL) {
47 exit(-1);
48 }
49 execvp(argv[0], argv);
50 }else {
E> 51 waitpid(pid, NULL, 0);
52 }
53 return 0