目录
1.进程创建
进程 = 内核数据结构(task_strct + mm_struct + vm_area_struct ...)(侧重进程管理)+ 代码和数据(侧重进程执行),其本质就是系统内多了一个进程。并且进程具有独立性,表现为内核结构独立,代码和数据独立。
1.1fork函数
在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>
pid_t fork(void);
返回值:⼦进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核会分配新的内存块和内核数据结构给子进程;将父进程部分数据结构内容拷贝到子进程;添加子进程到系统进程列表当中;fork返回,开始调度器调度。

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方,但每个进程都将开始它们各自的行为:
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
if ( (pid=fork()) == -1 )perror("fork()"),exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
运⾏结果:
[root@localhost linux]# ./a.out
Before: pid is 43676
After:pid is 43676, fork return 43677
After:pid is 43677, fork return 0
上述代码中,进程43677没有打印before,原因是:

所以fork之前,父进程独立运行,fork之后,父子两个执行流分别执行,而fork之后谁先执行由调度器决定。
fork函数返回值,子进程返回0,父进程返回的是子进程的pid。
1.2写时拷贝
通常,父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入时,便以写时拷贝的方式各自一份副本:

因为有写时拷贝的技术存在,所以父子进程得以彻底分离,完成了进程独立性的技术保证。写时拷贝是一种延时申请技术,可以提高整机内存的使用率。
1.3fork常规用法
1.一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待客户端请求,生成子进程来处理请求。
2.一个进程要执行一个不同的程序,例如子进程从fork返回后,调用exec函数。
1.4fork调用失败的原因
1.系统中有太多的进程
2.实际用户的进程数超过了限制
1.5实际运用
使用fork + 写时拷贝技术,进行安全备份。一般用在内存数据库中,进行数据持久化,也就是保存到磁盘当中。

1 #include <stdio.h>
2 #include <unistd.h>
3 #include <time.h>
E> 4 #include <stdlib.h>
5
6 #define NUM 10
7
8 int data[NUM] = {0};
9
10 void Backup()
11 {
12 // father
13 pid_t id = fork();
14 if(id == 0)
15 {
16 // child
17 int i = 0;
18 printf("Backip: ");
19 for(i = 0; i < NUM; i++)
20 {
21 printf("%d", data[i]);
22 }
23 printf("\n");
24 sleep(10);
E> 25 exit(0); // 进程结束
26 }
27 }
28
29 void ChangeData()
30 {
31 int i = 0;
32 for(; i < NUM; i++)
33 {
E> 34 data[i] = i + rand();
35 }
36 printf("origin data: ");
37 for(i = 0; i < NUM; i++)
38 {
39 printf("%d ", data[i]);
40 }
41 printf("\n");
42 }
43
44 int main()
45 {
E> 46 srand(time(NULL));
47 while(1)
48 {
49 // 修改
E> 50 ChangeData();
51 // 备份
52 Backup();
53 sleep(5);
54 }
55 }

2.进程终止
进程终止本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
2.1退出场景
正常退出:代码跑完,运算结果正确;代码跑完,结果不正确。
异常退出:代码没跑完,出现异常(比如除零错误)(收到了信号,导致异常)。
退出信号举例:

2.2常见退出方法
从main返回;调用exit;_exit
2.2.1退出码
int main() { return 0; } ,0表示进程的退出码,表示进程执行情况。0表示成功,非0表示失败。退出码是给父进程的,如:bash或者自定义父进程。
退出码可以让父进程知道子进程将任务完成的结果如何。
示例:


关于$?:?是一个变量名,保存的是bash命令行上运行的最近一个退出进程的退出码。

常见退出码:

2.2.2strerror
将错误码转换为字符串描述的函数。


部分结果:

2.2.3错误码和退出码区别
| 维度 | 错误码 | 退出码 |
|---|---|---|
| 粒度 | 细粒度(函数级) | 粗粒度(进程级) |
| 目的 | 诊断具体错误原因 | 表示整体成功/失败 |
| 使用者 | 程序员(调试/处理) | 系统/父进程(流程控制) |
| 时间点 | 运行时 | 程序结束时 |
| 持久性 | 临时,会被覆盖 | 最终,传递给父进程 |
| 标准化 | POSIX定义部分 | Shell惯例为主 |
核心区别 :错误码用于程序内部 的错误诊断和处理,而退出码用于进程间的成功/失败通信。一个程序内部可以使用多种错误码,但最终只能有一个退出码来总结整个程序的执行结果。
衡量一个进程运行结果是否"可信",其实可以用两个数字表示:exit code、signal number,当一个进程出现异常了,退出码没有意义:signal number != 0, exit code无意义。而这两个数字会出现在僵尸进程的pcb中。

2.2.4进程正常退出
1.main函数中return
在函数中调用return是指函数调用结束,在main中调用return是指进程结束

2.调用exit()

int status就是指退出码

结果:

可见,任意地方调用exit都表示进程结束。
3.调用_exit()

其实践结果与exit()相同,说明:虽然status是int,但是仅有低8位可以被⽗进程所用。_exit(-1)时,在终端执⾏$?发现返回值是255。
但是建议使用exit(),因为exit最后也会调用_exit,但在调用_exit之前,还做了其他工作:
1.执行用户通过 atexit或on_exit定义的清理函数。
2.关闭所有打开的流,所有的缓存数据均被写入
3.调用_exit

int main()
{
printf("hello");
exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
hello[root@localhost linux]#
int main()
{
printf("hello");
_exit(0);
}
运⾏结果:
[root@localhost linux]# ./a.out
[root@localhost linux]#
3.进程等待
3.1进程等待必要性
由于子进程退出,父进程如果不管不顾,就可能造成'僵尸进程"的问题,进而造成内存泄漏。
并且,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kil-9也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是
不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2进程等待的方法
3.2.1wait方法

返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL
解决僵尸问题:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <time.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <sys/wait.h>
7 #include <sys/types.h>
8
9 int main()
10{
11 printf("父进程:pid: %d, ppid: %d\n", getpid(), getppid());
12
13 pid_t id = fork();
14 if(id < 0)
15 {
16 perror("fork");
17 exit(1);
18 }
19 if(id == 0)
20 {
21 int cnt = 5;
22 while(cnt)
23 {
24 printf("子进程:pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
25 sleep(1);
26 cnt--;
27 }
28 printf("子进程退出\n");
29 exit(10);
30 }
31
32 sleep(10);
33
34 // 父进程
35
36 pid_t rid = wait(NULL);
37 if(rid > 0)
38 {
39 printf("等待子进程成功\n");
40 }
41
42 sleep(5);
43
44 return 0;
45 }
结论:
1.原则上,一般都是要保证父进程最后退出
2.父进程要通过wait等待子进程
3.如果子进程不退出,父进程就会阻塞在wait这里,等待子进程死亡
3.2.2waitpid方法

通过pid_t pid可以让父进程获取子进程退出信息。options是等待方式的获取:1.默认阻塞等待2.非阻塞
返回值:
当正常返回的时候waitpid返回收集到的⼦进程的进程ID;
如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;
如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指⽰错误所在;
参数:
pid:
Pid=-1,等待任⼀个⼦进程。与wait等效。
Pid>0.等待其进程ID与pid相等的⼦进程。
status: 输出型参数
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)
options:默认为0,表⽰阻塞等待
WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。
waitpid(-1,status,0) == wait(status)

测试代码:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <time.h>
4 #include <stdlib.h>
5 #include <string.h>
6 #include <sys/wait.h>
7 #include <sys/types.h>
8
9 int main()
10 {
11 printf("父进程:pid: %d, ppid: %d\n", getpid(), getppid());
12
13 pid_t id = fork();
14 if(id < 0)
15 {
16 perror("fork");
17 exit(1);
18 }
19 if(id == 0)
20 {
21 int cnt = 5;
22 while(cnt)
23 {
24 printf("子进程:pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
25 sleep(1);
26 cnt--;
27 }
28 printf("子进程退出\n");
29 exit(1);
30 }
31
32 // 父进程
33 // pid_t rid = wait(NULL);
34 int status = 0;
35 pid_t rid = waitpid(id, &status, 0);
36 if(rid > 0)
37 {
38 printf("等待子进程成功, status: %d\n", status);
39 }
40
41 return 0;
42 }
运行结果发现status并没有获得退出码1:

3.2.3获取子进程status
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。如果传NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):

status当中有32为比特位,其中高16位我们不使用,又将低16位分为次低8位和低8位,其中次低8位保存退出码,第七位表示退出信号,其中一位表示core dump标志。
所以对代码做以下修改:


那么为什么退出码为1时,status为256?
因为退出码为1,也就是说明第八位为1,而其后有8个0跟着,2^8也就是256。
结论:退出码的取值范围:[0,255]
提取退出信号:

父进程获取子进程的退出信息 = 退出码 + 退出信号
3.2.4如何获取到子进程退出信息
1.子进程退出的退出信息,是维护在子进程的PCB中(包括将自己设置为僵尸)
2.父进程wait子进程,本质就是去读取子进程PCB内部的退出信息
总结:检查子进程z状态,获取子进程task_struct内部记录的子进程退出信息数据。
3.2.5waitpid中的options
0:阻塞等待。只要是内核数据结构,内部就会有维护队列,所以当等待时就是将PCB投递到要被等待对象的数据队列里,状态由R变为S
WNOHANG:宏,表示非阻塞等待

阻塞等待过程中,父进程会卡在指定位置,什么都不做,非阻塞等待过程中父子进程之间会出现非阻塞轮询过程,并且函数调用之后会立即返回。

非阻塞轮询代码:其中有用于测试的野指针
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <time.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <sys/wait.h>
8 #include <sys/types.h>
9 int main()
10 {
11 printf("父进程: pid: %d, ppid: %d\n", getpid(), getppid());
12 pid_t id = fork();
13 if(id < 0)
14 {
15 perror("fork");
16 exit(1);
17 }
18 if(id == 0)
19 {
20 int cnt = 5;
21 while(cnt)
22 {
23 printf("子进程: pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
24 sleep(5);
25 cnt--;
26 int *p = NULL;
27 *p = 100;
28 }
29 printf("子进程退出\n");
30 exit(0);
31 }
32
33 while(1)
34 {
35 //父进程
36 int status = 0;
37 pid_t rid = waitpid(id, &status, WNOHANG);
38 if(rid > 0)
39 {
40 printf("等待子进程成功,who: %d, status:%d, exit code: %d, exit sig: %d\n", rid, status, (status>>8)&0xFF, status & 0x7F);
41 break;
42 }
43 else if(rid == 0)
44 {
45 sleep(1);
46 printf("子进程还没有退出,父进程轮询\n");
47 }
48 else
49 {
50 printf("等待子进程失败,who: %d, status:%d\n", rid, status);
51 break;
52 }
53 }
54 return 0;
55 }

在父进程等待时可以做它自己的事情,代码:

1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <time.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <sys/wait.h>
8 #include <sys/types.h>
9
10 void PrintLog()
11 {
12 printf("我要打印日志!\n");
13 }
14
15 void SyncMySQL()
16 {
17 printf("我要访问数据库!\n");
18 }
19
20 void Download()
21 {
22 printf("我要下载核心数据\n");
23 }
24
25 typedef void(*task_t)();
26
27 task_t tasks[3] = {
28 PrintLog,
29 SyncMySQL,
30 Download
31 };
32
33 int main()
34 {
35 printf("父进程: pid: %d, ppid: %d\n", getpid(), getppid());
36 pid_t id = fork();
37 if(id < 0)
38 {
39 perror("fork");
40 exit(1);
41 }
42 if(id == 0)
43 {
44 int cnt = 5;
45 while(cnt)
46 {
47 printf("子进程: pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);
48 sleep(5);
49 cnt--;
50 int *p = NULL;
51 *p = 100;
52 }
53 printf("子进程退出\n");
54 exit(0);
55 }
56
57 while(1)
58 {
59 //父进程
60 int status = 0;
61 pid_t rid = waitpid(id, &status, WNOHANG);
62 if(rid > 0)
63 {
64 printf("等待子进程成功,who: %d, status:%d, exit code: %d, exit sig: %d\n", rid, status, (status>>8)&0xFF, status & 0x7F);
65 break;
66 }
67 else if(rid == 0)
68 {
69 sleep(1);
70 printf("子进程还没有退出,父进程轮询\n");
71 for(int i = 0; i < 3; i++)
72 {
73 tasks[i]();
74 }
75 }
76 else
77 {
78 printf("等待子进程失败,who: %d, status:%d\n", rid, status);
79 break;
80 }
81 }
82 return 0;
83 }

在上述代码中,若想获得退出码,还需要我们进行位操作才可以,这样的做法显然太过麻烦,此后我们使用这两个宏,也可以完成这样的状态:
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)

将上述代码中野指针取消掉:

4.编写一个一次等待多个子进程的代码

1 #include <iostream>
2 #include <cstdlib>
3 #include <cstdio>
4 #include <unistd.h>
5 #include <string>
6 #include <vector>
7 #include <sys/types.h>
8 #include <sys/wait.h>
9
10 const int gnum = 5;
11
12
13 void Work()
14 {
15 int cnt = 5;
16 while(cnt)
17 {
18 printf("%d work..., cnt: %d\n", getpid(), cnt--);
19 sleep(1);
20 }
21 }
22
23 int main()
24 {
25 std::vector<pid_t> subs;
26 for(int idx = 0; idx < gnum; idx++)
27 {
28 pid_t id = fork();
29 if(id < 0)
30 exit(1);
31 else if(id == 0)
32 {
33 //child
34 Work();
35 exit(0);
36 }
37 else
38 {
39 subs.push_back(id);
40 }
41 }
42
43 for(auto &sub : subs)
44 {
45 int status = 0;
46 pid_t rid = waitpid(sub, &status, 0);
47 if(rid > 0)
48 {
49 if(WIFEXITED(status))
50 {
51 printf("child quit normal, exit code: %d\n", WEXITSTATUS(status));
52 }
53 else
54 {
55 printf("%d child quit error!\n", sub);
56 }
57 }
58 }
59
60
61 return 0;
62 }
本章完。