1、进程创建
1.1 fork函数
在Linux中,fork函数是非常重要的函数,新进程为子进程,而原进程为父进程
cpp
#include <unistd.h>
pid_t fork(void);
返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork后,当控制转移到内核中的 fork 代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork 返回,开始调度器调度

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程
所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行,注意,fork之后,谁先执行完全由调度器决定
1.2 fork函数的返回值
- 子进程返回0
- 父进程返回的是子进程的pid
1.3 写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

父进程在创建子进程时,操作系统会把父子进程页表中数据设置成只读,所以父子只要有一方想对数据进行写入,那么一定会触发权限问题,操作系统会把数据拷贝一份,然后把只读设置为可读可写
因为有写时拷贝技术的存在,所以父子进程得以彻底分离,完成了进程独立性的技术保证
写时拷贝是⼀种延时申请技术,可以提高整机内存的使用率
1.4 fork的常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
1.5 fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
2、进程终止
2.1 进程终止的3中场景
- 代码正常跑完,结果正确
main函数执行到末尾,执行return 0 ,全程无异常,无信号杀死
- 代码正常跑完,结果不正确
代码完整执行完毕,逻辑出错主动返回错误码,return返回一个不为0的数
- 代码没跑完,进程异常终止
核心原因:被操作系统信号杀死,此时退出码没有意义
当代码运行完毕,如果结果不正确,程序要将运行结果不正确的原因显示出来,这就是main函数中return的作用,return后面的数字是程序的退出码,0表示成功无错误,不同的数字表示不同的错误原因,利用echo $ 指令可以查看上一个进程的退出码

strerror函数:利用strerror函数可以查看函数的退出码对应的信息
cpp
#include <stdio.h>
#include <string.h>
int main()
{
int i = 0;
for (; i < 200; i++)
{
printf("%d->%s\n", i, strerror(i));
}
return 0;
}

errno变量:errno是C语言提供的一个变量,里面保存的是最近一次执行的错误码
cpp
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
FILE *fp = fopen("./logtxt", "r"); // 打开文件失败
if(fp == NULL)
{
printf("%d: %s\n", errno, strerror(errno));
}
return 0;
}

2.2 进程的退出方法
- main函数内部return
- 代码任意位置调用exit
- 调用_exit
2.3 exit函数
在代码的任意地方调用exit,调用后直接终止整个进程
非main函数的return进退出当前函数,进程会继续,非main函数的exit会直接结束进程
cpp
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
void Print()
{
printf("hello world!\n");
exit(12);
}
int main()
{
printf("我是一个进程: pid:%d,ppid:%d\n",getpid(), getppid());
Print();
exit(123);
}

程序运行进入main函数,打印当前PID和PPID,调用Print()函数,执行Print()函数中的内容,执行到exit(12)时终止整个程序
2.4 _exit函数
_exit函数和exit函数类似,区别在于:exit会强制刷新所有缓冲区 ,_exit直接跳过库层,完全不刷新用户缓冲区
cpp
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("测试1");
exit(0); // 刷新缓冲,屏幕输出 测试
printf("测试2");
_exit(0); // 无换行、无手动fflush时,缓冲区丢失,无输出
}

程序只打印了测试1,说明exit在退出时会刷新缓冲区,而_exit不会
所以,_exit是系统调用,exit是库函数,exit最终会调用_exit,调用_exit前还会执行用户定义的清理函数、冲刷缓冲,关闭流等

通过上面的内容可知,缓冲区和刷新缓冲区的操作一定不在内核中,而是在用户空间中
3、进程等待
3.1 进程等待的重要性
- 子进程退出,父进程如果不管不顾,就可能造成"僵尸进程"的问题,进而造成内存泄漏
- 另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2 进程等待的方法
3.2.1 wait方法
cpp
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
- 返回值:成功返回被等待进程pid,失败返回-1
- 参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
父进程的阻塞式等待:
cpp
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// 子进程执行逻辑
int cnt = 5;
while(cnt--)
{
printf("我是子进程: pid: %d\n", getpid());
sleep(1);
}
exit(0);
}
else if(id > 0)
{
sleep(10);
pid_t rid = wait(NULL);
if(rid == id)
{
printf("pid: %d, wait success!\n",getpid());
}
sleep(5);
}
return 0;
}
子进程结束退出时会在一段时间内处于僵尸状态:

一段时间后,父进程执行wait函数,回收僵尸进程,如果成功返回就返回子进程的pid:

3.2.2 waitpid方法
cpp
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:默认为0,表示阻塞等待,WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID
注意:在status存有子进程的退出信息,如果子进程正常调用exit跑完退出,那么status记录exit(数字)里面的数字,也就是退出码,如果被信号杀死,那么status保存收到的信号
int status 是一个32位的整形图,我们只关注低16位比特位
- 正常终止:
低7位(bit0~bit6):进程正常终止,终止编号为0
bit7:core dump标志,判断是否生成崩溃转存文件
高8位(bit8~bit15):存储exit(xx)里的退出码
- 被信号杀死:
低 7 位(bit0~bit6):终止信号编号 ≠ 0
bit7:core dump 标志
高 8 位(bit8~bit15):无意义,子进程被信号杀死,退出码无效

示例:
cpp
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 5;
while(cnt--)
{
printf("我是子进程: pid: %d\n", getpid());
sleep(1);
}
exit(1);
}
else if(id > 0)
{
sleep(10);
//pid_t rid = wait(NULL);
int status = 0;
// 参数1 id:指定要等待回收的子进程PID;参数2 &status:把状态位图写入status变量;参数3:0阻塞
// 返回值rid:成功返回被回收的子进程PID;失败返回-1
pid_t rid = waitpid(id, &status, 0);
if(rid == id)
{
printf("pid: %d, wait success!, status: %d\n",getpid(), status);
}
sleep(5);
}
}

由上图可知,子进程退出码为1,所以status的结构中:高8位:00000001 ,低8位:00000000 ,二进制16位:00000001 00000000 ,换算成16进制:1*2^8=256
利用status表示退出码:(status >> 8)&0xFF
利用status表示退出信号:status&0x7F
cpp
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt--)
{
printf("我是子进程: pid: %d\n", getpid());
sleep(1);
}
exit(10);
}
else if (id > 0)
{
int status = 0;
pid_t rid = waitpid(-1, &status, 0);
if (rid > 0)
{
printf("wait success, 退出的子进程: %d, exit_code:%d ,exit sig:%d\n",
rid, (status >> 8) & 0xFF, status & 0x7F);
}
}
}

上面所呈现的都是阻塞状态下父进程等待子进程,即父进程在调用waitpid时若子进程尚未退出,操做系统检测子进程未退出,父进程就一直阻塞在这里
3.3 非阻塞等待
在阻塞状态下,如果子进程没有处于僵尸状态,那么父进程将一直处于阻塞状态,若父进程采用非阻塞等待,那么子进程没有处于僵尸状态时,父进程就可以干其他事情,在子进程变成僵尸之前,父进程要轮询检测子进程状态
此时非阻塞状态option选项要替换成宏WNOHANG
cpp
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
int cnt = 5;
while (cnt--)
{
printf("我是子进程: pid: %d\n", getpid());
sleep(1);
}
exit(10);
}
else if (id > 0)
{
while (1)
{
int status = 0;
pid_t rid = waitpid(-1, &status, WNOHANG);
if (rid > 0)
{
printf("wait success, 退出的子进程: %d, exit_code:%d\n", rid, WEXITSTATUS(status));
break;
}
else if (rid == 0)
{
printf("子进程还在运行,父进程还需要等待!\n");
usleep(100000);
}
else
{
perror("waitpid");
break;
}
}
}
}

在等待过程中父进程可以去执行自己的任务,但也要轮询子进程。父子进程哪个先执行由调度器决定,但退出时确保父进程是最后一个退出的,避免内存泄漏