本文首发于 http://oliveryang.net,转载时请包含原文或者作者网站链接。
本文主要围绕 Linux 内核调度器 Preemption 的相关实现进行讨论。其中涉及的一般操作系统和 x86 处理器和硬件概念,可能也适用于其它操作系统。
1. 背景知识
要深入理解 Preemption 必须对操作系统的 Context Switch 做一个全面的梳理。最终可以了解 Preemption 和 Context Switch 概念上的区别与联系。
1.1 Context Switch
Context Switch (上下文切换) 指任何操作系统上下文保存和恢复执行状态,以便于被安全地打断和稍后被正确地恢复执行。一般操作系统中通常因以下三种方式引起上下文切换,
Task Scheduling (任务调度)
任务调度一般是由调度器代码在内核空间完成的。
通常需要将当前 CPU 执行任务的代码,用户或内核栈,地址空间切换到下一个要运行任务的代码,用户或内核栈,地址空间。Interrupt (中断) 或 Exception (异常)
中断和异常是由硬件产生但由软件来响应和处理的。
这个过程中,涉及到将用户态或内核态代码切换至中断处理代码。同时可能还涉及到用户进程栈或内核栈切换到中断栈。支持保护模式的处理器可能还涉及到保护模式的切换。x86 处理器是通过 Interrupt Gate (中断门) 完成的。System Call (系统调用)
系统调用是由用户态代码主动调用,使用户进程陷入到内核态调用内核定义的各种系统调用服务。这个过程中,涉及到将任务的用户态代码和栈在同一任务上下文上切换至内核系统调用代码和同一任务的内核栈。
1.2 Preemption
Preemption (抢占) 是指操作系统允许满足某些重要条件(例如:优先级,公平性)的任务打断当前正在 CPU 上运行的任务而得到调度执行。并且这种打断不需要当前正在运行的任务的配合,同时被打断的程序可以在后来可以再次被调度恢复执行。
多任务操作系统可以按照 Cooperative Multitasking (协作多任务) 和 Preemptive Multitasking (抢占式多任务) 来划分。本质上,抢占就是允许高优先级的任务可以立即打断低优先级的任务而得到运行。对低 Scheduling Latency (调度延迟) 或者 Real Time (实时) 操作系统的需求来说,支持完全抢占的特性是必须的。
三种上下文切换方式中,系统调用始终发生在同一任务的上下文中,只有中断异常和任务调度机制才涉及到一个任务被令一个上下文打断。Preemption 最终需要借助任务调度来完成任务的打断。但是,任务调度却和这三种上下文切换方式都密切相关,要理解 Preemption,必须对三种机制有深入的了解。
2. 任务调度
任务的调度需要内核代码通过调用调度器核心的 schedule
函数引起。它主要完成以下工作,
- 完成任务调度所需的 Context Switch (上下文切换)
- 调度算法相关实现:选择下一个要运行的任务,任务运行状态和 Run Queue (运行队列) 的维护等
本文主要关注上下文切换和引起任务调度的原因。
2.1 任务调度上下文切换
内核 schedule 函数其中一个重要的处理就是 Task Context Switch (任务上下文切换)。调度器的任务上下文切换主要做两件事,
任务地址空间的上下文切换。
在 Linux 上通过 switch_mm 函数完成。
x86 CPU 通过装载下一个待运行的任务的页目录地址 mm->pgd 到 CR3 寄存器来实现。任务 CPU 运行状态的上下文切换。
主要是 CPU 各寄存器的切换,包括通用寄存器,浮点寄存器和系统寄存器的上下文切换。
在 Linux x86 64位的实现里,指令`CS:EIP` 和栈 `SS:ESP` 还有其它通用寄存器的切换由 switch_to 完成。Linux 描述任务的数据结构是
struct task_struct
,其中的 thread 成员(struct thread_struct
)用于保存上下文切换时任务的 CPU 状态。由于浮点寄存器上下文切换代价比较大,而且,很多使用场景中,被调度的任务可能根本没有使用过 FPU (浮点运算单元),所以 Linux 和很多其它 OS 都采用了 Lazy FPU 上下文切换的设计。但随着 Intel 今年来引入 XSAVE 特性来加速 FPU 保存和恢复,Linux 内核在 3.7 引入了non-lazy FPU 上下文切换。当内核检测到 CPU 支持 XSAVE 指令集,就使用 non-lazy 方式。这也是Intel Intel 64 and IA-32 Architectures Software Developer’s Manual Volume 3的章节 13.4 DESIGNING OS FACILITIES FOR SAVING X87 FPU, SSE AND EXTENDED STATES ON TASK OR CONTEXT SWITCHES 里建议的方式。
一般来说,任务调度,或者说任务上下文切换,可以分为以下两大方式来进行,
- Voluntary Context Switch (主动上下文切换)
- Involuntary Context Switch (强制上下文切换)
2.2 主动上下文切换
主动上下文切换就是任务主动通过直接或者间接调用 schedule 函数引起的上下文切换。引起主动上下文切换的