# 内核调度 —— 调度器怎么决定"下一个跑谁"，以及有几种队列

> 机器上有几百个可运行的 task,却只有几个 CPU 核。**内核调度器(scheduler)** 就是那个反复回答"这个核,下一纳秒该跑哪个 task、跑多久"的裁判。本篇从**内核视角**讲清:调度器长什么样(调度类的分层)、普通任务的 **CFS/EEVDF** 凭什么选人、**到底有几种队列**(每核运行队列、红黑树、实时的多级队列、dl 树、睡眠等待队列)、抢占与上下文切换何时发生、以及怎么观测和调节。

> 相关:被调度的实体是什么见 [task-struct.md](/concepts/process/task-struct.md)(task/`task_struct`);把 task 钉在固定核上见 [thread-affinity.md](/concepts/process/thread-affinity.md);上下文切换的开销与观测见 [../cpu/pidstat.md](/tools/cpu/pidstat.md)、[../cpu/vmstat.md](/tools/cpu/vmstat.md);切换要保存/恢复的现场见 [task-struct.md](/concepts/process/task-struct.md) §一。

## 零、一句话认知：调度 = 从"每核的运行队列"里挑一个 task 上 CPU

内核给**每个 CPU 核**配一个**运行队列(`struct rq`，per-CPU run queue)**——只装**此刻可运行(TASK_RUNNING)** 的 task。调度器做的事,就是每次需要切换时,从当前核的运行队列里**按规则挑一个**放上 CPU 跑,时间到了或有更该跑的来了就换下一个。

```plantuml
@startuml 
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<cpu>> #E3F2FD
  BorderColor<<cpu>> #1976D2
  BackgroundColor<<rq>> #C8E6C9
  BorderColor<<rq>> #388E3C
  BackgroundColor<<w>> #FFE0B2
  BorderColor<<w>> #EF6C00
  BackgroundColor<<sched>> #E1BEE7
  BorderColor<<sched>> #7B1FA2
}
' === 硬件 ===
rectangle "CPU 核 0" <<cpu>> as C0
rectangle "CPU 核 1" <<cpu>> as C1
' === 运行队列 (per-CPU) ===
rectangle "运行队列 rq(核0)\n可运行: [A][B][C]" <<rq>> as R0
rectangle "运行队列 rq(核1)\n可运行: [D][E]" <<rq>> as R1
' === 调度器 (纯软件) ===
rectangle "**调度器**\n schedule()\n → pick_next_task()\n(纯软件，无独立进程)" <<sched>> as SCHED
' === 等待队列 (per-内核对象，归组) ===
package "等待队列 — 各自内嵌在内核对象中，无全局注册表" {
  rectangle "磁盘 IO\n[X 等磁盘]" <<w>> as WIO
  rectangle "缺页等待\n[P 等 page in]" <<w>> as WPF
  rectangle "futex\n[F 等用户态锁]" <<w>> as WFUTEX
  rectangle "内核锁\n[L 等 mutex]" <<w>> as WLOCK
  rectangle "Socket\n[S 等网络包]" <<w>> as WSOCK
  rectangle "Pipe\n[Q 等管道数据]" <<w>> as WPIPE
  rectangle "epoll\n[E 等多 fd 就绪]" <<w>> as WEPOLL
  rectangle "定时器\n[M 等 sleep]" <<w>> as WTIMER
}
R0 --> C0 : 挑 task 上 CPU
R1 --> C1 : 挑 task 上 CPU
SCHED .left.> R0 : 从 rq 取最该跑的
SCHED .right.> R1
WIO ..> R0 : 唤醒
WPF ..> R0 : 唤醒
WFUTEX ..> R1 : 唤醒
WLOCK ..> R1 : 唤醒
WSOCK ..> R0 : 唤醒
WPIPE ..> R1 : 唤醒
WEPOLL ..> R0 : 唤醒
WTIMER ..> R1 : 唤醒
note bottom of WTIMER : 等待队列各自内嵌在内核对象中 | 调度器是内核函数
note left of R0 : 运行队列 per-CPU
@enduml
```

### 等待队列不是"一个"，而是一堆——按等待原因各管各

上面画了八种典型等待队列，实际内核中**每个"可等待的事件源"都有自己的等待队列**（本质是 `wait_queue_head_t` 链表头），task 睡眠时挂在对应事件的队列上：

| 等待队列类型 | 对应的内核对象 | 典型场景 | 唤醒者 |
|-------------|---------------|---------|--------|
| **磁盘 IO 等待** | block_device 的 `io_queue`、page 的 `PG_locked` | `read()` 等磁盘数据、`write()` 刷脏页 | 磁盘中断处理程序（DMA 完成后） |
| **缺页等待（major fault）** | page 的 `PG_locked`（同一机制） | `mmap` 访问未载入的页，触发缺页中断后等磁盘读入 | 磁盘中断 → 页锁定释放 |
| **futex 等待** | `futex_q` 链表（在 futex hash table 里） | `pthread_mutex_lock()` 抢不到锁 → `futex(FUTEX_WAIT)` | 持锁者 `futex(FUTEX_WAKE)` |
| **内核锁等待** | `mutex->wait_list`、`sem->wait_list`、`rwsem->wait_list` | 内核路径中 `mutex_lock()` 等内部锁 | 持锁者释放时调用 `__mutex_unlock_slowpath()` |
| **Socket/网络等待** | socket 的 `sk_wq` | `recv()` 没数据可读、`send()` 发送缓冲区满、`accept()` 无新连接、`connect()` 等待握手 | 网卡中断处理程序（包到达/发送完成） |
| **Pipe/FIFO 等待** | pipe 的 `wait` 队列头（`pipe_inode_info->wait`） | 读空管道、写满管道时阻塞 | 对方读写后触发唤醒 |
| **epoll/poll/select 等待** | `eventpoll->wq`（epoll 的等待队列） | `epoll_wait()` 等任意 fd 就绪 | 任一被监控 fd 的事件到达 |
| **定时器等待** | 定时器红黑树 + timer wheel | `sleep()`、`poll(timeout)`、`epoll_wait(timeout)` | 时钟中断 → `run_local_timers()` |
| **信号等待** | `sighand->wait`（每个 task 的信号等待链） | `sigsuspend()`、`sigwaitinfo()`、`pause()` | 信号递达时 `signal_wake_up()` |
| **子进程等待** | `task->signal->wait_chldexit` | `waitpid()`/`waitid()` 等子进程退出 | 子进程退出时 `do_notify_parent()` |
| **内核 completion** | `struct completion` 内嵌等待队列头 | 内核线程间同步，如 `wait_for_completion()` | 对方调 `complete()`/`complete_all()` |
| **通用 wait_event** | `wait_event_*` 宏创建的任意等待队列头 | 内核代码等待自定义条件满足 | 条件满足方调用 `wake_up_*()` |

> **关键认知 0 — 内核没有全局等待队列注册表**：不同于运行队列是集中分配的（每 CPU 一个 `struct rq`，定义在 `kernel/sched/sched.h`），内核**根本没有一个 `all_wait_queues[]` 数组或者全局链表来登记所有等待队列**。每个等待队列头只是所属内核对象结构体里的一个字段：


 ```c
 struct mutex {
     ...
     struct list_head wait_list;  // ← 锁对象自带，不是全局分配的
 };
 
 struct socket {
     ...
     struct socket_wq *wq;        // ← socket 自带
 };

 struct page {
     ...
     unsigned long flags;  // PG_locked 位 + 等待者链表
 };
 ```


> 对象创建时自然就有了等待队列头，对象销毁时自然消失。内核不需要"管"等待队列——谁拥有这个对象，谁就拥有上面的等待队列。唤醒时，唤醒方直接操作**那个特定对象**的等待队列头，把 task 摘下来塞回运行队列。这就是为什么图中等待队列用 `package` 包起来——它们在逻辑上是一类东西，但在物理上**散布在内核各处的对象结构体中，没有统一的存放位置**。

**关键认知 1**：task 不存在"全局睡眠池"里，而是**挂在某个具体事件的等待队列上**。比如一个进程 `read()` 等磁盘，它挂在对应 page 的等待队列上，和等锁/等定时器的任务互不干扰。事件发生时，内核能精确找到"该叫醒谁"——这也是 O(1) 唤醒的基础。

**关键认知 2**：等待队列**和 CPU 没有绑定关系**。不同于运行队列是 per-CPU 的（每个核一个 `struct rq`），等待队列的**归属实体就是被等待的内核对象本身**：

- `mutex` 对象结构体里内嵌一个 `wait_list` 链表头 → 等这把锁的 task 全挂在这
- `struct socket` 里内嵌 `sk_wq` → 等这个 socket 数据的 task 全挂在这
- 文件 page cache 的 `struct page` 有 `PG_locked` 标志和等待者链表 → 等这个 page IO 完成的 task 全挂在这

内核对象创建时等待队列头随之诞生，对象销毁时随之消亡。唤醒操作只是把 task 从该对象的等待队列摘下来、塞回某个 CPU 的运行队列——**塞回哪个核是调度器决定的**（优先唤醒核，见 §五 负载均衡），等待队列本身不 care CPU 编号。

### 调度器是什么实体——纯软件，不是独立进程也不是硬件

一个常见的误解是以为调度器是一个"独立的后台守护进程"。**调度器不是独立的进程/线程，而是内核代码路径**。

- **它是软件**：调度器的核心是 `kernel/sched/core.c` 中的 `schedule()` 函数，由 C 代码实现。CPU 不内置"调度硬件"，调度全部靠软件逻辑做决策。
- **它没有自己的 task_struct**：调度器不是一个 task，你在 `ps aux` 里看不到叫 "scheduler" 的进程。`pick_next_task()` 的代码只是**一个函数调用链**，不是独立执行的实体。
- **调度器代码跑在调用者的上下文中**：谁触发了 `schedule()`，调度器代码就跑在谁的上下文里（用那个 task 的内核栈）。具体包括：
  - **被切换出去的 task**：当前 task 主动阻塞（如 `read()` 等 IO）、或被 tick 中断发现时间片用完了，它自己调用 `schedule()` → `pick_next_task()` 选出继承人 → `context_switch()` 把 CPU 交出去。调度逻辑跑在"即将下 CPU 的人"的上下文中。
  - **idle task**：某核完全没有可运行的 task 时，idle task（per-CPU 的 `swapper/N`，PID=0）在跑，它调 `schedule()` 试图找活干——一有 task 被唤醒，idle 的上下文切换就跑出去。
  - **中断/系统调用返回路径**：tick 中断或系统调用返回时，若 `need_resched` 被置位，返回路径上的代码会调 `schedule()`，此时跑在"刚被打断的那个 task"的上下文。

> **一句话**：调度器是**内核里的纯软件函数**，不独立存在，代码跑在触发调度的那个 task（或 idle task）的上下文/内核栈上，用完就返回——它不需要自己的 task_struct，也不需要独立调度。

## 一、调度类：一个核内的优先级分层

Linux 不是只有一套调度算法,而是把调度器做成**分层的调度类(scheduling class)**。同一个核的运行队列里,**高优先级的类里只要有可运行 task,就轮不到低优先级的类**。从高到低：

```plantuml
@startuml
left to right direction
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<h>> #FFCDD2
  BorderColor<<h>> #C62828
  BackgroundColor<<m>> #FFE0B2
  BorderColor<<m>> #EF6C00
  BackgroundColor<<n>> #C8E6C9
  BorderColor<<n>> #388E3C
  BackgroundColor<<l>> #ECEFF1
  BorderColor<<l>> #607D8B
}
rectangle "① stop_sched_class\n最高,内核内部用(如迁移线程)\n不可抢占" <<h>> as S
rectangle "② dl (SCHED_DEADLINE)\n截止期限调度,按 deadline 排\n实时性最强" <<h>> as D
rectangle "③ rt (SCHED_FIFO/RR)\n实时:固定优先级 0-99\nFIFO 跑到底 / RR 时间片轮转" <<m>> as RT
rectangle "④ fair/CFS (SCHED_OTHER/BATCH/IDLE)\n**普通任务几乎全在这**\n按虚拟运行时间公平分配" <<n>> as CFS
rectangle "⑤ idle_sched_class\n什么都没得跑时跑 idle(省电)" <<l>> as I
S -down-> D
D -down-> RT
RT -down-> CFS
CFS -down-> I
note bottom of RT : 只要 rt 里有可运行 task,\nCFS 的任务就得等——\n实时任务可饿死普通任务
note bottom of CFS : 你写的绝大多数程序\n(SCHED_OTHER)落在这层
@enduml
```

| 调度类 | 调度策略 | 谁用 | 队列结构 |
|--------|---------|------|---------|
| **stop** | 不可抢占 | 内核内部(CPU 热插拔、迁移) | 无(直接跑) |
| **dl** | `SCHED_DEADLINE` | 有硬实时期限的任务 | 按 deadline 排的**红黑树** |
| **rt** | `SCHED_FIFO`/`SCHED_RR` | 实时任务(优先级 0-99) | **每优先级一条链表**(多级队列 + bitmap) |
| **fair(CFS)** | `SCHED_OTHER`/`BATCH`/`IDLE` | **普通任务(默认)** | 按 vruntime 排的**红黑树** |
| **idle** | — | 无任务可跑时 | 无 |

> **要点**:一个核的运行队列 `rq` 里其实**内嵌了多个子队列**——rt 的多级链表、cfs 的红黑树、dl 的红黑树各是一个。调度器 `pick_next_task()` 按调度类**从高到低**问:"你这类有可运行的吗?"第一个有的就出人。所以"有几种队列"要分两层看:**跨类是分层(§一),类内各有自己的队列结构(§三)**。

## 二、CFS / EEVDF：普通任务凭什么被选中

绝大多数程序是 `SCHED_OTHER`,归 **CFS(Completely Fair Scheduler,完全公平调度器)** 管(Linux 6.6 起换成改进版 **EEVDF**,思想一脉相承)。核心思想:**让每个 task 获得"公平的一份"CPU 时间**。

### 虚拟运行时间 vruntime —— 公平的度量

CFS 给每个 task 记一个 **`vruntime`(虚拟运行时间)**:task 在 CPU 上跑,vruntime 就增加;**谁的 vruntime 最小,说明它"欠跑得最多",下次就选它**。

- **nice 值/权重**:`vruntime` 的增速被 **nice 值**(-20~+19)加权——nice 低(优先级高)的 task,vruntime 涨得慢,于是能更频繁被选中、拿到更多 CPU。nice 每差 1,CPU 时间比例约差 1.25 倍。
- **为什么叫"公平" **：不设固定时间片,而是动态让所有 task 的 vruntime 尽量**趋同**——CPU 时间按权重比例公平切分。
- **EEVDF(6.6+)**:在公平基础上引入**虚拟截止期(virtual deadline)**,更好地兼顾延迟敏感任务(交互任务能更快被响应),但"按权重公平 + 红黑树选最小"的骨架不变。

### CFS 的队列:一棵按 vruntime 排序的红黑树

```plantuml
@startuml
left to right direction
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<t>> #C8E6C9
  BorderColor<<t>> #388E3C
  BackgroundColor<<p>> #FFE0B2
  BorderColor<<p>> #EF6C00
}
rectangle "CFS 运行队列(每核一棵红黑树)\n按 vruntime 排序" <<t>> as T {
}
rectangle "最左节点\n= vruntime 最小\n= 下一个要跑的" <<p>> as L
T -down-> L : pick_next 直接取最左\nO(log n) 插入/删除, O(1) 取最左
note bottom of L : 选中 → 上 CPU 跑一会 → vruntime 增长 →\n重新插回树里(可能不再是最左) → 换下一个
@enduml
```

- **数据结构就是一棵红黑树**:节点按 vruntime 排序,**最左边的节点 vruntime 最小 = 下一个被选中**。取最左 O(1)、插入删除 O(log n)。
- **一轮调度周期**:CFS 尽量在一个"调度周期(sched period)"内让每个可运行 task 都跑一次;可运行 task 越多,每个分到的时间片越短(但有 `sched_min_granularity` 下限,防止切换太频繁)。
- **新建/唤醒的 task**:vruntime 会被设成接近当前队列最小值(不让它因为"欠跑很多"一上来霸占 CPU,也不让它饿死)。

## 三、到底有几种队列：一张总表

把"队列"这个词彻底拆开,内核调度涉及的队列/队列式结构有这么几种,别混为一谈：

| # | 队列 | 位置/数量 | 装什么 | 结构 | 谁看它 |
|---|------|----------|--------|------|--------|
| 1 | **运行队列 `rq`** | **每 CPU 核一个** | 该核上所有**可运行**的 task | 容器,内嵌下面 2/3/4 | 调度主循环 |
| 2 | **CFS 红黑树** | 每核 rq 内一棵 | 普通任务(SCHED_OTHER) | 按 **vruntime** 排的红黑树 | CFS 类 |
| 3 | **rt 多级队列** | 每核 rq 内 | 实时任务(FIFO/RR) | **每优先级(0-99)一条链表** + 优先级 bitmap | rt 类 |
| 4 | **dl 红黑树** | 每核 rq 内一棵 | SCHED_DEADLINE 任务 | 按 **deadline** 排的红黑树 | dl 类 |
| 5 | **等待队列 wait queue** | 每个"事件"一个(锁、IO、socket…) | **睡眠中**等事件的 task(S/D 状态) | 链表 | 事件发生时唤醒,不属调度主循环 |

```plantuml
@startuml
skinparam shadowing false
skinparam rectangle {
  BackgroundColor<<rq>> #E3F2FD
  BorderColor<<rq>> #1976D2
  BackgroundColor<<sub>> #C8E6C9
  BorderColor<<sub>> #388E3C
  BackgroundColor<<w>> #FFE0B2
  BorderColor<<w>> #EF6C00
}
package "每个 CPU 核一个 runqueue(rq)" {
  rectangle "rq(核 N)" <<rq>> as RQ {
    rectangle "dl 红黑树\n(按 deadline)" <<sub>> as DL
    rectangle "rt 多级链表\n(每优先级一条, 0-99)" <<sub>> as RT
    rectangle "cfs 红黑树\n(按 vruntime)" <<sub>> as CFS
  }
}
rectangle "等待队列们(全局,按事件)\n锁等待 / 磁盘IO等待 / socket等待..." <<w>> as WQ
note top of RQ : pick_next_task: dl → rt → cfs 从高到低问\n(§一的调度类分层)
WQ ..> RQ : 唤醒:事件到 → 从 wait queue\n摘下 → 塞回某核 rq
RQ ..> WQ : 阻塞:task 要等事件 →\n从 rq 摘下 → 挂到对应 wait queue
@enduml
```

> **一句话理清**:**运行队列(每核 1 个,内含 dl/rt/cfs 三种子队列结构)** 装可运行的,**等待队列(按事件多个)** 装睡着的。跨调度类是**优先级分层**(dl>rt>cfs),类内各用最合适的结构(实时用多级链表按固定优先级、普通用红黑树按 vruntime)。task 在"运行队列↔等待队列"之间来回搬,就是"可运行↔睡眠"状态转换(见 [task-struct.md](/concepts/process/task-struct.md) §三的 R/S/D)。

## 四、什么时候会重新调度：抢占与切换时机

调度不是持续发生的,而是在几个**明确的时机点**触发"重新挑一个 task"(可能换人、也可能还是原来那个)：

- **主动让出**:task 阻塞(等锁/IO/`sleep`)、主动 `sched_yield()`、退出——它从运行队列移出,必须选别人。
- **时间片/周期到**:**调度时钟中断(scheduler tick，默认 100~1000Hz)** 定期检查当前 task 是否跑够了它该跑的份额,够了就置"需要重新调度"标志。
- **唤醒抢占**:一个 task 被唤醒(如高优先级 rt 任务、或 vruntime 更小的任务变为可运行),若它比当前 task 更该跑,**立刻抢占**当前 task。
- **返回用户态/中断返回时**:内核检查 `need_resched` 标志,若置位就调用 `schedule()` 真正切换。

```plantuml
@startuml
skinparam shadowing false
skinparam ArrowColor #37474F
start
:触发点(阻塞/tick/唤醒/系统调用返回);
if (need_resched 置位?) then (是)
  :schedule():\npick_next_task() 从 rq 挑下一个;
  if (挑中的 ≠ 当前?) then (是)
    :context_switch()\n保存旧 task 现场(寄存器/栈/页表)\n恢复新 task 现场;
    note right : 换页表(CR3)=换地址空间\n(同进程线程间切换不用换 mm)
  else (否)
    :继续跑当前 task;
  endif
else (否)
  :继续跑当前 task;
endif
stop
@enduml
```

- **上下文切换的代价**:保存/恢复寄存器、切页表(跨进程要换 CR3 → TLB 多半失效,见 [../cache/tlb.md](/concepts/cache/tlb.md))、缓存变冷。**切换太频繁本身就是开销**——这正是绑核(减少迁移)、减少锁争用的意义(见 [thread-affinity.md](/concepts/process/thread-affinity.md))。切换的完整实现(switch_mm/switch_to、切线程 vs 切进程)见 [context-switch.md](/concepts/process/context-switch.md)。
- **抢占内核**:现代内核是可抢占的(`CONFIG_PREEMPT`),即使在内核态执行,高优先级任务也能抢占(有临界区保护除外)。

## 五、多核：负载均衡与队列间迁移

每核一个运行队列,自然带来"各核忙闲不均"的问题。调度器周期性做**负载均衡(load balancing)**:把 task 从繁忙核的运行队列**迁移**到空闲核的运行队列。

- **迁移触发**:周期性 tick 里检查、或某核变空闲时主动"拉"任务过来。按调度域(scheduling domain，反映 CPU 拓扑:同 SMT 超线程 < 同 L3 < 同 NUMA 节点 < 跨节点)由近到远地找任务迁移,优先在"便宜"的层级内均衡。
- **迁移的代价**:task 被迁到新核,养热的 L1/L2/TLB 全丢在旧核 → 冷缓存 → 尾延迟尖刺。这正是 [thread-affinity.md](/concepts/process/thread-affinity.md) 讲的:**绑核 = 禁止迁移、保住缓存热度**,代价是放弃自动均衡。
- **亲和性约束迁移**:`task->cpus_ptr`(亲和性掩码)限定 task 只能进哪些核的运行队列;`isolcpus` 把某些核**排除**在负载均衡之外,留给关键线程独占。

> 观测迁移:`perf stat` 看 `migrations`、`pidstat -w` 看上下文切换/迁移(见 [../cpu/pidstat.md](/tools/cpu/pidstat.md))。

## 六、观测与调节

```bash
# 看/改一个进程的调度策略与优先级
chrt -p <pid>                 # 查看:显示 SCHED_OTHER/FIFO/RR/... 及优先级
chrt -f -p 50 <pid>           # 改成 SCHED_FIFO 优先级 50(实时,需权限)
nice -n 10 ./app              # 以 nice=10(更谦让)启动
renice -n -5 -p <pid>         # 调整已运行进程的 nice(负=更优先,需权限)
# 看调度相关的运行时状态
cat /proc/<pid>/sched         # 该任务的 vruntime、被调度次数、等待时间等(CFS 细节)
cat /proc/<pid>/status        # State、Cpus_allowed(亲和性掩码)、voluntary/nonvoluntary_ctxt_switches
cat /proc/<pid>/stat          # priority、nice、policy、utime/stime、处理器号(第39字段=上次跑在哪个核)
cat /proc/schedstat           # 每 CPU 的调度统计
pidstat -w 1                  # 每秒的自愿/非自愿上下文切换次数(非自愿高=被频繁抢占)
vmstat 1                      # 'cs' 列=全系统每秒上下文切换;'r' 列=运行队列里可运行任务数
```

- **`vmstat` 的 `r` 列**就是"所有运行队列里可运行 task 总数"——**持续 > CPU 核数**说明 CPU 过载、任务在排队(见 [../cpu/vmstat.md](/tools/cpu/vmstat.md))。
- **非自愿上下文切换(nonvoluntary)高**:任务频繁被抢占/时间片耗尽,可能 CPU 争抢激烈。
- **实时优先级要慎用**:`SCHED_FIFO` 任务不主动让出会**饿死**同核的普通任务,甚至挂起系统(有 `sched_rt_runtime_us` 限流兜底)。

## 七、和本仓库其他文档的关系

- **被调度的实体**:[task-struct.md](/concepts/process/task-struct.md)——task/`task_struct`、调度字段(`sched_class`/`prio`/`cpus_ptr`)、R/S/D/T/Z 状态(运行队列↔等待队列的两端)。
- **绑核/亲和性**:[thread-affinity.md](/concepts/process/thread-affinity.md)——限制 task 能进哪些核的运行队列、禁止迁移保缓存热度、isolcpus/nohz_full。
- **进程/线程创建**:[process-creation.md](/concepts/process/process-creation.md)、[thread-creation.md](/concepts/process/thread-creation.md)——新 task 建好后"加入运行队列、等调度器挑中"。
- **切换代价**:[../cache/tlb.md](/concepts/cache/tlb.md)(换页表→TLB 失效)、[../cpu/pidstat.md](/tools/cpu/pidstat.md)(上下文切换观测)、[../cpu/vmstat.md](/tools/cpu/vmstat.md)(`r`/`cs` 列)、[../cpu/top.md](/tools/cpu/top.md)(负载/状态)。

## 八、一句话总结

> **调度器给每个 CPU 核配一个运行队列(rq),只装可运行的 task;睡眠等事件的 task 待在按事件分的等待队列里,被唤醒才塞回某核 rq。一个核内按调度类分层选人——dl(截止期红黑树) > rt(每优先级一条链表的多级队列) > cfs(按 vruntime 排的红黑树,普通任务几乎全在这,谁欠跑得最多即 vruntime 最小就选谁,nice 加权、6.6 起用 EEVDF),从高到低第一个有可运行 task 的类出人。切换发生在阻塞/时钟tick/唤醒抢占/返回用户态这些时机,代价是保存现场+换页表致 TLB 失效;多核间周期性做负载均衡迁移任务,而绑核就是禁止迁移保住缓存热度。所以'有几种队列'要分两层看:每核一个运行队列(内含 dl/rt/cfs 三种子结构) + 按事件多个的等待队列。**
