【Linux】一文看懂linux进程
文章目录
-
一、前言
-
二、什么是进程
-
三、进程与程序的区别
-
四、进程的五大特征
-
五、进程控制块详解
-
六、进程的状态与切换
-
七、进程管理全解析
-
1、进程创建
-
fork()函数详解
-
fork()原理全解析
-
父进程与子进程的区别与联系
-
vfork() 与 fork() 的区别
-
示例
-
-
2、进程等待
-
wait()函数详解
-
waitpid()函数详解
-
wait()和waitpid()对比
-
其他相关函数
-
-
3、进程终止
-
exit()函数详解
-
_exit()函数详解
-
abort()函数详解
-
exit()和_exit()和abort()对比
-
-
4、进程替换
-
exec 函数族成员详解
-
-
5、其他进程函数
-
getpid()函数详解
-
getppid()函数详解
-
示例
-
-
6、特殊进程
-
1、守护进程
-
2、僵尸进程
-
3、孤儿进程
-
4. 孤儿进程 vs. 僵尸进程
-
-
-
八、进程间通信(IPC)
-
九、写在最后
一、前言
对于开发者、运维工程师或任何希望深入 Linux 世界的人来说,掌握进程的管理与运作原理是一项不可或缺的核心技能。不理解进程,就难以真正理解操作系统是如何工作的。
本文旨在用一篇通晓的文章,为您系统地剖析 Linux 进程的方方面面。我们将从进程的定义、特征、状态与切换、原理、函数详解、特殊进程、进程间通信等方面深入全解析。
无论您是刚刚入门的新手,还是希望巩固知识的开发者,本文都将为您提供一个清晰、全面的视角,带您真正“看懂”Linux进程,从而更自信、更高效地驾驭您的Linux系统。
二、什么是进程
一个进程(Process)是正在执行的程序的一个实例。是 Linux 操作系统执行任务的基本单元,是系统资源分配和调度的基石。
你可以把它想象成一个“活着的”程序。它不仅仅是静态的代码(即程序本身),还包括了代码在执行过程中所需要的所有动态资源和状态。
程序是存储在磁盘上的静态指令集合。
进程是程序被加载到内存中执行的动态实例,它包含了代码、数据和所有的运行状态。
简单来说:程序是菜谱,进程是厨师按照菜谱做饭的整个过程(包括用了哪些厨具、食材进行到了哪一步)。
进程的引入是为了实现并发执行、资源隔离和系统资源的有效管理,它是现代操作系统实现多任务和稳定运行的核心概念。
每个进程都被分配一个独立的虚拟地址空间,这使得它感觉自己独占了系统的内存资源。一个进程通常包含以下几个部分:
- 代码段(.text):程序的机器指令(即编译后的可执行代码)。
- 数据段(.rodata、.data、.bss):包括全局变量、静态变量等。
- 堆(heap):进程运行时动态分配的内存(如 malloc 或 new 申请的内存)。
- 栈(stack):用于存储局部变量、函数参数、返回地址等。
- 进程控制块(Process Control Block):这是操作系统的核心数据结构,用来描述进程的状态和信息。下文我们会详细总结进程控制块。
因此,一个完整的进程 = 独立的地址空间(代码、数据、堆、栈) + 操作系统内核中的进程控制块。
备注:关于代码段和数据段
|
段名 |
内容 |
传统分类 |
内存权限 |
现代处理方式 |
|---|---|---|---|---|
| .text |
程序代码(机器指令) |
代码段 |
R-X (读、执行) |
独立段,但常与.rodata在同一个只读内存页 |
| .rodata |
只读数据(字符串常量、const全局变量等) |
数据段 |
R-- (只读) |
常被链接器与.text段合并到同一个“加载段” |
| .data |
已初始化的可读写全局/静态变量 |
数据段 |
RW- (读、写) |
独立的数据段 |
| .bss |
未初始化的全局/静态变量 |
数据段 |
RW- (读、写) |
紧接在.data段之后,在加载时初始化为零 |
三、进程与程序的区别
|
特征 |
程序 |
进程 |
|---|---|---|
| 本质 |
静态的 |
动态的 |
| 存在形式 |
一组指令和数据的集合。 |
程序的一次执行过程,是内存中的活动实体。 |
| 生命周期 |
永久的。只要你不删除它,它就一直在硬盘里。 |
暂时的。有创建、运行、中断、终止的过程。任务完成或强制关闭后,进程就消亡了。 |
| 资源占用 |
不占用系统运行资源(CPU、内存),只占用硬盘存储空间。 |
是系统进行资源分配和调度的一个独立单位,运行时必须为其分配内存、CPU时间片等资源。 |
| 对应关系 |
一对多。一个程序可以对应多个进程。 |
多对一。多个进程可以是同一个程序的不同执行实例。 |
| 核心 |
指令集 |
指令集 + 运行状态 + 所需资源 |
一个生动的比喻:
程序就像一张乐谱(《致爱丽丝》),它本身不会发出声音。
进程就像一位钢琴家根据乐谱演奏的过程。这个过程是动态的,有开始和结束,会占用钢琴(CPU)和场地(内存)。同一个乐谱可以被多个钢琴家同时演奏(多进程),每个演奏都是独立的。
四、进程的五大特征
进程的特征可以概括为以下五大核心点,它们相互关联,共同定义了进程的行为和本质。
1、动态性
- 核心含义:进程是程序的一次执行过程,因此它拥有一个完整的生命周期。它的状态会随着时间的推移而动态地改变。
- 详细解释:这是进程最根本的特征,也是它与“程序”最本质的区别。程序是静态的、永久的代码集合,而进程是动态的、暂时的执行活动。一个进程会经历“创建 -> 运行 -> 等待 -> 终止”等一系列状态变化。操作系统需要不断地跟踪和管理这些变化,例如保存和恢复运行现场(寄存器值、程序计数器等)。
2、并发性
- 核心含义:多个进程可以在一段时间内同时运行。
- 详细解释:这是引入进程概念的主要目的。单核CPU在任何一刻只能执行一个进程的指令。但操作系统通过进程调度,在不同的进程之间极快地切换CPU,给每个进程分配一小段运行时间(时间片)。由于切换速度非常快,在用户宏观上看来,多个进程就像在同时向前推进。并发性极大地提高了CPU的利用率和系统的整体效率。
- 重要性:没有并发性,计算机就无法实现多任务。你无法同时听音乐、浏览网页和编辑文档。
一个单核CPU在同一时刻可以运行多少个进程?一个多核CPU在同一时刻可以运行多少个进程?
无论是单核还是多核CPU,在任何一个时刻,一个CPU核心只能执行一个进程。单核CPU在同一时刻运行 1个 进程。多核CPU在同一时刻运行 其物理核心数量个 进程。
并发是宏观上同时运行多个任务。单个核心通过快速切换来实现。通过时间片轮转、上下文切换实现。
并行是微观上同时执行多个任务。多个物理核心同时工作来实现。可通过多核CPU实现
一个CPU核心在任一精确时刻只能执行一个进程的一条指令。但通过操作系统的并发调度,它可以让人感觉像是在同时运行多个进程。而要实现真正的并行同时运行多个进程,则必须依赖多个CPU核心。
3、独立性
- 核心含义:每个进程都是一个独立的资源分配和接受调度的基本单位。每个进程都拥有自己独立的虚拟地址空间和系统资源。
- 详细解释:内存独立性:进程之间是相互隔离的。一个进程通常不能直接访问另一个进程的内存数据。这种隔离是通过硬件和操作系统提供的内存管理单元来实现的。这为系统提供了巨大的稳定性和安全性保障——一个进程的崩溃(如浏览器闪退)不会影响到其他进程(如你的Word文档)。资源独立性:每个进程拥有独立的文件描述符、网络连接等资源。
- 例外:进程间通信(IPC)机制(如管道、消息队列、共享内存)是打破这种独立性、允许进程有控制地交换数据的特殊手段。
4、异步性
- 核心含义:进程以不可预知的速度向前推进。
- 详细解释:由于多个进程在并发执行,它们共享有限的系统资源(如CPU、I/O设备)。一个进程何时能够获得CPU时间片、其I/O操作何时完成,都是不可预知的,完全取决于当时的系统状况和调度策略。例如,一个进程可能因为等待键盘输入而暂停(阻塞),此时CPU会立即去执行其他就绪的进程。
- 导致的问题与解决方案:异步性可能导致与时间相关的错误(竞态条件)。为了解决这个问题,操作系统提供了进程同步机制(如信号量、互斥锁),让进程在需要协调的地方能够“同步”他们的步伐。
5、结构性
- 核心含义:进程并非一团模糊的执行流,而是由一个清晰的数据结构来描述和标识的。
- 详细解释:操作系统为了管理进程,为每一个进程都维护了一个庞大的数据结构,称为进程控制块(Process Control Block)。PCB是进程存在的唯一标志,操作系统对进程的所有操作都依赖于PCB。当一个进程被创建时,操作系统就为它创建一个PCB;当进程终止时,其PCB也随之被销毁。
- PCB中包含的信息(体现了进程的结构性):
进程标识信息:唯一的进程ID,父进程ID等。
处理器状态信息:当进程被切换时,需要保存的程序计数器、CPU寄存器等值,以便下次执行时能恢复现场。
进程调度信息:进程状态(就绪、运行、阻塞等)、优先级、等待事件等。
进程控制信息:内存地址指针、打开的文件列表、使用的I/O设备信息等。
这五个特征相互关联,共同描绘了进程的全貌:
1、动态性是进程的本质,它区分了“程序”和“进程”。
2、并发性是引入进程概念的目的,为了高效利用硬件资源。
3、独立性是实现并发性的基础和保护机制,保证了系统的稳定和安全。
4、异步性是并发执行带来的自然结果和挑战,它引出了“同步”这一重要课题。
5、结构性是操作系统管理进程的实现手段,PCB是进程在操作系统内部的“身份证”和“档案袋”。
五、进程控制块详解
进程控制块是操作系统内核中最核心的数据结构之一,理解了它就理解了操作系统如何管理和控制进程。
1、什么是进程控制块(PCB)
进程控制块是操作系统用于管理进程的专门数据结构。它也被称为PCB。
你可以把它想象成一个进程的 “档案”或 “身份证”。
-
当一个程序被加载到内存中执行时,操作系统会首先为它创建一个PCB。
-
当进程运行结束时,它的PCB也会被操作系统销毁。
核心思想:操作系统管理进程,实际上就是在管理这些PCB。PCB是进程存在的唯一标志。操作系统通过PCB来感知一个进程的存在,并依据PCB中的信息对进程进行控制和管理。
2、为什么需要PCB?
进程是动态的、并发的。为了在多个进程之间高效、准确地切换,操作系统必须记住每个进程的“当前状态”。当CPU需要切换到另一个进程时,它必须:
-
将当前进程的所有状态信息妥善保存起来。
-
再找到下一个进程之前被保存的状态,并将其恢复。
PCB就是用来保存这些“状态信息”的地方。没有PCB,操作系统就无法实现多进程的并发执行,因为每次切换后它都无法记住上一个进程执行到哪里了。
3、PCB中具体包含哪些信息
PCB是一个非常复杂的数据结构,其具体内容因操作系统而异,但通常都包含以下四大类信息:
(1)、进程标识信息
用于唯一的标识一个进程。
- 进程标识符:操作系统内部使用的唯一ID。
- 用户标识符:创建该进程的用户ID,用于权限控制和资源配额。
- 父进程标识符:指明是哪个进程创建了它,从而形成进程树(如在Linux中,pstree命令可以看到)。
(2)、处理器状态信息
这部分信息非常重要,它构成了进程的“执行现场”或“上下文”。当进程被切换时,这些数据必须被保存到其PCB中,以便下次执行时能原样恢复,就像从未中断过一样。
- 程序计数器:存放下一条要执行的指令的地址。
- CPU寄存器:通用寄存器、累加器、栈指针、状态字等所有寄存器的当前值。
- 栈指针:指向进程内核栈和用户栈的当前位置。
(3)、进程调度信息
操作系统调度程序根据这些信息来决定哪个进程应该获得CPU。
- 进程状态:指明进程当前处于何种状态(如:就绪、运行、阻塞/等待、挂起等)。
- 进程优先级:用于调度算法,优先级高的进程更优先获得CPU。
- 调度所需的其他信息:如进程已等待CPU的时间、上次执行所占用的CPU时间等。
- 事件:如果进程处于阻塞状态,这里会记录它在等待什么事件(如等待某个I/O操作完成)。
(4)、进程控制信息
用于进程管理和资源分配。
-
内存管理信息:指向该进程的页表或段表的指针。这些数据结构描述了进程的虚拟地址空间如何映射到物理内存,是进程独立性和内存保护的基础。还包括内存上下界地址、内存段的权限等。
-
资源清单:列出该进程所拥有的系统资源,以及当前已打开使用的资源。
打开文件列表:进程当前打开的所有文件描述符。
I/O设备分配情况:进程正在使用的I/O设备信息。
-
进程间通信信息:记录该进程与其他进程之间的通信链接,如消息队列、共享内存段、信号量等。
4、PCB的组织方式
系统中通常有大量的PCB(几个到几百个),操作系统需要高效地组织和管理它们。常见的方式有:
(1)、链表式:最常见的组织方式。
-
将所有处于相同状态(如所有就绪态、所有阻塞态)的PCB通过链表链接在一起。
-
例如,操作系统会维护一个就绪队列、多个等待队列(每个等待事件一个队列,如等待磁盘I/O一个队列,等待键盘输入一个队列)。
-
这种方式插入和删除PCB非常高效,便于调度。
(2)、索引式:
-
根据进程状态建立索引表(如就绪索引表、阻塞索引表)。
-
索引表项指向对应状态的PCB。
-
这种方式也便于对某一类状态的进程进行管理。
5、PCB的生命周期与关键作用
(1)、创建进程:当 fork() 或 exec() 等系统调用被执行时,操作系统第一步就是在内存中为其分配一个PCB,并初始化其中的各项信息。
(2)、执行进程:调度程序从就绪队列中选中一个进程,执行上下文切换:将PCB中保存的处理器状态信息(寄存器、程序计数器等)加载到CPU中,进程开始/恢复执行。
(3)、中断/切换进程:当进程的时间片用完或需要等待I/O时,CPU硬件会产生一个中断。操作系统将当前CPU的所有寄存器值保存到该进程的PCB中,然后选择另一个进程执行。
(4)、终止进程:进程执行完毕或被迫终止,操作系统回收其占用的所有资源,并最终销毁其PCB。
六、进程的状态与切换
因为CPU核心数量远少于进程数量,所以操作系统需要像一位高效的调度员,让多个进程在一颗或几颗CPU上“同时”运行(宏观并行,微观串行)。这就引出了进程的状态 和切换。
1. 进程的状态
在任何操作系统中,进程至少包含三种基本状态。这是一个最经典、最简单的模型(三态模型):
【1】. 运行态
- 含义:进程正在CPU上执行指令。
- 细节:在单核CPU系统中,任何时刻最多只有一个进程处于运行状态。在多核系统中,则可以有多个进程同时处于运行状态(每个核心一个)。
【2】. 就绪态
- 含义:进程已经具备了除CPU以外的所有运行所需的资源,万事俱备,只欠“CPU”。
- 细节:这些进程已经加载到内存中,可以立即投入运行,只是因为CPU正在执行其他进程,所以暂时等待。操作系统维护一个就绪队列,按照某种调度算法(如先来先服务、优先级、时间片轮转等)从队列中选择下一个要运行的进程。
【3】. 阻塞态
- 含义:进程正在等待某个事件的发生,在事件发生之前无法继续执行,即使CPU空闲也不会运行。
- 细节:导致阻塞的典型事件包括:等待用户输入、等待磁盘I/O操作完成、等待网络数据包、等待另一个进程释放锁(如信号量)等。操作系统会为不同类型的事件维护多个阻塞队列。
这三种状态及其转换关系可以用下图清晰地表示:

在实际的操作系统中,为了更精细地管理,特别是处理内存不足的情况,通常会引入两个额外的状态,形成五态模型:
【4】. 创建态
- 含义:进程正在被创建( fork()或 exec()等系统调用被执行),但尚未完成。操作系统正在为其分配PCB(进程控制块)、加载程序到内存等,这个过程尚未完成。
【5】. 终止态
- 含义:进程已经结束执行(正常结束或被强制杀死)。此时,进程不再运行,但其PCB和一些资源统计数据(如CPU时间)仍然保留着,直到其父进程读取完这些信息后(通过wait()系统调用),该进程才会彻底从系统中消失。这个状态也称为“僵尸态”。
加入了创建和终止状态后,状态图变得更加完整:

2. 进程状态的切换
进程状态之间的切换是由操作系统内核操控的,由操作系统事件触发(如调度、I/O请求、时间片用完),是操作系统进行资源管理和多任务并发的核心机制。
进程状态切换是保存当前进程的上下文到其PCB,并恢复下一个进程的上下文到CPU。
切换过程需要CPU时间,是操作系统开销的一部分,过于频繁的切换会影响性能。
主要状态转换如下:
|
状态转换 |
触发事件 |
|---|---|
| NULL → 创建态 |
执行了创建新进程的系统调用(如fork())。 |
| 创建态 → 就绪态 |
操作系统完成了进程的创建工作,将其加入就绪队列,等待调度。 |
| 就绪态 → 运行态 |
进程调度发生。操作系统从就绪队列中选择该进程,并为其分配CPU。 |
| 运行态 → 就绪态 |
最常见的原因是时间片用完(Time-out)。或者一个有更高优先级的进程就绪,抢占了当前进程的CPU。 |
| 运行态 → 阻塞态 |
进程主动请求操作系统提供服务,而这个服务需要等待(如read() 系统调用等待I/O)。或者申请一个当前无法获取的资源(如锁)。 |
| 阻塞态 → 就绪态 |
进程所等待的事件发生了(如I/O操作完成、获取到了锁)。操作系统将其从阻塞队列移回就绪队列。 |
| 运行态 → 终止态 |
进程执行完毕自行退出,或因为出现错误、被其他进程杀死而强制终止。 |
七、进程管理全解析
进程管理本身相关的主要函数,这些函数主要涉及进程的创建、等待、终止、替换、获取属性等。
1. 进程创建
在 Linux 中,创建一个新进程的核心系统调用是 fork()。此外,exec() 系列函数通常与 fork() 结合使用,用于启动全新的程序。
fork()函数详解
- 函数原型
#include <unistd.h>pid_t fork(void);
-
作用
创建子进程。 fork() 通过复制调用进程来创建一个新进程。新进程称为子进程。调用进程称为父进程。 -
参数
无 -
返回值
1. 成功时,在父进程中返回子进程的 PID(一个大于0的正整数),在子进程中返回 0。失败时,在父进程中返回 -1,不创建子进程,并设置 errno 以指示错误。
2. pid_t 是一个整数类型,它的具体大小和表示范围取决于系统和编译器,但通常是 int 或 long 类型。
fork()原理全解析
-
fork()创建子进程的本质
1. fork 是Linux中用于创建新进程的一个系统调用。它的核心特性是:通过复制调用它的进程(称为父进程)来创建一个新的进程(称为子进程)。调用 fork() 的进程称为 父进程。新创建的进程称为 子进程。
2. 最关键的一点是:子进程几乎是父进程的一个完全相同副本。fork() 的工作就是“复制”,它本身不执行新的程序。
3. 子进程是父进程的副本,它获得父进程的:
数据空间的副本(堆、栈、初始化的数据段、未初始化的数据段)
代码段的副本(共享只读,现代操作系统通常使用写时复制技术优化)
文件描述符表的副本
实际用户ID、组ID,有效用户ID、组ID
进程组ID、会话ID
当前工作目录、根目录
信号处理方式(忽略、默认、捕获的信号集)
环境变量
-
调用一次,返回两次
1. 一次调用在父进程和子进程中都返回,但返回值不同。通过检查 fork 的返回值,程序就可以分流,让父进程和子进程执行不同的代码。
2. fork() 并非真正返回了两次,而是被调用了一次,但通过内核的进程复制和精巧的上下文设置,之后有两个独立的进程(父进程和子进程)都从这次调用中继续执行,因为两个进程都从同一条指令,即fork() 调用之后的下一条指令,开始执行代码,所以从程序员的视角看,就像是 fork() 调用在那条指令上同时返回了两个不同的值,因此每个进程都会得到一次返回。
-
执行顺序
fork() 之后,父进程和子进程的执行顺序是不确定的(实际上它是由操作系统调度器决定)。 -
写时复制(Copy-On-Write, COW)
为了效率,Linux 在 fork() 后并不会立即复制父进程的整个地址空间。内核会先将父进程的页表设置为只读。只有当父或子进程尝试修改某个数据页时,内核才会为该页创建一个副本。这避免了不必要的拷贝,大大提高了 fork() 的效率。
父进程与子进程的区别与联系
如上文所讲,fork()时,子进程"复制"了一份父进程,在linux中,fork()是一个轻量级的"复制"。父进程与子进程的区别与联系如下:
|
具体属性 |
继承性 |
说明与关键细节 |
|---|---|---|
| 代码段
(.text)和只读数据段(.rodata) |
共享 |
父子进程执行相同的程序代码。该段通常是只读的,物理内存中仅存一份。 |
| 数据段
(.data、.bss、)、堆(.heap)、栈(.stack) |
Copy-On-Write |
核心机制:fork()后,父子进程数据在逻辑上独立。现代OS使用写时复制(COW) 实现:内存页初始共享,只有当任一进程试图修改数据时,内核才会为该进程复制相应的内存页。效率极高。 |
| 打开的文件描述符 |
共享 |
极其重要
:子进程获得父进程所有打开文件描述符的副本,但这些副本指向内核中同一个“打开文件描述”结构体。这意味着它们共享文件偏移量。一个进程的读写操作会影响另一个进程的文件指针位置。 |
| 当前工作目录 |
继承 |
子进程起始于父进程的当前工作目录,但之后可通过chdir()独立更改。 |
| 根目录 |
继承 |
子进程继承父进程的根目录(通常为/),可独立更改(需权限)。 |
| 环境变量 |
复制 |
子进程获得父进程环境变量的一份完整副本。子进程可修改其副本,不影响父进程。若后续调用exec(),新程序可指定全新的环境。 |
| 信号处理方式 |
复制 |
子进程继承父进程对每个信号(如SIGINT, SIGTERM)的设置(忽略、默认、捕获)。但子进程可以重新设置自己的信号处理方式。 |
| 文件锁 |
不继承 |
父进程持有的任何文件锁都不会被子进程继承。这是为了避免复杂的锁竞争和死锁问题。 |
| 警报与计时器
(alarm, timer) |
不继承 |
父进程设置的任何定时器(如alarm())在子进程中被重置清零。 |
| 资源统计
(CPU时间等) |
不继承 |
子进程的各种资源使用统计(用户/系统CPU时间、页面错误次数等)从零开始计数。 |
备注:文件描述符 备注
通过fork()创建的子进程会继承父进程所有打开的文件描述符的副本。这些描述符指向相同的系统级打开文件表项,因此父子进程共享文件偏移量和文件状态标志。对文件的读写操作会相互影响对方看到的文件位置。
如果需要父子进程对同一文件进行完全独立的操作(拥有独立的偏移量),应在fork()后,在需要独立操作的进程中调用open()再次打开该文件。
vfork() 与 fork() 的区别
|
特性 |
fork() |
vfork() |
|---|---|---|
| 地址空间 | 拷贝(写时复制)
。子进程获得父进程地址空间的独立副本。 |
共享
。子进程与父进程共享地址空间,直到它调用 exec() 或 _exit()。 |
| 执行顺序 | 不确定
。fork() 返回后,父进程和子进程的执行顺序由调度器决定,可能并行执行。 |
确定
。父进程会被挂起,直到子进程调用 exec() 或 _exit() 之后才会继续执行。 |
| 性能 |
传统实现开销大,但现代采用“写时复制”(Copy-On-Write),开销极小。 |
历史上作为没有“写时复制”时的性能优化手段而存在,避免了不必要的拷贝。 |
| 安全性 | 非常安全
。因为地址空间是独立的,父子进程不会相互干扰。 |
极其危险
。因为共享地址空间,子进程的任何修改(如变量、栈)都会直接破坏父进程的状态,导致未定义行为。 |
| 现代使用 | 标准和推荐的方法
。是创建新进程的首选方式。 |
已过时且被废弃
。在 POSIX.2008 标准中已被标记为“已过时”,应避免使用。 |
深入解析
1. 地址空间行为(最关键的区别)
- fork(): 创建一个子进程。在现代系统中,它使用 “写时复制”(Copy-On-Write, COW) 技术。这意味着内核并不会立即复制父进程的整个地址空间(代码段、数据段、堆、栈),而是让父子进程共享这些物理内存页,并将这些页标记为只读。
当任何一方尝试写入这些共享页面时,CPU 会触发一个异常。内核此时才会拦截这个操作,为该进程透明地复制这个特定的页面,然后让其写入自己的副本。
优点: 如果子进程之后立即调用 exec(),那么大部分地址空间根本不会被写入,也就避免了大量不必要的拷贝,效率极高。 - vfork(): 创建子进程时,完全不拷贝地址空间。子进程直接在父进程的地址空间中运行。这是一种“孤注一掷”的优化,它的存在是因为在早期没有“写时复制”技术的系统上,fork() 的完整拷贝开销巨大。
2. 执行顺序
- fork()
调用返回后,父进程和子进程谁先运行是不确定的,这取决于系统的进程调度策略。
- vfork()
为了保证父进程的地址空间不被子进程破坏殆尽,内核会强制挂起父进程,直到子进程执行完毕。子进程必须做以下两件事之一:
调用 exec() 系列函数来加载并执行一个全新的程序(这会用新程序的地址空间替换当前地址空间,从而解除对父进程的威胁)。
调用 _exit()退出(注意是 _exit(),不是 exit(),因为 exit()会执行一些清理工作如刷新stdio缓冲区,这可能会破坏父进程的共享状态)。
只有在子进程做完上述任何一件事之后,父进程才会被唤醒并继续执行。
示例
基本的 fork() 使用
#include <stdio.h>
#include <unistd.h>int main() {pid_t pid;printf("Before fork, PID [%d]\n", getpid());pid = fork();if (pid < 0) { // fork failedperror("fork");return 1;} else if (pid == 0) { // Child processprintf("I am the child. My PID is [%d], my PPID is [%d]\n", getpid(), getppid());} else { // Parent process (pid > 0)printf("I am the parent. My PID is [%d], my child's PID is [%d]\n", getpid(), pid);}// Both parent and child execute this lineprintf("This is printed by both processes? PID [%d]\n", getpid());return 0;}
注意:由于fork() 之后,父进程和子进程的执行顺序是不确定的,父进程的打印顺序可能在前,也可能在后。
2. 进程等待
在多进程编程中,父进程创建子进程(通过 fork())后,通常需要等待子进程终止并获取其退出状态。这个行为非常重要,主要原因有:
-
避免僵尸进程(Zombie Processes):子进程终止后,其退出状态需要被父进程读取(称为“回收”)。如果父进程没有这么做,子进程的进程描述符将一直保留在系统进程表中,成为“僵尸进程”,浪费系统资源。
-
同步(Synchronization):父进程可能需要子进程的计算结果或完成特定任务后才能继续执行。
-
获取状态信息:父进程需要知道子进程是正常退出(退出码是多少?)、被信号杀死(哪个信号?)还是其他状态。
在 Linux/Unix 系统中,完成这一任务的核心函数是 wait() 和它的变体家族。
wait()函数详解
- 函数原型
#include <sys/wait.h>pid_t wait(int *wstatus);
-
参数
wstatus:一个指向整型的指针。用于存储子进程的退出状态信息。你可以传入 NULL,表示不关心子进程的退出状态,仅仅是为了回收子进程,避免僵尸进程。 -
返回值
成功:返回被等待终止的子进程的进程ID (PID)。
失败:返回 -1,并设置 errno(例如,当所有子进程都已等待完毕,再次调用 wait() 时会失败)。 -
作用
wait一旦被调用,就会一直阻塞在这里,直到有一个子进程退出出现为止。该函数有三个功能:
1.阻塞等待子进程退出,wait() 会阻塞调用它的父进程,直到任意一个子进程终止。
2.回收子进程系统资源,一旦有子进程终止,wait() 会立即回收该子进程的系统资源(清除僵尸进程)。
3.获取子进程结束状态信息(退出原因),如果 wstatus 参数不是 NULL,该指针指向的内存会被填入子进程的退出状态信息。
如何使用 wstatus?
直接检查 wstatus 整数的值是没有意义的。必须使用系统提供的宏来解析它。这些宏接受 wstatus 作为参数,而不是指针。
-
WIFEXITED(wstatus):如果子进程正常终止(例如调用 exit 或从 main 返回),则返回真。
WEXITSTATUS(wstatus):如果 WIFEXITED 为真,这个宏提取子进程传递给 exit 或 _exit 的退出状态码(低8位)。这个值通常在 0-255 之间。 -
WIFSIGNALED(wstatus):如果子进程被一个信号(signal)杀死,则返回真。
WTERMSIG(wstatus):如果 WIFSIGNALED 为真,这个宏提取导致子进程终止的信号编号。 -
WIFSTOPPED(wstatus):如果子进程目前处于暂停状态(例如被 SIGSTOP 信号暂停),则返回真。(较少在 wait() 中使用,更多用于 waitpid() 的 WUNTRACED 选项)。
WSTOPSIG(wstatus):如果 WIFSTOPPED 为真,这个宏提取导致子进程暂停的信号编号。
示例
#include <stdio.h>
#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == -1) {perror("fork failed");exit(1);}if (pid == 0) {// 子进程代码printf("Child process (PID: %d) is running.\n", getpid());sleep(2);printf("Child process is exiting.\n");exit(42); // 子进程退出,状态码为 42} else {// 父进程代码printf("Parent process (PID: %d) is waiting...\n", getpid());int wstatus;pid_t child_pid = wait(&wstatus); // 阻塞等待if (child_pid == -1) {perror("wait failed");exit(1);}printf("Parent: Child with PID %d has finished.\n", child_pid);if (WIFEXITED(wstatus)) {printf("Parent: Child exited normally with status: %d\n", WEXITSTATUS(wstatus));} else if (WIFSIGNALED(wstatus)) {printf("Parent: Child was killed by signal: %d\n", WTERMSIG(wstatus));}}return 0;}
waitpid()函数详解
wait() 有一些局限性:它只能等待任意一个子进程,并且总是阻塞。waitpid() 提供了更精确的控制。
- 函数原型
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *wstatus, int options);
-
参数
1. pid:
> 0:等待进程ID等于 pid 的特定子进程。
-1:等待任意子进程,行为与 wait() 相同。
0:等待与父进程属于同一个进程组的任意子进程。
< -1:等待进程组ID等于 pid 绝对值的任意子进程。2. wstatus:
与 wait() 中的相同,用于存储状态信息。3. options:这是一个非常重要的参数,用于修改函数的行为。可以是以下常量的按位或(|)组合:
0:默认选项,表示阻塞等待。
WNOHANG:非阻塞模式。如果没有子进程退出,waitpid() 会立即返回 0,而不是阻塞父进程。这对于需要同时处理其他任务(如事件循环)的父进程非常有用。
WUNTRACED:除了返回已终止的子进程信息外,也返回因信号而暂停(stopped) 的子进程信息。
WCONTINUED:返回之前被暂停后又被 SIGCONT 信号恢复执行的子进程信息。 -
返回值
1. 成功:返回状态发生变化的那个子进程的 PID。如果使用了 WNOHANG 且没有子进程退出,则返回 0。
2. 失败:返回 -1,并设置 errno(例如,指定的 pid 不存在或无权访问)。 -
作用
作用同wait()函数,但比wait()函数更灵活。下文会详细对比wait()函数和waitpid()函数。
示例
#include <stdio.h>
#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>int main() {pid_t pid = fork();if (pid == 0) {sleep(5); // 子进程睡眠 5 秒exit(0);}// 父进程int wstatus;pid_t child_pid;while (1) {child_pid = waitpid(pid, &wstatus, WNOHANG); // 非阻塞检查if (child_pid == 0) {printf("Child is still running. Parent can do other work...\n");sleep(1); // 模拟父进程做其他工作} else if (child_pid == pid) {printf("Child %d has exited.\n", child_pid);break;} else {perror("waitpid error");break;}}return 0;}
wait()和waitpid()对比
|
特性 |
wait 函数 |
waitpid 函数 |
|---|---|---|
| 目标子进程 |
只能等待任意一个子进程结束。你无法指定要等哪个子进程,它返回任何一个终止的子进程的信息。 |
可以等待一个特定的子进程,也可以等待任意一个(通过参数控制)。 |
| 阻塞行为 |
永远是阻塞的。调用 wait 后,父进程会被挂起,直到有一个子进程状态改变。 |
可以通过选项设置为非阻塞。使用 WNOHANG 选项可以让父进程不被阻塞,继续执行其他任务。 |
| 功能控制 |
没有选项参数,行为是固定的。 |
有 options 参数,可以精细控制其行为(如 WNOHANG, WUNTRACED 等)。 |
| 等待进程组 |
不支持。 |
支持。可以等待属于特定进程组的所有子进程。 |
其他相关函数
wait3() 和 wait4():这些是历史遗留函数,除了获取状态信息,还能获取子进程的资源使用情况(如 CPU 时间、内存使用等)。wait3 等待任意子进程,而 wait4 可以等待特定子进程。它们已被标记为废弃,建议使用 waitpid 或 wait 函数替代。
3、进程终止
核心的进程终止函数主要有以下几个:exit(), _exit(),abort(), 以及 return 关键字。
exit()函数详解
- 函数原型
#include <stdlib.h>void exit(int status);
-
参数
status:是进程的退出状态,这是一个整数值,通常用来向父进程传递程序执行成功或失败的信息。0 或宏 EXIT_SUCCESS 表示成功。非 0 值(通常是 1)或宏 EXIT_FAILURE 表示失败。你也可以用不同的非零值来表示不同的错误类型。父进程可以通过 wait() 或类似系统调用获取这个值。 -
返回值
无 -
作用
exit() 是 C 标准库提供的终止函数,是一个高级的、安全的进程终止函数,它会进行彻底的清理(调用 atexit 函数、刷新 I/O 缓冲区)后再终止进程。
_exit()函数详解
- 函数原型
#include <unistd.h>void _exit(int status);
-
参数
status:与 exit() 相同,表示退出状态。 -
返回值
无 -
作用
_exit()函数是“立即终止”函数,它们会绕过所有标准库的清理流程,直接请求操作系统内核终止进程。_exit() 是一个低级的、粗暴的系统调用,它立即终止进程,不做任何用户空间的清理。
abort()函数详解
函数原型
#include <stdlib.h>void abort(void);
参数
无。
返回值
该函数永远不会返回。
作用
abort() 函数引发一个异常信号来终止进程,通常会产生一个核心转储(Core Dump)文件用于调试。
exit()和_exit()和abort()对比
1. 行为与清理工作 (最关键的区别)
exit(): “优雅的终止”
-
它是高级别的库函数,旨在提供一种安全的退出方式。
-
它的核心工作是执行一系列清理工作,包括刷新缓冲区、调用注册的退出函数等,确保资源被正确释放、数据被正确写入磁盘,然后再通知操作系统终止进程。
-
适用于: 绝大多数正常的程序终止场景。
_exit(): “粗暴的终止”
-
它是低级别的系统调用,是 exit() 的最终实现基础。
-
它绕过所有用户空间的清理流程,直接请求内核终止进程。这意味着任何在标准I/O缓冲区中的数据都会丢失。
-
适用于:
-
1.子进程:在 fork() 后,子进程通常与父进程共享相同的打开文件和缓冲区。如果子进程调用 exit() 刷新缓冲区,可能会破坏父进程的I/O状态。因此,子进程在终止时应使用 _exit()。
-
2.处理信号时,需要退出程序(因为 exit() 不是异步信号安全的,而 _exit() 是)。
-
3.发生无法恢复的严重错误,需要立即终止。
-
abort(): “异常中止”
-
它的目的是引发一个异常信号(SIGABRT) 来中止程序。
-
它会产生一个核心转储(core dump) 文件(如果系统允许),该文件记录了进程终止时的内存状态,用于后续的调试,定位程序崩溃的原因。
-
它是否会进行清理是由实现定义的,但大多数实现会尝试刷新并关闭标准I/O流,但不会调用注册的退出函数。
-
适用于: 当程序检测到内部的严重错误、断言失败(assert() 的底层实现就调用 abort())或不可恢复的不一致状态时。
2. 参数与返回值
exit(status) 和 _exit(status):
-
都需要一个整型参数 status 作为退出状态码。
-
惯例:0 表示成功,非零值表示失败。父进程可以通过 wait() 或waitpid() 系统调用获取这个值。
-
注意:虽然参数是 int,但只有低8位(一个字节)会被传递给父进程,所以有效范围是 0-255。
abort():
-
不需要任何参数。
-
它总是表示异常失败。
3. 底层机制
-
exit(): 是C库函数,它先完成清理工作,然后调用 _exit() 系统调用。
-
_exit(): 是系统调用(如Linux中的 exit_group),直接陷入内核,由内核销毁进程。
-
abort(): 是C库函数,它的核心操作是向当前进程发送 SIGABRT 信号。默认情况下,这个信号会终止进程并产生core dump。
4、进程替换
exec 函数族的主要作用是进程替换。调用 exec 函数的进程会立即停止运行当前的程序,转而执行由 exec 指定的新程序。新程序会完全替代原进程的代码段、数据段、堆和栈,但进程ID(PID)保持不变。它就像是“灵魂附体”,一个进程的躯壳(PID)被注入了另一个程序的灵魂(代码和数据)。
本质上它并不是创建一个新进程,而是在当前进程的上下文中加载并运行一个全新的程序。一个经典的使用模式是:先通过 fork() 创建一个子进程,然后在子进程中调用 exec() 来执行另一个程序。
exec 函数族成员详解
函数原型
exec 函数族包含 6 个函数,它们的函数原型如下:
#include <unistd.h>
extern char **environ;int execl(const char *path, const char *arg, ... /* (char *) NULL */);int execlp(const char *file, const char *arg, ... /* (char *) NULL */);int execle(const char *path, const char *arg, ... /*, (char *) NULL, char *const envp[] */);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[], char *const envp[]);
命名规律
函数名中的字母揭示了其参数特点和功能:
- l (list)
:表示参数采用列表 (list) 形式,即逐个传入。你需要将命令行参数作为一个一个的 char * 类型参数传递,最后必须以 NULL 结尾。
- v (vector)
:表示参数采用数组/向量 (vector) 形式。你需要先构造一个 char * 类型的参数数组 argv[],然后将该数组作为参数传入。这个数组的最后一个元素必须是 NULL。
- p (path)
:表示函数会使用系统的 PATH 环境变量来查找可执行文件。你只需要提供文件名(如 ls),系统会在 PATH 列出的目录中自动搜索。不带 p 的函数,必须提供完整的路径名(如 /bin/ls)。
- e (environment)
:表示可以自定义环境变量。允许调用者自行指定一个环境变量数组 envp[] 传递给新程序。不带 e 的函数,默认使用当前进程的环境变量(即 extern environ)。
execvpe 是一个 GNU 扩展,并非所有 UNIX 系统都支持。
参数详解
1.path / file
-
path: 指的是完整的路径名,例如 /bin/ls, /usr/bin/vim。
-
file: 指的是文件名。对于带 p 的函数,系统会去 PATH 环境变量中查找该文件。如果文件名中包含 /,则将其视为路径名,不再使用 PATH 搜索。
2.arg… / argv[]
-
这是传递给新程序的参数列表 (argument list),也就是 main(int argc, char *argv[]) 中的 argv。
-
按照约定,argv[0] 通常是程序名本身。虽然你可以将其设置为任意字符串,但大多数程序期望它是自己的名字。
-
列表必须以 NULL 结尾,这是函数判断参数结束的标志。
3.envp[] (仅 execle 和 execvpe)
-
一个指向环境变量字符串数组的指针。数组中的每个字符串的格式通常是 “KEY=VALUE”(例如 “HOME=/home/user”),数组的最后一个元素必须是 NULL。
-
如果不需要自定义环境变量,使用其他四个函数会更简单。
返回值
- 成功时
:exec 函数不会返回。因为调用进程的代码已经被新程序完全替换,没有代码可以继续执行了。
- 失败时
:返回 -1,并设置全局变量 errno 以指示错误原因(例如,文件不存在 ENOENT,没有执行权限 EACCES 等)。
示例
pid_t pid = fork();if (pid == 0) { // 子进程execl("/bin/ls", "ls", "-l", NULL);// 如果 exec 成功,下面的代码绝不会执行perror("execl failed"); // 只有失败才会到这里exit(EXIT_FAILURE); // exec 失败后,子进程应立即退出}// 父进程继续...
5、其他进程函数
getpid()函数详解
- 函数原型
#include <unistd.h>pid_t getpid(void);
- 参数
void: 明确表示该函数不接受任何参数。
- 返回值
返回类型为 pid_t(通常定义为整型,如 int)。
返回值永远是成功的,它返回当前调用进程的进程ID(Process ID)。 - 作用
获取当前进程的唯一标识符,即进程ID(PID)。
在系统运行的瞬间,每个进程的PID都是唯一的。虽然PID最终会复用,但在任何一个时间点,所有活动进程的PID都是不同的。
PID是操作系统管理和控制进程的核心依据。许多系统调用(如 kill,waitpid 等)和命令(如 kill,ps)都需要通过PID来指定操作的目标进程。
getppid()函数详解
- 函数原型
#include <unistd.h>pid_t getppid(void);
- 参数
void: 同样,该函数不接受任何参数
- 返回值
返回类型同样为 pid_t。
返回值永远是成功的,它返回当前调用进程的父进程的进程ID(Parent Process ID)。 - 作用
获取创建当前进程的父进程的ID。
示例
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>int main(){pid_t child_pid;child_pid = fork(); // 创建子进程if (child_pid == -1) {perror("fork failed");return 1;}if (child_pid == 0) {// 这里是子进程执行的代码printf("This is the CHILD process.\n");printf("My PID is: %d\n", getpid());printf("My Parent's PID is: %d\n", getppid());sleep(2); // 睡眠2秒,让我们有机会观察父进程终止后的情况printf("After parent ended, my PPID is: %d (probably 1 - init)\n", getppid());} else {// 这里是父进程执行的代码printf("This is the PARENT process.\n");printf("My PID is: %d\n", getpid());printf("I forked a child with PID: %d\n", child_pid);printf("My Parent's PID (shell's PID) is: %d\n", getppid());sleep(1); // 父进程睡眠1秒后退出,让子进程成为孤儿printf("Parent exiting...\n");}return 0;}
6、特殊进程
1、守护进程
守护进程全解析篇幅稍长,会总结到守护进程、进程树、进程组、进程查看命令等内容,故该系列后续会有一篇文章专门总结全解析。
2、僵尸进程
-
定义:僵尸进程是指已经执行完毕、但其父进程尚未使用 wait() 或 waitpid() 系统调用来读取其退出状态的子进程。
-
关键点
1.进程已死:僵尸进程不是活着的进程。它已经终止,不再运行任何代码,也不消耗任何 CPU 和内存资源。
2.残留数据:在内核的进程表中,仍然保留着它的进程描述符。这个描述符中记录了该进程的退出状态、CPU 时间等少量信息,等待其父进程查询。
3.状态标识:在 ps或 top 命令中,其状态(STAT)被标记为 Z(Zombie的缩写)。
-
僵尸进程的影响和危害:
虽然单个僵尸进程占用资源极少,但如果大量产生,危害是严重的:1.耗尽进程号(PID):每个僵尸进程都仍然占用着一个进程号。系统可用的进程号是有限的(可通过 /proc/sys/kernel/pid_max 查看)。如果僵尸进程数量过多,导致可用 PID 耗尽,系统将无法创建任何新进程。这会导致严重的系统问题。
2 .内核资源占用:僵尸进程的进程描述符仍然保留在内核的进程表中。大量的僵尸进程会填满进程表,消耗系统内核资源。
3.指示程序缺陷:僵尸进程的存在通常意味着父进程的程序编写有缺陷(没有正确处理子进程的退出),这可能暗示着程序中有其他更严重的逻辑错误。
-
如何处理:
1.你不能用 SIGKILL (kill -9) 来杀死一个僵尸进程,因为它已经死了!无法接收和处理信号。 它的资源已经释放,剩下的只是一个数据条目。清除僵尸进程的根本方法是处理它的父进程。
2.最直接的方法就是终止僵尸进程的父进程。父进程被终止后,僵尸进程会被init进程收养,init 进程会周期性地调用 wait() 或 waitpid() 来清理这些被它收养的僵尸进程,从而彻底清除它们。(备注:init进程收养,见下文孤儿进程中的解析。)
-
示例
一个简单的 C 代码示例来制
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>int main() {pid_t pid = fork(); // 创建子进程if (pid == 0) {// 子进程代码printf("I am the child with PID: %d. I‘m exiting now!\n", getpid());exit(0); // 子进程立即退出,成为僵尸} else if (pid > 0) {// 父进程代码printf("I am the parent with PID: %d. I created a child %d.\n", getpid(), pid);printf("Parent is going to sleep for 30 seconds and will NOT call wait().\n");sleep(30); // 父进程不调用 wait(),在此期间子进程将是僵尸状态printf("Parent wakes up and exits. The zombie child should now be gone (reparented to init).\n");} else {perror("fork failed");exit(1);}return 0;}
3、孤儿进程
-
定义
1. 当一个父进程产生一个或多个子进程后,如果父进程由于某种原因(如:任务完成自然退出、被 kill 命令杀死、出现错误崩溃等)先于其子进程终止,那么这些还在运行的子进程就失去了父进程,从而变成了孤儿进程。
2. 孤儿进程仍然是一个正常的、活着的进程,它仍在执行其代码。
3. 它与“僵尸进程”有本质区别(后面会详细对比)。
-
系统如何管理孤儿进程?—— “被收养”机制
1.init 进程:PID 为 1 的进程,通常是 init 或现代系统中的 systemd。它是所有进程的始祖,在系统启动时由内核直接启动。
2.收养过程:当一个进程终止时,内核会遍历其所有还在运行的子进程。内核将这些子进程的父进程 PID 重新设置为 1(即 init / systemd)。从此,init 进程就成为了这些孤儿进程的新父进程。
3.init 的职责:作为一个负责任的“养父”,init 进程会定期调用 wait() 或 waitpid() 系统调用来等待其子进程(包括所有被收养的孤儿进程)结束。当一个孤儿进程最终结束时,init 进程会回收它的进程描述符等内核资源,确保不会留下任何垃圾。
4.这个机制的重要性在于:它保证了所有进程最终都会有一个父进程来为其“收尸”,避免了资源泄漏。这是 Linux 进程管理健壮性的体现。
-
孤儿进程的影响与危害
与僵尸进程不同,孤儿进程本身通常是无害的。1.资源占用:它会正常消耗系统资源(CPU、内存),就像任何其他用户进程一样。
2.无资源泄漏:因为它最终会被 init 进程正确回收,所以不会造成内核资源(如进程表项)的泄漏。
3.主要“危害”:可能在于其非预期的行为。例如,一个后台任务本应由其父进程管理和控制,父进程意外退出后,它变成了孤儿并继续在后台运行,这可能不是用户想要的行为。它可能会继续持有文件锁、占用网络端口,或者向日志文件写入信息,造成干扰。
-
示例
一个简单的 C 代码示例来制造孤儿进程
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/wait.h>int main() {pid_t pid = fork(); // 创建子进程if (pid == 0) {// 子进程代码printf("I am the child, my PID is %d. My parent‘s PID is %d.\n", getpid(), getppid());printf("Child is going to sleep for 10 seconds...\n");sleep(10); // 子进程睡眠 10 秒printf("Child wakes up. Now my parent‘s PID is %d.\n", getppid()); // 此时父进程已退出,ppid 变为 1exit(0);} else if (pid > 0) {// 父进程代码printf("I am the parent, my PID is %d. I created a child with PID %d.\n", getpid(), pid);printf("Parent is exiting now!\n");exit(0); // 父进程立即退出} else {// fork 失败perror("fork failed");exit(1);}return 0;}
4. 孤儿进程 vs. 僵尸进程
|
特征 |
孤儿进程 |
僵尸进程 |
|---|---|---|
| 定义 |
活着的进程,父进程已退出。 |
已终止的进程,但其父进程尚未读取其退出状态。 |
| 状态 |
处于运行状态(S, R, D等) |
状态为 Z (Zombie) |
| 资源 |
占用系统资源(内存、CPU等)。 |
不占用内存、CPU等资源,但占用进程号。 |
| 本质 |
一个仍在执行代码的进程。 |
内核进程表中的一个条目(entry),等待被清理。 |
| 如何产生 |
父进程先于子进程退出。 |
子进程先退出,而父进程未调用 wait()。 |
| 危害 |
基本无害(除了可能非预期运行)。 |
有害,消耗内核资源,且大量僵尸进程会耗尽可用进程号,导致新进程无法创建。 |
| 解决方案 |
无需解决,会被 init 自动回收。 |
需要杀死其父进程,让 init 接管并调用 wait() 来清理它们。 |
八、进程间通信(IPC)
|
IPC机制 |
适用关系 |
通信方向 |
数据形式 |
效率 |
同步需求 |
|---|---|---|---|---|---|
| 匿名管道 |
亲缘进程 |
单向 |
字节流 |
高 |
内置 |
| 命名管道 |
任意进程 |
单向 |
字节流 |
高 |
内置 |
| 消息队列 |
任意进程 |
双向 |
消息 |
中 |
内置 |
| 共享内存 |
任意进程 |
双向 |
字节/结构 |
最高 |
需额外机制 |
| 信号量 |
任意进程 |
- |
计数器 |
高 |
内置 |
| 信号 |
任意进程 |
单向 |
信号编号 |
高 |
异步 |
| 套接字 |
任意进程/主机 |
双向 |
字节流 |
中 |
内置 |
表格中的几种进程间通信的详细总结,该系列的后续文章会从定义、特点、适用关系、创建方式、本质、效率、示例代码等方面进行全解析总结比较,本文不再赘述。
更多推荐

所有评论(0)