当前位置: 首页 > news >正文

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 找到页目录表,进而实现虚拟地址到物理地址的转换
  • 写入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;
  • 返回前一个任务的指针(现在已成为被切换出去的任务)

关键点总结:

  1. 上下文保存:保存前一个任务的 FPU 状态、段寄存器等
  2. 上下文恢复:恢复下一个任务的栈指针、段寄存器、调试寄存器等
  3. 特权资源设置:设置 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);
  1. 内核线程的情况 (!mm):
    • 内核线程没有自己的用户空间内存映射
    • 复用前一个任务的 active_mm
    • 增加内存描述符的引用计数
    • enter_lazy_tlb():延迟 TLB 刷新(因为内核线程共享地址空间)
  2. 用户进程的情况
    • 调用 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 调度器中上下文切换的完整逻辑,是任务调度的核心组成部分

http://www.dtcms.com/a/431731.html

相关文章:

  • 移动网站开发教学大纲安装wordpress 000
  • 《小米 17:创新与争议并存的科技新品》
  • thinkphp怎么做网站壹六八信息科技网站建设
  • 网站建站企业wex5 后端实现全网站开发
  • leetcode 47 全排列II
  • 注册公司是在哪个网站有哪些做普洱茶网站的
  • Vscode安装Element ui
  • Unity游戏基础-1(安装~工作区构建)
  • 超声波测距
  • 公司网站建设收费惠州市网站建设
  • 彩票的网站怎么做的太原关键词优化软件
  • 做的网站需要什么技术wordpress好用的编辑器插件
  • 数字信号处理 第一章(离散时间信号与系统)【上】
  • 【视觉】使用 mediamtx 怎么支持多个rtsp播放
  • 懂得网站推广东莞市建设安监局网站首页
  • 数据结构与算法9:查找
  • 免杀技术中的shell之 webshell shellcode
  • 网站的系统帮助免费下载教学设计的网站
  • 怎么弄网站朝阳区规划网站
  • 使用 Git Submodule 管理微服务项目:从繁琐到高效
  • OSPF ExStart 状态 概念及题目
  • 如何网站建设 需要详细的步骤长春建站免费模板
  • 北京微信网站建设费用网络设计主要是干什么的
  • 网站互联网推广营销云产品
  • Python题目:日期与数字补零
  • Week09-Clustering聚类k-mean
  • [Windows] 视频画质修复软件v3.0
  • 投资网站源码怎样创建音乐网站
  • 搭建网站的价格wordpress套模板教程
  • ECDSA 数字签名简介与 jsjiami 的结合使用探讨