一,引言

        在操作系统中,进程(Process)是资源分配和独立运行的基本单位。每一个正在运行的程序(如浏览器、编辑器、游戏),本质上都是一个或多个进程的集合。每个进程都有自己独立的地址空间、资源和执行状态,看似互不干扰,却又能协同工作完成复杂任务,这一切的起点,就是进程的创建。本章将从进程的唯一标识符(PID)入手,结合 Chrome 浏览器的真实例子,帮助你直观理解父进程与子进程的关系。随后,我们重点介绍 Linux 下最经典、最常用的进程创建方式。

二,进程的唯一标识符

        进程的唯一标识符叫做process-id也就是进程ID简称为pid,他就像进程的身份证号你可以直接通过身份证号(pid)找到正在运行的那个进程,以常用的windows为例,打开终端输入命令:

tasklist /fi "pid eq 20844"

来寻找pid = 20844为哪一个进程,输出:

可以知道身份证20844对应的人物叫做chrome.exe,也就是谷歌的启动程序。

三,父进程子进程

        父进程 (Parent Process): 发起创建请求的进程。它像是一个“管理者”,负责申请资源并启动新任务。

        子进程 (Child Process): 被创建出来的产物。它从父进程那里继承了大部分属性,并在自己的空间里执行任务

我们再以chrom浏览器为例:

可见运行chrome应用程序是在运行一系列进程的集合其中包含了主进程和它的子进程(或者孙子进程)

主进程(Browser Process)

        程序的入口点是你所打开的应用的顶级父进程。在 Linux 运行 a.out 或 Windows 运行 a.exe 时,OS 创建的初始进程即为主进程。作为“总司令”,它拥有最高权限,负责 UI、磁盘 I/O 和网络。注意: 若主进程异常退出,子进程在 OS 层面会变成“孤儿进程”,通常由系统级的 init 进程接管。

子进程(Child Processes)

        由父进程通过系统调用(如 fork())创建。它们是专职的“打工仔”,在 Chrome 中实现了沙箱隔离,即使一个渲染进程(标签页)崩溃,也不会影响主进程和其他标签页。

下图简单展示一下父子进程和主进程之间的联系(以linux系统为例):

所有的应用主进程(Chrome, 微信, Nginx 等)要么直接由系统初始进程(PID 1)启动,要么是由它派生出的某个服务启动的,一般情况下子进程的pid会大于父进程但是如果发生了进程pid的回绕重用,子进程的pid就可能会分配到比父进程pid小的序号

四,创建进程的系统调用

        Windows 的进程创建 API(如 CreateProcess)相对复杂,而 Linux 的 fork() 更简洁直接,我们以下的实验都用linux环境,在linux中创建进程有fork(),vfork(),clone()因为fork是最常见,最安全,且容易上手的,故我先来介绍fork另外两个再在后续对比fork进行介绍

4.1 fork()系统调用简介

        1. fork()的作用:

现代 Linux 使用写时复制(COW)技术。当执行 fork() 后,操作系统逻辑上为子进程“克隆”了父进程的资源,但物理上仅在数据被修改时才触发真正的内存复制:

  • 代码段:所有的程序指令(只读,不复制只共享)
  • 数据段,堆和栈:所有的全局静态变量与所有的局部变量、函数调用链(逻辑独立,物理上通过 COW 延迟复制)
  • 文件描述符:如果父进程打开了一个文件,子进程也同样打开了并且共享文件指针即一个进程读到文件某个位子另一个进程的读指针也会到那个位置

简而言之fork()就像是细胞分裂,分裂出来的子细胞和母细胞有完全一样的内部结构但他们现在是两个独立的生命体,至于程序员如何区分哪个是子细胞哪个是母细胞就看其fork的返回值

        2. fork的返回值

在进程通过pid_t pid = fork()进行细胞分裂时分裂出来的细胞所获得的fork返回值是不一样的,如果是子进程那么它所获得的pid是0,如果是父进程那么它所获得的pid是它所创建的子进程的pid,如果pid = -1那么进程创建失败。

        3. 返回值特点

        现代操作系统返回一般会返回signed int类型来表示所创建进程的id,为了保证代码在不同系统间的兼容性(可移植性)并一眼识破其用途(语义化),人们用 typedef 把底层通用的 signed int 重新命名为 pid_t来声明进程的id。

4.2代码示例与讲解

        1.代码示例
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t pid;
    printf("【开始】唯一进程 PID = %d\n", getpid());
    pid = fork(); 
    if (pid < 0)
    {
        printf("fork 失败!\n");
        return 1;
    }
    else if (pid == 0)
    {
        // 子进程
        printf("【子进程】PID = %d,父进程 PID = %d\n", getpid(), getppid());
        sleep(5); // 模拟工作
        printf("【子进程】结束\n");
    }
    else
    {  
        // 父进程
        printf("【父进程】PID = %d,创建了子进程 PID = %d\n", getpid(), pid);
        wait(NULL); // 等待子进程结束
        printf("【父进程】结束(子进程已完成)\n");
    }
    return 0;
}
        2.运行结果

        3.代码讲解
  • 父进程顺序执行:main开头 → printf("【开始】...") → fork() → 进入 else 分支...
  • 当执行到 pid = fork(); 这行时,操作系统创建子进程,子进程的代码和父进程完全一样。
  • 子进程的程序计数器(PC)被设置成正好指向 fork() 调用返回后的下一条指令。  也就是说:子进程从 fork() 返回的那一刻开始继续执行后续代码,不是从 main 开头重新开始。
  • 子进程中 fork() 返回 0,因此进入 else if (pid == 0) 分支;父进程返回子进程 PID(>0),进入 else{...} 分支

        关键观察点

  • 输出顺序:你会看到父进程先打印创建信息,子进程随后打印自己的信息。这说明 fork 后两个进程都在运行(并发执行),但具体谁先输出取决于操作系统调度,顺序并不固定。
  • wait(NULL) 的作用:如果暂时注释掉 wait(NULL),你会发现父进程先结束退出,而子进程仍在运行(可能成为孤儿进程)。 wait() 让父进程阻塞等待子进程结束,这是进程间的一种基本同步方式。
  • 资源回收 wait():不仅用于同步,还用于回收子进程资源,防止产生“僵尸进程”。

五,扩展: fork() 之外的进程创建方式

Linux 提供了多种进程创建系统调用,其中最常用的是 fork()。

vfork()

        历史上的优化版本,不复制内存页表,父子共享地址空间,开销极低,但要求子进程必须立即调用 exec 或 exit,且父进程会被阻塞。由于共享内存极易导致崩溃,且现代 fork() 已通过写时复制(COW)优化到足够快,vfork() 已被视为过时,不推荐使用。

clone():

        Linux 特有的超级灵活接口,可精确控制父子进程共享哪些资源(内存、文件描述符、信号处理等)。线程库(pthread_create)底层就是用 clone() 实现的,容器技术(如 Docker)也依赖它创建命名空间隔离的进程。普通应用无需直接使用,过于复杂。

结论:对于学习和实际开发,掌握 fork() 即可,它安全、高效、通用,是 Linux 进程创建的基石

六,本章小结

  1. fork() 是 Linux 下创建进程的最基本方式
  2. 父子进程代码相同,但通过返回值区分执行路径
  3. 父子进程并发执行,输出顺序不确定
  4. wait() 可用于等待与回收(细节后述)
  5. 本章重点介绍了进程的创建方式。在实际运行中,你已经隐约看到了几个重要现象:
  6. 父子进程同时运行(并发)
  7. wait() 使父进程暂停等待(阻塞)
  8. 子进程结束后的资源回收 这些内容将在后续章节深入展开

下一章:深入理解Linux进程: 生命周期管理与异常进程深度解析

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐