总答案
线程迁移并不是“把正在跑的线程拽走”,
而是:在线程“没在跑”的瞬间,
把它从一个 core 的可运行队列,挪到另一个 core 的可运行队列。
⚠️ 关键点:
👉 迁移几乎总是发生在“线程不在 Running 状态”时
一、调度器真正能“操作”的是什么?
调度器不能直接操作 Core,也不能强行移动正在执行的指令流。
它唯一能直接操作的对象是:
**线程的调度元数据(TCB / task_struct)
- 线程所在的 runnable 队列**
所以迁移的本质是:
修改“这个线程下一次会被哪个 core 选中”的规则
二、Linux 调度里的基本结构
在 Linux(以 CFS 为例)中,逻辑上是这样:
-
每个 CPU core:
- 有一个 本地 run queue
-
每个 runnable 线程:
- 只会在 某一个 run queue 里
-
一个线程在哪个队列里:
- 决定了它只能被哪个 core 调度执行
所以:
迁移 = 把线程从一个 run queue 挪到另一个 run queue
三、什么时候“会考虑迁移”?
调度器并不是每一刻都在做迁移,它通常在几个明确的时机点考虑这件事。
常见触发点(重要)
1️⃣ 周期性负载均衡(Periodic Load Balancing)
-
内核有定时机制
-
定期检查:
-
哪些 core 很忙
-
哪些 core 很闲
-
如果发现负载不均:
- 就会尝试把一些 runnable 线程迁移出去
2️⃣ 线程刚变成 runnable 的时候
比如:
-
一个线程:
-
刚从 IO 返回
-
刚被
condvar唤醒 -
刚释放锁、从 blocked → runnable
-
这时:
调度器会重新决定:
你是回原来的 core,
还是放到更空闲的 core?
3️⃣ 新线程创建时
新线程(fork / clone / pthread_create):
-
一开始并不“属于”某个 core
-
调度器会选择一个当前负载较轻的 core
-
直接把它放进那个 core 的 run queue
四、迁移“具体是怎么做的”(一步一步)
走一遍迁移流程
场景设定
-
Core 0:很忙(run queue 很长)
-
Core 3:很闲(run queue 很短)
-
线程 T:当前是 runnable,但没有在 Running
Step 1:调度器发现“不平衡”
调度器检测到:
load(core0) >> load(core3)
于是决定:
👉 “应该把一些线程从 core0 挪到 core3”
Step 2:选择“可迁移的线程”
⚠️ 不是所有线程都能迁移
调度器会排除:
-
正在 Running 的线程
-
被设置了严格 CPU affinity 的线程
-
某些调度策略下不宜迁移的线程
剩下的候选线程中:
-
通常选择:
-
最近没怎么跑的
-
或者负载贡献较小的
-
或者“迁移成本较低”的
-
Step 3:从原 run queue 移除
调度器会:
-
给 core0 的 run queue 加锁
-
把线程 T 从 core0 的 runnable 队列中摘下来
-
更新 T 的调度元数据:
- “我不再属于 core0 了”
Step 4:加入目标 run queue
然后:
-
给 core3 的 run queue 加锁
-
把线程 T 插入 core3 的 runnable 队列
-
更新 T 的“目标 CPU”字段
此时:
线程 T 的“归属 core”已经变了
但注意:
-
它还没跑
-
只是“排队位置变了”
Step 5:等待被调度执行
接下来发生的事情是自然的:
-
core3 结束当前线程 / 变空闲
-
从自己的 run queue 里选一个线程
-
发现线程 T
-
执行 T
👉 这时你才“观察到”:
线程 T 跑到了 core3
五、非常关键的一点(很多人误解)
❌ 迁移 ≠ 抢走正在执行的线程
调度器不会:
-
把正在 core0 上跑的线程硬拽走
-
在执行中途“扔到另一个 core”
✅ 迁移发生在“安全点”
-
线程未运行
-
或刚结束一次运行
-
或刚被唤醒、尚未执行
这样才能:
-
保证执行语义正确
-
避免破坏寄存器 / pipeline 状态
六、那 cache 是什么时候“冷”的?
-
线程 T 在 core0 跑时:
- cache line 在 core0 的 L1/L2
-
迁移后:
- core3 的 L1/L2 没有这些数据
-
第一次在 core3 跑时:
-
cache miss 爆发
-
工作集重新加载
-
这就是在性能分析里看到的:
迁移 → cache 冷启动 → 延迟抖动