linux系统中进程控制
目录
一、进程的概述
1. 进程的定义
2. 进程的特性
3. 进程和程序的区别
4.进程的三种基本状态
5.进程各状态间的切换
二、进程相关命令
ps 查看进程
kill消灭进程
三、进程相关名词
①. 父子进程
②. 祖先进程
③. 守护进程
④. 僵尸进程
⑤. 孤儿进程
进程相关名词对比表
相关名词一句话总结
四、进程控制相关函数
常见进程控制相关头文件说明
1.getpid() / getppid() ------ 获取进程(父)PID
2.system() ------ 运行进程
3. exec() ------ 替换进程
4.fork()/vfork() ------ 创建进程
fork()
三进程轮流报数
父子孙三进程报数
一父二儿三进程报数
vfork()
fork() 和 vfork() 的异同
5.exit()/_exit() ------ 销毁进程
进程在哪些情况下会被销毁
return、exit()、_exit() 区别
exit()
_exit()
6.wait()/waitpid() ------ 等待进程
为什么要等待进程结束?
一、进程的概述
1. 进程的定义
进程是一个正在执行的程序,是操作系统进行资源分配和调度的基本单位。
它体现了程序的动态执行过程,和静态的程序文件不同。
进一步解释
当一个进程开始运行时,就启动了一个动态的过程,主要包括两部分内容:
-
程序的执行过程 —— 指令的逐条运行。
-
所需的数据 —— 包括代码区、数据区、堆、栈以及进程运行时所占用的各种资源(如 CPU 时间、内存、文件句柄等)
2. 进程的特性
-
1.动态性
-
进程是程序一次执行的过程,从创建到消亡是一个动态变化的过程。
-
-
2.并发性(重点)
-
多个进程可以在宏观上“同时”运行。
-
微观上,CPU 在某一时刻只执行一个进程,但通过快速切换(时间片轮转),让用户感觉是并行执行。
-
示例:进程 A(QQ)、进程 B(Word)交替运行。
-
-
3.独立性
-
进程是系统资源分配和调度的最小单位。
-
每个进程都有唯一的标识符(PID,类型为
pid_t
,本质是一个非负整数),类似身份证,确保唯一性。
-
-
4.异步性
-
各进程独立运行,速度不可预知,可能相互制约,导致进程的执行呈现“间隙性”。
-
-
5.结构性
-
进程由三部分组成:
-
程序(代码)
-
数据(运行所需数据)
-
进程控制块(PCB)
-
-
PCB 是操作系统为管理进程设置的核心数据结构,是系统感知进程存在的唯一标志。
-
PCB(进程控制块)的主要组成部分
-
程序计数器(PC)
-
保存将要执行的下一条指令的地址。
-
-
进程状态
-
记录进程当前所处状态(如 new、running、waiting、blocked 等)。
-
-
存储器管理信息
-
包括页表、段表等,用于管理该进程占用的内存资源。
-
-
输入输出信息
-
保存该进程所使用的 I/O 设备及文件信息,例如打印机、磁带机等。
-
3. 进程和程序的区别
-
1.静态与动态
-
程序:是一个静态的文件,存放在磁盘中(可以长期保存),仅仅是指令和数据的集合。
-
进程:是程序在系统中的一次执行过程,是动态的,从创建到消亡都处于变化中。
-
-
2.存储位置
-
程序:可以以文件形式存放在外存(硬盘/SSD)中,不一定会被立即执行。
-
进程:必须加载到内存中才能运行,并且占用 CPU、内存、I/O 等资源。
-
-
3.对应关系
-
一个程序 可以对应 多个进程(比如你开了多个 QQ 窗口,每个窗口对应一个进程)。
-
一个进程 只能对应 一个特定的程序。
-
-
4.本质
-
程序是资源,进程是执行者。
-
没有执行的程序只是“躺着的代码”;只有当它运行时,才成为进程。
-
4.进程的三种基本状态
-
1.就绪态(Ready)
-
定义:进程已经分配到除 CPU 以外的所有资源,只差 CPU。
-
特点:一旦调度器把 CPU 分给它,就可以立刻运行。
-
类比:学生已经准备好考试,只差老师发试卷。
-
-
2.运行态(Running)
-
定义:进程正在 CPU 上执行。
-
特点:同一时刻,一个 CPU 核心只能运行一个进程。
-
类比:学生正在答卷。
-
-
3.阻塞态(Blocked / Waiting)
-
定义:进程因为等待某个事件(而不是因为 CPU 不够)而暂时不能运行。
-
特点:即使有 CPU 时间片,也不能运行,必须等事件完成后才能转为就绪态。
-
常见原因:
-
等待 I/O 完成(比如读取文件)。
-
等待缓冲区可用。
-
等待某个条件或信号(比如进程间同步)。
-
-
类比:学生在考试时必须等老师发参考资料才能继续写题。
-
5.进程各状态间的切换
-
就绪 → 运行:进程获得 CPU。
-
运行 → 就绪:时间片用完,或被更高优先级进程抢占。
-
运行 → 阻塞:执行过程中,等待 I/O 或条件未满足。
-
阻塞 → 就绪:等待的事件完成(I/O 完成、信号到达)。
注意:
进程处于阻塞态,如果获取了事件请求,只能回到就绪态,不能回到运行态;
只有在运行态才能进入阻塞态,就绪态不能进入阻塞态。
二、进程相关命令
ps
查看进程
1.ps -aux
-
查看系统中所有进程及其详细状态(用户、PID、CPU 占用率、内存占用率、状态等)。
-
常用于整体查看进程运行情况。
-
USER:进程的所属用户(谁启动的)。
-
PID:进程的唯一标识符(Process ID)。
-
%CPU:进程占用 CPU 的百分比。
-
%MEM:进程占用物理内存的百分比。
-
VSZ(Virtual Memory Size):虚拟内存大小(单位 KB),进程申请的虚拟地址空间总量。
-
RSS(Resident Set Size):常驻内存集大小(单位 KB),实际占用的物理内存。
-
TTY:进程关联的终端(控制台)。
-
?
表示该进程没有控制终端(例如系统守护进程)。
-
-
STAT:进程的状态标识符。
-
常见的有:
-
R:运行中(Running)
-
S:睡眠(Sleeping,可中断)
-
D:不可中断的睡眠(通常是等待 I/O)
-
T:暂停(Stopped)
-
Z:僵尸进程(Zombie)
-
-
还可能带附加符号:
-
<:高优先级
-
N:低优先级(nice 值)
-
s:会话首进程
-
l:多线程进程
-
+:位于前台进程组
-
-
-
START:进程的启动时间。
-
TIME:进程累计使用 CPU 的时间。
-
COMMAND:启动该进程的命令。
2.ps -cf
-
以树状结构显示进程之间的父子关系。
-
常用于分析进程是由谁启动的、层级关系如何。
-
UID:进程所属用户。
-
PID:进程 ID。
-
PPID(Parent PID):父进程 ID,说明该进程由哪个进程启动。
-
CLS(Scheduling Class):进程调度策略类别。
-
TS
:Time Sharing,分时调度(大多数普通进程)。 -
FF
:First in First out(实时调度)。 -
RR
:Round Robin(实时调度)。
-
-
PRI:进程优先级(数值越小优先级越高)。
-
STIME:进程的启动时间。
-
TTY:进程对应的终端。
-
TIME:进程使用 CPU 的累计时间。
-
CMD:启动该进程的命令。
kill消灭进程
kill
命令的本质
-
作用:向指定的进程 发送信号(signal)。
-
默认信号:
kill PID
默认发送的是SIGTERM (15)
,表示“请求终止”,允许进程做清理工作后再退出。 -
强制信号:
kill -9 PID
发送的是SIGKILL (9)
,表示“强制杀死”,进程立即结束,不能被捕获、阻塞或忽略。
常用信号
-
SIGTERM (15)
:正常终止进程(默认)。 -
SIGKILL (9)
:强制立即杀死进程(不可拦截)。 -
SIGSTOP (19)
:暂停进程(类似 Ctrl+Z)。 -
SIGCONT (18)
:恢复被暂停的进程。 -
SIGHUP (1)
:让进程重新读取配置文件(常用于守护进程)。
测试killl
1.编译并运行程序:
gcc test.c -o test
./test
test.c代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>int main(int argc,char *argv[])
{ pid_t pid;// 获取当前进程 PIDpid = getpid();// 输出 PIDprintf("当前进程的 PID = %d\n", pid);while(1); // 让程序保持运行,方便用 ps / kill 测试return 0;
}
程序会打印出 PID,然后一直运行。
2.开另一个终端,可以找到该进程:
3.用 kill
结束它:
可以看到进程被杀死释放
也找不到改进程了
三、进程相关名词
①. 父子进程
1.定义
-
在操作系统中,大多数进程不是凭空产生的,而是由另一个进程通过 系统调用(如
fork()
)创建的。 -
创建者进程称为 父进程,被创建的进程称为 子进程。
2.祖先进程
-
在父子关系链条中,最顶层的父进程称为 祖先进程。
-
在 Linux 系统中,所有进程最终的祖先进程是 init 进程 (PID=1),它负责收养孤儿进程。
3.资源继承
-
子进程会继承父进程的大部分资源,如:
-
代码段
-
数据段
-
打开的文件描述符
-
环境变量
-
工作目录
-
-
但子进程拥有自己独立的 PID 和运行空间。
4.子进程回收
-
子进程运行结束后,系统会保留它的 退出状态信息(如退出码、CPU 时间等)。
-
父进程必须调用
wait()
或waitpid()
来读取这些信息,并释放子进程占用的 PCB(进程控制块)等资源。 -
如果父进程不回收,子进程就会变成 僵尸进程。
②. 祖先进程
1.定义
-
在进程链条中,最顶层的父进程称为 祖先进程。
-
在 Linux 系统中,所有进程最终的祖先进程是 init 进程 (PID=1)。
2.系统启动过程与祖先进程的产生
-
计算机加电启动时,BIOS/UEFI(BOSS 😉)会从磁盘中加载 引导程序,将 Linux 内核装入内存。
-
内核初始化完成后,会创建 进程 0(又称为 swapper 进程 或 idle 进程)。
-
进程 0 随后创建 进程 1,即 init 进程。
3.进程 1 (init) 的作用
-
init
是系统中第一个用户态进程,也是所有其他进程的“祖先”。 -
主要职责:
-
负责系统和用户进程的初始化。
-
创建并管理各种后台服务进程。
-
启动 shell 进程,提供用户与系统交互的接口。
-
收养孤儿进程,防止产生僵尸进程。
-
③. 守护进程
1.定义
-
守护进程(又叫 精灵进程)是一类特殊的进程。
-
特点是 独立于控制终端,在 后台长期运行,通常会周期性地执行某些任务或等待特定事件发生。
2.特点
-
后台运行:不直接与用户交互。
-
无控制终端:不会随着终端的关闭而退出。
-
生命周期长:往往自系统启动时就运行,一直持续到系统关闭。
-
提供服务:通常为其他进程或用户提供系统级服务。
3.常见的守护进程
-
sshd
:提供远程登录服务。 -
crond
:定时任务调度。 -
syslogd
:系统日志服务。 -
httpd
/nginx
:Web 服务。
4.作用
-
保证一些系统服务长期稳定运行。
-
提供后台支持,让用户即使不操作终端,服务也能自动执行。
④. 僵尸进程
1.定义
-
僵尸进程是指 已经结束运行,但仍然在进程表中保留条目的进程。
-
它本身已经不再占用 CPU 和内存等运行资源,但会占用一个 PID 和少量系统内核资源。
-
可以把它理解为系统里的“垃圾进程”。
2.产生原因
-
当 子进程结束 时,内核会保留其退出状态信息(如退出码、资源使用情况)。
-
如果 父进程还在运行,但没有调用
wait()
或waitpid()
来回收子进程的退出信息,那么子进程就会变成 僵尸进程。
3.危害
- 少量僵尸进程问题不大,但大量僵尸进程会导致系统的 进程号(PID)耗尽,从而影响新进程的创建。
4.解决办法
-
在父进程中调用
wait()
或waitpid()
,主动回收子进程。 -
如果父进程没有正确回收,可以让父进程退出,这样子进程会变为孤儿进程,由 init 进程 接管并回收,从而避免僵尸进程长期存在。
⑤. 孤儿进程
1.定义
- 如果 父进程先结束,而它的 子进程仍在运行,这些没有父进程的子进程就称为 孤儿进程。
2.处理机制
-
在 Linux 系统中,孤儿进程不会一直“无依无靠”。
-
它们会被 祖先进程 收养。
-
当孤儿进程最终结束时,由
init
负责调用wait()
回收它们的资源。
3.特点
-
孤儿进程 不会对系统造成危害,因为系统会自动交给
init
进程托管并正确回收。 -
与僵尸进程不同,僵尸进程是因为父进程不回收造成的,而孤儿进程则是父进程先退出。
进程相关名词对比表
名词 | 定义 | 特点 | 是否有危害 | 典型处理方式 |
---|---|---|---|---|
父子进程 | 父进程创建子进程,二者形成父子关系 | 子进程继承父进程的大部分资源,但有独立 PID | 无 | 父进程需管理、回收子进程 |
祖先进程 | 最顶层的父进程,在 Linux 中是 init (PID=1) | 所有进程最终的祖先,负责收养孤儿进程 | 无 | 系统启动时由内核创建 |
守护进程 | 在后台长期运行、无控制终端的特殊进程 | 周期性执行任务或等待事件,提供系统服务 | 无 | 系统启动时创建并常驻后台 |
僵尸进程 | 子进程结束但父进程未回收其资源 | 占用 PID,不能运行,系统垃圾 | 有(大量僵尸会耗尽 PID) | 父进程调用 wait() /waitpid() 回收,或交给 init 进程回收 |
孤儿进程 | 父进程先结束,子进程仍在运行 | 自动由 init 收养,运行正常 | 无 | 由 init 回收,不会造成危害 |
相关名词一句话总结
父子进程是进程间最基本的关系,子进程的正常回收是避免系统出现僵尸进程的关键。
Linux 中的 祖先进程 是由内核在启动时产生的 init
(PID=1),它是所有进程的最早祖先,也是系统运行的核心支撑进程。
守护进程就是在后台默默运行的“精灵”,不依赖终端,负责为系统和用户提供长期服务。
僵尸进程 = 子进程已死 + 父进程未收尸。必须由父进程调用 wait()
系统调用来清理,否则就是系统垃圾。
孤儿进程 = 父进程已死 + 子进程未结束,它们会被 init
收养,不会变成系统垃圾。
四、进程控制相关函数
相关头文件
#include <sys/types.h> // 定义 pid_t 类型
#include <unistd.h> // 提供 getpid()、getppid()
#include <stdlib.h> // 提供 exit() 等
#include <sys/wait.h> // 提供 wait() 等
#include <stdio.h> // 提供 printf()
常见进程控制相关头文件说明
-
#include <sys/types.h>
-
作用:定义一些系统调用会用到的基本数据类型。
-
常见类型:
-
pid_t
—— 进程ID类型 -
uid_t
/gid_t
—— 用户ID/组ID -
off_t
—— 文件偏移量
-
-
示例:
pid_t pid = fork();
-
-
#include <unistd.h>
-
作用:POSIX 标准 API 函数原型。
-
常见函数:
-
fork()
/vfork()
—— 创建子进程 -
getpid()
/getppid()
—— 获取进程/父进程 ID -
exec*()
系列函数 —— 执行新程序 -
sleep()
/usleep()
—— 进程睡眠
-
-
示例:
pid_t pid = vfork();
-
-
#include <stdlib.h>
-
作用:提供通用的工具函数。
-
常见函数:
-
exit()
/_exit()
—— 退出进程 -
malloc()
/free()
—— 内存管理 -
atoi()
/atof()
—— 字符串转数值
-
-
示例:
exit(0);
-
-
#include <sys/wait.h>
-
作用:提供与 进程等待 相关的函数与宏。
-
常见函数:
-
wait()
—— 等待任意子进程结束 -
waitpid()
—— 等待指定子进程结束
-
-
常见宏:
-
WIFEXITED(status)
—— 判断子进程是否正常退出 -
WEXITSTATUS(status)
—— 获取子进程的退出码
-
-
-
#include <stdio.h>
-
作用:标准 I/O 函数库。
-
常见函数:
-
printf()
/scanf()
—— 格式化输入输出 -
fprintf()
—— 文件流输出
-
-
示例:
printf("pid=%d\n", getpid());
-
1.getpid()
/ getppid() ------
获取进程(父)PID
1. 头文件
#include <sys/types.h>
#include <unistd.h>
2. 作用
-
getpid()
:获取当前进程的 进程 ID (PID)。 -
getppid()
:获取当前进程的 父进程 ID (PPID)。
3. 函数原型
pid_t getpid(void);
pid_t getppid(void);
4. 参数
-
无参数。
5. 返回值
-
getpid()
:返回当前进程的 PID。 -
getppid()
:返回父进程的 PID。 -
这两个函数调用总是成功,不会返回错误(
errno
不会被设置)。
6. 示例代码
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>int main(void) {pid_t pid, ppid;// 获取当前进程IDpid = getpid();// 获取父进程IDppid = getppid();printf("当前进程ID (pid) = %d\n", pid);printf("父进程ID (ppid) = %d\n", ppid);return 0;
}
现象:
7. 注意事项
-
PID 命名空间:如果父进程在不同的 PID namespace,
getppid()
可能返回0
。 -
glibc 缓存机制:
-
从 glibc 2.3.4 起,
getpid()
内部会缓存 PID。 -
如果通过
syscall()
直接调用fork()
/clone()
,而不是 glibc 的封装函数,子进程中getpid()
可能返回错误的值(父进程 PID)。
-
-
孤儿进程:如果父进程先退出,子进程的
ppid
会变为收养它的进程 ID(通常是init
或systemd
,PID=1)。
📌 总结口诀:
-
getpid()
→ 自己是谁。 -
getppid()
→ 父亲是谁(但父亲没了就会变成“被 init/systemd 收养”)。
2.system() ------ 运行进程
1. 头文件
#include <stdlib.h>
2. 作用
-
用来在 C 程序中执行一个 shell 命令。
-
内部会调用
fork()
→execl("/bin/sh", "sh", "-c", command, …)
→waitpid()
来完成命令的执行和回收子进程。 -
相当于在程序中直接运行
sh -c "command"
。
3. 函数原型
int system(const char *command);
4. 参数
-
const char *command
:要执行的 shell 命令字符串。-
如果为
NULL
,函数只检查系统是否有可用的/bin/sh
,返回结果表示 shell 是否存在。
-
5. 返回值
-
command == NULL
:-
返回 非零值 → 系统有可用 shell;
-
返回 0 → 系统没有可用 shell。
-
-
出错时:
-
如果子进程无法创建 / 无法获取状态 → 返回
-1
。 -
如果无法执行 shell → 返回值等效于子进程调用
_exit(127)
。
-
-
正常执行时:
-
返回值是 子 shell 的退出状态(可用
WIFEXITED(status)
、WEXITSTATUS(status)
等宏分析)。 -
即命令本身的最后一条语句的退出码。
-
6.运行机制
system("ls -l");
相当于:
-
fork()
—— 创建子进程。 -
子进程调用
exec()
—— 执行命令(由/bin/sh
解释执行)。 -
父进程
wait()
—— 等待子进程结束。 -
父进程继续运行。
👉 特点:执行完成后会回到原进程,继续往下执行。
7.示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>int main(void) {pid_t pid, ppid;// 第一步:打印当前进程的PID和PPIDpid = getpid();ppid = getppid();printf("当前进程 PID = %d\n", pid);printf("父进程 PPID = %d\n", ppid);// 第二步:利用 system() 查看本进程信息printf("\n=== 使用 system() 查看进程信息 ===\n");system("ps -o pid,ppid,cmd | grep main");// 第三步:利用 system() 执行 pwd 和 ls -lprintf("\n=== 当前路径 ===\n");system("pwd");printf("\n=== 当前目录内容 ===\n");system("ls -l");return 0;
}
现象:
8. 注意事项
-
简便但低效:调用
system()
会多启动一个 shell,执行效率比exec()
低。 -
信号处理:在父进程执行命令期间,
SIGCHLD
被阻塞,SIGINT
和SIGQUIT
被忽略。 -
安全性问题:
-
不要在 setuid/setgid 程序中使用
system()
,因为环境变量可能被利用,导致安全漏洞。 -
更安全的方式是使用
exec()
系列函数。
-
-
区别返回值 127 的情况:
-
命令本身返回 127;
-
或 shell 无法执行(两种情况返回值一样,不可区分)。
-
📌 总结口诀:
-
简单场景 →
system("ls");
最方便。 -
需要控制/更安全 → 用
fork() + exec()
。
3. exec() ------ 替换进程
1. 头文件
#include <unistd.h>
2. 作用
-
功能:用新程序替换当前进程的执行代码,但 进程ID (PID) 不变。
-
注意:
-
进程的 代码段、数据段会被新程序替换。
-
进程 ID 不变,父进程ID也保持不变。
-
成功执行后,execl() 之后的代码不会被执行。
-
3.函数族(exec 系列)
exec 函数家族名字里通常包含 l/v/e/p:
|
---|
解释
l (list):参数逐个列出,最后以
NULL
结尾。execl("/bin/ls", "ls", "-l", NULL);
v (vector):参数打包成
char *argv[]
数组。char *argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv);
p (path):会在
PATH
环境变量中搜索程序,而不需要写绝对路径。execlp("ls", "ls", "-l", NULL); // 会在 PATH 中找 ls
e (environment):允许传入自定义环境变量
envp[]
。
char *argv[] = {"ls", "-l", NULL}; char *envp[] = {"PATH=/usr/bin", NULL}; execvpe("ls", argv, envp);
关键点
exec
族 不会返回(除非失败返回 -1),因为它会用新程序替换掉当前进程映像。PID 不变,只是代码和数据段换了。
常用的其实就 2 个:
execlp
(方便直接写命令,查 PATH)。
execvp
(一般配合argv[]
,更灵活)。
4. 函数原型
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /* (char *) NULL, char * const envp[] */);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
5. 参数说明
-
path / file:要执行的文件名或路径。
-
如果是
execlp/execvp/execvpe
,则file
中不包含/
时,会在PATH
环境变量指定的目录中搜索。
-
-
arg / argv[]:新程序的参数列表。
-
arg0
一般约定为程序名本身。 -
execl/execlp/execle
:参数用可变参数列出,最后必须以(char *)NULL
结尾。 -
execv/execvp/execvpe
:参数通过字符串数组argv[]
传递,以NULL
结尾。
-
-
envp[]:新程序的环境变量表(仅
execle
和execvpe
可指定)。
6. 返回值
-
成功:不返回(原进程映像完全被替换)。
-
失败:返回
-1
,同时设置errno
。
7.示例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>int main(void)
{pid_t pid, ppid;// 执行前,打印自身进程号和父进程号pid = getpid();ppid = getppid();printf("执行前:PID = %d, PPID = %d\n", pid, ppid);// 提示即将执行新程序printf("即将用 sleep 10 替换当前进程...\n");// 替换进程映像:把当前程序替换为 "sleep 10"execlp("sleep", "sleep", "10", NULL);// 如果 exec 成功,下面的 printf 永远不会执行// exec 失败时才会执行这里printf("exec 执行失败");return 1;
}
现象:
编译并运行
gcc exec_demo.c -o exec
./exec
进程的PID为8457
在程序运行时,打开另一个终端,输入:
ps -ef | grep sleep
会看到
PID 8457 和刚才 exec
打印的完全一样;
说明进程号没变,只是进程映像从 exec
变成了 sleep
。
4.fork()/vfork() ------ 创建进程
fork()
1. 头文件
#include <unistd.h>
2. 作用
-
在当前进程中创建一个 子进程。
-
子进程几乎是父进程的副本,拥有独立的 PID,独立的地址空间。
-
父子进程会从
fork()
调用处继续往下执行。
3. 函数原型
pid_t fork(void);
4. 参数
-
无参数。
5. 返回值
-
父进程中:
fork()
返回子进程的 PID(正整数)。 -
子进程中:
fork()
返回 0。 -
出错时:返回 负数(-1),表示创建失败。
6.特点
-
fork()
调用时,操作系统会把当前进程 复制一份,生成一个几乎一模一样的子进程。 -
子进程会继承父进程的代码段、数据段、堆、栈、文件描述符等。
-
但父子进程拥有各自独立的内存空间,所以变量值最初相同,但之后各自修改互不影响。
-
fork()
前的代码只执行一次(因为那时还没分叉)。 -
fork()
后的代码会被执行两次(父子进程各一次)。 -
子进程不是从 main 开始重新运行,而是从 fork() 调用点继续执行。
7.示例:
#include <unistd.h>
#include <stdio.h>int main() {printf("创建子进程\r\n");pid_t ret = fork();if(ret < 0) {printf("创建子进程失败\r\n");}else if(ret == 0) {printf("fork()的返回值在子进程为0 :ret = %d\r\n", ret);printf("我是子进程 pid = %d ppid = %d\r\n", getpid(), getppid());}else {printf("fork()的返回值在父进程为子进程的PID :ret = %d\r\n", ret);printf("我是父进程 pid = %d ppid = %d\r\n", getpid(), getppid());}}
现象:
为什么子进程的 ppid 不是 8932?
这其实是运行环境导致的:
-
当父进程结束得比子进程快时,子进程会成为“孤儿进程”。
-
孤儿进程会被操作系统的 init/systemd(PID 一般很小,比如 1 或 1378)收养。
-
所以看到子进程的 ppid 不是 8932,而是 1378(系统收养它的进程)。
-
#include <unistd.h> #include <stdio.h>int main() {printf("创建子进程\r\n");pid_t ret = fork();if(ret < 0) {printf("创建子进程失败\r\n");}else if(ret == 0) {printf("fork()的返回值在子进程为0 :ret = %d\r\n", ret);printf("我是子进程 pid = %d ppid = %d\r\n", getpid(), getppid());}else {printf("fork()的返回值在父进程为子进程的PID :ret = %d\r\n", ret);printf("我是父进程 pid = %d ppid = %d\r\n", getpid(), getppid());sleep(2);}}
三进程轮流报数
父子孙三进程报数
代码
#include <unistd.h>
#include <stdio.h>int main() {pid_t ret1 = fork();if(ret1 < 0) {printf("创建子进程失败\r\n");}else if(ret1 == 0) { //子进程pid_t ret2 = fork();if(ret2 < 0) {printf("创建孙进程失败\r\n");}else if(ret2 == 0) { //孙进程int c = 3;while (1){sleep(2);printf("grandson num is %d\r\n",c);c += 3;sleep(1);} }else { //子进程int b = 2;while (1){sleep(1);printf("son num is %d\r\n",b);b += 3;sleep(2);}} } else { //父进程int a = 1;while (1){printf("parent num is %d\r\n", a);a += 3;sleep(3);}}
}
现象:
一父二儿三进程报数
代码:
#include <unistd.h>
#include <stdio.h>int main() {pid_t ret1 = fork();if(ret1 < 0) {printf("创建子进程失败\r\n");}else if(ret1 == 0) { //子进程1 int b = 2;while (1){sleep(1);printf("son1 num is %d\r\n",b);b += 3;sleep(2);}} else { //父进程pid_t ret2 = fork();if(ret2 < 0) {printf("创建子进程2失败\r\n");}else if(ret2 == 0) { //子进程2int c = 3;while (1){sleep(2);printf("son2 num is %d\r\n",c);c += 3;sleep(1);} }else { //父进程int a = 1;while (1){printf("parent num is %d\r\n", a);a += 3;sleep(3);}}}
}
现象:
vfork()
1.头文件
#include <unistd.h>
2.作用
创建一个子进程,并让子进程先运行,父进程挂起,直到子进程结束(调用 _exit()
或 exec()
)之后父进程才恢复运行。
3.函数原型
pid_t vfork(void);
4.参数
-
无参数。
5. 返回值(和fork一样)
-
父进程中:
fork()
返回子进程的 PID(正整数)。 -
子进程中:
fork()
返回 0。 -
出错时:返回 负数(-1),表示创建失败。
6.示例:
#include <sys/types.h> // 定义 pid_t 类型
#include <unistd.h> // 提供 vfork()、getpid()、getppid()
#include <stdio.h> // 提供 printf()
#include <stdlib.h> // 提供 exit() / _exit()int main(void)
{pid_t pid;int num = 10;printf("父进程开始运行 (pid=%d, ppid=%d), num=%d\n", getpid(), getppid(), num);// 创建子进程pid = vfork();if (pid < 0) {perror("vfork error");exit(1);}else if (pid == 0) { // 子进程printf("子进程运行中 (pid=%d, ppid=%d), num=%d\n", getpid(), getppid(), num);num += 5;printf("子进程修改 num=%d\n", num);// 子进程必须用 _exit() 或 exec() 结束,避免破坏父进程栈_exit(0);}else { // 父进程// 注意:父进程会等子进程 _exit() 后才继续执行num += 10;printf("父进程继续运行 (pid=%d), num=%d\n", getpid(), num);}return 0;
}
现象
fork() 和 vfork() 的异同
✅ 共同点
-
1.都可以用来 创建子进程。
-
2.都有相同的 返回值规则:
-
父进程返回子进程 PID(正整数)
-
子进程返回 0
-
出错返回 -1
-
❌ 不同点
-
1.执行顺序
-
fork()
:父子进程独立调度,执行顺序不确定,可以交替执行。 -
vfork()
:父进程会阻塞,必须等待子进程调用_exit()
或exec()
之后,父进程才能继续执行。
-
-
2.内存空间
-
fork()
:父子进程拥有各自独立的虚拟地址空间(写时拷贝机制)。 -
vfork()
:父子进程共享同一块地址空间(栈、数据段、堆),子进程的修改会影响父进程。
-
-
3.使用场景
-
fork()
:通用,适用于所有情况。 -
vfork()
:主要用于子进程 立即调用exec()
启动新程序的场景,效率更高。
-
总结一句话
-
fork → 父子进程“各自一份”,并行独立。
-
vfork → 父子进程“共用一份”,子进程先跑完,父进程再继续。
5.exit()/_exit() ------ 销毁进程
进程在哪些情况下会被销毁
-
1.进程代码执行结束 —— 程序从
main()
正常返回(return),进程自然退出。 -
2.调用退出函数 —— 显式调用
exit()
或_exit()
等结束当前进程。 -
3.外部信号终止 ——
-
用户使用
Ctrl + C
(向进程发送SIGINT
信号)。 -
使用
kill
命令向进程发送终止信号(如SIGKILL
)。
-
-
4.异常或错误 —— 进程运行过程中出现严重错误(例如非法内存访问、除零错误),操作系统会向进程发送信号(如
SIGSEGV
),导致进程被销毁。
return、exit()、_exit() 区别
-
return
:-
用于结束函数。
-
在
main()
中的return
实际上等价于调用exit()
,但return
本身只会结束函数,不是结束进程的专用方式。
-
-
exit(int status)
:-
功能:结束进程,会执行清理工作
-
-
_exit(int status)
:-
功能:立即结束进程,不会执行清理工作
-
exit()
1.头文件
#include <stdlib.h>
exit()
是一个高级封装,属于 C 标准库,由 glibc(或其他 C 库)实现,需要包含 C 标准库的头文件 <stdlib.h>
。
2.原型
void exit(int status);
3.参数
-
int status
:退出状态码,通常0
表示正常退出,非 0 表示异常退出。 -
这个值会传递给父进程(父进程通过
wait()
或waitpid()
可以获取)。
4.示例代码
#include <stdio.h>
#include <stdlib.h>int main(void) {printf("Hello"); // 没有换行,存在缓冲区exit(0); // 会刷新缓冲区,所以 "Hello" 会被打印出来printf("World\n"); // 永远不会执行
}
现象:
exit()
会刷新标准 I/O 缓冲区,把 "Hello"
打印出来。
_exit()
1.头文件
#include <unistd.h>
_exit()是
系统调用的一部分,直接和内核交互,所以声明在 <unistd.h>
里。
2.原型
void _exit(int status);
3.参数
- 和exit()一样
-
int status
:退出状态码,通常0
表示正常退出,非 0 表示异常退出。 -
这个值会传递给父进程(父进程通过
wait()
或waitpid()
可以获取)。
4.示例代码
#include <stdio.h>
#include <unistd.h>int main(void) {printf("Hello"); // 没有换行,内容在缓冲区_exit(0); // 不会刷新缓冲区printf("World\n"); // 永远不会执行
}
现象:
_exit()
不会刷新缓冲区,所以 "Hello"
没有机会打印出来。
(空输出,什么都没有)
6.wait()/waitpid() ------ 等待进程
为什么要等待进程结束?
1. 避免产生 僵尸进程
子进程结束时,操作系统并不会立刻把它的所有资源回收。
内核会保留一部分信息(退出状态、PID 等),让父进程可以查询。
在父进程调用
wait()
或waitpid()
之前,这个子进程会处于 僵尸状态 (Zombie)。如果父进程不去等待,僵尸进程会一直留在系统里,浪费系统资源。
👉 等待子进程 = 回收子进程资源。
2. 避免 孤儿进程 混乱
如果父进程不管子进程直接退出,那么子进程会变成 孤儿进程,被
init
(PID=1)收养。虽然系统会替它收尸(不会长期占资源),但逻辑上可能导致业务混乱,比如:
父进程要处理子进程的计算结果。
父进程需要知道子进程是否执行成功。
👉 等待子进程可以保持 父子逻辑关系清晰。
3. 获取子进程的退出状态
父进程可以通过
wait()
/waitpid()
拿到子进程exit()
返回的值。这样父进程就能判断子进程是:
正常退出(
WIFEXITED(status)
)。非正常退出(信号终止
WIFSIGNALED(status)
)。在很多场景下,父进程需要根据子进程的执行结果做后续处理。
👉 等待子进程 = 获取子进程的运行结果。
4. 保证父子进程的执行顺序
有时业务需要父进程必须等子进程结束之后才能继续。
wait()
就是一个 同步机制。例如:
父进程让子进程计算一个结果,父进程必须等子进程退出并返回值,再继续执行。
1.头文件
#include <sys/wait.h>
2.作用
-
1.父进程等待子进程结束
-
通过
wait()
/waitpid()
,父进程能够阻塞等待子进程运行结束,避免子进程成为 孤儿进程。
-
-
2.回收子进程资源
-
子进程结束后,内核仍会保留其退出状态等信息。
-
如果父进程不调用
wait()
/waitpid()
来读取这些信息,子进程就会进入 僵尸状态,占用系统资源。 -
调用等待函数后,系统会清理(释放 PCB 等资源),避免僵尸进程的产生。
-
-
3.同步与结果获取
-
wait()
是一个阻塞函数,会让父进程挂起,直到某个子进程退出。 -
waitpid()
功能更灵活,可以指定等待某个子进程,甚至支持非阻塞模式(WNOHANG
),父进程能及时获取子进程的退出状态。
-
3.wait() 的函数原型
pid_t wait(int *status);
-
参数:
-
int *status
:用于保存子进程的退出状态。-
如果不关心退出状态,可以传
NULL
。
-
-
-
返回值:
-
成功:返回已结束子进程的 PID。
-
失败:返回 -1,并设置
errno
。
-
4.waitpid() 的函数原型
pid_t waitpid(pid_t pid, int *status, int options);
-
参数:
-
pid
:-
pid > 0
→ 等待指定子进程 PID。 -
pid = 0
→ 等待同进程组的任意子进程。 -
pid = -1
→ 等待任意子进程(相当于wait()
)。 -
pid < -1
→ 等待进程组 ID 为|pid|
的任意子进程。
-
-
int *status
:保存子进程退出状态。 -
int options
:选项参数-
0
→ 阻塞等待。 -
WNOHANG
→ 非阻塞等待,没有子进程退出时立即返回 0。
-
-
-
返回值:
-
成功:返回已结束子进程的 PID。
-
0
:在WNOHANG
下,没有子进程退出。 -
失败:返回 -1,并设置
errno
。
-
5.相关的宏定义
在使用 wait()
/ waitpid()
时,配合 一些宏定义 来解析 status
,这些宏定义都在:
#include <sys/wait.h>
1.判断子进程退出方式
-
WIFEXITED(status)
-
若子进程 正常退出(调用
exit()
或从main
返回),返回 非零。
-
-
WIFSIGNALED(status)
-
若子进程是被 信号终止 的(如
SIGKILL
),返回 非零。
-
-
WIFSTOPPED(status)
-
若子进程被 信号暂停(如
SIGSTOP
),返回 非零。
-
-
WIFCONTINUED(status)
(POSIX.1-2001 标准后增加)-
若子进程被
SIGCONT
信号 恢复运行,返回 非零。
-
2. 获取退出码 / 信号编号
-
WEXITSTATUS(status)
-
在
WIFEXITED(status)
为真时使用,得到子进程调用exit()
传递的 退出码(0~255)。
-
-
WTERMSIG(status)
-
在
WIFSIGNALED(status)
为真时使用,得到 导致子进程终止的信号编号。
-
-
WSTOPSIG(status)
-
在
WIFSTOPPED(status)
为真时使用,得到 导致子进程暂停的信号编号。
-
示例
if (WIFEXITED(status)) {printf("子进程正常退出,退出码 = %d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {printf("子进程被信号终止,信号编号 = %d\n", WTERMSIG(status));
} else if (WIFSTOPPED(status)) {printf("子进程被信号暂停,信号编号 = %d\n", WSTOPSIG(status));
}
6.示例代码
示例 1:wait()
回收子进程
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main(void) {pid_t ret;int status;ret = fork();if (ret < 0) {perror("fork error");return -1;}if (ret == 0) { // 子进程printf("Child: pid=%d, ppid=%d\n", getpid(), getppid());sleep(2);exit(5); // 子进程退出码 5} else { // 父进程printf("Parent: waiting child...\n");pid_t cpid = wait(&status); // 阻塞等待printf("Parent: child pid=%d ended\n", cpid);if (WIFEXITED(status)) {printf("Child exited normally, return code=%d\n", WEXITSTATUS(status));} else {printf("Child exited abnormally\n");}}return 0;
}
现象:
示例 2:waitpid()
等待指定子进程
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main(void) {pid_t pid;int status;pid = fork();if (pid < 0) {perror("fork error");return -1;}if (pid == 0) { // 子进程printf("Child running... pid=%d\n", getpid());sleep(3);exit(7); // 子进程退出码 7} else {// 父进程printf("Parent waiting for child %d...\n", pid);pid_t wpid = waitpid(pid, &status, 0); // 阻塞等待指定 pidprintf("Parent: child %d finished\n", wpid);if (WIFEXITED(status)) {printf("Child exit code=%d\n", WEXITSTATUS(status));}}return 0;
}
现象: