10 KiB
好,那我带你走一遍一个线程从诞生到被CPU执行的完整生命周期。这段如果你吃透了,操作系统调度这一块基本就打通了。
我尽量讲成“过程动画”的感觉👇
🧵 0️⃣ 起点:你写了一行代码
pthread_create(...)
⚙️ 1️⃣ 用户态 → 内核态(系统调用)
实际上会走到:
clone(...)
👉 关键参数(简化):
CLONE_VM(共享地址空间)CLONE_FILESCLONE_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 被打断
可能发生:
- 时间片用完(调度器强制切换)
- 主动让出 CPU(yield)
- 阻塞(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 ↑ │
│ [新分配内存] │
├──────────────────────┤
👉 本质:
brk或mmap- 增加 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 对应这张图
👉 或者带你分析一个程序运行时的真实内存布局(非常爽)