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