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

575 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
好,那我带你走一遍**一个线程从诞生到被CPU执行的完整生命周期**。这段如果你吃透了,操作系统调度这一块基本就打通了。
我尽量讲成“过程动画”的感觉👇
---
# 🧵 0⃣ 起点:你写了一行代码
```c
pthread_create(...)
```
---
# ⚙️ 1⃣ 用户态 → 内核态(系统调用)
实际上会走到:
```c
clone(...)
```
👉 关键参数(简化):
* `CLONE_VM`(共享地址空间)
* `CLONE_FILES`
* `CLONE_THREAD`
👉 内核看到后就明白:
> “哦,这是要创建一个线程(共享资源的 task
---
# 🧱 2⃣ 内核创建 task_struct
内核会干这些事:
### ✅ 分配一个新的:
* `task_struct`
* 内核栈kernel stack
---
### ✅ 初始化内容:
* 寄存器状态(让它从某个函数开始执行)
* 指向同一个 `mm_struct`(共享地址空间)
* 设置调度信息(优先级等)
---
👉 此时内核里出现了一个“新任务”:
```text
task_struct (NEW)
```
---
# 📦 3⃣ 分配用户栈(很关键)
👉 pthread 库会:
* 在进程的虚拟地址空间里
* `mmap` 一块内存
作为:
> 🧵 **这个线程的用户栈**
---
# 🔗 4⃣ 加入各种结构(你刚问的重点)
新线程会被挂到:
---
### ① 线程组链表
```text
main thread ↔ new thread
```
---
### ② 全局任务链表
```text
... → taskA → taskB → new_task → ...
```
---
### ③ 最关键调度队列runqueue
👉 每个 CPU 有一个:
```text
CPU0:
runqueue
红黑树
[task1, task2, new_task, ...]
```
---
👉 插入红黑树(按 vruntime 排序)
---
# 🟡 5⃣ 线程状态READY就绪
此时线程:
> ❗ 还没运行,只是在排队
---
# 🧠 6⃣ 调度器开始工作CFS
调度器会:
👉 在红黑树里选一个:
> **vruntime 最小的线程**
---
👉 选中:
```text
next = leftmost node in rb-tree
```
---
# 🔄 7⃣ 上下文切换(核心!)
CPU 做:
---
### 保存当前线程:
* 寄存器
* PC程序计数器
* 栈指针
---
### 加载新线程:
* 寄存器
* PC
*
---
👉 这个过程叫:
> 🔁 **context switch**
---
# 🚀 8⃣ 开始运行线程
此时:
👉 CPU 真正执行:
```c
start_routine(arg);
```
---
# ⏱️ 9⃣ 时间片到了 or 被打断
可能发生:
* 时间片用完(调度器强制切换)
* 主动让出 CPUyield
* 阻塞IO
---
👉 状态变化:
| 状态 | 含义 |
| ------- | ---- |
| RUNNING | 正在执行 |
| READY | 等待调度 |
| BLOCKED | 等待事件 |
---
# 🔁 🔁 循环
```text
READY → RUNNING → BLOCKED → READY → ...
```
---
# 💀 🔟 线程结束
当线程函数返回:
```c
return;
```
---
内核会:
* 标记 task_struct 为退出状态
* 从调度队列移除
* 释放资源(延迟回收)
---
# 🧠 整个流程一张图
```text
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 之后)
```text
虚拟地址空间(进程刚启动)
高地址
┌──────────────────────┐
│ stack │ ← 主线程栈(向下增长)
│ │
├──────────────────────┤
│ (空) │
│ │
├──────────────────────┤
│ heap │ ← 堆向上增长malloc
├──────────────────────┤
│ BSS未初始化变量
├──────────────────────┤
│ data已初始化
├──────────────────────┤
│ text代码段
└──────────────────────┘
低地址
```
👉 此时:
* 只有一个线程(主线程)
* 一个 `mm_struct`
* 一套页表
---
# 🚀 1⃣ 程序运行中malloc 发生)
```c
malloc(1MB);
```
👇 变化:
```text
heap 向上扩展
├──────────────────────┤
│ heap ↑ │
│ [新分配内存] │
├──────────────────────┤
```
👉 本质:
* `brk``mmap`
* 增加 VMA虚拟内存区域
---
# 📦 2⃣ 使用 mmap比如加载库 / 大块内存)
```c
mmap(...);
```
👇 变化:
```text
高地址
┌──────────────────────┐
│ stack │
├──────────────────────┤
│ mmap 区域 │ ← 动态库 / 文件映射 / 大块分配
│ [libc.so / etc] │
├──────────────────────┤
│ │
├──────────────────────┤
│ heap │
...
```
👉 你会看到:
* 地址空间开始“碎片化”
* 多个 VMA 区域出现
---
# 🧵 3⃣ 创建新线程(重点来了!)
```c
pthread_create(...)
```
👇 **不会创建新地址空间!而是在原空间里加东西:**
```text
高地址
┌──────────────────────┐
│ thread2 stack │ ← 新线程栈
├──────────────────────┤
│ thread1 stack │ ← 主线程栈
├──────────────────────┤
│ mmap 区域 │
├──────────────────────┤
│ heap │
...
```
---
👉 关键变化:
* ❌ 没有新 mm_struct
* ✅ 在同一虚拟空间里 **新增一块栈**
---
# 🔥 4⃣ 多线程运行中(共享内存)
假设代码:
```c
int x = 0;
```
两个线程:
```c
x++;
```
👇 内存是这样:
```text
data段
┌────────────┐
│ x=0 │ ← 所有线程共享!
└────────────┘
```
👉 所以:
* 一个线程改 → 所有线程看到
* → 产生 race condition
---
# ⚠️ 5⃣ 栈是隔离的(非常重要)
```text
thread1 stack: thread2 stack:
┌────────────┐ ┌────────────┐
│ funcA() │ │ funcB() │
│ 局部变量a │ │ 局部变量b │
└────────────┘ └────────────┘
```
👉 栈:
* 在同一地址空间
* 但逻辑隔离
---
# 💀 6⃣ 线程结束
```c
pthread_exit();
```
👇 变化:
```text
thread2 stack 被释放
┌──────────────────────┐
│ thread1 stack │
├──────────────────────┤
│ mmap 区域 │
...
```
👉 本质:
* 对应 VMA 被释放(或标记可复用)
---
# 🧨 7⃣ fork对比一下你就彻底懂了
```c
fork();
```
👇 发生的是:
```text
父进程 mm_struct ——COW——→ 子进程 mm_struct复制
(虚拟地址一样,但页表不同)
```
👉 注意:
* 虚拟地址空间“看起来一样”
* 实际是两份COW
---
# 🎯 最终一张“全景图”(多线程进程)
```text
高地址
┌────────────────────────────┐
│ thread N stack │
├────────────────────────────┤
│ ... │
├────────────────────────────┤
│ thread 2 stack │
├────────────────────────────┤
│ thread 1 (main) stack │
├────────────────────────────┤
│ mmap 区域(库/文件) │
├────────────────────────────┤
│ heap共享
├────────────────────────────┤
│ BSS / data共享
├────────────────────────────┤
│ text代码共享
└────────────────────────────┘
低地址
```
---
# 🔥 你必须抓住的本质(最重要)
👉 **线程改变的是“执行结构”,不是“内存结构”**
---
## 🧠 换句话说:
* 创建进程 → 新世界(新地址空间)
* 创建线程 → 同一个世界,多个人干活
---
# 🚀 给你一个“顿悟级”问题
你现在可以思考:
👉 为什么线程栈要放在高地址?
👉 为什么栈向下增长,而 heap 向上?
👉 如果两个线程栈撞了会发生什么?
---
如果你想,我可以下一步给你画:
👉 **真实 Linux `/proc/[pid]/maps` 对应这张图**
👉 或者带你分析一个程序运行时的真实内存布局(非常爽)