公众号旧文

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 为了方便理解的一种观察方式。