前言
在 Linux 中执行一次 ls、启动一个应用程序、运行一个 ROS2 节点,背后都离不开进程。但一个进程,远不只是“正在运行的程序”。
本文阅读路线
进程是什么 → task_struct → 状态与 PID → 进程树 → 内存 → 文件系统 → 文件描述符 → Namespace → 调度与排查
01 · 什么是 Linux 进程?
程序是存储在磁盘上的静态可执行文件;进程是程序运行时在操作系统中的执行实例。
./hello & ./hello & ./hello &
这三次运行的是同一个程序,但会创建三个独立的进程。它们拥有不同的进程 ID,通常具有各自独立的虚拟地址空间,并分别参与内核调度。
02 · 一张图看懂进程结构

图:Linux 进程核心数据结构关系(示意图,具体字段随内核版本变化)
Linux 内核使用 task_struct 描述一个任务(task)。一个任务可以是普通进程中的主线程,也可以是同一线程组中的其他线程。

记住一句话:task_struct 是任务信息的核心入口,通过指针关联地址空间、文件、目录、命名空间等资源。
03 · 状态与身份:state / pid / tgid
任务状态
任务不一定时刻占用 CPU,它可能运行、等待事件、停止或退出。常见的 ps 状态包括:
| 标识 |
含义 |
| R |
运行或可运行 |
| S |
可中断睡眠 |
| D |
不可中断睡眠 |
| T |
停止 |
| Z |
僵尸状态 |
内核任务运行状态与退出状态分别涉及不同字段;这里是用户空间观察到的简化分类。
pid 与 tgid
pid 标识一个内核任务;tgid 标识其所属线程组。 同一进程的多个线程通常具有不同的 pid,但共享 tgid。
| 线程 |
pid |
tgid |
| 主线程 |
1000 |
1000 |
| 工作线程 A |
1001 |
1000 |
| 工作线程 B |
1002 |
1000 |
ps -eo pid,ppid,stat,comm
ls /proc/1000/task
04 · 进程关系:父进程与子进程
进程之间往往具有父子关系。real_parent 指向真实父任务;children 是子任务链表的表头;sibling 将子任务连接到父任务的子链表;group_leader 指向线程组组长。
pstree -p
通常可以通过 fork() 创建子进程,再通过 execve() 在子进程中加载新的程序映像。
05 · 内存管理:mm_struct
mm 指向 mm_struct,描述用户虚拟地址空间。它涉及代码段、数据段、BSS、堆、栈、内存映射区域以及页表等信息。
| 成员或区域 |
作用 |
| start_code / end_code |
代码区域边界 |
| start_data / end_data |
数据区域边界 |
| start_brk / brk |
堆的初始与当前 program break |
| start_stack |
初始用户栈位置 |
| VMA |
描述各段虚拟内存映射 |
程序使用的是虚拟地址,CPU 借助 MMU 和页表转换为物理地址。不同进程的相同虚拟地址,不一定映射到同一物理页面。
内核版本提示:图中的 mm_struct.mmap 体现的是较早版本的 VMA 链表管理方式;现代内核已使用 Maple Tree 管理虚拟内存区域。
内核线程一般没有独立的用户地址空间,因此其 mm 通常为 NULL。
cat /proc/1234/maps
cat /proc/1234/smaps_rollup
06 · 文件系统:fs_struct
fs 指向 fs_struct,保存任务的文件系统上下文。其中 root 表示进程视角中的根目录,pwd 表示当前工作目录。
它们并非普通路径字符串,而是由 struct path 关联挂载点与目录项。目录项 dentry 又可关联描述文件对象元数据的 inode。
readlink /proc/1234/cwd
readlink /proc/1234/root
07 · 打开的文件:files_struct
files 指向 files_struct,其中的文件描述符表将整数 fd 映射到内核的 struct file 对象。
| 文件描述符 |
常见用途 |
| 0 |
标准输入 stdin |
| 1 |
标准输出 stdout |
| 2 |
标准错误 stderr |
| 3、4、... |
文件、管道、Socket 等 |
一个文件描述符只是表中的索引,不是文件本身。多个描述符还可能指向同一个 struct file,例如 dup() 后会共享打开文件对象及其文件偏移量。
ls -l /proc/1234/fd
08 · 命名空间:nsproxy
nsproxy 关联任务的部分命名空间,帮助实现进程视图隔离。常见类型有 PID、Mount、Network、UTS、IPC、Cgroup 和 Time Namespace。
例如,容器内部看到的 PID 1,在宿主机中可能是另一个 PID;不同网络命名空间可以拥有各自的网络接口和路由配置。
User Namespace 等信息并非全部直接保存在 nsproxy 中。容器隔离通常还结合 Cgroup 和其他内核机制。
ls -l /proc/1234/ns
09 · CPU 调度:prio
prio 等字段记录任务的调度优先级属性。内核还会结合调度策略、调度类与调度实体等信息决定任务如何获得 CPU。
常见调度策略包括 SCHED_OTHER、SCHED_FIFO、SCHED_RR 和 SCHED_DEADLINE。
ps -eo pid,comm,pri,ni,cls
10 · 一张排查命令速查表
| 想看什么 |
命令 |
| 进程状态 |
cat /proc/PID/status |
| 虚拟内存映射 |
cat /proc/PID/maps |
| 线程列表 |
ls /proc/PID/task |
| 打开的文件 |
ls -l /proc/PID/fd |
| 工作目录 |
readlink /proc/PID/cwd |
| 命名空间 |
ls -l /proc/PID/ns |
| 进程树 |
pstree -p |
写在最后
Linux 进程并不只是一个正在运行的程序,而是内核围绕任务执行、资源管理和系统隔离组织起来的一整套运行环境。
最后再记住这张关系图:
task_struct 管任务;mm_struct 管地址空间;fs_struct 管目录上下文;files_struct 管文件描述符;nsproxy 关联部分命名空间。
理解这些关系,有助于排查进程阻塞、内存异常、文件描述符泄漏、线程调度以及容器隔离等实际问题。更多操作系统底层的深入讨论,也可以在云栈社区与大家一起交流。
注:文中代码与结构体字段为讲解示意,实际以目标内核版本源码为准。