文章目录

  • 一、前言

  • 二、什么是进程

  • 三、进程与程序的区别

  • 四、进程的五大特征

  • 五、进程控制块详解

  • 六、进程的状态与切换

  • 七、进程管理全解析

    • 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 failed        perror("fork");        return 1;    } else if (pid == 0) { // Child process        printf("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 line    printf("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 变为 1        exit(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机制

    适用关系

    通信方向

    数据形式

    效率

    同步需求

    匿名管道

    亲缘进程

    单向

    字节流

    内置

    命名管道

    任意进程

    单向

    字节流

    内置

    消息队列

    任意进程

    双向

    消息

    内置

    共享内存

    任意进程

    双向

    字节/结构

    最高

    需额外机制

    信号量

    任意进程

    -

    计数器

    内置

    信号

    任意进程

    单向

    信号编号

    异步

    套接字

    任意进程/主机

    双向

    字节流

    内置

    表格中的几种进程间通信的详细总结,该系列的后续文章会从定义、特点、适用关系、创建方式、本质、效率、示例代码等方面进行全解析总结比较,本文不再赘述。

    Logo

    智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

    更多推荐