﻿## 一、先分两大类：主动调度 VS 被动抢占（中断强制打断）

### 1、主动调度（你说的阻塞场景）

线程在用户态执行阻塞调用（read、mutex、sleep等），执行syscall陷入内核，内核发现资源不可用，**线程主动调用`schedule()`放弃CPU，主动保存上下文，切换其他线程运行**。

全程没有硬件打断，是执行流自己走到调度函数。

---

# 二、被动抢占：时钟中断强行打断线程，分两大场景：

### 场景A：中断发生时，线程正在【用户态运行】（最容易抢占）

硬件时钟中断触发CPU：CPU强制打断用户态指令流，保存用户态寄存器现场，陷入内核中断入口。

完整步骤：

1. CPU收到定时器中断，强制打断正在执行的用户态指令，清空流水线、指令预取队列，保存用户态所有通用寄存器、栈指针到内核栈；
2. 进入内核中断处理函数`scheduler_tick()`：检查当前线程运行时间，时间片耗尽，调用`resched_curr()`，设置线程标志位：`TIF_NEED_RESCHED = 1`（告诉内核：这个线程需要被换下CPU）；
3. 中断处理全部执行完毕，内核准备**返回用户态**；

   在返回用户态之前，内核汇编代码一定会检查：`TIF_NEED_RESCHED`是否置位；

4. 标志位存在，不直接返回用户态，转而调用`schedule()`执行线程上下文切换，切换到其他就绪线程；
5. 等新线程时间片到期，后续再次中断切换回来，恢复原来线程的用户态上下文，继续执行被打断的指令。

> 关键点：

用户态运行的线程，**随时可以被时钟中断打断，且一定能被抢占**，没有任何限制。只要设置了need_resched，在返回用户态那一刻必然发生调度。

---

### 场景B：中断发生时，线程正在【内核态运行】（系统调用执行中）

这个是重点，Linux分两种内核配置：不可抢占内核、可抢占内核（CONFIG_PREEMPT），服务器默认普通内核是**非完全抢占内核**。

## B1：标准服务器内核（关闭内核抢占，绝大多数线上CentOS系统）

流程：

1. 时钟中断打断正在内核态执行的线程，CPU保存内核态上下文，进入中断处理；
2. 中断里依旧判断时间片用完，设置`TIF_NEED_RESCHED`标志；
3. 中断处理完成，此时CPU还在内核空间，不会立刻调度！

普通内核规则：**一旦进入内核态执行，除非主动调用schedule()，否则必须等这段内核代码执行完毕，返回到用户态的那一刻，才会检查标志、执行抢占切换线程**。

也就是说：

线程在内核态跑系统调用，就算时间片用完，也只能标记一个待调度标志，内核会继续跑完当前系统调用全部逻辑，直到准备返回用户态之前，才做线程切换。

内核代码运行期间，不会中途被抢占踢下CPU。

## B2：开启内核抢占 PREEMPT（实时内核/RT内核，量化低延迟系统常用）

内核允许在内核执行中途直接抢占，依靠两个东西：

1. `TIF_NEED_RESCHED`：时间片用完标记需要调度；
2. `preempt_count`抢占计数器：线程在内核临界区（持有自旋锁、中断上下文）时计数器>0，禁止抢占；计数器等于0，内核代码可被打断。

完整打断流程：

1. 时钟中断打断内核态运行的线程，设置need_resched标志；
2. 中断退出时，内核判断两个条件：

   `preempt_count == 0 && TIF_NEED_RESCHED置位`；

3. 条件满足：不用等到返回用户态，**在内核上下文直接执行preempt_schedule_irq()，立即做线程上下文切换，把当前内核态线程换下CPU**；
4. 如果preempt_count>0（线程拿着自旋锁、处于中断处理中），则不能抢占，只能继续执行，等到临界区退出：解锁自旋锁时`preempt_enable()`，计数器减到0，再主动检查标志、触发调度切换。

---

# 三、总结两种状态下被打断的区别表格

| 线程运行状态 | 时钟中断到来 | 普通服务器内核(不可抢占内核) | PREEMPT可抢占内核 |
|---|---|---|---|
| 用户态执行 | 被硬件强行打断 | 中断返回用户态前必然触发调度切换线程 | 同样中断返回用户态前切换，逻辑一致 |
| 内核态执行（系统调用中） | 被硬件强行打断，仅设置TIF_NEED_RESCHED标志 | 不立即切换，等待内核代码执行完毕，返回用户态才调度 | preempt_count=0：内核中途直接抢占切换；preempt_count>0：等待临界区结束再抢占 |

---

# 四、核心几个关键点总结（结合你之前所有知识点）

1. 所有被动抢占的源头只有一个：**硬件中断（定时器中断为主）**，没有中断，CPU永远不会主动把正在运行的线程踢下来；
2. 中断只会做两件事：①保存当前CPU现场；②设置need_resched标记；本身不做上下文切换；
3. 用户抢占统一发生在：中断/系统调用返回用户空间的统一检查点；
4. 内核态能不能中途被抢占，只看`preempt_count`计数器，普通线上服务器内核默认不允许内核中途抢占，必须跑完内核路径回到用户态才切换；
5. 上下文切换保存的内容：用户态/内核态寄存器、栈指针、task_struct状态，I-cache/D-cache/TLB大部分保留，只有流水线、指令预取队列被清空。

如果你需要，我可以画一张PlantUML时序图：时钟中断→标记调度→用户态抢占/内核态抢占两条分支完整流程。

## 一、先给核心结论

不管是**主动陷入内核（syscall阻塞调用schedule）**，还是**时钟中断强行打断执行流**：

CPU都会把**完整的用户态执行现场（所有通用寄存器、指令指针RIP、栈指针RSP、全部调用上下文）完整保存到内核栈上**。

线程切换时，保存的是内核栈里这份用户态快照；等线程再次被调度运行，内核先恢复内核栈内容，最后恢复整套用户态现场，回到原来被打断的位置继续执行，调用栈完全不变。

用户态调用序列一直存在于**用户态虚拟地址栈空间里，从来没有移动过**，CPU只是保存了指向它的指针。

---

## 二、拆分两种场景，现场保存位置一模一样

### 1. 用户栈本身不会动

你的函数调用栈、局部变量、返回地址，全部在进程私有用户虚拟地址空间的栈区域。

中断、系统调用都不会去拷贝这一大片内存，只是CPU把「当前执行位置、栈指针」这几十个寄存器保存起来。

---

## 场景1：主动陷入内核（阻塞调用，主动调用schedule）

1. 用户态执行`read()`系统调用，执行`syscall`指令；
2. CPU硬件自动把：RIP（下一条要执行的用户态指令地址）、RSP（用户栈栈指针）、所有通用寄存器，压入当前线程的**内核栈**；
3. 进入内核，内核发现资源不可用，主动调用`schedule()`；
4. `schedule()`做上下文切换：把当前线程整个硬件上下文（内核栈里保存的用户态全套寄存器）记录到`task_struct`线程描述符中，切换页表、切换内核栈，切换到其他线程运行；
5. 后续重新调度到这个线程时：先恢复内核栈数据，再通过`sysret`指令，从内核栈里取出之前保存的RIP、RSP、通用寄存器，恢复用户态执行，直接回到syscall之后的那条指令，用户栈、调用链路完全原样。

---

## 场景2：时钟中断强行打断（被动抢占，正在用户态运行）整个保存逻辑几乎一致

### 完整步骤：

1. CPU正在用户态执行普通指令，时钟中断触发，CPU硬件强制打断指令流；
2. CPU硬件自动执行动作（和syscall陷入几乎一样）：

   - 保存当前用户态RIP（被打断那一条指令的地址）；
   - 保存用户态RSP（用户栈栈顶指针，整个用户调用栈的入口）；
   - RAX~R15全部通用寄存器，依次压入**当前线程的内核栈**；

3. 进入中断处理函数`scheduler_tick`，设置`TIF_NEED_RESCHED`标记；
4. 中断处理完成，内核检查需要调度，调用`schedule()`执行线程切换；
5. `schedule()`保存当前线程全部硬件上下文到task_struct：内核栈里存放的整套用户态寄存器快照一并被保存；切换其他线程运行。

### 重点回答你的疑问：

> 用户态调用序列存在哪里？

1. **用户函数调用栈（各级函数返回地址、局部变量）：依然在用户地址空间的用户栈内存区域，原封不动，没有复制、没有移动。**
2. CPU只保存了指向这片用户栈的指针`RSP`，以及指令断点地址`RIP`，这两个关键值存放在内核栈。

> 下次怎么恢复？

当线程再次被调度执行：

1. 内核切换回来，先加载该线程的内核栈；
2. 内核栈里存放着之前中断时刻保存的：RIP、RSP、所有通用寄存器；
3. 内核执行iret/sysret指令，硬件自动恢复全部寄存器：RSP恢复成原来用户栈栈顶，RIP回到被打断的指令地址；
4. CPU直接继续执行之前被中断打断的指令，用户态的整套调用上下文、栈数据完全没有变化，程序感知不到曾经被打断过。

---

## 三、特殊情况：中断发生时，线程正在内核态运行

1. 此时CPU已经在内核空间，RSP指向内核栈，用户态RSP、RIP早就存在内核栈里面了（之前陷入内核的时候保存的）；
2. 时钟中断只会打断当前内核执行流，再次把**内核态的寄存器现场压入内核栈内层**；
3. 内核标记需要调度，切换线程；
4. 恢复的时候：先恢复内核态上下文，等内核代码执行完毕，准备返回用户态时，再取出最开始保存的用户态RSP、RIP，恢复用户调用栈执行。

也就是说：

内核栈是分层存放的：

```bash
内核栈顶层：中断时候保存的内核态现场
内核栈下层：系统调用陷入时保存的用户态完整现场（RIP/RSP/全部通用寄存器）
```

不管多少次中断嵌套，用户态那一份现场始终保存在内核栈固定位置，不会丢失。

---

## 四、极易混淆的两个点

1. **上下文切换保存的是寄存器快照，不是用户栈内存拷贝**

用户栈很大几MB，不可能每次切换都拷贝。只保存RSP指针即可，用户栈位于进程虚拟地址空间，切换页表之后地址依然有效，数据不变。

2. syscall主动陷入 和 硬件中断抢占，二者保存用户态现场的机制完全相同

唯一区别：主动陷入是线程自愿放弃CPU；中断是硬件强制抢占，但是现场保存、恢复的硬件流程一模一样，程序层面无差别。

---

## 五、极简总结

1. 用户态调用栈、函数执行序列：常驻进程用户虚拟栈内存，全程不动；
2. 中断强行打断CPU时，硬件把断点地址RIP、用户栈指针RSP、所有通用寄存器，存入当前线程内核栈；
3. schedule切换线程时，把内核栈里这份寄存器快照存入task_struct；
4. 线程重新调度：恢复内核栈 → 硬件指令恢复RIP+RSP，程序回到被打断位置，调用链路完整保留；
5. 多次中断嵌套只会在内核栈叠加保存内核现场，底层保存的用户态现场一直保留不变。

如果你需要，我可以画一张PlantUML：中断打断用户态执行→内核栈保存现场→线程切换→恢复执行完整时序图。

完全正确，你总结得非常精准，两点全部命中：

## 1、陷入过程（syscall / 硬件中断打断用户态）

**CPU硬件自动完成，不需要内核代码插手**

CPU检测到中断/系统调用指令之后，硬件自动执行一套固定动作：

1. 把用户态所有通用寄存器、RIP（断点指令地址）、RSP（用户栈指针）依次压入当前线程的内核栈；
2. 切换RSP到内核栈栈顶；
3. 切换特权级，跳转到内核预设的入口地址。

整个压栈动作是CPU电路硬逻辑，内核汇编入口只在这之后才开始执行。

## 2、返回过程（iret / sysret 指令）同样是CPU硬件自动完成弹出

内核最后执行 `iretq` 或者 `sysret` 指令，这条指令也是硬件指令：

CPU自动从当前内核栈顶部，依次弹出之前保存的：RIP、RSP、段寄存器、全部通用寄存器；

硬件自动恢复特权级回到用户态，指令指针跳转到保存的RIP地址，继续执行被打断的代码。

内核只负责调用这条指令，数据弹出的全过程依然是CPU硬件自动完成。

---

## 补充一个细节，区分两种场景的细微差别

1）**时钟中断（iretq指令返回）**

中断进入时CPU完整保存全部寄存器到内核栈；退出执行iretq，硬件严格按照压栈顺序全部弹出，现场完整还原。

2）**syscall系统调用陷入（sysret/sysretq）**

syscall指令是x86_64专门优化的快速指令，硬件保存的寄存器数量略少一些，但核心的RIP、RSP依然由硬件自动存入内核栈；返回sysret同样硬件自动恢复寄存器。

### 再提炼一句核心：

内核栈上用户态现场的入栈、出栈，都是CPU硬件行为；

内核软件只负责：提供一块内核栈内存地址、最后触发一条返回指令而已。

线程被中断抢占之后能无缝恢复执行，本质就是CPU这套自动保存/恢复寄存器的硬件机制。

## 额外一点：如果打断发生在内核态呢？

当CPU已经在内核栈运行时，再次来了中断：

CPU只会把**当前内核态的寄存器现场压入内核栈（继续往下压栈）**；

而更早之前保存的那一份用户态RSP、RIP，还留在内核栈底部，不会被覆盖。

等所有中断处理完毕，一层层iret弹出内核现场，最后才会弹出最底层的用户态现场，回到用户程序。

## 参考文档

- [context-switch.md](/concepts/process/context-switch.md)：上下文切换总览
- [interrupts.md](/concepts/process/interrupts.md)：中断处理机制
- [syscall-details.md](/concepts/process/syscall-details.md)：系统调用深入细节

