575 lines
10 KiB
Markdown
575 lines
10 KiB
Markdown
好,那我带你走一遍**一个线程从诞生到被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 被打断
|
||
|
||
可能发生:
|
||
|
||
* 时间片用完(调度器强制切换)
|
||
* 主动让出 CPU(yield)
|
||
* 阻塞(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` 对应这张图**
|
||
👉 或者带你分析一个程序运行时的真实内存布局(非常爽)
|