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

Linux 进程调度(二)之进程的上下文切换

目录

  • 一、概述
  • 二、上下文切换的实现
    • 1、context_switch
    • 2、switch_mm
    • 3、switch_to
  • 三、观测进程上下文切换


一、概述

进程的上下文切换是指在多任务操作系统中,当操作系统决定要切换当前运行的进程时,将当前进程的状态保存起来,并恢复下一个要运行的进程的状态。上下文切换是操作系统实现进程调度和实现多任务的关键机制之一。

操作系统一个非常重要的功能就是进程的管理,通过调度策略选择合适的进程来执行,对于单个 CPU 而言,进程是串行分时执行,这就需要内核支持进程切换,挂起一个正在 CPU 中执行的进程,恢复执行之前挂起的进程。

CPU 和寄存器是所有进程共用的,CPU 在运行任何 task 之前,必须地依赖一些环境,包括 CPU 寄存器和程序计数器,除此之外,进程运行过程中还需要用到虚拟内存。进程在切换过程中,主要的工作就是切换进程空间(虚拟内存)切换 CPU 寄存器和程序计数器。

二、上下文切换的实现

进程切换由两部分组成:

  • 切换页全局目录安装一个新的地址空间;
  • 切换内核态堆栈及硬件上下文。

Linux 内核中由 context_switch 实现了上述两部分内容。

  • 调用 switch_mm 完成用户空间切换;
  • 调用 switch_to 完成内核栈及寄存器切换。

1、context_switch

下面是上下文切换的内核源码,完整的源码见目录 kernel/sched/core.ccontext_switch 函数:

static inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,struct task_struct *next)
{struct mm_struct *mm, *oldmm;prepare_task_switch(rq, prev, next);mm = next->mm;           // 下一个要执行的进程的虚拟内存oldmm = prev->active_mm; // 将要被切换出去的进程的虚拟内存arch_start_context_switch(prev);if (!mm) { // 内核线程的 mm 为 NULLnext->active_mm = oldmm;atomic_inc(&oldmm->mm_count);enter_lazy_tlb(oldmm, next);} else // 用户进程的 mm 不为 NULLswitch_mm(oldmm, mm, next);if (!prev->mm) {prev->active_mm = NULL;rq->prev_mm = oldmm;}spin_release(&rq->lock.dep_map, 1, _THIS_IP_);context_tracking_task_switch(prev, next);switch_to(prev, next, prev); // 切换寄存器和内核栈barrier();return finish_task_switch(prev);
}

执行流程如下:

  • 通过进程描述符 next->mm 是否为空判断当前进程是否是内核线程,因为内核线程的内存描述符 mm_struct *mm 总是为空。
  • 如果是内核线程则借用 prev 进程的 active_mm,对于用户进程,active_mm == mm;对于内核线程,mm = NULLactive_mm = prev->active_mm
  • 如果 prev->mm 不为空,则说明 prev 是用户进程,调用 mmgrab 增加 mm->mm_count 引用计数。
  • 对于内核线程,会启动懒惰 TLB 模式。懒惰 TLB 模式是为了减少无用的TLB刷新。enter_lazy_tlb 与体系结构相关。
  • 如果是用户进程则调用 switch_mm (或 switch_mm_irqs_off) 完成用户地址空间切换,switch_mm (或 switch_mm_irqs_off) 与体系结构相关。
  • 调用 switch_to 完成内核态堆栈及硬件上下文切换,switch_to 与体系结构相关。
  • switch_to 执行完成后,next 进程获得 CPU 使用权,prev 进程进入睡眠状态。
  • 调用 finish_task_switch,如果 prev 是内核线程,则调用 mmdrop 减少内存描述符引用计数。如果引用计数为 0,则释放与页表相关的所有描述符和虚拟内存。

2、switch_mm

对于用户进程需要完成用户空间的切换,switch_mm 函数完成了这个任务。switch_mm 是与体系架构相关的函数。更确切地说,是切换地址转换表(pgd),由于 pgd 包括进程 系统空间(0xc000 0000 ~ 0xffff ffff)和 用户空间(0x0000 0000 ~ 0xbfff ffff)的地址映射,但是由于所有进程的系统空间的地址映射都是相同的。所以实质上就是进行用户空间的切换。

Linux 5.6.4 内核调用 switch_mm_irqs_off 切换用户进程空间,对于没有定义该函数的架构,则调用的是switch_mm。x86 体系架构定义了 switch_mm_irqs_off 函数,ARM 体系架构没有定义。

#ifndef switch_mm_irqs_off#define switch_mm_irqs_off switch_mm
#endif

函数定义为:

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); // 将下一个进程页表的 pgd 装载进 CR3 寄存器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); // 将下一个进程页表的 pgd 装载进 CR3 寄存器load_LDT_nolock(&next->context, cpu);}}
#endif
}

这部分核心的代码是 load_cr3,这个函数加载下一个进程页表 pgd 地址加载进 CR3 寄存器。CR3 是 CPU 的一个寄存器,它存储了当前进程的顶级页表 pgd。

如果 CPU 要使用进程的虚拟内存,内核可以从 CR3 寄存器里面得到 pgd 在物理内存的地址,通过页表就可以得到虚拟内存对应的物理地址,这样就可以得到物理内存的数据。

3、switch_to

对于内核空间及寄存器的切换,switch_to 函数完成了这个任务。

switch_to 调用到 __switch_to,该宏函数定义在目录 arch/x86/include/asm/switch_to.h

#define switch_to(prev, next, last)                 \
do {                                    \/*                              \* Context-switching clobbers all registers, so we clobber  \* them explicitly, via unused output variables.        \* (EAX and EBP is not listed because EBP is saved/restored \* explicitly for wchan access and EAX is the return value of   \* __switch_to())                       \*/                             \unsigned long ebx, ecx, edx, esi, edi;              \\asm volatile("pushfl\n\t"       /* save    flags */ \"pushl %%ebp\n\t"      /* save    EBP   */ \"movl %%esp,%[prev_sp]\n\t"    /* save    ESP   */ \"movl %[next_sp],%%esp\n\t"    /* restore ESP   */ \"movl $1f,%[prev_ip]\n\t"  /* save    EIP   */ \"pushl %[next_ip]\n\t" /* restore EIP   */ \__switch_canary                    \"jmp __switch_to\n"    /* regparm call  */ \"1:\t"                     \"popl %%ebp\n\t"       /* restore EBP   */ \"popfl\n"          /* restore flags */ \\/* output parameters */                \: [prev_sp] "=m" (prev->thread.sp),        \[prev_ip] "=m" (prev->thread.ip),        \"=a" (last),                 \\/* clobbered output registers: */        \"=b" (ebx), "=c" (ecx), "=d" (edx),      \"=S" (esi), "=D" (edi)               \\__switch_canary_oparam               \\/* input parameters: */              \: [next_sp]  "m" (next->thread.sp),        \[next_ip]  "m" (next->thread.ip),        \\/* regparm parameters for __switch_to(): */  \[prev]     "a" (prev),               \[next]     "d" (next)                \\__switch_canary_iparam               \\: /* reloaded segment registers */         \"memory");
} while (0)

switch_to 宏用于进程切换,给定了前一个进程结构体指针 prev,以及需要切换到的进程结构体指针 next,从 prev 切换到 next。

prev 和 next 是输入参数,分别表示被替换进程和新进程描述符的地址在内存中的位置。而 last 是输出参数,假设内核决定暂停进程 A 而激活进程 B,而后又激活进程 A(则必须暂停另一个进程 C,通常不同于进程 B),则它表示宏把进程 C 的描述符地址写在内存的什么位置(在 A 恢复执行后)。

在进程切换之前,宏把第一个输入参数 prev(即在 A 的内核堆栈中分配的 prev 局部变量)表示的变量的内容存入 CPU 的 eax 寄存器。在完成进程切换,A 已经恢复执行时,宏把 CPU 的 eax 寄存器的内容写入由第三个输出参数 last 所指示的 A 在内存中的位置。因为 CPU 寄存器不会在切换点发生变化,所以 C 的描述符地址也存在内存的这个位置。在 schedule() 执行过程中,参数 last 指向 A 的局部变量 prev,所以 prev 被 C 的地址覆盖。

三、观测进程上下文切换

systemtap 提供了跟踪进程释放执行权被切换出 CPU 的 probe 方法 scheduler.cpu_off ,这个 probe 的定义
如下:

/*** probe scheduler.cpu_off - Process is about to stop running on a cpu* * @name: name of the probe point* @task_prev: the process leaving the cpu(same as current)* @task_next: the process replacing current* @idle: boolean indicating whether current is the idle process** Context: The process leaving the cpu.**/
probe scheduler.cpu_off =kernel.trace("sched_switch") !,kernel.function("context_switch")
{name ="cpu off"task_prev = $prevtask next = $nextidle = __is_idle()
}

可以看到 cpu_off 时间其实是 sched_switch 内核 trace 事件和 context_switch 内核函数的封装,同时提供了 task_prevtask_next 两个有用的参数。

task_prev 表示当前进程的 task struct 结构体,也就是马上要释放执行权的 task structtask_next 表示马上要执行的进程的 task struct 结构体。

注意,这里的进程是广义的进程,也可以是线程,本质是一个 task struct

我们就可以通过 cpu_off 事件来统计一段时间内的进程切换情况,完整的 systemtap 脚本如下所示:

global csw_countprobe scheduler.cpu_off {csw_count[task_prev,task_next]++
}function fmt_task(task_prev, task_next){return sprintf("tid(%d)->tid(%d)",task_tid(task_prev), task_tid(task_next))
}function print_context_switch_top5() {fprintf("%45s %10s\n", "Context switch", "COUNT")foreach([task_prev,task_next] in csw_count- limit 5) {printf("%45s %10d\n", fmt_task(task_prev, task_next), csw_count[task_prev, task_next])}delete csw_count
}probe timer.s(1) {print_context_switch_top5()printf("-----------------------------------------------\n")
}

其中 csw_countsystemtap 的关联数组,虽然这名字叫数组,其实是一个字典,跟其它语言的 map/dict/hash 类似。csw_count[task_prev,task_next] 语法的含义是将 task_prevtask_next 两个值联合起来为字典的 key。

如果我们由进程 A 切换到 B,B 切换到 C,C 切换到 A,那么这个关联数组的形式如下:

csw_count[AB]=1
csw_count[BC]=1
csw_count[CA]=1

接下来我们来执行 4 个跑满 CPU 的单线程程序,在我双核机器上每个程序会占据 50% 的 CPU 左右,开启四个终端,执行四次下面的程序:

$ sha256sum /dev/zero

top 命令的输出如下,这四个进程分别为 27458、27460、27590、27636。

  PID USER   PR   NI     VIRT		RES		SHR S	%CPU  %MEM		TIME+   COMMAND
27460 root   20   0    116664      1140     856 R   50.8   0.1    0:35.12 sha256sum
27636 root   20   0    116664	   1140		856 R   50.3   0.1    0:24.84 sha256sum
27458 root   20   0    116664      1140		856 R   49.7   0.1    0:36.18 sha256sum
27590 root   20   0    116664      1140		856 R   49.7   0.1    0:28.66 sha256sum

然后使用 stap 执行上面的 systemtap 脚本:

Context switch                COUNT
tid(27460)->tid(27636)           62
tid(27636)->tid(27460)           62
tid(27590)->tid(27458)           44
tid(27458)->tid(27590)           43
tid(27458)->tid(25116)           10

可以看到,1s 内这四个进程切换得非常频繁。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Vue2与Vue3响应式原理对比
  • 基于cookie的会话保持
  • linux的UDP通讯方式
  • 暑期破防实录——捡漏腾讯
  • 【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁
  • 第三次北漂,入职UE
  • Flink 常见问题汇总:反压积压,checkpoint报错,窗口计算,作业报错,无产出,流批不一致,调优等。
  • EasyX自学笔记3(割草游戏2)
  • CCF编程能力等级认证GESP—C++7级—20240629
  • C#复习之封装_构造函数,析构函数,垃圾回收
  • 技术周总结 08.05-08.11周日(scala git回滚)
  • Android Basis - 密钥和ID认证
  • 代理IP如何助力社交媒体数据挖掘
  • leetcode26_删除有序数组中的重复项
  • 时序数据库TDengine和QuestDB对比
  • $translatePartialLoader加载失败及解决方式
  • 〔开发系列〕一次关于小程序开发的深度总结
  • AzureCon上微软宣布了哪些容器相关的重磅消息
  • canvas 高仿 Apple Watch 表盘
  • JavaSE小实践1:Java爬取斗图网站的所有表情包
  • Python利用正则抓取网页内容保存到本地
  • select2 取值 遍历 设置默认值
  • Shadow DOM 内部构造及如何构建独立组件
  • 成为一名优秀的Developer的书单
  • 对JS继承的一点思考
  • 构建工具 - 收藏集 - 掘金
  • 开源地图数据可视化库——mapnik
  • 深入浅出webpack学习(1)--核心概念
  • kubernetes资源对象--ingress
  • 仓管云——企业云erp功能有哪些?
  • ​低代码平台的核心价值与优势
  • ​力扣解法汇总1802. 有界数组中指定下标处的最大值
  • ‌U盘闪一下就没了?‌如何有效恢复数据
  • # Python csv、xlsx、json、二进制(MP3) 文件读写基本使用
  • #1015 : KMP算法
  • #Datawhale AI夏令营第4期#AIGC文生图方向复盘
  • #我与Java虚拟机的故事#连载18:JAVA成长之路
  • (1) caustics\
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (7)svelte 教程: Props(属性)
  • (二) 初入MySQL 【数据库管理】
  • (二)windows配置JDK环境
  • (附源码)c#+winform实现远程开机(广域网可用)
  • (附源码)springboot宠物医疗服务网站 毕业设计688413
  • (回溯) LeetCode 78. 子集
  • (十六)Flask之蓝图
  • (算法)求1到1亿间的质数或素数
  • (转)Oracle存储过程编写经验和优化措施
  • .gitignore不生效的解决方案
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .NET/C# 的字符串暂存池
  • /var/spool/postfix/maildrop 下有大量文件
  • @hook扩展分析
  • [ Socket学习 ] 第一章:网络基础知识
  • [ 蓝桥杯Web真题 ]-布局切换