微信公众号:奔跑吧linux社区

本文节选自《奔跑吧linux内核》第二版卷1第9.3.5章

现在JD有半价抢购活动,如果您觉得本文还可以,千万不要错过哟!

https://item.jd.com/13051268.html

1. 问题引入

假设Linux内核只有3个内核线程(见图9.15),0号线程创建了内核线程1和内核线程2,它们永远不会退出。当系统时钟中断到来时,时钟中断处理函数会检查是否有进程需要调度。当有进程需要调度时,调度器会选择运行线程1或者线程2。

假设0号线程先运行,那么在这个场景下会发生什么情况?
这是一个有意思的问题,涉及调度器的实现机制、中断处理、内核抢占、新建进程如何被调度、进程切换等知识点。我们只有把这些知识点都弄明白了,才能真正搞明白这个问题。

2.场景分析

这个场景中的主要操作步骤如下。
(1)start_kernel()运行在0号线程里。0号线程创建了内核线程1和内核线程2。函数调用关系是start_kernel()→kernel_thread()→_do_fork()。在_do_fork()函数会创建新线程,并且把新线程添加到调度器的就绪队列中。0号线程创建内核线程1和内核线程2后,进入while死循环,0号线程不会退出,它正在等待被调度出去。
(2)产生时钟中断。处理器采用时钟定时器来周期性地提供系统脉搏。时钟中断是普通外设中断的一种。调度器利用时钟中断来定时检测当前正在运行的线程是否需要调度。
(3)当时钟中断检测到当前线程需要调度时,设置need_resched标志位。
(4)当时钟中断返回时,根据Linux内核是否支持内核抢占来确定是否需要调度,下面分两种情况来讨论。
 支持内核抢占的内核:发生在内核态的中断返回时,检查当前线程的need_resched标志位是否置位,如果置位,说明当前线程需要调度。
 不支持内核抢占的内核:发生在内核态的中断在中断返回时不会检查是否需要调度。

不支持内核抢占的内核

在不支持内核抢占功能的Linux内核(见图9.16)里,即使0号线程的need_resched标志位置位了,Linux内核也不会调度内核线程1或者内核线程2来运行。只有发生在用户态的中断返回或者系统调用返回用户空间时,才会检查是否需要调度。处理流程如下所示。

(1)发生时钟中断。触发时钟中断时当前进程(线程)有可能在用户态执行,也可能在内核态执行。如果进程运行在用户态时发生了中断,那么会进入异常向量表的el0_irq汇编函数;如果进程运行在内核态时发生了中断,那么会进入异常向量表的el1_irq汇编函数中。在本场景中,因为3个线程都是内核线程,所以时钟中断只能跳转到el1_irq汇编函数里。当进入中断时,CPU会自动关闭中断。
(2)在el1_irq汇编函数里,首先会保存中断现场(也称为中断上下文)到当前进程的栈中,Linux内核使用pt_regs数据结构来实现一个栈框,用来保存中断现场(本节称为pt_regs栈帧)。
(3)中断处理过程包括切换到Linux内核的中断栈、硬件中断号的查询、中断服务程序的处理等,详细分析可以参考本书卷2的2.4节以及2.5节。
(4)当确定当前中断源是时钟中断后,scheduler_tick()函数会取检查当前进程的是否需要调度。如果需要调度,则设置当前进程的need_resched标志位(thread_info中的TIF_NEED_ RESCHED标志位),详细分析请参考8.1.7节。
(5)中断返回。这里需要给中断控制器返回一个中断结束(End Of Interrupt, EOI)信号。
(6)在el1_irq汇编函数直接恢复中断现场,这里会使用0号线程的pt_regs栈框来恢复中断现场。在不支持内核抢占的系统里,el1_irq汇编函数不会检查是否需要调度。在中断返回时,CPU打开中断,然后从中断的地方开始继续执行0号进程。

支持内核抢占的内核

在支持内核抢占功能的Linux内核中,中断返回时会检查当前进程是否设置了need_resched标志位置位。如果置位,那么调用preempt_schedule_irq()函数以调度其他进程(线程)并运行。如图9.17所示,在支持内核抢占的Linux内核中,中断与调度的流程和图9.16略有不一样。在el1_irq汇编函数即将返回中断现场时,判断当前进程是否需要调度。如果需要调度,调度器会选择下一个进程,并且进行进程的切换。如果选择了内核线程1,则从内核线程1的pt_regs栈框中恢复中断现场并打开中断,然后继续执行内核线程1的代码。

3.如何让新进程执行

可能读者对图9.17会有如下疑问:

  1. 如果内核线程1是新创建的进程,它的栈应该是空的,那它第一次运行时如何恢复中断现场呢?

  2. 如果不能从内核线程1的栈中恢复中断现场,那是不是内核线程1一直在关闭中断的状态下运行?
    对于内核线程来说,在创建时会对如下两部分内容进行设置与保存。

  3. 进程的硬件上下文。它是保存在进程中的cpu_context数据结构,进程硬件上下文包括X19~X28寄存器、FP寄存器、SP寄存器以及PC寄存器,详见8.1.6节。对于ARM64处理器来说,设置Pc寄存器为ret_from_fork,即指向ret_from_fork汇编函数。设置SP寄存器指向栈的pt_regs栈框。

  4. pt_regs栈框。

上述内存的设置与保存是在copy_thread()函数里实现的。

<arch/arm64/kernel/process.c>int copy_thread( )
{…
childregs->pstate = PSR_MODE_EL1h;p->thread.cpu_context.x19 = stack_start;p->thread.cpu_context.x20 = stk_sz;p->thread.cpu_context.pc = (unsigned long)ret_from_fork;p->thread.cpu_context.sp = (unsigned long)childregs;…
}

stack_start指向内核线程的回调函数,而x20指向回调函数的参数。
在进程切换时,switch_to()函数会完成进程硬件上下文的切换,即把下一个进程(next进程)的cpu_context数据结构保存的内容恢复到处理器的寄存器中,从而完成进程的切换。此时,处理器开始运行next进程了。根据PC寄存器的值,处理器会从ret_from_fork汇编函数里开始执行,新进程的执行过程如图9.18所示。

ret_from_fork汇编函数实现在arch/arm64/kernel/entry.S文件中。

1 ENTRY(ret_from_fork)
2     bl  schedule_tail
3     cbz x19, 1f       // 不是一个内核线程
4     mov x0, x20
5     blr x19
6 1:  get_thread_info tsk
7     b   ret_to_user

在第2行中,调用schedule_tail()函数来对prev进程做收尾工作。在finish_lock_switch()函数里会调用raw_spin_unlock_irq()函数来打开本地中断。因此,next进程是运行在打开中断的环境下的。
在第3行中,判断next线程是否为内核线程。如果next进程是内核线程,在创建时会设置X19寄存器指向stack_start。如果X19的值寄存器为0,说明这个next进程是用户进程,直接跳转到第6行,调用ret_to_user汇编函数,返回用户空间。
在第4~5行中,如果next进程是内核线程,那么直接跳转到内核线程的回调函数里。
综上所述,当处理器切换到内核线程1时,它从ret_from_fork汇编函数开始执行,schedule_tail()函数会打开中断,因此,不用担心内核线程1在关闭中断的状态下运行。另外,此时的内核线程1不会从中断现场返回,因为到目前为止,内核线程1还没有触发任何一个中断。那么,对于0号线程触发的中断现场怎么办呢?中断现场是保存在中断进程的栈里,只有当调度器再一次调度该进程时,它才会从栈中恢复中断现场,然后继续运行该进程。

4.调度的本质

下面是一个常见的思考题。

raw_local_irq_disable() //关闭本地中断schedule()  //调用schedule()函数来切换进程raw_local_irq_enable()  //打开本地中断

有读者这么认为,假设进程A在关闭本地中断的情况下切换到进程B来运行,进程B会在关闭中断的情况下运行,如果进程B一直占用CPU,那么系统会一直没有办法响应时钟中断,系统就处于瘫痪状态。
显然,上述分析是不正确的。因为进程B切换执行时会打开本地中断,以防止系统瘫痪。我们接下来详细分析这个问题。
调度与中断密不可分,而调度的本质是选择下一个进程来运行。理解调度有如下几个关键点。

  1. 调度的时机,即什么情况下会触发调度。

  2. 如何合理和高效选择下一个进程?

  3. 如何切换到下一个进程来执行?

  4. 下一个进程如何返回上一次暂停的地方?

我们以一个场景为例,假设系统中只有一个用户进程A和一个内核线程B,在不考虑自愿调度和系统调用的情况下,请描述这两个进程(线程)是如何相互切换并运行的。
如图9.19所示,用户进程A切换到内核线程B的过程如下。
(1)假设在T0时刻之前,用户进程A正在用户空间运行。
(2)在T0时刻,时钟中断发生。
(3)CPU打断正在运行的用户进程A,处于异常模式。CPU会跳转到异常向量表中的el0_irq里。在el0_irq汇编函数里,首先把中断现场保存到进程A的pt_regs栈框中。
(4)处理中断。
(5)调度滴答处理函数。在调度滴答处理中,检查当前进程是否需要调度。如果需要调度,则设置当前进程的need_resched标志位(thread_info中的TIF_NEED_RESCHED标志位)。
(6)中断处理完成之后,返回el0_irq汇编函数里。在即将返回中断现场前,ret_to_user汇编函数会检查当前进程是否需要调度。
(7)若当前进程序需要调度,则调用schedule()函数来选择下一个进程并进行进程切换。
(8)在switch_to()函数里进行进程切换。
(9)T1时刻,switch_to()函数返回时,CPU开始运行内核线程B了。
(10)CPU沿着内核线程B保存的栈帧回溯,一直返回。返回路径为finish_task_switch() →el1_preempt()→el1_irq。
(11)在el1_irq汇编函数里把上一次发生中断时保存在栈里的中断现场进行恢复,最后从上一次中断的地方开始执行内核线程B的代码。

从栈帧的角度来观察,进程调度的栈帧变化情况如图9.20所示。

首先,对于用户进程A,从中断触发到进程切换这段时间内,内核栈的变化情况如图9.20左边视图所示,栈的最高地址位于pt_regs栈框,用来保存中断现场。
然后,依次保存el0_irq汇编函数、ret_to_user汇编函数、_schedule()函数、context_switch()函数以及switch_to()函数的栈帧,此时SP寄存器指向switch_to()函数栈帧,这个过程称为压栈。
接下来,切换进程。
switch_to()函数返回之后,即完成了进程切换。此时,CPU的SP寄存器指向了内核线程B的内核栈中的switch_to()函数栈帧。CPU沿着栈帧一直返回,并且恢复了上一次保存在pt_regs栈框的中断现场,最后跳转到内核线程B中断的地方并开始执行,这个过程称为出栈。
综上所述,上述过程有几个比较难理解的地方。

  1. 刚切换到CPU运行的进程(next进程),它需要沿着上一次调度时保留在栈中的踪迹一直返回,并且从栈中恢复上一次的中断现场。我们假设只考虑中断导致的调度,对于主动发生调度的情况以及系统调用返回时发生调度的情况,留给读者思考。

  2. next进程需要为刚调度出去的进程(prev进程)做一些收尾工作,比如,调用raw_spin_unlock_irq()来释放锁并打开本地中断,见finish_task_switch()函数。

  3. switch_to()函数是进程切换的场所,对于系统中所有的进程,不管是运行在用户态的用户进程,还是运行在内核态的内核线程,都必须在switch_to()函数里进行进程切换。对于用户进程来说,它必须借助中断或者系统调用陷入内核,才能有机会从switch_to()函数里把自己调度出去,这个过程必然会在栈中留下踪迹。当用户进程需要重新调度执行时,它也必须根据帧栈的回溯返回用户态,才能继续执行进程本身的代码。

  4. 以时钟中断驱动的进程切换涉及两种上下文(一个是中断上下文,一个是进程上下文)的保存和恢复。中断上下文保存在中断进程的栈(即pt_regs栈框)中。进程上下文保存在进程的task_struct数据结构里。

    最后留给读者一个有意思的思考题:在中断处理函数中能不能调用schedule()函数?有兴趣的读者可以参考本书卷2的2.5.3节。

新书预告

《奔跑吧linux内核》第二版卷1已经上架了。现在JD上有半价抢购活动哟!千万不要错过了,全球首本最有深度和广度的Linux 5.x内核分析书籍,融入笨叔十多年的工作经验。点击“阅读原文”进入JD购买。

https://item.jd.com/13051268.html

《奔跑吧linux内核》第二版卷2预计春节后上架!

金色年华,流金岁月,奔二入门篇预计春节后上架!

进程调度案例分析:为何不能调度?相关推荐

  1. linux 定位 踩内存_运维必备的问题定位工具及案例分析

    [摘要]本文主要介绍各种问题定位的工具,并结合案例分析问题. 1. 背景 有时候会遇到一些疑难杂症,并且监控插件并不能一眼立马发现问题的根源.这时候就需要登录服务器进一步深入分析问题的根源.那么分析问 ...

  2. 案例分析:机场运作系统

    08年9月入学,12年7月毕业,结束了我在软件学院愉快丰富的大学生活.此系列是对四年专业课程学习的回顾,索引参见:http://blog.csdn.net/xiaowei_cqu/article/de ...

  3. 【收藏】运维必备的问题定位工具及案例分析

    [欢迎关注微信公众号:厦门微思网络] 微思网络(官网):https://www.xmws.cn/ [摘要]本文主要介绍各种问题定位的工具,并结合案例分析问题. [作者]李航,多年的底层开发经验,在高性 ...

  4. 核电集团数字化转型该怎么做?这篇案例分析帮你节省90%时间

    当前,我国智能电网的最终目标是建设成为覆盖电力系统整个生产过程,包括发电.输电.变电.配电.用电及调度等多个环节的全景实时系统. 而支撑智能电网安全.可靠运行的基础是电网全景实时数据采集.传输和存储, ...

  5. 案例分析 | 由Decimal操作计算引发的Spark数据丢失问题

    转载自  案例分析 | 由Decimal操作计算引发的Spark数据丢失问题 供稿 | Hadoop Team 编辑 | 顾欣怡 本文3058字,预计阅读时间10分钟 导读 eBay的Hadoop集群 ...

  6. 超详攻略!Databricks 数据洞察 - 企业级全托管 Spark 大数据分析平台及案例分析

    简介: 5分钟读懂 Databricks 数据洞察 ~ 更多详细信息可登录 Databricks 数据洞察 产品链接:https://www.aliyun.com/product/bigdata/sp ...

  7. 7种主流案例,告诉你调度器架构设计通用法则(干货!)

    女主宣言 今天小编为大家转载一篇来自DBAplus社群的干货文章,希望能够帮助大家对关于调度器的理解.作者张晨,Strikingly数据平台工程师,算法.分布式系统和函数式编程爱好者.Shanghai ...

  8. 智慧园区主要功能及典型案例分析

    智慧园区主要功能及典型案例分析 智慧园区是指融合新一代信息与通信技术,具备迅速信息采集.高速信息传输.高度集中技术.智慧实时处理和服务提供能力,实现产业园区内及时.互动.整合的信息感知.传递和处理,以 ...

  9. ds18b20温度转换指令_学习心得 | 温度检测工程模块划分总结与案例分析

    本文为明德扬原创文章,转载请注明出处! 很多朋友在学习FPGA的时候会发现模块划分很令人头大,今天我就通过明德扬温度检测工程来与大家分享一下本人的划分思路. 明德扬温度检测工程是基于FPGA的一个实用 ...

  10. 读书笔记:大型网站技术架构-核心原理与案例分析

    李智慧<大型网站技术架构-核心原理与案例分析> 性能 可用性 伸缩性 扩展性 安全性 总结 这本书组织的很不错,语言精练,篇幅也不长,对网站架构的要点讲的狠清楚透彻,思路清晰.主要围绕架构 ...

最新文章

  1. 先发制人!Waymo将首推商用载人自动驾驶服务,Uber们怕不怕?
  2. Ajax 通过 Request Payload 体发送 JSON 数据体
  3. Ubuntu下Qt配置Opencv
  4. MspEmu 一阶段小结
  5. 【HDU 2814 扩展欧拉 a^b ≡ (a mod c)^b mod ϕ(c)+ϕ(c) modc,b=ϕ(c) 】
  6. Python----Requests库基本使用
  7. SAP Spartacus pop over 元素的单元测试
  8. 折线图后面无数据_老板让数据师分析二八法则,此图表完美解决,项目管理师专用图表...
  9. 第十一章 策略梯度(Policy Gradient)-强化学习理论学习与代码实现(强化学习导论第二版)
  10. matlab低通滤波器库函数代码_【转】Matlab中模拟低通滤波器的函数
  11. 骨干考核系统系统流程及整体规则
  12. octobercms mysql_在Ubuntu 18.04/Debian 9上安装October CMS
  13. 计算机地理绘图软件叫什么,地理教师如何选择理想的绘图软件 ──基于对常用绘图软件的比较与分析...
  14. 怎样用电池给铁锅作防锈
  15. 人人都能读懂的react源码解析(大厂高薪必备)
  16. SSD学习系列(二)LMDB概念以及将VOC数据集转换成LMDB格式
  17. SQL Server 事务日志已满的解决方案
  18. typec扩展坞hdmi没反应_type-c扩展坞是什么?
  19. Linux打开最大文件数限制
  20. js 防止连续点击按钮

热门文章

  1. 大学python实训总结-python实训总结
  2. windows默认共享的打开和关闭
  3. Python实现网页自动截图
  4. Secondary NameNode:它究竟有什么作用?
  5. mysql视频怎么存_学习怎样把视频文件存储到mysql数据库
  6. 纯swift开发,弹幕,演唱会广告牌
  7. 2021年中国旅游城市星级饭店总体发展概况分析:营业收入总额874.51亿元[图]
  8. mysql 表分区 排序_mysql 表分区
  9. 浏览器能上网,qq,百度云不能上
  10. python取字符串首字母_python字符串操作