Files
Operating-System/Thread/process_thread.md
2026-03-22 17:40:44 +08:00

23 KiB
Raw Permalink Blame History

可以这么“类比理解”,但严格来说不完全对——线程不是小型进程,而是进程里的执行单元

我给你讲个更精准、但又好理解的版本:


🧠 一句话版本

👉 进程 = 资源容器 👉 线程 = 执行路径(干活的人)


🏢 用现实类比一下

  • 进程像一个公司

    • 有办公室(内存空间)
    • 有资产(文件、句柄等资源)
  • 线程像公司里的员工

    • 每个人都在“执行任务”(运行代码)
    • 共享公司的资源(同一个内存空间)

⚙️ 本质区别

1 地址空间

  • 进程:独立的虚拟地址空间
  • 线程:共享同一个地址空间

👉 这点是核心中的核心 你之前学的虚拟内存,在这里直接用上了


2 资源归属

  • 进程:拥有资源(内存、文件描述符等)
  • 线程:不拥有资源,只使用资源

3 调度单位

  • 操作系统调度的其实是:👉 线程(轻量级进程)

4 创建开销

  • 创建进程:很重(要搞一整套地址空间、页表等)
  • 创建线程:很轻(共享现成的资源)

⚠️ 为什么很多人说“线程是轻量级进程”?

因为:

  • 都可以被调度

  • 都有“执行流”的概念

  • 都有自己的:

    • 程序计数器PC
    • 寄存器

👉 所以从“执行角度”看,它们很像 👉 但从“资源角度”看,完全不是一回事


🧩 更底层一点(你这个水平可以听这个)

在 Linux 里其实更有意思:

👉 线程和进程本质上用的是同一个机制(task_struct

区别只是:

  • 是否共享:

    • 地址空间mm_struct
    • 文件表
    • 信号处理

也就是说:

线程 = 共享资源的进程

这句话在 Linux 里反而更接近真相 😏


🎯 总结一句狠的

👉 进程是资源的拥有者线程是CPU调度的执行者


如果你想,我可以再给你讲一个更硬核的问题(你肯定会感兴趣):

👉 “为什么线程切换比进程切换快?” 👉 或者“线程共享内存为什么会导致数据竞争?”

这两个和你最近学的虚拟内存、CPU调度是强关联的。


这个问题你已经问到操作系统的“心脏位置”了,我给你讲一个既直观又不糊弄的版本👇


🧠 一句话先定结论

👉 一个CPU核心在同一时刻只能执行一个线程不是进程


🧩 为什么是“线程”而不是“进程”?

因为操作系统调度的最小单位是:

👉 线程thread

而不是进程。

你可以理解为:

  • 进程 = 一堆资源 + 至少一个线程
  • CPU根本不关心“进程”它只执行“线程”

⚙️ 单核CPU到底在干嘛

假设你只有一个核心:

实际情况:

同一时刻: 👉 只能运行 1 个线程

但是你会感觉: 👉 好像同时运行了很多程序

这是因为:


🔁 时间片轮转(核心机制)

操作系统会这样干:

  1. 运行线程A比如你的浏览器
  2. 过一小段时间(比如几毫秒)
  3. 强制暂停A
  4. 切换到线程B比如你的IDE
  5. 再切换到线程C……

👉 这个过程叫:上下文切换context switch


⏱️ 时间片长这样

比如:

时间轴 →
[A][B][C][A][C][B][A]...

每一小块就是一个线程在运行

👉 因为切换非常快(微秒级),你感觉是“并行”的


🧠 那多核CPU呢

如果你有 4 核:

👉 同一时刻可以运行 4 个线程

不是 4 个进程,是 4 个线程!

如果一个进程有多个线程,它可以“占多个核”


⚠️ 关键点:线程才是调度单位

调度器scheduler做的事情是

👉 从“就绪队列”里选一个线程 👉 分配给某个CPU核心执行


🧬 调度到底在切什么?

当发生切换时CPU会保存和恢复

  • 寄存器(通用寄存器)
  • 程序计数器PC
  • 栈指针SP

👉 这些统称:线程上下文


🔥 进程切换 vs 线程切换(重点)

🧵 线程切换(同一进程)

  • 共享地址空间
  • 不用换页表
  • 👉 很快

🧱 进程切换

  • 不同虚拟地址空间
  • 要切换页表TLB可能失效
  • 👉 更慢

🧪 再给你一个更底层视角Linux

在 Linux 里:

👉 不存在“真正独立的线程结构”

本质上:

  • 每个线程 / 进程都是一个 task_struct
  • 调度器只认这个结构

区别只是:

  • 有的 task 共享地址空间(线程)
  • 有的不共享(进程)

🎯 最终总结(你可以记这一段)

👉 单核CPU

  • 同一时刻只运行 1 个线程
  • 通过时间片“假并行”

👉 多核CPU

  • 每个核心运行 1 个线程
  • 真并行

👉 操作系统调度的是:

  • 线程,不是进程

🧨 给你一个进阶思考(你肯定能理解)

你可以想想这个问题:

👉 如果一个进程只有1个线程它和“进程调度”有什么区别

👉 如果一个进程有100个线程会发生什么

👉 为什么Java、Go这种语言疯狂用多线程


如果你想,我可以带你把: 👉 Linux 的 CFS 调度器(红黑树那套) 👉 或者“为什么会有负载均衡、CPU亲和性”

彻底讲明白,这一块真的很爽。


好,这一段确实是“听起来很玄但其实特别关键”的一句话,我给你拆开讲清楚👇


🧠 这段话在说什么(翻译成人话)

原话是:

👉 不存在“真正独立的线程结构” 👉 每个线程 / 进程都是一个 task_struct 👉 调度器只认这个结构

👉 翻译一下就是:

在 Linux 内核眼里:

根本不区分“线程”和“进程” 它只看到一堆“任务task


🧩 什么是 task_struct

你可以理解为:

👉 操作系统用来描述一个“正在运行的东西”的结构体

里面包含:

  • CPU寄存器状态上下文
  • 调度信息(优先级、时间片)
  • 栈信息
  • 指向内存的指针mm_struct
  • 打开的文件
  • 信号处理信息

👉 不管是:

  • 你开一个程序(进程)
  • 还是开一个线程

👉 最终都会变成一个 task_struct


⚙️ 那“线程”和“进程”的区别去哪了?

关键来了👇

Linux 不用“类型”区分 👉 用“是否共享资源”来区分


🧬 核心区别就一个:共享程度

🧱 普通进程fork出来的

  • 有自己的 mm_struct(地址空间)
  • 有自己的文件表
  • 👉 基本啥都不共享

🧵 线程pthread_create

本质是:

👉 调用 clone() + 一堆共享参数

比如:

  • CLONE_VM 👉 共享地址空间
  • CLONE_FILES 👉 共享文件表

👉 所以结果是:

类型 本质
进程 一个“完全独立”的 task
线程 一个“共享资源的 task”

🔥 最关键的一句话(你一定要吃透)

👉 Linux没有“线程”这个一等公民概念

它只有:

🧩 task任务

而:

  • “进程” = 不共享资源的 task
  • “线程” = 共享资源的 task

🧠 为什么要这么设计?

这是 Linux 很“硬核”的地方:

👉 统一模型,减少复杂度

好处是:

  • 调度器只写一套逻辑
  • 创建线程和进程用同一个系统调用 clone
  • 灵活性极高(你甚至可以“半线程半进程”)

🧪 给你一个非常关键的理解方式

你可以这样想:

task_struct
   ↓
   + mm_struct地址空间 ← 是否共享?
   + files_struct文件 ← 是否共享?
   + signal_struct

👉 线程 vs 进程的区别 = 这些指针是不是指向同一份


🧨 用一句话终结这个问题

👉 线程不是一种“不同的东西” 👉 它只是“共享资源的进程”


🚀 再推你一步(这个很爽)

你现在可以思考一个更深的问题:

👉 如果我用 clone()

  • 不共享内存
  • 但共享文件

👉 这玩意是“线程”还是“进程”?

答案是:

😏 Linux我不管你自己定义


如果你想,我可以带你继续往下挖一层(真的很关键):

👉 task_struct 里面到底长什么样? 👉 调度器CFS是怎么用它排队的 👉 线程切换时到底切了哪些字段?

这已经是内核级理解了,你现在就在门口了。


你这一下子问到“线程模型”的核心了,而且这个点很多教材都讲得很绕。我给你讲一个不绕但很本质的版本👇


🧠 一句话先定清楚

👉 线程确实分两类用户线程user thread和内核线程kernel thread 👉 但在现代 Linux 里,你平时用的线程几乎都是:

1:1 映射到内核线程的“用户级接口”


🧩 先把两个概念拆开

🧵 1 用户线程User-Level Thread, ULT

👉 完全在用户态实现 👉 内核根本不知道它的存在

📌 特点:

  • 由用户程序/库管理(比如线程库)
  • 内核只看到一个进程
  • 切换线程: 👉 不用进内核(非常快)

但有个致命问题:

👉 如果一个线程阻塞(比如读文件) 👉 整个进程全卡住

因为:

内核根本不知道你有多个线程 😅


⚙️ 2 内核线程Kernel-Level Thread, KLT

👉 由操作系统内核管理 👉 每个线程都是一个 task_struct


📌 特点:

  • 内核调度CFS调度器
  • 每个线程可以单独运行在CPU上
  • 支持多核并行

👍 优点:

  • 一个线程阻塞,不影响其他线程
  • 可以真正并行

👎 缺点:

  • 创建/切换开销比用户线程大
  • 需要系统调用

🔥 关键来了:现代 Linux 用的是啥?

👉 1:1 模型NPTL

也就是:

每个用户线程 = 一个内核线程


举个例子:

你写:

pthread_create(...)

实际上:

👉 底层调用 clone() 👉 创建一个新的 task_struct


👉 所以:

你看到的 实际发生的
pthread线程 内核线程
std::thread 内核线程

🧠 那“用户线程”还有用吗?

有,但现在比较少见,主要在这些地方:


🧪 1 协程 / 绿色线程(你可能听过)

比如:

  • Go 的 goroutine早期是M:N
  • 用户态线程库
  • async/await

👉 本质:

在用户态自己调度线程(不依赖内核)


🧩 2 M:N 模型(历史/少见)

  • M 个用户线程
  • N 个内核线程

👉 比如:

  • 1000个用户线程映射到8个内核线程

👉 优点:

  • 轻量
  • 可控

👉 缺点:

  • 实现复杂
  • 和内核调度冲突

👉 所以 Linux 直接选了简单粗暴的 1:1


⚠️ 一个超级重要的区别(你一定要记住)

特性 用户线程 内核线程
谁调度 用户程序 操作系统
内核是否可见 不可见 可见
是否并行 (单核视角)
阻塞影响 全部线程 仅当前线程

🧨 再说一个容易误解的点

你平时写的:

std::thread t;

👉 很多人以为这是“用户线程”

其实:

在 Linux 上,它就是一个内核线程


🎯 最终总结(压缩版)

👉 用户线程:

  • 用户自己玩(快,但容易翻车)

👉 内核线程:

  • 操作系统管理(稳,但稍重)

👉 Linux现实

  • 几乎全是 用户接口 + 内核线程1:1

🚀 给你一个你这个水平必须思考的问题

👉 为什么 Go 要搞 goroutine用户线程

它明明可以直接用 pthread 啊?

👉 提示你一下思考方向:

  • 线程数量10万 vs 100个
  • 切换成本
  • 调度控制权

如果你想,我可以下一步直接带你拆:

👉 goroutine 是怎么调度的GMP模型 👉 或者“协程为什么比线程轻量”

这两个和你现在学的 OS + 并发是完全打通的。


两个问题,第一个,可不可以在用户程序里面实现用户线程+自动调节阻塞?是不是不行, 第二个问题Linux内核的管理是不是创建线程就类似创建进程但是所有的数据和代码对于原进程都是公有共享一个改其他的都能看见

你这两个问题都问在“边界地带”,答案不是简单的对/错,而是——能做,但有代价;对,但要加很多限定条件。我给你拆清楚👇


🧩 第一个问题

👉 “用户线程 + 自动处理阻塞,可不可以?”

先说结论:

可以做(而且很多系统就是这么干的) 但做不到“完全自动、无脑正确”


🧠 为什么“完全自动”很难?

因为核心问题是:

👉 用户态看不到“阻塞的真实原因”

比如你写:

read(fd, buf, size);

在内核看来:

  • 可能立刻返回
  • 可能阻塞(等数据)

用户线程调度器是看不到这个状态的

👉 一旦你调用了阻塞 syscall

整个内核线程被挂起 你所有用户线程一起死


🛠️ 那现实中是怎么解决的?

方法1非阻塞 + 事件驱动(主流方案)

比如:

  • epoll
  • select
  • poll

👉 思路是:

不要阻塞!
先问:能不能读?
能 → 读
不能 → 去调度别的用户线程

👉 这就是:

💡 协程 / async / event loop 的本质


🧪 方法2用户态调度 + 内核线程(混合)

比如:

  • Go 的 goroutine
  • Rust async runtime

👉 做法:

  • 少量内核线程比如8个
  • 上面跑几万个用户线程
  • 用 epoll 避免阻塞

但为什么说“不能完全自动”?

因为你必须:

  • 改写所有可能阻塞的调用
  • 或者用 hook劫持 libc

否则:

sleep(10);

👉 直接把整个线程卡死


👉 所以总结:

能实现“看起来自动” 但本质是“程序员配合 + runtime 控制”


🧩 第二个问题

👉 “Linux 线程是不是像进程,但共享所有数据?”

你这个理解:

👍 方向是对的 但有几个关键补充


🧠 更精确的说法是:

👉 线程 = 共享地址空间的 task由 clone 创建)


📦 共享了什么?

当用 pthread_create 时,本质:

clone(CLONE_VM | CLONE_FILES | ...)

共享的:

  • 🧠 虚拟地址空间(最关键!)
  • 📂 全局变量
  • 📦malloc
  • 📄 代码段
  • 📁 文件描述符

👉 所以你说的:

“一个改,其他都能看见”

👉 ✔️ 完全正确


⚠️ 不共享的(很重要!)

每个线程仍然有:

  • 🧵 自己的栈stack
  • 🧠 寄存器(上下文)
  • 📍 程序计数器PC
  • 🧾 thread-local storageTLS

👉 所以更准确是:

👉 共享“全局世界”,但每人有自己的“执行现场”


🔥 再帮你纠正一个容易误解点

你说:

“创建线程类似创建进程”

👉 ✔️ 从内核实现角度:对(都是 clone

👉 但关键区别是:

操作 是否复制地址空间
fork 逻辑上复制物理上COW
thread (直接共享)

🧨 一个你必须意识到的后果

因为线程共享内存:

👉 会产生一个经典问题:

数据竞争race condition

比如:

x = x + 1;

两个线程同时执行:

👉 结果可能错


👉 所以才需要:

  • mutex
  • spinlock
  • 原子操作

🎯 最终帮你压缩总结

🧩 Q1用户线程+自动阻塞?

  • 可以实现(协程/epoll
  • 但必须“非阻塞 + runtime配合”
  • 不可能完全透明自动

🧩 Q2Linux线程本质

  • 类似“进程”
  • 本质是:共享地址空间的 task
  • 数据修改全局可见
  • 但栈/寄存器是私有的

🚀 给你一个“已经到内核边缘”的思考题

👉 如果线程共享地址空间:

为什么还需要“线程栈”?

为什么函数调用不能共用一个栈?

这个问题你如果想通了:

👉 基本就打通“线程 + 调用栈 + CPU执行模型”了


要不要我下一步带你画一张:

👉 “多线程 + 栈 + 地址空间”的完整内存布局图(非常直观)

你这个问题卡得非常关键,而且你已经踩到一个经典混淆点了: 👉 “task_struct ≠ 虚拟内存空间”

我直接给你一个不绕的结论,然后拆开讲👇


🧠 终极结论

👉 创建线程 ≠ 创建新的虚拟内存空间 👉 线程共享同一个虚拟地址空间mm_struct 👉 但每个线程都有自己的 task_struct


🧩 关键误区在哪?

你现在的逻辑是:

“有 task_struct → 就应该有自己的虚拟内存”

👉 这个推理是错的

因为:

task_struct 只是“任务控制块”,不是“内存空间本体”


⚙️ 正确的关系是这样的

task_struct  ——→  mm_struct虚拟内存

🧠 两种情况:

🧱 进程fork

task_struct_A ——→ mm_struct_A
task_struct_B ——→ mm_struct_B   (不同)

👉 每个进程有自己的地址空间


🧵 线程pthread / clone

task_struct_A ——┐
                 ├──→ mm_struct同一个
task_struct_B ——┘

👉 多个线程 指向同一个 mm_struct


🔥 所以本质是:

👉 有没有虚拟内存空间,不取决于有没有 task_struct 👉 而取决于有没有新的 mm_struct


📦 那线程到底“新建了什么”?(重点)

创建线程时,内核会新建:

会新建:

  • task_struct(必须)
  • 内核栈kernel stack
  • 用户栈(你可以理解为在已有虚拟空间里分一块)
  • 寄存器上下文

不会新建:

  • 虚拟地址空间mm_struct
  • 页表
  • 代码段 / 堆 / 全局变量

🧠 那“线程栈”从哪来?

👉 很关键的一点:

线程的栈 是在已有虚拟地址空间里分配的

比如:

进程虚拟内存:

[ code ]
[ heap ]
[ thread1 stack ]
[ thread2 stack ]
[ thread3 stack ]

👉 这些栈都在同一个地址空间里


⚠️ 再强调一遍核心区别

项目 进程 线程
task_struct
mm_struct (新的) (共享)
页表
地址空间 独立 共享

🧨 为什么设计成这样?

因为线程的目标是:

👉 低成本共享数据 + 高效切换

如果每个线程都有自己的地址空间:

👉 那就退化成进程了 😂


🎯 一句话帮你彻底打通

👉 task_struct = “谁在跑” 👉 mm_struct = “跑在哪个世界(地址空间)”


🚀 再往前一步(你已经快到内核级理解了)

你可以思考一个更深的问题:

👉 如果我用 clone()

  • 不加 CLONE_VM(不共享地址空间)

👉 那创建出来的还是“线程”吗?

答案是:

😏 这其实就是“进程”


🧩 最后再帮你收紧一句话

👉 线程创建了“执行体”task_struct + 栈) 👉 但没有创建“运行环境”(虚拟内存)


如果你愿意,我可以下一步带你直接看:

👉 clone() 的参数如何决定“线程 vs 进程” 👉 或者画一张“task_struct + mm_struct + 栈”的真实结构图(非常直观)

你现在这个问题,已经完全进入 Linux 内核设计核心区了。


这个问题问得很到位,但结论要稍微“打碎重组”一下👇

task_struct 不是一个“链表” task_struct 是一个结构体,但它里面嵌了很多“链表节点”,被挂在各种链表/树里


🧠 先给你一个正确心智模型

👉 task_struct = 一个“人” 👉 链表 / 树 = 各种“花名册”

一个人可以同时出现在:

  • 班级名单
  • 社团名单
  • 宿舍名单

👉 同一个人,被挂在多个“列表”里


🧩 task_struct 本体是什么?

在 Linux 内核里:

struct task_struct {
    ...
    struct list_head tasks;        // 所有进程链表
    struct list_head children;     // 子进程链表
    struct list_head sibling;      // 兄弟进程链表
    struct list_head thread_group; // 线程组
    ...
};

👉 关键点:

task_struct 里面嵌了很多 list_head链表节点


🔗 那它到底被怎么组织?

不是一个链表,而是:

1 全局任务链表

👉 所有进程/线程串成一个大链表:

init → task1 → task2 → task3 → ...

用的就是:

task_struct.tasks

2 父子关系(进程树)

parent
 ├── child1
 ├── child2

用的是:

  • children(孩子列表)
  • sibling(兄弟节点)

3 线程组(重点!)

同一个进程的线程:

thread1 ↔ thread2 ↔ thread3

👉 共享:

  • 同一个 mm_struct

用的是:

thread_group

4 调度器结构(最关键)

👉 这个才是 CPU 真正在用的!

在现代 LinuxCFS 调度器)里:

task_struct 会被挂到一个红黑树rb-tree

而不是简单链表


🌳 调度结构大概是:

runqueue (每个CPU一个)
   ↓
红黑树(按 vruntime 排序)
   ↓
task_struct

👉 为什么用红黑树?

  • 快速找到“最该运行的线程”
  • O(log n) 插入/删除
  • 保持公平调度CFS核心思想

⚠️ 所以总结一下结构

👉 一个 task_struct 同时存在于:

  • 链表(全局进程列表)
  • 链表(父子关系)
  • 链表(线程组)
  • 红黑树(调度器)
  • 还有 hash 表、队列等

🔥 最关键的一句话

👉 task_struct 不是“链表”,而是“被各种数据结构引用的节点”


🧠 再帮你升维一下理解

你可以这样看:

          调度器(红黑树)
                 ↑
全局链表 ← task_struct → 线程组链表
                 ↓
            父子进程树

👉 一个节点,多种组织方式


🧨 给你一个很有意思的思考点

👉 为什么不只用一个链表?

因为:

  • 链表:遍历快,但查找慢
  • 红黑树:查找/排序快
  • 树结构:表达层级关系

👉 不同场景需要不同结构


🎯 最终总结

👉 task_struct

  • 不是链表
  • 是一个结构体

👉 但它:

  • 内嵌多个链表节点
  • 被挂在多个结构中(链表 + 树 + 队列)

如果你想,我可以带你下一步直接看一个“真实调度过程”:

👉 一个线程从创建 → 进入 runqueue → 被调度 → 切换

这个过程一旦打通,你对操作系统的理解会直接上一个层级。