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

10 KiB
Raw Permalink Blame History

好,那我带你走一遍一个线程从诞生到被CPU执行的完整生命周期。这段如果你吃透了,操作系统调度这一块基本就打通了。

我尽量讲成“过程动画”的感觉👇


🧵 0 起点:你写了一行代码

pthread_create(...)

⚙️ 1 用户态 → 内核态(系统调用)

实际上会走到:

clone(...)

👉 关键参数(简化):

  • CLONE_VM(共享地址空间)
  • CLONE_FILES
  • CLONE_THREAD

👉 内核看到后就明白:

“哦,这是要创建一个线程(共享资源的 task


🧱 2 内核创建 task_struct

内核会干这些事:

分配一个新的:

  • task_struct
  • 内核栈kernel stack

初始化内容:

  • 寄存器状态(让它从某个函数开始执行)
  • 指向同一个 mm_struct(共享地址空间)
  • 设置调度信息(优先级等)

👉 此时内核里出现了一个“新任务”:

task_struct (NEW)

📦 3 分配用户栈(很关键)

👉 pthread 库会:

  • 在进程的虚拟地址空间里
  • mmap 一块内存

作为:

🧵 这个线程的用户栈


🔗 4 加入各种结构(你刚问的重点)

新线程会被挂到:


① 线程组链表

main thread ↔ new thread

② 全局任务链表

... → taskA → taskB → new_task → ...

③ 最关键调度队列runqueue

👉 每个 CPU 有一个:

CPU0:
   runqueue
      ↓
   红黑树
      ↓
   [task1, task2, new_task, ...]

👉 插入红黑树(按 vruntime 排序)


🟡 5 线程状态READY就绪

此时线程:

还没运行,只是在排队


🧠 6 调度器开始工作CFS

调度器会:

👉 在红黑树里选一个:

vruntime 最小的线程


👉 选中:

next = leftmost node in rb-tree

🔄 7 上下文切换(核心!)

CPU 做:


保存当前线程:

  • 寄存器
  • PC程序计数器
  • 栈指针

加载新线程:

  • 寄存器
  • PC

👉 这个过程叫:

🔁 context switch


🚀 8 开始运行线程

此时:

👉 CPU 真正执行:

start_routine(arg);

⏱️ 9 时间片到了 or 被打断

可能发生:

  • 时间片用完(调度器强制切换)
  • 主动让出 CPUyield
  • 阻塞IO

👉 状态变化:

状态 含义
RUNNING 正在执行
READY 等待调度
BLOCKED 等待事件

🔁 🔁 循环

READY → RUNNING → BLOCKED → READY → ...

💀 🔟 线程结束

当线程函数返回:

return;

内核会:

  • 标记 task_struct 为退出状态
  • 从调度队列移除
  • 释放资源(延迟回收)

🧠 整个流程一张图

pthread_create
      ↓
clone()
      ↓
创建 task_struct
      ↓
分配栈
      ↓
加入 runqueue红黑树
      ↓
等待调度
      ↓
context switch
      ↓
CPU执行
      ↓
时间片/阻塞
      ↓
再次调度
      ↓
结束 & 回收

🔥 你要抓住的3个“灵魂点”


🧩 1 CPU只认 task_struct

👉 不认进程、不认线程 👉 只认:

“下一个要执行的 task”


🌳 2 调度核心是红黑树CFS

👉 目标:

谁“欠CPU时间”最多就先运行谁


🔄 3 本质是不断切换上下文

👉 并发的本质:

时间复用 CPU


🧨 给你一个“突然开窍”的问题

你现在可以想:

👉 为什么线程切换比进程切换快?

提示你几个关键词:

  • 页表mm_struct
  • TLB
  • cache

如果你愿意,我可以下一步带你深入一个非常关键的点:

👉 context switch 到底切了哪些寄存器?(带汇编) 👉 或者 CFS 的 vruntime 是怎么算的(非常核心)

这两个是从“理解”走向“掌控”的分水岭。


好,这个我给你画一版**“会动的虚拟内存演化图”**,从程序启动到多线程运行,一步步变化,你一眼就能把整个体系串起来👇


🧠 0 初始程序刚加载exec 之后)

虚拟地址空间(进程刚启动)

高地址
┌──────────────────────┐
│        stack         │ ← 主线程栈(向下增长)
│                      │
├──────────────────────┤
│        (空)          │
│                      │
├──────────────────────┤
│        heap          │ ← 堆向上增长malloc
├──────────────────────┤
│   BSS未初始化变量 │
├──────────────────────┤
│   data已初始化    │
├──────────────────────┤
│   text代码段      │
└──────────────────────┘
低地址

👉 此时:

  • 只有一个线程(主线程)
  • 一个 mm_struct
  • 一套页表

🚀 1 程序运行中malloc 发生)

malloc(1MB);

👇 变化:

heap 向上扩展

├──────────────────────┤
│        heap ↑        │
│    [新分配内存]      │
├──────────────────────┤

👉 本质:

  • brkmmap
  • 增加 VMA虚拟内存区域

📦 2 使用 mmap比如加载库 / 大块内存)

mmap(...);

👇 变化:

高地址
┌──────────────────────┐
│        stack         │
├──────────────────────┤
│      mmap 区域       │ ← 动态库 / 文件映射 / 大块分配
│   [libc.so / etc]    │
├──────────────────────┤
│                      │
├──────────────────────┤
│        heap          │
...

👉 你会看到:

  • 地址空间开始“碎片化”
  • 多个 VMA 区域出现

🧵 3 创建新线程(重点来了!)

pthread_create(...)

👇 不会创建新地址空间!而是在原空间里加东西:

高地址
┌──────────────────────┐
│   thread2 stack      │ ← 新线程栈
├──────────────────────┤
│   thread1 stack      │ ← 主线程栈
├──────────────────────┤
│      mmap 区域       │
├──────────────────────┤
│        heap          │
...

👉 关键变化:

  • 没有新 mm_struct
  • 在同一虚拟空间里 新增一块栈

🔥 4 多线程运行中(共享内存)

假设代码:

int x = 0;

两个线程:

x++;

👇 内存是这样:

data段

┌────────────┐
│    x=0     │  ← 所有线程共享!
└────────────┘

👉 所以:

  • 一个线程改 → 所有线程看到
  • → 产生 race condition

⚠️ 5 栈是隔离的(非常重要)

thread1 stack:        thread2 stack:

┌────────────┐        ┌────────────┐
│ funcA()    │        │ funcB()    │
│ 局部变量a  │        │ 局部变量b  │
└────────────┘        └────────────┘

👉 栈:

  • 在同一地址空间
  • 但逻辑隔离

💀 6 线程结束

pthread_exit();

👇 变化:

thread2 stack 被释放

┌──────────────────────┐
│   thread1 stack      │
├──────────────────────┤
│      mmap 区域       │
...

👉 本质:

  • 对应 VMA 被释放(或标记可复用)

🧨 7 fork对比一下你就彻底懂了

fork();

👇 发生的是:

父进程 mm_struct  ——COW——→ 子进程 mm_struct复制

(虚拟地址一样,但页表不同)

👉 注意:

  • 虚拟地址空间“看起来一样”
  • 实际是两份COW

🎯 最终一张“全景图”(多线程进程)

高地址
┌────────────────────────────┐
│   thread N stack           │
├────────────────────────────┤
│   ...                      │
├────────────────────────────┤
│   thread 2 stack           │
├────────────────────────────┤
│   thread 1 (main) stack    │
├────────────────────────────┤
│   mmap 区域(库/文件)      │
├────────────────────────────┤
│   heap共享              │
├────────────────────────────┤
│   BSS / data共享        │
├────────────────────────────┤
│   text代码共享        │
└────────────────────────────┘
低地址

🔥 你必须抓住的本质(最重要)

👉 线程改变的是“执行结构”,不是“内存结构”


🧠 换句话说:

  • 创建进程 → 新世界(新地址空间)
  • 创建线程 → 同一个世界,多个人干活

🚀 给你一个“顿悟级”问题

你现在可以思考:

👉 为什么线程栈要放在高地址? 👉 为什么栈向下增长,而 heap 向上?

👉 如果两个线程栈撞了会发生什么?


如果你想,我可以下一步给你画:

👉 真实 Linux /proc/[pid]/maps 对应这张图 👉 或者带你分析一个程序运行时的真实内存布局(非常爽)