4 Linux系统中进程控制
本篇要点:
2025/12/27
①了解进程是什么,有什么特性(重点理解并发性),进程的3种状态:就绪态、运行态和阻塞态,以及3种状态的切换。
②理解进程相关命令ps和kill。
③了解进程的相关概念:父子进程(重点),祖先进程、守护进程、僵尸进程、孤儿进程。
④熟悉进程相关的函数:getpid\gitppid、 exec系列函数、ststem、fork\vfork、exit\ _exit、wait\waitpid。
目录
1.2.2.1 补充1:什么时钟频率的芯片可以带动Linux
1.2.5.1 程序计数器(Program Counter, PC)
1.2.5.3 存储器管理信息(Memory Management Information)
1.2.5.4 输入/输出状态(I/O Status Information)
1.2.5.5 进程标识符(Process ID, PID)
1.2.5.6 调度信息(Scheduling Information)
3.2.2.3 进程 0 (swapper/idle 进程)
3.3.4.1 sshd(Secure Shell Daemon)
3.3.4.3 syslogd(System Logging Daemon)
4.5.3 execlp() —— 命令参数以列表形式并自动寻找路径
4.5.4 execvp() —— 命令参数以数组形式并自动查找路径
4.4.5 execle() —— 命令参数以列表形式并可指定环境变量
4.4.6 execve() —— 命令参数以数组形式并可指定环境变量
4.6.2 父进程结束的比子进程快导致子进程的PPID不是父进程的PID的例子
4.11 waitpid() —— 等待特定子进程退出并收集退出状态
4.12.3 return、exit()、_exit() 区别
1 进程的概述
1. 1 进程的定义
进程是一个正在执行的程序,是操作系统进行资源分配和调度的基本单位。
它体现了程序的动态执行过程,和静态的程序文件不同。
main函数时一个程序的入口点。程序启动后,操作系统会把控制权交给 main 函数,标志着进程的开始。实际上,main 函数不仅是程序逻辑的起始点,也是进程动态执行的开始。可以把它看作是进程的“标志”。
进一步解释
当一个mian函数开始运行时,就启动了一个进程,主要包括两部分内容:
-
程序的执行过程 —— 指令的逐条运行。
-
所需的数据 —— 包括代码区、数据区、堆、栈以及进程运行时所占用的各种资源(如 CPU 时间、内存、文件句柄等)
1.2 进程的特性
1.2.1 动态性
进程是程序的一次执行过程,它是动态的、不断变化的。进程的生命周期包括创建、执行、挂起、终止等多个阶段,并且在执行过程中,它的状态、所使用的资源等都在不断变化。这些动态特性是操作系统调度和管理进程的关键。
特点:
-
进程的状态随着时间不断变化。
-
进程的执行时间和资源使用量是无法预知的,它可能会在执行过程中挂起或切换。
-
进程的创建和销毁是动态发生的。
1.2.2 并发性(重点)
并发性是指多个进程在同一时段内交替执行,给用户在宏观上造成“同时执行”的错觉。实际上,CPU在微观上在某一时刻只执行一个进程,但通过快速切换(时间片轮转),从而让多个进程“并发”执行。
CPU时间片切换:每个进程在分配到CPU时,通常会有一个固定的时间片,一旦时间片结束,操作系统会打断当前进程,保存它的状态,并切换到另一个进程执行。这种快速切换使得似乎有多个进程在同时运行,但实际上每次只有一个进程在执行。
1.2.2.1 补充1:什么时钟频率的芯片可以带动Linux
假如现在有进程A和进程B,CPU的时钟频率是1MHz(1 MHz = 10^6 Hz),即1秒钟1百万次时钟信号,也就是1微秒可以进行一次切换,所以进程A和进程B就可以以1微秒切换1次的时间片进行并发运行。然而对于操作系统来说,MHz量级的时钟频率还不够(32单片机的芯片的时钟频率就是MHz量级的,所以32单片机是带不动Linux操作系统的),对于能带动Linux操作系统的计算机芯片,CPU的时钟频率通常在GHz级别(1 GHz = 10^9 Hz),即每秒有数十亿次时钟信号,也就是1纳秒可以进行一次切换,这个量级的时钟频率能够在非常非常非常短的时间内频繁切换进程,确保多个进程在宏观上是同时在运行的。
1.2.2.2 补充2:并发与并行
并发(Concurrency)强调的是进程间的交替执行,用户感受到多个进程的执行是同时的,但实际上是交替进行的。它是通过任务之间的时间片切换来实现的。
并行(Parallelism)则是多个进程或线程在物理上真正同时执行,通常需要多核处理器支持。每个CPU核心可以同时执行一个进程或线程,这样多个任务可以并行运行
1.2.3 独立性
独立性是指每个进程都有自己独立的执行空间和资源,它们相互之间是隔离的,不会直接干扰到对方。
1.2.3.1 进程是资源分配和调度的最小单位
-
进程是操作系统管理和调度的基本单位。操作系统会为每个进程分配独立的资源,包括内存空间、CPU时间、I/O设备等。
-
每个进程的资源是相互独立的,因此即使是多个进程同时运行,它们之间也不会直接影响资源的使用,操作系统会负责管理和协调这些资源,确保它们不会冲突。
-
这种资源的隔离确保了进程的独立性,即一个进程的崩溃或错误不会直接影响到其他进程。比如,当一个进程由于访问非法内存而崩溃时,操作系统可以确保其他进程继续运行。
1.2.3.2 进程具有唯一的标识符(PID)
- 每个进程在操作系统中都有一个唯一的标识符,即进程ID(PID),它是操作系统用于区分和管理进程的依据。这个标识符通常是一个非负整数,并且是唯一的。

1.2.4 异步性
各进程独立运行,速度不可预知,可能相互制约,导致进程的执行呈现“间隙性”。
1.2.5 结构性
进程的结构性是指进程由多个组成部分构成,这些组成部分协同工作,帮助操作系统管理和调度进程:
-
程序(代码)
-
数据(运行所需数据)
-
进程控制块(PCB)
其中,**进程控制块(PCB)**是最为重要的部分,它记录了进程的关键信息,使操作系统能够跟踪进程的状态、资源使用情况以及执行过程。是系统为了管理进程专门设立的一个数据结构。每个正在执行的进程都会有一个独立的PCB,操作系统通过PCB来感知进程的存在并对其进行管理。
进程控制块(PCB)是系统感知进程存在的唯一标志。
PCB(进程控制块)的主要组成部分:
1.2.5.1 程序计数器(Program Counter, PC)
作用:程序计数器存储的是进程的下一条要执行的指令的地址。
-
当进程运行时,程序计数器指示着CPU从哪个地址开始读取并执行指令。
-
在上下文切换过程中,操作系统会保存当前进程的程序计数器,并在恢复时将它设置为进程被暂停时的位置,从而确保进程能继续执行。
1.2.5.2 进程的状态(Process State)
作用:进程的状态描述了进程当前所处的生命周期阶段。常见的进程状态包括:
-
New(新建):进程正在创建中,还没有被调度执行。
-
Ready(就绪):进程已准备好,等待CPU的调度。
-
Running(运行中):进程正在CPU上执行。
-
Waiting/Blocked(等待/阻塞):进程因等待某些资源(如I/O操作)而挂起,不能继续执行。
-
Terminated(终止):进程执行完毕或异常终止。
进程的状态是进程生命周期的重要指标,操作系统通过该信息来调度进程。
1.2.5.3 存储器管理信息(Memory Management Information)
作用:这一部分记录了进程在内存中的布局和使用情况。它可能包括:
-
基址寄存器(Base Register)和界限寄存器(Limit Register):用于标识进程的地址空间范围。
-
页表(Page Table):对于分页系统,存储进程虚拟地址与物理地址之间的映射关系。
-
段表(Segment Table):对于分段系统,存储段的逻辑和物理地址映射。
这部分信息对于内存管理至关重要,帮助操作系统确保进程访问合法的内存空间,并有效管理物理内存和虚拟内存。
1.2.5.4 输入/输出状态(I/O Status Information)
作用:进程在执行过程中可能会使用多个I/O设备(如打印机、磁带机、网络设备等)。这部分信息记录了进程使用的I/O资源及其状态,包括:
-
I/O设备列表:进程使用的具体I/O设备。
-
I/O请求队列:记录当前进程正在等待的I/O操作。
-
文件描述符:进程打开的文件或资源。
这部分信息帮助操作系统管理I/O设备的使用,避免不同进程间的资源冲突,并在进程切换时保存当前的I/O操作状态。
1.2.5.5 进程标识符(Process ID, PID)
作用:进程ID是操作系统为每个进程分配的唯一标识符,用于区分和管理不同的进程。
-
进程ID(PID)是系统用来调度和控制进程的基础,系统通过PID来识别进程,终止进程或管理进程间的通信。
1.2.5.6 调度信息(Scheduling Information)
作用:进程的调度信息记录了操作系统用来调度进程的各种参数。常见的调度信息包括:
-
优先级(Priority):决定进程的调度优先级,高优先级进程会被优先调度。
-
调度队列指针:指向进程所处的调度队列,帮助操作系统管理进程的调度顺序。
1.2.5.7 其他信息
-
父进程和子进程的关系:记录当前进程的父进程ID(PID)以及子进程的PID。
-
信号处理信息:进程对外部信号的响应行为(如中断处理)。
1.3 进程和程序的区别
1.3.1 程序是静态的概念,进程是动态产生和消亡的过程
-
程序:程序本身没有生命周期,它只是一段静态的代码,保存于磁盘中。程序可以被加载进内存,但它不进行任何操作,直到操作系统将其转化为进程。
-
进程:进程是程序的执行过程,它有自己的生命周期,从创建到执行,再到终止。每当一个程序被操作系统加载并执行时,它就变成了一个进程。在此过程中,进程会动态地分配资源、执行任务,并且在执行完成后会被终止。
1.3.2 存储位置
-
程序:是存储在磁盘上的文件,执行前是静态的,占用的是存储器内存,不占用系统内存资源(除非它被加载到内存中)。
-
进程:是程序在运行时的实例,它有自己的系统内存空间,会占用内存空间、CPU时间、I/O设备等资源,并且必须在系统内存中运行。
1.3.3 对应关系
1.3.3.1 一个程序 可以对应 多个进程
当一个程序被启动时,它通常会变成一个进程,但是一个程序可以启动多个进程,每个进程都可以执行该程序的不同实例或任务。这个情况在多任务操作系统中是非常常见的,尤其是在一些需要并发处理多个任务的应用中。
示例:多个进程运行同一程序
-
浏览器进程: 比如,现代的Web浏览器(如Chrome)就会为每个打开的标签页或窗口创建一个独立的进程。这些进程都运行着相同的程序(浏览器程序),但是每个进程都是独立的、互不干扰的。所以Chrome浏览器能确保某一个标签页崩溃时不会影响其他标签页
-
文本编辑器: 当你打开多个文本文件时,每个文件可能会启动一个新的进程,每个进程都运行相同的程序(比如
notepad.exe),但每个进程独立处理不同的文件。
1.3.3.2 一个进程 只能对应 一个特定的程序
一个进程在系统中只能执行一个特定的程序——这意味着每个进程都对应一个程序代码。虽然一个程序可能会被多次运行,每次运行会创建一个新的进程,但每个进程都只会执行某个特定程序的指令。
1.3.4 本质
-
程序是资源,进程是执行者。
-
没有执行的程序只是“躺着的代码”;只有当它运行时,才成为进程。
1.4 进程的三种基本状态
1.4.1 就绪态(Ready)
定义:当一个进程已经获得了除 CPU 以外的所有资源(例如内存、I/O设备等),并且准备好执行,只要 CPU 可用,它就会立即开始执行。此时,进程处于 就绪态。
特点:
- 进程已经分配到除 CPU 以外的所有资源,只差 CPU
- 一旦调度器把 CPU 分给它,就可以立刻运行。
1.4.2 运行态(Running)
定义:进程处于 执行态 时,它已经获得了 CPU 资源,并且正在执行程序中的指令。此时进程正在运行,CPU 执行的是进程的指令代码。
特点:
- 进程正在 CPU 上执行。
- 同一时刻,一个 CPU 核心只能运行一个进程。
1.4.3 阻塞态(Blocked / Waiting)
定义:进程在执行时,如果由于等待某些资源(如 I/O 操作、外部设备的响应等)而无法继续执行,它就会进入 阻塞态。阻塞态的进程无法继续执行,直到它等待的事件发生,进程才会被唤醒,转为 就绪态。
特点:
- 进程因为等待某个事件而暂时不能运行。
- 即使有 CPU 时间片,也不能运行,必须等事件完成后才能转为就绪态。
常见原因:
-
等待 I/O 完成(比如读取文件)。
-
等待缓冲区可用。
-
等待某个条件或信号(比如进程间同步)。
1.5 进程各状态间的切换

-
就绪 → 运行:进程获得 CPU。
-
运行 → 就绪:时间片用完,或被更高优先级进程抢占。
-
运行 → 阻塞:执行过程中,等待 I/O 或条件未满足。
-
阻塞 → 就绪:等待的事件完成(I/O 完成、信号到达)。
注意:
进程处于阻塞态,如果获取了事件请求,只能回到就绪态,不能回到运行态;
只有在运行态才能进入阻塞态,就绪态不能进入阻塞态。
2 进程相关命令
2.1 ps 查看进程
2.1.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.2.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:启动该进程的命令。
2.2 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;
// 获取当前进程 PID
pid = getpid();
// 输出 PID
printf("当前进程的 PID = %d\n", pid);
while(1); // 让程序保持运行,方便用 ps / kill 测试
return 0;
}
程序会打印出 PID,然后一直运行。

2.开另一个终端,可以找到该进程:
3.用 kill 结束它:
![]()
可以看到进程被杀死释放

也找不到改进程了

3 进程相关名词
3.1 父子进程
3.1.1 父子进程的定义
-
父进程(Parent Process):在操作系统中,大多数进程不是直接创建的,而是由另一个进程通过系统调用(如
fork())创建的。这个创建进程的进程被称为 父进程。在父子关系链条中,最顶层的父进程 被称为 祖先进程。 -
子进程(Child Process):由父进程创建的进程被称为 子进程。子进程是父进程的副本,虽然它会有自己独立的资源和执行环境,但最初它会继承父进程的资源。
3.1.2 子进程的资源继承
子进程在创建时,会继承父进程的大部分资源,但是也有一些独立的特性。常见的继承资源包括:
-
代码段:子进程执行的代码是父进程的代码副本。
-
数据段:子进程会继承父进程的数据段,包括静态数据和全局变量的初始值。
-
打开的文件描述符:父进程打开的文件描述符会被子进程继承。这意味着子进程可以继续读取或写入父进程打开的文件。
-
环境变量:子进程会继承父进程的环境变量,包括路径、配置等。
但是,子进程会有自己的独立资源,包括:
-
进程标识符(PID):每个进程都有独特的PID。
-
进程控制块(PCB):每个进程有自己的PCB,包含与执行相关的状态信息。
-
内存空间:尽管子进程最初会继承父进程的内存内容,但其内存空间是独立的。父子进程之间的内存不会直接共享,只有通过特定的进程间通信(IPC)机制进行交互。
3.1.3 子进程回收
-
子进程运行结束后,系统会保留它的 退出状态信息(如退出码、CPU 时间等)。
-
父进程必须调用
wait()或waitpid()来读取这些信息,并释放子进程占用的 PCB(进程控制块)等资源。 -
如果父进程不回收,子进程就会变成 僵尸进程。
3.2 祖先进程
3.2.1 祖先进程的定义
-
祖先进程(Ancestor Process) 是进程链条中,最顶层的父进程。换句话说,所有进程最终都可以追溯到一个顶层的父进程,而这个顶层的父进程就被称为祖先进程。
-
在 Linux 系统 中,所有进程最终的祖先进程是
init进程,即 PID = 1 的进程。
3.2.2 系统启动过程与祖先进程的产生
当计算机加电并启动时,操作系统的启动过程会经历一系列阶段:
3.2.2.1 BIOS/UEFI 启动
计算机加电后,BIOS(基本输入输出系统) 或 UEFI(统一可扩展固件接口) 会运行,并完成硬件的初始化。它们会从磁盘中读取引导扇区,加载操作系统的引导程序。
3.2.2.2 加载操作系统内核
引导程序将操作系统的内核加载到内存中。此时,内核完成系统的初始化,并开始执行。
3.2.2.3 进程 0 (swapper/idle 进程)
在内核初始化完成后,内核会创建第一个进程,这个进程叫做 进程 0,也叫 swapper 进程 或 idle 进程。
-
swapper 进程的主要作用是负责管理 CPU 和内存资源,它是系统中最基本的内核进程。其本质是一个空闲的进程,通常处于 睡眠状态,它会让 CPU 进入空闲状态,直到有新的任务或进程需要 CPU 资源。
3.2.2.4 创建进程 1 (init 进程)
进程 0(swapper)会创建 进程 1,即 init 进程。init 进程是操作系统启动后第一个用户态进程,它会执行系统初始化任务,启动后续的服务和用户进程。
3.3.3 进程 1 (init) 的作用
init 是系统中第一个用户态进程,也是所有其他进程的“祖先”。
-
主要职责:
-
负责系统和用户进程的初始化。
-
创建并管理各种后台服务进程。
-
启动 shell 进程,提供用户与系统交互的接口。
-
收养孤儿进程,防止产生僵尸进程。
-
3.3 守护进程
3.3.1 守护进程的定义
守护进程(Daemon)是一类特殊的进程,它通常在系统后台运行,并且独立于用户的控制终端。这些进程的作用是执行一些系统级任务或为其他进程提供服务。守护进程不会与用户直接交互,而是持续在后台运行,周期性地执行某些任务或等待特定事件的发生。
精灵进程(Sprite Process)是守护进程的另一种叫法,强调它们像“精灵”一样在系统中无声地工作。
3.3.2 特点
-
后台运行:不直接与用户交互。
-
无控制终端:不会随着终端的关闭而退出。
-
生命周期长:往往自系统启动时就运行,一直持续到系统关闭。
-
提供服务:通常为其他进程或用户提供系统级服务。
3.3.3 守护进程与普通进程的区别:
-
普通进程通常是由用户启动并与终端进行交互的,而守护进程则是在后台运行,不会直接与用户交互。
-
守护进程的生命周期通常比普通进程长得多,通常在操作系统启动时就会启动,一直到操作系统关闭。
3.3.4 常见的守护进程
守护进程的用途广泛,下面列出了一些常见的守护进程,它们为系统提供了各种基础服务:
3.3.4.1 sshd(Secure Shell Daemon)
-
功能:提供远程登录服务。
-
守护进程
sshd监听来自远程计算机的 SSH 连接请求,它允许用户通过网络安全地登录到本地计算机。无论用户是否实际使用终端,sshd进程都会保持在后台运行,等待用户的远程连接。
3.3.4.2 crond(Cron Daemon)
-
功能:定时任务调度。
-
守护进程
crond负责根据用户的配置文件(如/etc/crontab)定期运行任务。crond进程会周期性地执行用户指定的命令或脚本,如每小时备份文件、每周发送报告等。无论系统是否有人登录,crond都会定时执行任务。
3.3.4.3 syslogd(System Logging Daemon)
-
功能:系统日志服务。
-
守护进程
syslogd负责记录系统的日志消息。它会将内核、应用程序、系统服务等产生的日志信息收集到日志文件中,供系统管理员查看,帮助排查系统问题。syslogd进程会持续运行,确保系统日志信息及时保存。
3.3.4.4 httpd / nginx(Web服务器)
-
功能:提供 Web 服务。
-
作为守护进程,
httpd(Apache HTTP Server)和nginx等 Web 服务进程会在后台运行,监听来自客户端的 HTTP 请求并提供网页、资源和内容。这些进程常年运行,确保 Web 服务的正常访问。
3.3.4.5 dovecot(邮件服务)
-
功能:邮件服务。
-
dovecot是一个守护进程,用于提供 IMAP 和 POP3 邮件服务,它不断地运行,等待用户的邮件请求并处理。
3.3.4.6 mysqld(MySQL数据库守护进程)
-
功能:提供数据库服务。
-
mysqld进程是 MySQL 数据库的守护进程,负责监听客户端请求、处理数据库操作、管理数据库事务等。它通常在后台运行,持续为数据库提供支持。
3.3.5 守护进程的作用
-
保证一些系统服务长期稳定运行。
-
提供后台支持,让用户即使不操作终端,服务也能自动执行。
3.4 僵尸进程
3.4.1 僵尸进程的定义
僵尸进程(Zombie Process)是指 已经结束运行 的进程,但它仍然保留在进程表中。换句话说,虽然该进程的执行已完成,且不再占用CPU或内存资源,但它仍占用一个 PID(进程标识符)和一些少量的系统资源(如内核资源)。僵尸进程本质上是一个“垃圾进程”,因为它不再执行任何任务,但仍占用系统资源。
3.4.2 僵尸进程产生的原因
僵尸进程的产生通常是因为父进程没有及时回收子进程的退出状态信息。具体流程如下:
-
子进程退出:当一个子进程执行完毕后,它会向操作系统报告退出状态(退出码、资源使用情况等)。操作系统会保留这些信息,以便父进程可以在之后通过
wait()或waitpid()获取这些状态。 -
父进程没有回收:如果父进程仍然在运行,但没有调用
wait()或waitpid()来获取子进程的退出状态,那么该子进程就会保持在进程表中,成为僵尸进程。 -
父进程未正确回收:如果父进程没有及时回收子进程,子进程的PID和其他信息就不会被释放,导致这些进程信息继续占据系统的资源。
举个例子:
-
假设父进程A启动了子进程B,子进程B执行完成并退出,但父进程A没有调用
wait()来回收子进程B的退出状态。此时,子进程B会变成僵尸进程,并保留在进程表中。
3.4.3 僵尸进程的危害
尽管少量的僵尸进程对系统影响不大,但大量僵尸进程会引发一些问题,主要包括:
-
PID耗尽:
-
每个进程都会占用一个唯一的 PID(进程标识符)。如果系统中积累了大量僵尸进程,它们仍然占用 PID,可能会导致可用的 PID 耗尽。这样一来,新的进程就无法被创建,因为系统已经没有空闲的 PID 来分配给新的进程。
-
-
系统资源浪费:
-
尽管僵尸进程不再占用 CPU 和内存,但它们仍然占用进程表的条目,并且消耗一些内核资源。随着时间的推移,大量僵尸进程的存在会浪费系统资源,降低系统性能。
-
-
影响系统稳定性:
-
长期存在大量僵尸进程可能影响系统的健康,增加管理系统进程的复杂度,甚至导致其他重要的进程无法正常启动或运行。
-
3.3.4 解决办法
-
在父进程中调用
wait()或waitpid(),主动回收子进程。 -
如果父进程没有正确回收,可以让父进程退出,这样子进程会变为孤儿进程,由 init 进程 接管并回收,从而避免僵尸进程长期存在。
3.5 孤儿进程
3.5.1 孤儿进程的定义
如果 父进程先结束,而它的 子进程仍在运行,这些没有父进程的子进程就称为 孤儿进程。
3.5.2 孤儿进程的处理机制
在 Linux 系统中,孤儿进程并不会一直“无依无靠”。它们会被操作系统的 祖先进程(init 进程) 收养并进行管理。
3.5.3 孤儿进程的特点
-
孤儿进程 不会对系统造成危害,因为系统会自动交给
init进程托管并正确回收。 -
与僵尸进程不同,僵尸进程是因为父进程不回收造成的,而孤儿进程则是父进程先退出。
3.6 进程相关名词对比表
| 名词 | 定义 | 特点 | 是否有危害 | 典型处理方式 |
|---|---|---|---|---|
| 父子进程 | 父进程创建子进程,二者形成父子关系 | 子进程继承父进程的大部分资源,但有独立 PID | 无 | 父进程需管理、回收子进程 |
| 祖先进程 | 最顶层的父进程,在 Linux 中是 init (PID=1) | 所有进程最终的祖先,负责收养孤儿进程 | 无 | 系统启动时由内核创建 |
| 守护进程 | 在后台长期运行、无控制终端的特殊进程 | 周期性执行任务或等待事件,提供系统服务 | 无 | 系统启动时创建并常驻后台 |
| 僵尸进程 | 子进程结束但父进程未回收其资源 | 占用 PID,不能运行,系统垃圾 | 有(大量僵尸会耗尽 PID) | 父进程调用 wait()/waitpid() 回收,或交给 init 进程回收 |
| 孤儿进程 | 父进程先结束,子进程仍在运行 | 自动由 init 收养,运行正常 |
无 | 由 init 回收,不会造成危害 |
3.7 相关名词一句话总结
父子进程是进程间最基本的关系,子进程的正常回收是避免系统出现僵尸进程的关键。
Linux 中的 祖先进程 是由内核在启动时产生的 init(PID=1),它是所有进程的最早祖先,也是系统运行的核心支撑进程。
守护进程就是在后台默默运行的“精灵”,不依赖终端,负责为系统和用户提供长期服务。
僵尸进程 = 子进程已死 + 父进程未收尸。必须由父进程调用 wait() 系统调用来清理,否则就是系统垃圾。
孤儿进程 = 父进程已死 + 子进程未结束,它们会被 init 收养,不会变成系统垃圾。
4 进程控制相关函数
4.1 常见进程控制相关头文件说明
#include <sys/types.h> // 定义 pid_t 类型
#include <unistd.h> // 提供 getpid()、getppid()
#include <stdlib.h> // 提供 exit() 等
#include <sys/wait.h> // 提供 wait() 等
-
#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)—— 获取子进程的退出码
-
-
4.2 getpid() —— 获取当前进程ID
① 头文件
#include <sys/types.h> //提供pid_t
#include <unistd.h>
② 函数原型
pid_t getpid(void);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| 无 | 无 | 无参数。此函数不接受任何参数。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| 进程ID | pid_t | 返回调用进程的进程ID(PID)。此值是一个非负整数。 |
⑤ 函数功能
getpid() 用于返回当前进程的进程ID(PID)。
-
进程ID(PID)是操作系统用于唯一标识每个运行中的进程的一个标识符。
-
该函数返回当前进程的进程ID,通常用于进程间的管理、调试或与系统其他部分进行交互。
-
返回值类型是
pid_t,它通常是一个整数类型(在大多数平台上为int)。 -
对于获取父进程的进程ID,可以使用
getppid()函数。
⑥ 示例代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
//获取当前进程的PID
pid_t pid = getpid();
//输出当前进程的PID
printf("当前进程的PID : %d \n", pid);
return 0;
}

4.3 getppid() —— 获取父进程ID
① 头文件
#include <sys/types.h> //提供pid_t
#include <unistd.h>
② 函数原型
pid_t getppid(void);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| 无 | 无 | 无参数。此函数不接受任何参数。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| 父进程ID | pid_t | 返回父进程的进程ID(PID)。此值是一个非负整数。 |
⑤ 函数功能
getppid() 用于返回当前进程的父进程ID(PID)。
-
父进程ID(PPID)是操作系统用于唯一标识当前进程的父进程的标识符。
-
返回的值是当前进程的父进程的PID,通常用于进程间的管理或父子进程的通信。
-
在某些情况下(如孤儿进程),父进程的PID可能是
1,即由系统进程(如init)接管。 -
如果进程是系统进程或没有父进程,返回的父进程ID也是
1。
⑥ 示例代码
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
//获取当前进程的PID
pid_t pid = getpid();
//输出当前进程的PID
printf("当前进程的PID : %d \n", pid);
//获取当前进程的父进程的PID
pid_t ppid = getppid();
// 输出当前进程的父进程的PID
printf("当前进程的父进程的PID : %d \n", ppid);
return 0;
}

4.4 system() —— 执行系统命令
① 头文件
#include <stdlib.h>
② 函数原型
int system(const char *command);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
command |
const char* |
要执行的系统命令或程序的字符串。该命令将在子进程中执行,通常为 shell 命令。如果该参数为 NULL,则 system() 返回 -1,并设置 errno 表示出错。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
0 |
int |
命令执行成功并且返回退出状态为 0(通常表示成功)。 |
非零值 |
int |
命令执行失败,返回子进程的退出状态。返回值的具体值与命令的退出状态有关,通常可以通过 WEXITSTATUS 宏获取。 |
-1 |
int |
执行命令时发生错误,如无法创建子进程等。 |
⑤ 函数功能
system() 用于调用操作系统的命令行解释器,执行指定的命令。相当于在一个运行的进程中打开另外一个进程,运行完成之后返回原来的进程继续执行。

-
它将传入的字符串
command作为一个系统命令执行。 -
在大多数操作系统中,
system()会调用 shell(如 Linux 的sh或 Windows 的cmd.exe)来执行命令。 -
如果传入空字符串
"",它会返回 0,并不执行任何操作。 -
执行的命令会阻塞当前进程,直到命令执行完毕。
-
返回值是命令的退出状态码,通常为 0 表示成功,非 0 表示失败。如果调用出错,则返回 -1。
⑥ 示例代码
#include <stdlib.h>
#include <stdio.h>
#define COMMAND "pwd"
int main() {
//使用 system 执行 pwd 命令
int ret = system(COMMAND);
if(ret == -1) {
perror("system");
} else {
printf("成功执行 %s 命令 \n", COMMAND);
}
}

4.5 exec() 系列 —— 执行程序替换当前进程
和 system() 不一样的是,exec() 是用新进程将当前进程替换掉,执行新进程后不会返回原进程执行原进程未执行的代码。新进程会继承原进程的PID,所以新进程的PID是不变的,变化的只有执行代码。

exec 函数家族名字里通常包含 l/v/e/p:
-
l (list):参数逐个列出,最后以
NULL结尾。 -
v (vector):参数打包成
char *argv[]数组。 -
p (path):会在
PATH环境变量中搜索程序,而不需要写绝对路径。 -
e (environment):允许传入自定义环境变量
envp[]。
|
|---|
4.5.1 execl() —— 命令参数以列表形式
① 头文件
#include <unistd.h>
② 函数原型
int execl(const char *path, const char *arg, ..., (char *) NULL);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| path | const char* | 要执行的程序路径,通常是绝对路径,也可以是相对路径。 |
| arg | const char* | 启动程序时传递的第一个参数,通常是程序的名称。 |
| ... | ... | 可选的更多参数(最多可以传递多个)。 |
| NULL | (char*) NULL | 参数列表的结束标志,必须传入 NULL 以标识参数的结束。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| -1 | int | 执行失败,返回 -1,并设置 errno 以指示错误原因。 |
⑤ 函数功能
execl() 用于将当前进程替换为指定路径的程序,并且可以传递参数给新的程序。
-
该函数会替换当前进程的内存映像,即当前进程的代码、数据、堆栈等将被新程序的代码和数据替换。
-
execl()接受一个或多个命令行参数,首个参数通常是程序的名称,后续参数是程序的命令行参数,最后以NULL结束。 -
execl()不会返回,除非调用失败。如果成功,当前进程将被新程序替换,后续代码不会再执行。 -
常与
fork()一起使用,父进程创建子进程后,子进程调用execl()来执行另一个程序。 -
execl()是exec()系列函数之一,适用于需要明确指定程序路径和参数的情况。
⑥ 示例代码
#include <stdio.h>
#include <unistd.h>
int main() {
if (execl("/bin/ls", "ls", "-l", NULL) == -1) {
perror("execl failed");
return -1;
}
// 如果 execl() 成功执行,下面的代码将不会被执行
printf("This line will not be printed if execl is successful.\n");
}

也可以调用自己编译的可执行程序:

4.5.2 execv() —— 参数形式以数组形式
① 头文件
#include <unistd.h>
② 函数原型
int execv(const char *path, char *const argv[]);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| path | const char* | 要执行的程序路径,可以是绝对路径或相对路径。 |
| argv | char *const[] | 参数数组,数组中的第一个元素通常是程序的名称,后续元素为传递给程序的参数,最后必须以 NULL 结束。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| -1 | int | 执行失败,返回 -1,并设置 errno 以指示错误原因。 |
⑤ 函数功能
execv() 用于用指定路径和参数替换当前进程。
-
该函数会将当前进程的内存映像替换为指定路径的可执行文件。
-
execv()使用一个数组来传递命令行参数,数组的第一个元素通常是程序的名称,之后是程序的其他参数,最后以NULL结束。 -
成功执行后,当前进程将被替换为新程序,后续代码不会再执行。
-
execv()不接受环境变量参数,因此它适合那些需要自己设置环境变量的场景。 -
如果
execv()执行失败(例如,找不到指定的可执行文件),它返回-1并设置errno。
⑥ 示例代码
#include <stdio.h>
#include <unistd.h>
int main()
{
// 准备命令行参数
char *argv[] = {"ls", "-l", NULL};
// 使用 execv() 执行 /bin/ls 程序
if (execv("/bin/ls", argv) == -1)
{
// 如果 execv() 执行失败,打印错误信息
perror("execv");
return -1;
}
// 如果 execv() 执行成功,下面的代码将不会被执行
printf("This line will not be printed if execv is successful.\n");
return 0;
}

4.5.3 execlp() —— 命令参数以列表形式并自动寻找路径
① 头文件
#include <unistd.h>
② 函数原型
int execlp(const char *file, const char *arg, ..., (char *) NULL);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| file | const char* | 要执行的程序的文件名,可以是命令名而不是路径。 |
| arg | const char* | 要传递给程序的第一个参数,通常是程序名。 |
| ... | ... | 可选的更多参数(多个参数,最后必须以 NULL 结束)。 |
| NULL | (char*) NULL | 参数列表的结束标志,必须传入 NULL 以标识参数的结束。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| -1 | int | 执行失败,返回 -1,并设置 errno 以指示错误原因。 |
⑤ 函数功能
execlp() 是 exec() 系列函数之一,类似于 execl(),但是它会在系统的 PATH 环境变量指定的目录中查找可执行文件。
-
execlp()查找路径时会使用环境变量PATH,因此只需要指定程序的文件名,而不必提供绝对路径。 -
execlp()通过传递命令行参数来启动新程序,首先传递程序的名称作为第一个参数,然后是传递给新程序的其他命令行参数。 -
一旦
execlp()成功执行,当前进程会被新程序替换,后续的代码将不会被执行。 -
如果找不到指定的程序或其他错误,
execlp()返回-1,并设置errno。 -
与
execl()相比,execlp()具有路径查找功能,适用于仅知道程序名称而不确定程序路径的情况。
⑥ 示例代码
#include <stdio.h>
#include <unistd.h>
int main()
{
// 使用 execlp() 执行 ls 命令,传递参数 "-l"
if (execlp("ls", "ls", "-l", (char *)NULL) == -1)
{
// 如果 execlp() 执行失败
perror("execlp");
return -1;
}
// 如果 execlp() 执行成功,下面的代码将不会被执行
printf("This line will not be printed if execlp is successful.\n");
return 0;
}

4.5.4 execvp() —— 命令参数以数组形式并自动查找路径
① 头文件
#include <unistd.h>
② 函数原型
int execvp(const char *file, char *const argv[]);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| file | const char* | 要执行的程序的文件名,可以是命令名而不是路径。 |
| argv | char *const[] | 参数数组,第一个元素通常是程序的名称,后续元素是程序的命令行参数,最后必须以 NULL 结束。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| -1 | int | 执行失败,返回 -1,并设置 errno 以指示错误原因。 |
⑤ 函数功能
execvp() 是 exec() 系列函数之一,类似于 execlp(),但是它使用参数数组而非单独的参数来传递命令行参数。
-
execvp()会在系统的PATH环境变量指定的目录中查找可执行文件。 -
execvp()接受一个参数数组来传递命令行参数。数组的第一个元素通常是程序名(即执行的命令),后续元素是传递给新程序的其他参数,数组最后必须以NULL结束。 -
执行成功后,当前进程会被新程序替换,后续代码不会再执行。
-
如果找不到指定的程序或其他错误,
execvp()返回-1,并设置errno。 -
execvp()是一个非常常用的系统调用,特别是在需要执行系统命令时,并且通过PATH查找可执行文件时。
⑥ 示例代码
#include <stdio.h>
#include <unistd.h>
int main()
{
// 准备命令行参数
char *argv[] = {"ls", "-l", NULL};
// 使用 execvp() 执行 ls 命令,查找路径并传递参数
if (execvp("ls", argv) == -1)
{
// 如果 execvp() 执行失败,打印错误信息
perror("execvp");
return -1;
}
// 如果 execvp() 执行成功,下面的代码将不会被执行
printf("This line will not be printed if execvp is successful.\n");
return 0;
}

4.4.5 execle() —— 命令参数以列表形式并可指定环境变量
① 头文件
#include <unistd.h>
② 函数原型
int execle(const char *path, const char *arg, ..., (char *) NULL, char *const envp[]);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| path | const char* | 要执行的程序的路径,可以是绝对路径或相对路径。 |
| arg | const char* | 要传递给程序的第一个参数,通常是程序名。 |
| ... | ... | 可选的更多参数(多个参数,最后必须以 NULL 结束)。 |
| NULL | (char*) NULL | 参数列表的结束标志,必须传入 NULL 以标识参数的结束。 |
| envp | char* const[] | 环境变量数组(以 NULL 结束),用来传递给新程序的环境变量。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| -1 | int | 执行失败,返回 -1,并设置 errno 以指示错误原因。 |
⑤ 函数功能
execle() 是 exec() 系列函数之一,除了执行指定路径的程序外,还可以传递自定义的环境变量。
-
与
execl()类似,execle()会将当前进程替换为指定路径的程序,但不同之处在于,execle()允许传递自定义的环境变量。 -
参数列表中的环境变量通过
envp传递,这个数组包含环境变量键值对,数组以NULL结束。 -
成功执行后,当前进程将被新程序替换,后续代码不会执行。
-
如果找不到指定程序或其他错误,
execle()返回-1,并设置errno。 -
execle()是exec()系列函数之一,适用于需要修改环境变量的场景。
⑥ 示例代码
#include <stdio.h>
#include <unistd.h>
int main()
{
// 自定义环境变量
char *envp[] = {
"PATH=/bin:/usr/bin",
"USER=liao",
NULL // 环境变量数组以 NULL 结束
};
// 使用 execle() 执行 /bin/ls 程序,传递自定义环境变量
if (execle("/bin/ls", "ls", "-l", (char *)NULL, envp) == -1)
{
// 如果 execle() 执行失败,打印错误信息
perror("execle");
return -1;
}
// 如果 execle() 执行成功,下面的代码将不会被执行
printf("This line will not be printed if execle is successful.\n");
return 0;
}
"PATH=/bin:/usr/bin":
当输入ls命令时,系统会在PATH中的目录按顺序查找ls可执行文件。如果PATH被设置为/bin:/usr/bin,系统首先会在/bin目录下查找ls,如果没找到,接着会在/usr/bin中查找。"USER=liao":
设置了
USER环境变量的值为 liao。USER环境变量通常存储当前登录用户的名称,虽然它并不会改变实际用户的身份,但可以供程序使用。

4.4.6 execve() —— 命令参数以数组形式并可指定环境变量
① 头文件
#include <unistd.h>
② 函数原型
int execve(const char *path, char *const argv[], char *const envp[]);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| path | const char* | 要执行的程序的绝对路径或相对路径。 |
| argv | char *const[] | 参数数组,第一个元素通常是程序名,后续元素是程序的命令行参数,最后必须以 NULL 结束。 |
| envp | char *const[] | 环境变量数组(以 NULL 结束),用来传递给新程序的环境变量。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| -1 | int | 执行失败,返回 -1,并设置 errno 以指示错误原因。 |
⑤ 函数功能
execve() 是 exec() 系列函数之一,它提供了在指定路径下执行一个新的程序,并传递命令行参数和环境变量的功能。
-
execve()替换当前进程为指定路径的程序。它是最基础的exec()函数,没有路径查找和默认环境变量处理的功能,因此需要提供完整的路径和所需的环境变量。 -
execve()接受三个参数:一个程序路径path,一个命令行参数数组argv[],以及一个环境变量数组envp[]。 -
argv[]数组的第一个元素通常是程序的名称,后续元素是传递给新程序的其他参数,数组最后必须以NULL结束。 -
envp[]是一个环境变量数组,用来设置新程序的环境变量,数组以NULL结束。 -
成功执行时,当前进程将被新程序替换,后续代码将不再执行。如果执行失败,
execve()返回-1,并设置errno。
⑥ 示例代码
#include <stdio.h>
#include <unistd.h>
int main()
{
// 自定义环境变量
char *envp[] = {
"PATH=/bin:/usr/bin",
"USER=myuser",
NULL // 环境变量数组以 NULL 结束
};
// 准备命令行参数
char *argv[] = {"ls", "-l", NULL};
// 使用 execve() 执行 /bin/ls 程序,传递命令行参数和自定义环境变量
if (execve("/bin/ls", argv, envp) == -1)
{
// 如果 execve() 执行失败,打印错误信息
perror("execve");
return -1;
}
// 如果 execve() 执行成功,下面的代码将不会被执行
printf("This line will not be printed if execve is successful.\n");
return 0;
}

4.6 fork() —— 创建子进程
① 头文件
#include <sys/types.h> //提供pid_t
#include <unistd.h>
② 函数原型
pid_t fork(void);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| 无 | 无 | fork() 不接受任何参数。它用来创建一个新进程。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| 子进程PID | pid_t | 在父进程中,fork() 返回新创建子进程的进程ID(PID)。 |
| 0 | pid_t | 在子进程中,fork() 返回 0。 |
| -1 | pid_t | 出错时返回 -1,表示创建子进程失败。并且设置 errno 指示错误原因。 |
⑤ 函数功能
fork() 用于创建一个子进程,调用后会在父进程和子进程中分别返回不同的值。
-
父进程会接收到子进程的进程ID,而子进程会接收到 0。
-
父子进程的代码从
fork()返回的地方继续执行,因此需要通过判断fork()的返回值来区分父进程和子进程。 -
创建的子进程会复制父进程的所有资源(包括文件描述符、内存等),但是父进程和子进程的执行是并行的。
-
如果
fork()调用失败(如进程数达到系统限制),返回 -1,并设置errno。 -
父进程和子进程可以通过进程间通信(IPC)进行交互,或者父进程使用
wait()等函数等待子进程的结束。
⑥ 示例代码
4.6.1 简单示例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
//创建子进程
pid_t pid = fork();
if(pid == -1) {
perror("fork failed");
return -1;
}
else if(pid == 0) { //子进程代码
printf("这是子进程:PID:%d 父进程PID:%d\n",getpid(), getppid());
while(1);
return 0;
}
else { //父进程代码
printf("这是父进程:PID:%d 子进程PID:%d\n", getpid(), pid);
while(1);
return 0;
}
}

4.6.2 父进程结束的比子进程快导致子进程的PPID不是父进程的PID的例子
#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); } }
4.6.3 三进程轮流报数
4.6.3.1 父子孙三进程报数

代码
#include <stdio.h>
#include <unistd.h>
int main() {
int num = 0;
//创建子进程
pid_t pid1 = fork();
if(pid1 == -1) {
perror("fork child failed");
return -1;
}
else if(pid1 == 0) { //子进程代码
//创建孙进程
pid_t pid2 = fork();
if(pid2 == -1) {
perror("fork sun failed");
return -1;
}
else if(pid2 == 0) { //孙进程代码
num += 3;
for(int i = 0; i < 5; i++) {
sleep(2);
printf("孙进程:%d\n", num);
num += 3;
sleep(1);
}
}
else { //子进程代码
num += 2;
for(int i = 0; i < 5; i++) {
sleep(1);
printf("子进程:%d\n", num);
num += 3;
sleep(2);
}
}
}
else { //父进程代码
num += 1;
for(int i = 0; i < 5; i++) {
printf("父进程:%d\n", num);
num += 3;
sleep(3);
}
}
}
现象:

4.6.3.2 一父二儿三进程报数

代码:
#include <stdio.h>
#include <unistd.h>
int main() {
int num = 0;
//创建子进程1
pid_t pid1 = fork();
if(pid1 == -1) {
perror("fork child1 failed");
return -1;
}
else if(pid1 == 0) { //子进程1代码
num += 2;
for(int i = 0; i < 5; i++) {
sleep(1);
printf("父进程:%d\n", num);
num += 3;
sleep(2);
}
}
else { //父进程代码
//创建子进程2
pid_t pid2 = fork();
if(pid2 == -1) {
perror("fork child2 failed");
return -1;
}
else if(pid2 == 0) { //子进程2代码
num += 3;
for(int i = 0; i < 5; i++) {
sleep(2);
printf("孙进程:%d\n", num);
num += 3;
sleep(1);
}
}
else { //父进程代码
num += 1;
for(int i = 0; i < 5; i++) {
printf("子进程:%d\n", num);
num += 3;
sleep(3);
}
}
}
}
现象:

4.7 vfork() —— 创建子进程并等待子进程执行
vfork() —— 创建子进程并等待子进程执行
① 头文件
#include <unistd.h>
② 函数原型
pid_t vfork(void);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| 无 | 无 | vfork() 不接受任何参数。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| 子进程ID | pid_t | 如果 vfork() 在父进程中调用,它返回子进程的 PID;在子进程中返回 0。 |
| -1 | pid_t | 执行失败时返回 -1,并设置 errno 以指示错误原因。 |
⑤ 函数功能
vfork() 是一种特殊的系统调用,用于创建一个子进程,类似于 fork(),但其行为有所不同。
-
vfork()与fork()相似,都会创建一个子进程,但vfork()在创建子进程时,父进程会被挂起,直到子进程调用exec()或_exit()执行完毕后,父进程才会恢复执行。 -
vfork()通常用于创建一个子进程,并希望该子进程立即执行exec()系列函数来加载一个新程序,避免父进程和子进程同时运行带来的资源浪费。 -
在子进程执行期间,父进程会被挂起,子进程共享父进程的内存空间,直到子进程调用
exec()或_exit()。这就是为什么vfork()被称为 "轻量级 fork"。 -
vfork()主要目的是提高性能,因为它不需要复制父进程的所有内存资源(相较于fork())。但是,由于父子进程共享内存,需要注意在子进程执行时不要修改父进程的内存内容,以免造成不期望的结果。
⑥ 示例代码
vfork.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if(pid == -1) {
perror("vfork failed");
return -1;
}
else if(pid == 0) { //子进程代码
if(execl("/home/liao/stage1/1_process/fork/while1","while1", NULL) == -1) {
perror("execl failed");
return -1;
}
printf("执行子进程\n");
}
else { //父进程代码
printf("进入父进程\n");
}
}
count.c
#include <stdio.h>
#include <unistd.h>
int main() {
for(int i = 0; i < 5; i++) {
printf("while1程序执行中%d\n",i);
sleep(1);
}
return 0;
}

4.8 exit() —— 终止程序的执行
① 头文件
#include <stdlib.h>
② 函数原型
void exit(int status);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| status | int | 程序退出的状态码。通常 0 表示成功,非 0 表示错误。这个值会传递给父进程(父进程通过 wait() 或 waitpid() 可以获取) |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| 无 | void | exit() 不返回值。它终止程序的执行,并通过 status 退出状态码向操作系统报告退出状态。 |
⑤ 函数功能
exit() 用于终止当前程序的执行,并将退出状态码返回给操作系统。
-
调用
exit()时,程序会立即终止,不再执行后续代码。 -
在调用
exit()时,系统会做一些必要的清理工作,例如关闭文件描述符,释放内存等。 -
status参数可以是一个整数值,通常:-
0表示程序正常退出; -
非
0表示程序发生了错误。
-
-
调用
exit()后,程序的终止状态会被传递给操作系统,可以通过wait()等系统调用获取该状态。 -
exit()在程序退出时,会调用atexit()注册的函数,执行清理工作。这意味着你可以在程序退出前注册一些资源释放操作。
⑥ 示例代码
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Program is about to exit...");
// 退出程序并返回状态码 0(表示成功)
exit(0);
// 以下代码不会被执行
printf("This will never be printed.\n");
return 0;
}

4.9 _exit() —— 强制终止程序,不进行清理工作
① 头文件
#include <unistd.h>
② 函数原型
void _exit(int status);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| status | int | 程序退出的状态码。通常 0 表示成功,非 0 表示错误。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| 无 | void | _exit() 不返回值。它立即终止程序的执行并将退出状态码返回给操作系统。 |
⑤ 函数功能
_exit() 用于立即终止当前进程的执行,不执行正常的退出过程。
-
与
exit()不同,_exit()不会执行注册的atexit()函数,也不会进行文件流的缓冲区刷新(即不会调用fflush())。 -
它适用于子进程在执行完任务后不需要清理的场景,通常是在
fork()后的子进程中使用。 -
status参数用于指定退出状态码,0表示正常退出,非0表示错误发生。 -
_exit()是系统调用的一种,它直接通过操作系统内核终止进程,并返回给操作系统退出状态。
⑥ 示例代码
#include <stdio.h>
#include <unistd.h>
int main() {
printf("Program is about to exit...");
// 退出程序并返回状态码 0(表示成功)
_exit(0);
// 以下代码不会被执行
printf("This will never be printed.\n");
return 0;
}
没有调用fflush(),不会刷新缓存区,得加 换行符 '\n' 刷新缓存区才能输出。

4.10 wait() —— 等待子进程退出并收集退出状态
① 头文件
#include <sys/wait.h>
② 函数原型
pid_t wait(int *status);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| status | int* | 一个指向整型变量的指针,用于保存子进程的退出状态。如果不关心子进程的状态,可以传入 NULL。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| 子进程ID | pid_t | 返回子进程的 PID(进程ID)。如果成功等待到子进程退出,返回对应子进程的 PID。 |
| -1 | pid_t | 如果出错(例如没有子进程或调用失败),返回 -1,并设置 errno。 |
⑤ 函数功能
wait() 用于让父进程等待某个子进程的退出,并收集该子进程的退出状态。
-
当一个进程调用
wait()时,它会阻塞,直到有一个子进程退出。 -
status参数是一个整型指针,用于保存子进程的退出状态。如果不关心退出状态,可以传入NULL。 -
如果子进程退出,
wait()返回该子进程的进程 ID(PID)。 -
如果有多个子进程,
wait()会返回第一个退出的子进程的 PID。如果没有子进程,wait()会返回-1并设置errno。 -
在实际应用中,
wait()常用于父进程获取子进程的退出状态,或者确保父进程在子进程退出后再继续执行。
⑥ 示例代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
//创建子进程
pid_t pid = fork();
if(pid == -1) {
perror("fork failed");
return -1;
}
else if(pid == 0) { //子进程代码
printf("进入子进程\n");
_exit(0);
}
else {
int stat_loc;
printf("进入父进程\n");
pid_t child_pid = wait(&stat_loc);
printf("子进程结束,返回的子进程PID:%d,子进程的退出码为:%d\n", child_pid, stat_loc);
return 0;
}
}

4.11 waitpid() —— 等待特定子进程退出并收集退出状态
① 头文件
#include <sys/wait.h>
② 函数原型
pid_t waitpid(pid_t pid, int *status, int options);
③ 函数参数
| 函数参数 | 参数类型 | 参数功能 |
|---|---|---|
| pid | pid_t | 要等待的子进程的 PID。如果 pid 为 -1,则等待任何子进程。如果 pid 为正数,则等待指定 PID 的子进程。如果 pid 为 0,则等待与调用进程相同进程组的任何子进程。如果 pid 为负数,则等待与进程组 ID 相同的任何子进程。 |
| status | int* | 一个指向整型变量的指针,用于保存子进程的退出状态。如果不关心子进程的状态,可以传入 NULL。 |
| options | int | 控制 waitpid() 行为的选项。常用选项包括:0是阻塞等待- WNOHANG:如果没有子进程退出,waitpid() 不会阻塞并立即返回。- WUNTRACED:即使子进程没有退出,等待所有已经停止的子进程(这对于调试非常有用)。- WCONTINUED:如果子进程因接收到 SIGSTOP 信号而被暂停,waitpid() 会在子进程恢复时返回。 |
④ 返回值
| 返回值 | 返回值类型 | 返回值功能 |
|---|---|---|
| 子进程ID | pid_t | 如果 waitpid() 等待到子进程退出,返回该子进程的 PID。 |
| 0 | pid_t | 如果 WNOHANG 选项被指定,且没有子进程退出,返回 0。 |
| -1 | pid_t | 如果出错(例如没有子进程,或 pid 不合法),返回 -1,并设置 errno。 |
⑤ 函数功能
waitpid() 用于等待特定的子进程退出,并收集该子进程的退出状态。
-
与
wait()函数不同,waitpid()允许你指定要等待的特定子进程,并提供更多的控制选项。 -
pid参数可以用于指定特定的子进程 ID。如果pid为-1,则表示等待任何子进程。如果pid为正数,则表示等待指定的子进程。如果pid为0,则表示等待与调用进程相同进组的任何子进程。 -
status参数用于存储子进程的退出状态。通过宏WEXITSTATUS(status)获取子进程的退出状态。 -
options参数用于指定额外选项,例如WNOHANG可以使waitpid()在没有子进程退出时立即返回,而不会阻塞。 -
waitpid()在进程管理中具有更高的灵活性和控制力,特别适用于需要同时处理多个子进程的情况。
⑥ 示例代码
在使用 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));
}
示例 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); // 阻塞等待指定 pid
printf("Parent: child %d finished\n", wpid);
if (WIFEXITED(status)) {
printf("Child exit code=%d\n", WEXITSTATUS(status));
}
}
return 0;
}
现象:

4.12 补充:
4.12.1 fork() 和 vfork() 的异同
✅ 共同点
1.都可以用来 创建子进程。
2.都有相同的 返回值规则:
-
父进程返回子进程 PID(正整数)
-
子进程返回 0
-
出错返回 -1
❌ 不同点
1.执行顺序
-
fork():父子进程独立调度,执行顺序不确定,可以交替执行。 -
vfork():父进程会阻塞,必须等待子进程调用_exit()或exec()之后,父进程才能继续执行。
2.内存空间
-
fork():父子进程拥有各自独立的虚拟地址空间(写时拷贝机制)。 -
vfork():父子进程共享同一块地址空间(栈、数据段、堆),子进程的修改会影响父进程。
3.使用场景
-
fork():通用,适用于所有情况。 -
vfork():主要用于子进程 立即调用exec()启动新程序的场景,效率更高。
总结一句话
-
fork → 父子进程“各自一份”,并行独立。
-
vfork → 父子进程“共用一份”,子进程先跑完,父进程再继续。
4.12.2 进程在哪些情况下会被销毁
1.进程代码执行结束 —— 程序从 main() 正常返回(return),进程自然退出。
2.调用退出函数 —— 显式调用 exit() 或 _exit() 等结束当前进程。
3.外部信号终止 ——
-
用户使用
Ctrl + C(向进程发送SIGINT信号)。 -
使用
kill命令向进程发送终止信号(如SIGKILL)。
4.异常或错误 —— 进程运行过程中出现严重错误(例如非法内存访问、除零错误),操作系统会向进程发送信号(如 SIGSEGV),导致进程被销毁。
4.12.3 return、exit()、_exit() 区别
-
return:-
用于结束函数。
-
在
main()中的return实际上等价于调用exit(),但return本身只会结束函数,不是结束进程的专用方式。
-
-
exit(int status):-
功能:结束进程,会执行清理工作
-
-
_exit(int status):-
功能:立即结束进程,不会执行清理工作
-
4.12.4 为什么要等待进程结束?
1. 避免产生 僵尸进程
子进程结束时,操作系统并不会立刻把它的所有资源回收。
内核会保留一部分信息(退出状态、PID 等),让父进程可以查询。
在父进程调用
wait()或waitpid()之前,这个子进程会处于 僵尸状态 (Zombie)。如果父进程不去等待,僵尸进程会一直留在系统里,浪费系统资源。
👉 等待子进程 = 回收子进程资源。
2. 避免 孤儿进程 混乱
如果父进程不管子进程直接退出,那么子进程会变成 孤儿进程,被
init(PID=1)收养。虽然系统会替它收尸(不会长期占资源),但逻辑上可能导致业务混乱,比如:
父进程要处理子进程的计算结果。
父进程需要知道子进程是否执行成功。
👉 等待子进程可以保持 父子逻辑关系清晰。
3. 获取子进程的退出状态
父进程可以通过
wait()/waitpid()拿到子进程exit()返回的值。这样父进程就能判断子进程是:
正常退出(
WIFEXITED(status))。非正常退出(信号终止
WIFSIGNALED(status))。在很多场景下,父进程需要根据子进程的执行结果做后续处理。
👉 等待子进程 = 获取子进程的运行结果。
4. 保证父子进程的执行顺序
有时业务需要父进程必须等子进程结束之后才能继续。
wait()就是一个 同步机制。例如:
父进程让子进程计算一个结果,父进程必须等子进程退出并返回值,再继续执行。
更多推荐



所有评论(0)