一.进程创建
1.1 fork初识
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序。
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
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;
}
//执行结果
/*
Before: pid is 43676
After:pid is 43676(父进程pid), fork return 43677//fork的返回值
After:pid is 43677(子进程pid), fork return 0//fork的返回值
*/
fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
fork函数返回值:
- 子进程返回0,
- 父进程返回的是子进程的pid
fork常规用法
-
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
-
一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
fork调用失败的原因
-
系统中有太多的进程
-
实际用户的进程数超过了限制
1.2写时拷贝
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副 本。具体见下图:

二.进程终止
进程退出场景
-
代码运行完毕,结果正确
-
代码运行完毕,结果不正确
-
代码异常终止
退出知识补充
-
退出场景1和2是采用退出码(return值)来判定,可以用echo $?,来查看最近一次进程的退出码
-
谁会关心退出码呢,其实就是该进程的父进程,父进程之所以要关心其实就是要反映给用户
-
退出场景3此时我们不在关心退出码,而是关心异常中断后发出的信号(这个后面会讲是什么)
进程常见退出方法
正常终止:
-
从main的return返回
-
调用exit
-
_exit
异常退出:
- ctrl + c
正常终止补充
return和exit的区别
-
return:表示当前函数返回
-
exit:表示进程返回
exit和_exit的区别
- 前者是库函数后者是系统调用

*exit最后也会调用*exit,但在调用exit之前,还做了其他工作:
- 执行用户通过 atexit或on_exit定义的清理函数。
- 关闭所有打开的流,所有的缓存数据均被写入
- 调用_exit
因此返回只推荐使用return和exit。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。
三.进程等待
进程等待必要性
-
之前讲过,子进程退出,父进程如果不管不顾,就会造成'僵尸进程'的问题,进而造成内存泄漏。
-
另外,进程一旦变成僵尸状态,那就刀枪不入,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
-
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
-
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
了解进程等待
-
是什么
通过系统调用wait/waitpid,来进行对子进程进行状态检查和回收的功能
-
为什么
解决僵尸进程,获取子进程的退出情况
进程等待的方法
1. wait方法
cpp
// 等待任意一个子进程终止,并回收其资源
#include <sys/wait.h>
pid_t wait(int *wstatus);
// 参数:
// wstatus - 指向整型的指针,用于接收子进程的退出状态(可为 NULL)
// 返回值:
// 成功:返回已终止子进程的 PID
// 出错(如当前无子进程):返回 -1,并设置 errno
-
wait是等待任意一个子进程退出,然后对其进行回收,随后会立即返回
-
当子进程不退出的时候,那么父进程将保持这个状态直至子进程退出,这个情况叫做阻塞状态
-
如果不存在子进程,则报错返回
2. waitpid方法
cpp
// 等待指定子进程状态变化(如退出、暂停),并可选择是否阻塞
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
//返回值:
//当正常返回的时候waitpid返回收集到的子进程的进程Pid;
//如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
//如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
//参数:
// pid://只能等待当前进程的子进程,如果不是当前进程的子进程会报错
// Pid=-1,等待任意一个子进程。与wait等效
// Pid>0.等待与Pid相等的子进程。
//只讲解这两个重要的
// status:
// NULL://不关心子进程返回
// &status://想获取退出码
// options:
// 0://阻塞方式
//若Pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。
//若正常结束,则返回该子进程的Pid。
// WNOHANG://非阻塞
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
阻塞你可以理解,它不来你不走(父进程卡住);非阻塞可以理解,看到它一起走(返回子进程的Pid),没看到我自己走(父进程返回0)。
获取子进程status
-
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
-
如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
-
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

-
所以查看终止信号发现最大值是64不就刚好是用了七位
-
因此查看status是否有异常**(status&0x7F(0111 1111))**
-
看status的退出状态**((status>>8)&0xFF(1111 1111)))**
-
不过不推荐上面两种写法,只推荐下面的写法
cpp
WIFEXITED: //若为正常终止子进程返回的状态,则为真
WEXITSTATUS: //若WIFEXITED非零,提取子进程退出码
WTERMSIG: //WIFEXITED非零,用于获取杀死子进程的信号编号
if (WIFEXITED(status))
{
printf("正常退出,code = %d\n", WEXITSTATUS(status));
}
else if (WIFSIGNALED(status))
{
printf("被信号 %d 杀死\n", WTERMSIG(status));
}
非阻塞轮询
-
非阻塞:父进程不会等待waitpid的返回值,而是可以执行下面的代码
-
轮询:代码采用循环,让父进程多次询问其子进程的返回值
今天的知识都是非常非常非常重点的,重要的事情说三遍,因此一定一定要记牢,要清楚进程创建用什么,进程退出用什么,进程等待用什么!