Linux任务上下文切换context_switch函数的实现
文章目录
- 一、内存上下文切换`switch_mm`
- 1. 函数原型和初始设置
- 2. 不同地址空间切换处理
- 3. 更新前一个地址空间的CPU掩码
- 4. TLB状态更新
- 5. 更新新地址空间的CPU掩码
- 6. 加载新页表
- 7. LDT切换处理
- 8. 相同地址空间处理
- 9. 懒惰TLB模式处理
- 二、实际的硬件上下文切换`__switch_to`
- 三、实际执行上下文切换宏`switch_to`
- 1. 宏定义和局部变量
- 2. 保存当前任务状态
- 3. 切换到新任务的栈
- 4. 保存返回地址和设置新EIP
- 5. 跳转到切换函数
- 6. 返回点和新任务恢复
- 7. 操作数约束详解
- 9. 完整执行流程
- 四、任务上下文切换函数`context_switch`
一、内存上下文切换switch_mm
#define load_cr3(pgdir) \asm volatile("movl %0,%%cr3": :"r" (__pa(pgdir)))static inline void switch_mm(struct mm_struct *prev,struct mm_struct *next,struct task_struct *tsk)
{int cpu = smp_processor_id();if (likely(prev != next)) {cpu_clear(cpu, prev->cpu_vm_mask);
#ifdef CONFIG_SMPper_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK;per_cpu(cpu_tlbstate, cpu).active_mm = next;
#endifcpu_set(cpu, next->cpu_vm_mask);load_cr3(next->pgd);if (unlikely(prev->context.ldt != next->context.ldt))load_LDT_nolock(&next->context, cpu);}
#ifdef CONFIG_SMPelse {per_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK;BUG_ON(per_cpu(cpu_tlbstate, cpu).active_mm != next);if (!cpu_test_and_set(cpu, next->cpu_vm_mask)) {load_cr3(next->pgd);load_LDT_nolock(&next->context, cpu);}}
#endif
}
1. 函数原型和初始设置
static inline void switch_mm(struct mm_struct *prev,struct mm_struct *next,struct task_struct *tsk)
{int cpu = smp_processor_id();
参数:
prev
:前一个进程的内存管理结构next
:下一个进程的内存管理结构tsk
:任务结构
获取当前CPU:
int cpu = smp_processor_id();
- 获取当前执行代码的CPU编号
- 用于更新每CPU的状态变量
2. 不同地址空间切换处理
if (likely(prev != next)) {
likely(prev != next)
:告诉编译器两个进程使用不同地址空间的情况很常见- 这是进程切换的典型情况
3. 更新前一个地址空间的CPU掩码
/* stop flush ipis for the previous mm */
cpu_clear(cpu, prev->cpu_vm_mask);
cpu_vm_mask
:
- 位图,记录哪些CPU正在使用该地址空间
- 当需要刷新TLB时,只向这些CPU发送IPI
操作含义:
- 当前CPU不再使用前一个地址空间
- 后续TLB刷新不需要通知这个CPU
4. TLB状态更新
#ifdef CONFIG_SMP
per_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK;
per_cpu(cpu_tlbstate, cpu).active_mm = next;
#endif
TLB状态管理:
state = TLBSTATE_OK
:标记TLB状态为正常active_mm = next
:记录当前活跃的地址空间
5. 更新新地址空间的CPU掩码
cpu_set(cpu, next->cpu_vm_mask);
作用:标记当前CPU正在使用新的地址空间
6. 加载新页表
/* Re-load page tables */
load_cr3(next->pgd);
load_cr3
宏:
#define load_cr3(pgdir) asm volatile("movl %0,%%cr3": :"r" (__pa(pgdir)))
操作详解:
movl %0,%%cr3
:将页目录物理地址写入CR3寄存器__pa(pgdir)
:将虚拟地址转换为物理地址CR3
寄存器的作用- 在 x86 分页机制中,
CR3
存储页目录表的物理地址(不是虚拟地址!) - 处理器通过
CR3
找到页目录表,进而实现虚拟地址到物理地址的转换
- 在 x86 分页机制中,
- 写入CR3会自动刷新TLB
7. LDT切换处理
/** load the LDT, if the LDT is different:*/
if (unlikely(prev->context.ldt != next->context.ldt))load_LDT_nolock(&next->context, cpu);
unlikely()
:LDT不同的情况很少见- 大多数进程使用相同的LDT(默认LDT)
LDT切换:
load_LDT_nolock(&next->context, cpu);
- 加载新进程的局部描述符表
- 只在LDT确实不同时才执行
8. 相同地址空间处理
#ifdef CONFIG_SMP
else {per_cpu(cpu_tlbstate, cpu).state = TLBSTATE_OK;BUG_ON(per_cpu(cpu_tlbstate, cpu).active_mm != next);
场景:
- 内核线程切换到另一个内核线程
- 两者共享相同的地址空间(
init_mm
)
状态验证:
BUG_ON(per_cpu(cpu_tlbstate, cpu).active_mm != next);
- 确保active_mm与next一致
- 如果不一致,触发内核错误
9. 懒惰TLB模式处理
if (!cpu_test_and_set(cpu, next->cpu_vm_mask)) {/* We were in lazy tlb mode and leave_mm disabled* tlb flush IPI delivery. We must reload %cr3.*/load_cr3(next->pgd);load_LDT_nolock(&next->context, cpu);
}
cpu_test_and_set
:
- 原子地测试并设置CPU掩码位
- 返回原来的值
懒惰TLB模式场景:
之前: CPU在懒惰TLB模式,没有在cpu_vm_mask中
现在: 需要重新激活该地址空间
重新加载操作:
load_cr3(next->pgd)
:重新加载页表load_LDT_nolock(...)
:重新加载LDT
二、实际的硬件上下文切换__switch_to
struct task_struct fastcall * __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{struct thread_struct *prev = &prev_p->thread,*next = &next_p->thread;int cpu = smp_processor_id();struct tss_struct *tss = &per_cpu(init_tss, cpu);__unlazy_fpu(prev_p);load_esp0(tss, next);load_TLS(next, cpu);asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));if (unlikely(prev->fs | prev->gs | next->fs | next->gs)) {loadsegment(fs, next->fs);loadsegment(gs, next->gs);}if (unlikely(next->debugreg[7])) {loaddebug(next, 0);loaddebug(next, 1);loaddebug(next, 2);loaddebug(next, 3);loaddebug(next, 6);loaddebug(next, 7);}if (unlikely(prev->io_bitmap_ptr || next->io_bitmap_ptr))handle_io_bitmap(next, tss);return prev_p;
}
这是一个 Linux 内核中的任务切换函数 __switch_to
struct task_struct fastcall * __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{// 获取前一个任务和下一个任务的线程结构指针struct thread_struct *prev = &prev_p->thread,*next = &next_p->thread;// 获取当前 CPU 的 IDint cpu = smp_processor_id();// 获取当前 CPU 的 TSS(任务状态段)struct tss_struct *tss = &per_cpu(init_tss, cpu);
变量初始化部分:
prev_p
:当前正在运行的任务(要被切换出去的任务)next_p
:将要运行的任务(要被切换进来的任务)prev/next
:分别指向两个任务的线程特定数据cpu
:当前处理器编号tss
:当前 CPU 的任务状态段,用于存储硬件上下文
注意事项
- 不要在切换函数中使用 printk,因为它可能间接调用唤醒函数导致死锁
__unlazy_fpu(prev_p);
- 保存前一个任务的 FPU(浮点运算单元)状态
load_esp0(tss, next);
- 设置 TSS 中的 esp0 字段
load_TLS(next, cpu);
- 设置下一个任务的 TLS 段
asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));
asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));
- 保存当前任务的 fs 和 gs 段寄存器值
if (unlikely(prev->fs | prev->gs | next->fs | next->gs)) {loadsegment(fs, next->fs);loadsegment(gs, next->gs);
}
- 如果需要,恢复下一个任务的 fs 和 gs 段寄存器
- 如果任一任务使用了这些段寄存器,就加载下一个任务的段寄存器值
if (unlikely(next->debugreg[7])) {loaddebug(next, 0);loaddebug(next, 1);loaddebug(next, 2);loaddebug(next, 3);/* no 4 and 5 */loaddebug(next, 6);loaddebug(next, 7);
}
- 如果下一个任务设置了调试寄存器(debugreg[7]不为0),则加载其调试寄存器
- 调试寄存器 4 和 5 保留未用
if (unlikely(prev->io_bitmap_ptr || next->io_bitmap_ptr))handle_io_bitmap(next, tss);
- 处理 I/O 位图:如果任一任务有 I/O 权限位图,就设置下一个任务的 I/O 权限
- I/O 权限位图定义了进程可以访问哪些 I/O 端口
return prev_p;
- 返回前一个任务的指针(现在已成为被切换出去的任务)
关键点总结:
- 上下文保存:保存前一个任务的 FPU 状态、段寄存器等
- 上下文恢复:恢复下一个任务的栈指针、段寄存器、调试寄存器等
- 特权资源设置:设置 TSS、LDT、I/O 权限位图等系统级资源
三、实际执行上下文切换宏switch_to
#define switch_to(prev,next,last) do { \unsigned long esi,edi; \asm volatile("pushfl\n\t" \"pushl %%ebp\n\t" \"movl %%esp,%0\n\t" /* save ESP */ \"movl %5,%%esp\n\t" /* restore ESP */ \"movl $1f,%1\n\t" /* save EIP */ \"pushl %6\n\t" /* restore EIP */ \"jmp __switch_to\n" \"1:\t" \"popl %%ebp\n\t" \"popfl" \:"=m" (prev->thread.esp),"=m" (prev->thread.eip), \"=a" (last),"=S" (esi),"=D" (edi) \:"m" (next->thread.esp),"m" (next->thread.eip), \"2" (prev), "d" (next)); \
} while (0)
这个宏是x86架构上实际执行上下文切换的核心代码,使用内联汇编实现任务状态的保存和恢复
1. 宏定义和局部变量
#define switch_to(prev,next,last) do { \unsigned long esi,edi; \
参数:
prev
:前一个任务(当前正在运行的任务)next
:下一个任务(将要运行的任务)last
:输出参数
局部变量:
unsigned long esi,edi;
- 用于保存ESI和EDI寄存器的临时变量
2. 保存当前任务状态
asm volatile("pushfl\n\t" \"pushl %%ebp\n\t" \"movl %%esp,%0\n\t" /* save ESP */ \
pushfl
- 将EFLAGS寄存器压栈
- 保存中断状态、方向标志等处理器状态
pushl %%ebp
- 将EBP寄存器(基址指针)压栈
- EBP用于函数调用帧指针
movl %%esp,%0\n\t"
%0
对应输出操作数0:prev->thread.esp
- 将当前栈指针保存到前一个任务的thread.esp中
3. 切换到新任务的栈
"movl %5,%%esp\n\t" /* restore ESP */ \
操作:
%5
对应输入操作数5:next->thread.esp
- 将栈指针切换到新任务的栈
- 这是实际的上下文切换点
4. 保存返回地址和设置新EIP
"movl $1f,%1\n\t" /* save EIP */ \"pushl %6\n\t" /* restore EIP */ \
movl $1f,%1\n\t"
$1f
:标签1的地址,前一个任务恢复执行时开始的位置%1
对应输出操作数1:prev->thread.eip
- 将标签1的地址(返回地址)保存到前一个任务的thread.eip中
pushl %6\n\t"
%6
对应输入操作数6:next->thread.eip
- 将新任务的EIP压栈
5. 跳转到切换函数
"jmp __switch_to\n" \
__switch_to
函数:
- 执行架构特定的切换操作
- 处理FPU状态、调试寄存器等
- 最终通过
ret
指令返回到新任务的代码
6. 返回点和新任务恢复
"1:\t" \"popl %%ebp\n\t" \"popfl" \
标签1:
:
- 这是前一个任务恢复执行时的返回点
- 当任务再次被调度时,从这里继续执行
恢复寄存器:
popl %%ebp
- 恢复EBP寄存器(基址指针)
popfl
- 恢复EFLAGS寄存器
7. 操作数约束详解
:"=m" (prev->thread.esp),"=m" (prev->thread.eip), \"=a" (last),"=S" (esi),"=D" (edi) \:"m" (next->thread.esp),"m" (next->thread.eip), \"2" (prev), "d" (next)); \
输出操作数:
"=m" (prev->thread.esp)
:内存操作数,保存ESP"=m" (prev->thread.eip)
:内存操作数,保存EIP"=a" (last)
:EAX寄存器,输出到last参数"=S" (esi)
:ESI寄存器,保存到esi变量"=D" (edi)
:EDI寄存器,保存到edi变量
输入操作数:
"m" (next->thread.esp)
:内存操作数,新任务的ESP"m" (next->thread.eip)
:内存操作数,新任务的EIP"2" (prev)
:使用与输出操作数2相同的约束(EAX)"d" (next)
:EDX寄存器,传入next参数
9. 完整执行流程
从任务A切换到任务B:
任务A的视角:
1. pushfl, pushl %ebp // 保存状态
2. movl %esp, A->thread.esp // 保存栈指针
3. movl B->thread.esp, %esp // 切换到B的栈
4. movl $1f, A->thread.eip // 保存返回地址
5. pushl B->thread.eip // 设置B的返回地址
6. jmp __switch_to // 执行切换
在__switch_to
中:
- 处理FPU、调试寄存器等
- 最终执行 ret 指令
任务B的视角:
- 通过ret指令跳转到
B->thread.eip
任务A再次被调度时:
// 从标签1:处继续执行
1: popl %ebp // 恢复EBPpopfl // 恢复EFLAGS// 继续执行A的代码
四、任务上下文切换函数context_switch
static inline
task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next)
{struct mm_struct *mm = next->mm;struct mm_struct *oldmm = prev->active_mm;if (unlikely(!mm)) {next->active_mm = oldmm;atomic_inc(&oldmm->mm_count);enter_lazy_tlb(oldmm, next);} elseswitch_mm(oldmm, mm, next);if (unlikely(!prev->mm)) {prev->active_mm = NULL;WARN_ON(rq->prev_mm);rq->prev_mm = oldmm;}/* Here we just switch the register state and the stack. */switch_to(prev, next, prev);return prev;
}
这是一个 Linux 内核中的任务上下文切换函数 context_switch
,它负责完成进程/线程的完整切换
static inline
task_t * context_switch(runqueue_t *rq, task_t *prev, task_t *next)
{// 获取下一个任务的内存描述符和当前任务的活跃内存描述符struct mm_struct *mm = next->mm;struct mm_struct *oldmm = prev->active_mm;
参数说明:
rq
:运行队列指针prev
:当前正在运行的任务(要被切换出去)next
:将要运行的任务(要被切换进来)
if (unlikely(!mm)) {// 情况1:下一个任务是内核线程(没有用户空间内存描述符)next->active_mm = oldmm;atomic_inc(&oldmm->mm_count);enter_lazy_tlb(oldmm, next);} else// 情况2:下一个任务是普通用户进程switch_mm(oldmm, mm, next);
- 内核线程的情况 (
!mm
):- 内核线程没有自己的用户空间内存映射
- 复用前一个任务的
active_mm
- 增加内存描述符的引用计数
enter_lazy_tlb()
:延迟 TLB 刷新(因为内核线程共享地址空间)
- 用户进程的情况:
- 调用
switch_mm()
切换完整的地址空间 - 这会加载新的页表、刷新 TLB 等
- 调用
if (unlikely(!prev->mm)) {// 如果前一个任务是内核线程prev->active_mm = NULL;WARN_ON(rq->prev_mm);rq->prev_mm = oldmm;}
清理前一个内核线程的内存上下文:
- 如果前一个任务是内核线程,清空其
active_mm
- 将旧的内存描述符保存在运行队列中,供后续清理使用
WARN_ON
确保运行队列中没有冲突的prev_mm
/* Here we just switch the register state and the stack. */// 执行实际的寄存器状态和栈切换switch_to(prev, next, prev);
核心的上下文切换:
- 调用
switch_to
宏(就是我们之前分析的那个)
return prev;// 返回前一个任务的指针
}
返回值: 返回被切换出去的任务指针,调度器可以用这个信息进行统计或清理工作
这个函数体现了 Linux 调度器中上下文切换的完整逻辑,是任务调度的核心组成部分