公众号旧文
Linux 里的线程和进程,到底是什么关系?
可能很多人都会以为:
进程 = process
线程 = thread
这是两种完全不同的东西。
但深入 Linux 后,你会发现:
Linux 内核并不严格区分进程和线程。
甚至可以说,Linux 眼里,线程本质上也是进程。
这个设计,也是 Linux 最经典、最优雅的地方之一。
一、从 pstree 开始
我第一次意识到 Linux 内部结构,是因为这个命令:
pstree -p
例如:
tailscaled(59327)
├─{tailscaled}(59328)
├─{tailscaled}(59329)
├─{tailscaled}(59330)
很多人第一反应会是:
59327 是进程ID
59328 是线程ID
但事情并没有这么简单。
二、Linux 里,线程也有 PID
在 Linux 中,线程也有自己的 PID。
例如:
{tailscaled}(59328)
这里 59328 也是一个 PID。
更准确地说,它是 TID(Thread ID)
但在 Linux 内核中,TID 和 PID 使用的是同一个编号空间。
因此 线程ID 和 进程ID 不会重复。
三、Linux 内核真正看到的东西
很多操作系统内部,线程和进程是两套完全不同的数据结构。
但 Linux 不是。
Linux 内核里,无论进程还是线程,本质上都叫:
task(任务)
对应的数据结构:
task_struct
也就是说,Linux 只有 task,所谓线程,只是共享资源的特殊进程。
例如:
- 共享内存
- 共享文件描述符
- 共享代码段
- 共享信号处理
所以,线程 ≈ clone 出来的特殊进程。
四、那 Linux 怎么知道“谁是谁的线程”?
答案是:
TGID(Thread Group ID)
也就是:
线程组ID
例如:
tailscaled(59327)
├─{tailscaled}(59328)
├─{tailscaled}(59329)
真实结构是:
线程组(TGID = 59327)
├─线程/TID 59327(主线程)
├─线程/TID 59328
├─线程/TID 59329
这里:
| ID | 含义 |
|---|---|
| 59327 | 主线程ID,同时也是线程组ID |
| 59328 | 普通线程ID |
| 59329 | 普通线程ID |
五、59327 到底是进程,还是线程组?
严格来说 59327 同时是:
- 一个真实线程
- 主线程
- 线程组组长
- 整个“进程”的代表PID
也就是说 59327 既是一个线程,又代表整个线程组。
因此 Linux 用户空间通常会把 59327 视为整个进程。
六、为什么 Linux 要这么设计?
因为这样 Linux 内核只需要维护一种核心对象:
task
线程只是:
共享资源的 task
因此:
- 调度器统一
- 上下文切换统一
- 信号机制统一
- fork/clone 统一
这是 Linux 很经典的哲学:少抽象,多复用。
七、为什么 ps 默认只显示一个 PID?
因为如果把所有线程都显示出来,一个 Chrome 浏览器可能出现几百行。
因此默认情况下:
ps
top
htop
通常只显示线程组组长
也就是 TGID
所以你通常看到的是 59327
而不是 59328、59329、59330
八、/proc 目录也体现了这一点
例如:
ls /proc/59327/task
你会看到:
59327
59328
59329
59330
这说明:
/proc/59327/
代表的是:
整个线程组(整个进程)
而:
/proc/59327/task/59328
才对应具体某个线程
九、Linux 的“进程”其实是一种视角
从用户角度:
tailscaled
是一个进程。
但从 Linux 内核更底层来看,其实是多个共享资源的 task
只是其中一个线程 59327 被选出来,作为整个线程组的代表。
十、理解这一点后,你会重新认识 Linux
真正理解 Linux 后,你会意识到 Linux 本质上是:
任务(task)
→ 线程组(thread group)
→ 进程视角(process)
整个系统是一棵不断生长的任务树。
而进程,很多时候,只是 Linux 为了方便理解的一种观察方式。