前言

进程是 Linux 系统编程最核心的基础,不管是嵌入式开发、后台服务开发,面试笔试都会高频考察。本文从概念、原理、API、代码示例、常见坑点完整梳理 Linux 进程相关知识点。

1. 什么是进程

程序:存放在磁盘上静态的可执行文件,只是一堆机器指令,不运行就不占用 CPU。 进程:程序被加载进内存之后,正在运行的实例,是操作系统资源分配的基本单位

每一个进程拥有独立:虚拟地址空间、文件描述符表、环境变量、信号处理方式、运行状态。

内核使用PCB(process control block,task_struct结构体)管理每一个进程,PCB 内部保存:PID 进程号、进程状态、内存映射指针、打开的文件、优先级、父子进程关系等全部信息。

一句话区分:程序是静态文件,进程是动态运行实体。

2. 进程 vs 线程

表格

对比项进程线程
核心定位资源分配的基本单位CPU 调度的基本单位
内存资源拥有完整独立地址空间,开销大共享所属进程地址空间;每个线程仅独有栈、寄存器上下文,开销小
崩溃影响进程之间相互隔离;一个进程崩溃不会干扰其他进程线程共享进程全部资源;任意一个线程崩溃,整个进程直接退出
通信方式进程间内存隔离,必须使用 IPC 通信:管道、消息队列、共享内存、socket同进程线程直接读写全局变量即可通信,注意互斥锁保护

记忆口诀:进程分资源,线程抢 CPU。

3. Linux 进程常见状态

使用pstop命令可以查看进程状态:

  • R 运行态:进程正在 CPU 执行,或者位于运行队列等待调度。
  • S 可中断睡眠:进程等待事件,收到信号可以被唤醒。
  • D 不可中断睡眠:大多等待 IO 操作,不能被信号打断
  • T 停止态:进程被暂停,收到 SIGCONT 信号恢复运行。
  • Z 僵尸态 (Zombie):子进程代码已经执行结束退出,但是父进程还没有调用wait()回收 PCB 资源;用户内存已经释放,PID 和 PCB 残留在内核。

⚠️僵尸进程不占用 CPU、用户内存,但是持续占用 PID 编号,大量僵尸会耗尽系统进程表。

4. 进程创建、退出核心 API

4.1 fork () 创建子进程

pid_t fork(void); 调用一次,返回两次

  1. 父进程返回值:大于 0,返回新创建子进程 PID;
  2. 子进程返回值:等于 0;
  3. 返回 - 1:fork 创建失败。
写时复制 COW(Copy‑On‑Write)

fork 之后,子进程不会立刻完整拷贝物理内存。 刚创建瞬间,父子进程共享同一份物理内存,虚拟地址完全一致; 只有父子任意一方修改变量数据的时候,内核才真正复制这一块内存,分配新物理页面。

COW 极大降低 fork 系统调用开销。

fork 之后行为特点
  1. fork 之后父子同时执行 fork 之后的代码,谁先运行完全由内核调度决定,顺序不可预测
  2. 子进程继承父进程:文件描述符、环境变量、信号处理;地址空间逻辑上完全拷贝,物理内存 COW 机制。
  3. 父子地址空间相互独立,子进程修改变量,父进程完全看不到。

示例模板:

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void)
{
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork failed");
        return 1;
    }
    else if(pid == 0)
    {
        printf("子进程 PID:%d\n", getpid());
    }
    else
    {
        printf("父进程 PID:%d,子进程PID:%d\n", getpid(), pid);
        wait(NULL);
    }
    return 0;
}

4.2 PID / PPID

  • getpid() 获取自己进程 PID
  • getppid() 获取父进程 PID

PID 是内核分配唯一编号,可以被复用。 系统 1 号进程systemd/init,是系统所有孤儿进程的养父。

孤儿进程:父进程先退出,子进程还活着,子进程 PPID 改为 1,被 init 收养。 僵尸进程:子进程结束,父进程活着,但是没有 wait 回收。

4.3 exit () 进程退出

exit(退出码);

  • 退出码:0 代表正常结束;非 0 代表异常退出。
  • 调用 exit 之后:用户空间内存、打开文件描述符被释放;但是 PCB 进程控制块保留在内核,变成僵尸进程,等待父进程 wait 回收

4.4 wait () 回收僵尸子进程

pid_t wait(int *wstatus);
  1. wait(NULL):只回收僵尸进程,不关心退出状态;
  2. wait(&status):传入变量地址,内核将子进程退出信息填充到 status; 配合宏解析 status:
  • WIFEXITED(status):判断子进程是否正常 exit 退出
  • WEXITSTATUS(status):提取子进程 exit 传入的退出码

重点:wait 会阻塞父进程,直到有一个子进程变成僵尸。

4.5 exec 族函数

execl / execv / execvp …

exec不会创建新进程!PID 保持不变。 使用全新程序代码,直接覆盖当前进程的代码段、数据段。 exec 调用成功,函数不会返回;只有出错才返回‑1。

经典用法:fork()创建子进程,子进程内部调用 exec,执行另外一个外部程序。

5. 进程常用命令

  • ps -ef / ps aux:查看进程快照
  • top:动态实时查看进程 CPU 内存占用
  • htop:top 增强工具
  • kill -信号 pid:发送信号,kill -9 pid强制杀死进程
  • pstree:树形打印父子进程关系

6. IPC 进程间通信

进程地址空间互相隔离,必须依靠内核提供的机制完成数据交互:

  1. 管道 pipe:匿名管道多用于父子进程单向通信;FIFO 命名管道支持无血缘关系进程。
  2. 消息队列:内核维护消息链表,支持双向收发消息。
  3. 共享内存:多个进程映射同一块物理内存,速度最快;需要信号量做同步互斥。
  4. 信号量:主要做同步、互斥,保护共享资源。
  5. Socket 套接字:本机通信,也可以跨网络主机通信。

7. 守护进程 Daemon

守护进程后台长期运行,脱离终端,例如 nginx、sshd。 创建守护进程标准步骤:

  1. fork,父进程退出,子进程成为孤儿,被 init 收养。
  2. setsid() 创建新会话,脱离终端控制。
  3. chdir 切换工作目录到根目录/
  4. umask (0) 修改文件掩码。
  5. 关闭标准输入输出错误文件描述符,脱离终端。

8. 高频面试简答总结

  1. fork 调用一次返回两次,COW 写时复制原理。
  2. 僵尸进程、孤儿进程产生原因以及解决办法。
  3. wait 作用;wait(NULL)wait(&status)区别。
  4. 进程和线程核心区别。
  5. exec 系列函数特点,不创建新进程。
  6. 常见 IPC 方式各自特点。

实践建议:多写 fork 代码,观察僵尸进程,ps 命令看 Z 状态,加深理解。


Logo

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

更多推荐