认识fork()

头文件<unistd.h>提供的fork()函数用于从已有的原进程创建一个新的子进程,而原进程在关系式称为父进程

fork()的返回值

1
2
#include <unistd.h>
pid_t id = fork();

父子进程中fork()函数的返回值(此处用变量id储存)是不同的:

父进程id的值为子进程的PID,其值>0;子进程id值固定为0

  • id > 0 父进程
  • id == 0 子进程
  • id < 0 fork()失败

分流

https://picbed0521.oss-cn-shanghai.aliyuncs.com/blogpic/PixPin_2024-07-09_13-00-38.png

利用父子进程中fork()返回值的不同,可以用if...else...进行分流,让父子进程执行不同的代码

fork()的过程

进程调用fork,当控制转移到内核中的fork代码后,内核

  • 分配新的内存块和内核数据结构给子进程
  • 将父进程部分数据结构内容拷贝至子进程
  • 添加子进程到系统进程列表当中
  • fork返回,开始调度器调度

https://picbed0521.oss-cn-shanghai.aliyuncs.com/blogpic/PixPin_2024-07-09_18-54-41.png

当一个进程调用fork之后,就有一对二进制代码相同的父子进程。而且它们都运行到相同的地方。但每个进程都可以独立地继续运行代码,并按代码分流至不同的代码段

但这两个进程谁先执行完全由调度器决定,而谁先结束,则由实际执行情况和调度器决定

拷贝的过程 与 写时拷贝

fork时,子进程会将父进程虚拟内存的内容都复制一份在自己的虚拟内存中,但通过页表,二者映射到了同一块物理内存的区域,这样在二者都没有写入行为时,减少了物理内存中冗余的拷贝行为,有效提高了运行效率

但当父子进程中有一方发生写入行为时,就会触发写时拷贝,此时物理内存中发生拷贝行为,但只拷贝进程映射的数据段,而由于不发生进程替换时父子进程的代码段一定相同,物理内存中,代码段映射部分并不发生拷贝

https://picbed0521.oss-cn-shanghai.aliyuncs.com/blogpic/PixPin_2024-07-09_19-38-58.png

进程退出

进程退出的场景

  • 代码运行完毕,结果正确
  • 代码运行完毕,结果不正确
  • 代码异常终止

进程常见退出方法

  • main函数返回
  • exit退出
  • _exit退出

异常退出: ctrl + c

查看退出码

在终端使用命令echo $?可以查看退出码

尽管子进程返回的是int,父进程只取退出码的最低8位,所以以下三种情况

  • main函数返回-1
  • exit(-1)
  • _exit(-1)

在终端输echo $?可得退出码255

exit_eixt 辨析

main函数返回就不惜说了,来辨析一下头文件<stdlib.h>提供的exit_exit函数

_exit()

_exit直接终止进程并返回状态码,而不会执行用户定义的清理函数,也不会清理缓冲区

exit()

exit实际上最后也会调用_exit,但它会先执行一系列善后工作,顺序如下:

  1. 执行用户通过 atexit或on_exit定义的清理函数
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

https://picbed0521.oss-cn-shanghai.aliyuncs.com/blogpic/PixPin_2024-07-09_22-10-26.png

return退出: 执行return n等同于执行exit(n)

进程等待

进程等待的必要性

  • 子进程退出后需要父进程来回收僵尸进程,防止产生其引发内存泄漏等问题
  • 僵尸进程难以处理,kill -9也清理不掉
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

进程等待的方法

wait方法

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int*status);

参数:显然status是输出型参数,获取子进程的退出状态,若不关心,可传参NULL

返回值: 若成功,则返回被等待进程的pid,若失败,则返回-1

waitpid方法

返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:
pid
pid == -1,等待任一个子进程。与wait等效。
pid > 0.等待其进程ID与pid相等的子进程。
status:
输出型参数传参&status: 将子进程的状态码存入status
以下两个宏函数用于处理状态码:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
0: 阻塞等待指定pid的进程

阻塞等待

option == 0waitpid采用阻塞等待,父进程会阻塞等待到子进程退出

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main()
{
int status;
pid_t id = fork();
if(id == 0)
{
for(int i = 1;i<=5;i++)
{
printf("child running [%d]s\n",i);
sleep(1);
}
printf("child exited\n");
exit(0);
}
else
{
waitpid(id,&status,0);
if(WIFEXITED(status))
printf("father wait success, exit code: %d\n",WEXITSTATUS(status));
else
printf("child failed to exit\n");
}
return 1;
}

非阻塞轮询

option == WNOHANG时,waitpid采用非阻塞等待,若等不到子进程退出,就会继续执行后面的代码,所以一般加上while等循环用于轮询,二者共同构成非阻塞轮询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

//非阻塞轮询
int main()
{
int status;
pid_t id = fork();
if(id == 0)
{
sleep(5);
printf("child exited\n");
exit(0);
}
else
{
while(1)//轮询
{
pid_t rid = waitpid(id,&status,WNOHANG);
if(rid > 0)
{
if(WIFEXITED(status))
printf("father wait success, exit code: %d\n",WEXITSTATUS(status));
else
printf("child failed to exit\n");

break;
}
else if(rid == 0)
{
printf("等待子进程中...\n");
sleep(1);
}
else//rid<0
{
printf("wait failed\n");
break;
}
}
}
return 1;
}