送给大家一句话:
我并不期待人生可以一直过得很顺利,但我希望碰到人生难关的时候,自己可以是它的对手。------ 加缪
开始学习进程控制
- [1 前言](#1 前言)
- [2 进程创建](#2 进程创建)
-
- [2.1 fork函数初识](#2.1 fork函数初识)
- [2.2 fork函数返回值](#2.2 fork函数返回值)
- [2.3 写时拷贝](#2.3 写时拷贝)
- [2.4 fork常规用法](#2.4 fork常规用法)
- [2.5 fork调用失败的原因](#2.5 fork调用失败的原因)
- [2 进程终止](#2 进程终止)
-
- [2.1 终止是在做什么](#2.1 终止是在做什么)
- [2.2 进程终止的情况](#2.2 进程终止的情况)
- [2.3 如何终止](#2.3 如何终止)
- [3 进程等待](#3 进程等待)
- [4 总结](#4 总结)
- Thanks♪(・ω・)ノ谢谢阅读!!!
- 下一篇文章见!!!
1 前言
通过对进程的学习,我们对虚拟地址,页表,物理地址有了大概认识。我们平时使用的地址都是虚拟地址,通过页表可以访问物理地址(统一的视角进行控制,保证数据安全)。也认识到写时拷贝
。
也认识O(1)调度算法,通过两个队列(活跃队列,过期队列)完成进程的分时控制,通过优先级来放入不同位置,以时间复杂度O(1)快速寻找进程。
2 进程创建
2.1 fork函数初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。fork函数具有两个返回值,通过对返回值的判断(if else )可以进行父进程和子进程的不同书写。
注意:进程调用fork,当控制转移到内核中的fork代码后,内核做以下工作:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程(进程:内核的相关数据管理的数据结构(task_struct + mm_struct + 页表)+ 代码与数据)
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
这里是为了保证父进程和子进程的独立性。
2.2 fork函数返回值
- 子进程返回0
- 父进程返回的是子进程的pid
那为什么父进程返回子进程PID ,给子进程返回0呢???
很好理解:就像现实生活中,父母有了孩子,会给他或她起一个名字,父母知道了名字,就可以很好管理孩子。父进程与子进程同理,父进程为了便于管理子进程,所以fork函数会返回对应子进程的pid。
2.3 写时拷贝
通过图解可以很好理解写时拷贝。
在创建子进程的时候,子进程的页表映射与父进程一致(默认继承的),一旦子进程要进行修改数据,为了保证进程的独立性(保证父进程安全运行),不得不开辟一个新空间,并修改子进程页表的映射(虚拟地址不变!)。
2.4 fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
一般使用if else 分开书写,也可以通过系统调用打开新的进程。
2.5 fork调用失败的原因
- 系统中有太多的进程(数据空间是有限的)
- 实际用户的进程数超过了限制(必须是有限的)
2 进程终止
2.1 终止是在做什么
进程终止会进行:
- 释放曾经的代码和数据所占据的空间
- 释放内核数据结构
但是task_struct会延期处理,因为终止的进程处于Z状态(僵尸进程)
2.2 进程终止的情况
我们的main函数常常会有一个返回值 0 ,那为什么要返回零呢???
c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5
6 int main()
7 {
8
9 printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
10 sleep(2);
11
12 return 100;
13 }
来看我们返回100(为了效果明显)时
echo 打印的是bash 的环境变量,这个100 就是刚才进程返回到父进程(bash)的退出码(环境变量 ?
表示最近一个进程的退出码),一般0表示正常运行,非零表示有问题。
父进程关心子进程的信息,想要知道子进程是否正常运行。不同的退出码表示不一样的失败原因,我们来获取一下:
c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4
5 int g_val = 10000;
6
7 int main()
8 {
9
10 for(int errcode = 0 ;errcode <=255; errcode++)
11 {
12 printf("%d: %s\n",errcode,strerror(errcode));
13
14 }
15 printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
16 sleep(1);
17
18 return 0;
19 }
这样就可以获取所有的退出码和对应的退出信息了:
通过退出码就能获取对应退出信息,来告知用户为什么退出了。其底层实现也好理解,通过数字与字符串的一一对应来做到。
常见进程退出场景:
- 代码运行完毕,结果正确(正常结束进程)
- 代码运行完毕,结果不正确
- 代码异常终止,出现异常提前退出
就像:VS编程运行的时候,如果崩溃了 --- 操作系统发现你的进程做了不应该做的事情,OS就杀死进程!!!
一旦出现异常,退出码就没有意义了!!! 为什么出异常才是最重要的!!!
那为什么会出现异常呢??? 原因是:进程出现异常的本质是进程收到来自OS发给进程的信号!(kill -9 就是一个信号)
注意:
- 先确认是否异常
- 不是异常就是代码正常跑完,看退出码即可。
- 可以通过退出信号来判断出现了什么异常
2.3 如何终止
正常终止(可以通过 echo $? 查看进程退出码):
- 从main函数return,表示进程终止
- 调用exit
- _exit
异常退出 :
ctrl + c,信号终止
来看手册中如何描述的:
调用exit 函数试试:
c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 int g_val = 10000;
6
7 int main()
8 {
9
10 printf("I am parent process,pid:%d,ppid:%d\n",getpid(),getppid());
11 sleep(1);
12 exit(123);
13
14 }
15
运行后是这样的效果:
exit比return 直接,调用一次就可以完全退出!
_exit 是一个系统调用(system call),参数与exit一致,使用与exit几乎一模一样。
**但是exit会冲刷缓冲区,而_exit 不会,**因为缓冲区在系统调用之上 ,而exit 是一个C语言库函数。图解:
exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
3 进程等待
3.1 进程等待必要性
- 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
也就是说,任何进程在退出时都要被父进程进行等待,不然子进程处于僵尸进程就会造成内存泄漏!!!
3.2 进程等待的方法
- wait方法
- waitpid方法
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回
wait方法
c
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
我们测试一下:
c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8 void Childrun()
9 {
10 int cnt = 5;
11 while(cnt)
12 {
13 printf("I am child ,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
14 sleep(1);
15 cnt--;
16 }
17 }
18
19 int main()
20 {
21
22 printf("I am father ,pid:%d,ppid:%d\n",getpid(),getppid());
23
24 pid_t id = fork();
25 if(id == 0)
26 {
27 //child
28 Childrun();
29 printf("child quit...\n");
30 exit(0);
31 }
32 //father
33 sleep(10);
34 pid_t rid = wait(NULL);
35 if(rid > 0)
36 {
37 printf("wait success,rid:%d\n",rid);
38 }
39 sleep(3);
40 return 0;
41 }
这个程序会在子进程运行结束前等待子进程,并且会存在一段时间的窗口期,此时子进程处于僵尸进程:
在这个父进程等待的过程中,父进程一直在等待子进程的退出,处于阻塞等待状态。父进程本质是等待某种软件条件就绪,那么如何理解阻塞等待子进程呢???
就是把自己列入等待队列,把状态列入不运行状态,等待子进程(类似scanf 的阻塞)。
waitpid方法
c
#include<sys/types.h>
#include<sys/wait.h>
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
pid_t rid = waitpid(-1,NULL,0);
与刚才的wait等价!!!如果id不为-1,是一个对应的id,那么就会等待指定进程!!!,如果id错误(不存在该进程),就会发生等待错误!!!
status
是一个输出型参数,需要我们传入一个指针来获取。来测试一下(子进程退出码设置为1 )
c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8 void Childrun()
9 {
10 int cnt = 5;
11 while(cnt)
12 {
13 printf("I am child ,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
14 sleep(1);
15 cnt--;
16 }
17 }
18
19 int main()
20 {
21
22 printf("I am father ,pid:%d,ppid:%d\n",getpid(),getppid());
23
24 pid_t id = fork();
25 if(id == 0)
26 {
27 //child
28 Childrun();
29 printf("child quit..\n");
30 exit(1); //退出码设置为1
31 }
32 //father
33 sleep(10);
34 int status = 0 ;
35 pid_t rid = waitpid(id , &status , 0);
36 if(rid > 0)
37 {
38 printf("wait success,rid:%d\n",rid);
39 }
40 printf("father quit...,status:%d\n",status);
41 sleep(3);
42 return 0;
43 }
这就成功获取了status!
我们需要的是 退出码 和 退出信号,那么我们如何通过status获取这两个数据呢???
也就通过位运算就可以成功获取了。
![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/2650b18879684c50981074b8e6bbdf7d.png)
这样就可以了:
通过两个信息就可以判断进程是否正常运行,如果异常,也能知道异常原因了。
当然,如果使用位运算就有点那啥了,我们可以使用宏:
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
对于第三个参数,就可以让父进程在等待的刚才中区做其他事情。也就是进行非阻塞等待:WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
- 阻塞等待就类似张三给李四打电话帮忙,李四正在忙,告诉张三等一会,然后张三这个电话就不挂了,等着李四完成工作,张三也不做其他事情。
- 非阻塞等待类似张三给李四打电话帮忙,李四正在忙,告诉张三等一会,然后张三说李四忙完打回来,张三就先去做其他事情。
下面写入了一段非阻塞轮询等待的代码,这样就能保证父进程在等待的过程中,可以去做其他事情!
c
34 while(1)
35 {
36 int status = 0 ;
37 pid_t rid = waitpid(id , &status , 0);
38 if(rid == 0)
39 {
40 sleep(1);
41 printf("child is running ,father check next time!\n");
42 }
43 else if(rid > 0)
44 {
45 if(WIFEXITED(status))
46 {
47 printf("child quit success,child exit code:%d\n",WEXITSTATUS(status));
48 }
49 else
50 {
51 printf("child quit unnormal!\n");
52 }
53 break;
54 }
55 else
56 {
57 printf("waitpid failed!\n");
58 break;
59 }
60 }
来看运行效果(父进程一直在查):
这样就完成了。
4 总结
- 等待很容易理解,等待是必须进行的,回收子进程资源,获取子进程退出信息
- 进程等待常用waitpid,并常用非阻塞等待