文章目录
- 1、进程创建
- 1.1 理解fork函数
- 1.2 fork函数的细节
- 2、进程退出
- 2.1 退出码
- 2.2 exit函数和_exit系统调用
- 3、进程等待
- 3.1 wait和waitpid
- 3.2 阻塞和非阻塞
1、进程创建
进程的创建主要依靠系统接口fork函数。
fork函数从已存在的一个进程中,创建一个子进程,原进程为父进程。
#include <unistd>
#include <sys/type.h>//用pid_t 需要包括这个文件
pid_t fork(void);
父进程返回子进程pid, 子进程返回0,出错返回-1。
1.1 理解fork函数
先从一个小程序看看fork函数的效果。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/type.h> //用pid_t 需要包括这个文件
4 int main()
5 {
6 pid_t id = fork();
7 if(id < 0)
8 {
9 printf("fork error!\n");
10 }
11
12 if(id > 0)
13 {
14 printf("当前进程的PID为: %d, 父进程PID是: %d, id: %d\n", getpid(), getppid(), id);
15 }
16
17 else
18 {
19 printf("当前进程的PID为: %d, 父进程PID是: %d, id: %d\n", getpid(), getppid(), id);
20 }
21 sleep(2);
22
23 return 0;
24 }
pid_t 是什么?
首先在/usr/include/sys/types.h中,通过通过/pid_t查询
再到/usr/include/bits/types.h中,通过/__pid_t查询
再到/usr/include/bits/typesizes.h中,通过/__PID_T_TYPE查询
回到/usr/include/bits/types.h中,通过/__S32_TYPE查询,发现其实就是int。
为什么要弄这么麻烦? 其实为了代码在不同平台上跑,可能其它平台是long,而不是int。(为了可移植性)
可能你会有疑惑,为什么会有两次打印?打印为什么是这个结果?代码是怎么走的?
我们的程序代码执行前
首先,我们所写的程序,在运行后加载到内存就成了Linux系统中的一个进程。
当我们运行编译好的程序后,程序加载到内存称为了一个Linux进程。
该进程(对应pid:31200)由命令行解释器bash(bash是一个系统进程,这里对应31107)创建,作为其子进程执行代码。
代码执行过程
进入main函数,执行pid_t id = fork();
此时转到操作系统内核fork定义处,执行fork函数代码。
(下图的子进程,其实不是在fork中马上创了一个空间,这里为了更好理解,下面会解释)
所以其实很简单,就是fork之后,有了两个执行流,通过返回值的不同走不同的代码路径。
1.2 fork函数的细节
有几个细节,能让我们更好理解fork。
在前面我们解释了fork函数为什么有两个返回值的问题,就是通过fork创建子进程,有了两个执行流。
首先
- 如何理解fork之后,父进程返回子进程id,子进程返回0?
我们都知道,一个父亲可以对应着多个孩子,而多个孩子只能对应一个父亲。
进程也一样,我们可以通过getpid和getppid得到唯一的自己和父亲,但对于孩子,如果我们需要找其中一个就需要有一个确定值。
其次
- 为什么会有一个变量id,储存两个不同的值?
pid id = fork(); 首先对于一个进程,我们并不确定父子进程哪个先执行完。
返回的本质,其实就是写入值到id,所以谁先返回谁就先写入id。
后写入的进程,因为进程独立性,为了不影响前面的一个进程就会发生写时拷贝。
-
我们看到fork失败会返回-1,那么什么情况会发生呢?
1、当系统中有太多进程,通常意味着某方面出了问题(比如 死循环调用fork)。
2、当该用户ID的进程数超过了系统的限制数。(CHILD_MAX) -
fork的通常用法
1、通过创建子进程,继承父进程的代码,运行和父进程运行的不同代码路径。
2、创建子进程,运行其它的进程(比如后续进程程序替换中的exec系列的函数)。
2、进程退出
2.1 退出码
从main函数开始。
我们之前写的程序很多在最后都会有一个return 0;
这个0其实就是退出码,它标识着程序运行成功。
int main()
{
return 0;
}
通过echo $?
可以查看记录最近一个进程在命令行执行完毕时对应的退出码。
- 进程退出的情况?
1、代码跑完,结果正确 — return 0;
2、代码跑完了,结果不正确 — return !0;
3、代码没跑完,程序异常了,退出码无意义。
如果我们关心退出码,可以通过不同的数字,表述不同的错误。
如果我们并不知道退出码对应的退出信息是什么,可以通过strerror(errno)。
如果熟悉个别退出码对应的信息,可以通过strerror(num) 打印退出信息。
1 #include <stdio.h>
2 #include <string.h>
3 #include <errno.h>
4
5 int main()
6 {
7 int i = 0;
8 for(i = 0; i < 200; ++i)
9 {
10 printf("[%d]: %s\n", i, strerror(i));
11 }
12 printf("return infor: %s\n", strerror(errno));
13 return 0;
14 }
由于结果太长,只截了开头一段和结尾一段。(通过运行看结果 可以看到退出码只有0-133)
2.2 exit函数和_exit系统调用
-
exit()
exit函数终止进程,返回对应退出码。#include <stdlib.h> void exit(int status);
exit虽然并没有返回值,但是会将status传给父进程接收退出码。(这个后面进程等待会解释,先了解)
通过man 3 exit
在C语言阶段,我们会在一些地方使用exit(num),里面的num其实就是退出码,退出码可以根据需要自己定义。#include <stdio.h> #include <stdlib.h> void fun() { exit(10);//从这里程序退出。 } int main() { fun(); printf("hello world"); }
-
_exit
_exit作为一个系统接口,在操作系统层。以及exit其实就是调_exit实现的。#include <unistd.h> void _exit(int status);
-
exit和_exit的区别
先通过一个小程序看exit
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 int main() 6 { 7 printf("process"); 8 sleep(2); 9 exit(1); 10 }
通过运行
再经过小小修改
1 #include <stdio.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 5 int main() 6 { 7 printf("process"); 8 sleep(2); 9 _exit(1); 10 }
其实exit和_exit的区别就是exit刷新缓冲区,但是_exit不刷新缓冲区。
sleep后,进程放入等待队列,输出在缓冲区,等进程重新回到运行队列_exit直接退出了,exit会刷新缓冲区,所以有这两个结果。根据这个结果我们也可以推出:缓存区其实是一个用户级的缓冲区。
3、进程等待
一个子进程在退出后,操作系统回收它的数据与代码,但是进程一定是为了什么目的才存在的,一个进程完成后可以不将结果汇报给创造它的父进程,但是不能没有结果。
其实,一个进程在退出后,操作系统依旧会保留其PCB,等待父进程或系统对该进程进行回收。
子进程在这个PCB被保留的状态就是一个僵尸进程,父进程通过进程等待的方式对子进程回收并且获得子进程退出信息。
3.1 wait和waitpid
- wait()
#include <sys/wait.h>
#include <sys/type.h>
pid_t wait(int* status);
当status值设为NULL时,只回收子进程,代表不在意回收的子进程的退出码。
当status不为NULL时,回收子进程,并且获得子进程的退出信息,存放在status中。
假设status不为NULL,status不是简单的存一个值,下面解释它如何保存信息。
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <assert.h>
5 #include <sys/wait.h>
6 #include <sys/type.h>
7
8 int main()
9 {
10 pid_t id = fork();
11 assert(id >= 0);
12 if(id == 0)
13 {
14 //子进程
15 printf("我是子进程: %d, 父进程: %d, id: %d\n", getpid(), getppid(), id);
16 exit(10); //随意设置
17 }
18
19 //父进程
20 sleep(2); //让子进程先运行完
21 int status = 0;
22 pid_t ret = wait(&status);
23 printf("return code : %d, sig : %d\n", (status >> 8), (status & 0x7F));
24 if(id > 0)
25 {
26 printf("wait success: %d\n", ret);
27
28 }
29 }
- waitpid()
#include <sys/wait.h>
#include <sys/type.h>
pid_t waitpid(pid_t pid, int* status, int options);
pid:进行等待的进程pid。
status:记录回收进程的退出信息。
options:一般选择是阻塞还是非阻塞两个状态。(下面会说啥是阻塞)
返回值返回回收的子进程pid,如果子进程还没退出返回0,如果waitpid调用失败返回-1。
稍稍修改一下代码
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <assert.h>
5 #include <sys/wait.h>
6 #include <sys/types.h>
7
8 int main()
9 {
10 pid_t id = fork();
11 assert(id >= 0);
12 if(id == 0)
13 {
14 //子进程
15 printf("我是子进程: %d, 父进程: %d, id: %d\n", getpid(), getppid(), id);
16 exit(10);
17 }
18
19 //父进程
20 sleep(2); //让子进程先运行完
21 int status = 0;
22 pid_t ret = waitpid(id, &status, 0);// 0 代表阻塞式等待 WNOHANG代表非阻塞式等待
23 printf("return code : %d, sig : %d\n", (status >> 8), (status & 0x7F));
24 if(id > 0)
25 {
26 printf("wait success: %d\n", ret);
27
28 }
29 }
-
子进程退出的退出信息存放在哪?
-
补充:宏函数
WIFEXITED(status)。W-wait,wait是否退出,若正常退出子进程,返回真。
WEXITSTATUS(status)。查看进程退出码,若WIFEXITED非零,提取子进程退出码。//是否正常退出 if(WIFEXITED(status)) { // 判断子进程运行结果是否ok printf("exit code: %d\n", WEXITSTATUS(status); }
3.2 阻塞和非阻塞
前面wait相关的测试都是在子进程已经退出的前提下进行的。
阻塞和非阻塞很简单,将waitpid设置为阻塞后如果子进程没有退出,那么父进程就会一直等待,直到子进程退出。
父进程查看子进程状态,子进程没有退出,父进程立即返回去执行其它任务,这一次的过程叫做非阻塞。(而父进程多次来回确认子进程有没有退出的过程称为轮询)
一个测试非阻塞的程序
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/wait.h>
4 #include <sys/types.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #define NUM 10
8
9 typedef void (*func_t)();
10 func_t handlerTask[NUM];
11
12 void task1()
13 {
14 printf("do task1!\n");
15 }
16
17 void task2()
18 {
19 printf("do task2!\n");
20 }
21
22 void loadTask()
23 {
24 memset(handlerTask, 0, sizeof(handlerTask));
25 handlerTask[0] = task1;
26 handlerTask[1] = task2;
27 }
28
29 int main()
30 {
31 pid_t id = fork();
32 if(id == 0)
33 {
34 while(1)
35 {
36 //child
37 int cnt = 3;
38 while(cnt)
39 {
40 printf("child running, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);
41 sleep(1);
42 }
43
44 exit(10);
45 }
46 }
47 //parent
48 loadTask();
49 int status = 0;
50 while(1)
51 {
52 pid_t ret = waitpid(id, &status, WNOHANG); //WNOHANG: 非阻塞 -> 子进程没有退出, 父进程检测时候, 立即返回.
53 if(ret == 0)
54 {
55 //waitpid调用成功 && 子进程没退出
56 //子进程没有退出,我的waitpid没有等待失败,仅仅是检测到了子进程没退出
57 printf("wait done, but child is runing , parent do :\n");
58 int i = 0;
59 for(i = 0; handlerTask[i]!=NULL; ++i)
60 {
61 handlerTask[i]();
62 }
63 }
64 else if(ret > 0)
65 {
66 //waitpid调用成功 && 子进程退出了
67 printf("wait success, exit code: %d, sig: %d\n", (status >> 8), (status & 0x7F));
68 break;
69 }
70 else
71 {
72 //waitpid调用失败
73 printf("waitpid call failed\n");
74 break;
75 }
76 sleep(1);
77 }
78
79 return 0;
80 }
非阻塞不会占用父进程所有精力,可以在轮询期间干点别的!
本章完~