临文乍了了,彻卷兀若无。这篇文章主要讲述灵魂拷问之调度与切换十六问相关的知识,希望能为你提供帮助。
调度与切换之灵魂拷问,看看你大学学的操作系统课是否学到老师的真本领?没答对,说明你上课和笨叔一样睡觉,旷课,炒股,打游戏,和女同学聊天了调度和切换的疑惑
有不少小伙伴问笨叔关于调度和切换的问题,在笨FAE看来,是对调度和切换没有理解透彻,处于一种是懂非懂的状态。就算大学里学过操作系统这课,也只是学点理论,没有学到老师的真本领。笨叔特意翻阅了好几个国外经典的教材,有的书简单提了一下,有的貌似干脆不讲切换比如《现代操作系统》。
比如这本书《深入理解计算机系统》,就简单几句,总结很好,不过一般人会看晕,笨叔也晕。
笨叔无聊,还特意到B站看了几个国内外经典的操作系统课程,比如MIT,加州理工,HMC等名校的课程,这些课程对初学者都是非常棒的。
加州理工
HMC学院的课程
国内某985高校的OS课程
上面这些课程都是非常非常优秀的课程,理论知识都讲的很棒。不过若我是学生的话,依然对调度和切换是云里雾里,是懂非懂,可能是笨叔比较笨的原因。对下文提到的灵魂拷问16问还需要继续探索,要深刻理解和领悟,比较好的办法是:
- 多多练习,使用Linux内核来多多调试和总结,可以使用奔跑吧提供的O0编译的RLK内核,
- 听笨FAE讲,看笨叔的视频
- 自己动手写一个OS,例如笨OS。
若对下面的灵魂拷问都能理解,说明对调度和切换,是真搞懂了。
- 调度究竟想干什么?
- 调度的时机是什么?操作系统在什么时候会发生调度?
- 如何合理选择下一个进程?
- 什么是进程上下文?进程上下文包含哪些内容?
- 进程上下文保存到哪里?
- 什么是中断现场?中断现场需要保存哪些内容?
- 中断现场保存在什么地方?
- 进程切换时候究竟需要切换哪些东西?
- 站在CPU角度,进程切换时候,CPU会区分谁是prev进程,谁是next进程?
- 假设next进程和prev进程都是用户进程,当prev进程切换到next进程后,next进程执行的下一条语句是什么?是next进程在用户空间被打断的那条指令吗?
- 假设prev进程正在执行时发生了时钟中断,然后发生了进程切换,切换到next进程,那么这个时钟中断的中断现场会在什么时候恢复?
- 假设prev进程在时钟中断驱动下发生了进程切换,并选择next进程是新创建的进程,那么新创建进程从哪里开始执行?
- 接上题,由于时钟中断处理是在关中断下进行的,若新创建的进程一直在loop里执行,那么是不是系统就一直没办法再一次响应时钟中断,导致系统一直运行这个新创建的进程?
- 在中断处理函数中,能不能直接调用schedule()函数?为什么?
- 小明同学在raw_local_irq_disable()函数后直接调用schedule()函数,若调度器选择了的next进程是一个loop执行的进程,那是不是系统就不能响应时钟中断,从而瘫痪了?
- 为什么switch_to()函数有3个参数?prev和next就够了,为何还需要last?
我们可以从一个小问题开始思考:
假设Linux内核只有三个内核线程,0号线程创建了内核线程1和内核线程2,
它们永远不会退出。当系统时钟中断到来时,时钟中断处理函数会检查是否有进程需要被调度。当有需要调度时,调度器会选择线程1或者线程2来运行。
假设0号进程先运行,那么在这个场景下会发生什么情况?
这是一个有意思问题,涉及到调度器的实现机制、中断处理、内核抢占、新创建进程如何被调度、进程切换等一些列知识点。我们只有把这些知识点都弄明白了才能真正搞明白这个问题。
场景分析
我们来回到刚才那个问题,这个场景的主要步骤如下。
- 第一个出场的是0号线程。我们都知道在Linux内核里,0号线程就是那个系统启动时候跑的那个线程。而start_kernel()是运行在0号线程里。0号线程创建了内核线程1和内核线程2。
函数调用关系:start_kernel()-> kernel_thread()-> _do_fork()。
在_do_fork()函数会创建新线程,并且把新线程添加到调度器的就绪队列中。
0号线程创建完内核线程1和内核线程2后,进入while死循环,0号线程不会退出,它正在等待被调度出去。
2.第二个要出场的是:时钟中断。时钟中断是系统里很重要的一个东西,处理器采用时钟定时器来周期性地提供系统脉搏。时钟中断也是普通外设中断的一种。调度器利用时钟中断来定时检测当前正在运行的线程是否需要被调度。
函数调用关系:
el1_irq-> handle_domain_irq-> __handle_domain_irq->
generic_handle_irq_desc-> handle_percpu_devid_irq->
arch_timer_handler_virt-> timer_handler->
tick_handle_periodic-> tick_periodic->
update_process_times-> scheduler_tick
- 当时钟中断检测到当前线程需要被调度时,设置need_resched标志位。
在check_preempt_tick()函数里。
- 时钟中断返回时,根据Linux内核是否支持内核抢占来确定是否需要调度,下面分两种情况来讨论。
支持内核抢占的内核:发生在内核态的中断在返回时,检查当前线程的need_resched标志位是否置位,如果置位说明当前线程需要被调度出来。
不支持内核抢占的内核:发生在内核态的中断在中断返回时不会检查是否需要调度。
- 时钟中断发生。时钟中断触发时当前进程(线程)有可能在用户态执行,也可能在内核态执行。当进程运行在用户态时发生了中断,那么会进入到异常向量表的el0_irq汇编函数中,当进程运行在内核态发生了中断,那么会进入到异常向量表的el1_irq汇编函数中。在本场景中,因为3个线程都是内核线程,因此时钟中断只能跳转到el1_irq汇编函数里。进入中断时,CPU会自动关闭中断。
- 在el1_irq汇编函数里,首先会保存中断现场(也称为中断上下文)到当前进程的栈中,Linux内核使用pt_regs数据结构来实现一个栈框,用来保存中断现场(本节称为pt_regs栈帧)。
- 中断处理过程,包括切换到Linux内核的中断栈、硬件中断号的查询、中断服务程序的处理等。
- 当确定当前中断源是时钟中断后,scheduler_tick()函数会取检查当前进程的是否需要调度。如果需要调度,则设置当前进程的need_resched标志位(thread_info中的TIF_NEED_RESCHED标志位)。
- 中断返回。这里需要给中断控制器返回一个中断结束信号(End Of Interrupt, EOI)。
- 在el1_irq汇编函数直接恢复中断现场,这里会使用0号线程的pt_regs栈框来恢复中断现场。在不支持内核抢占的系统里,el1_irq汇编函数不会检查是否需要调度。最后打开中断,CPU继续从被中断打断的地方开始继续执行0号进程。
可能大家会对之前这张图有疑问:
- 如果内核线程1是新创建的进程,它的栈应该是空的,那它第一次运行时如何恢复中断现场呢?
- 如果不能从内核线程1的栈中恢复中断现场,那是不是内核线程1一直处于关中断的状态下运行?
对于内核线程来说,在创建时会对如下两部分内容进行设置与保存。
- 进程的硬件上下文。它是保存在进程的cpu_context数据结构,进程硬件上下文包括x19~x28寄存器、fp寄存器、sp寄存器以及pc寄存器,详见第8.1.6节内容。
- pt_regs栈框。
由于篇幅关系,本文不对灵魂拷问16问做详细解析,若大家对上述灵魂拷问之16问有兴趣或者想听笨FAE给你深度解答,请订阅旗舰篇视频。
奔跑吧旗舰篇视频,是按照主题内容来分成多个“季”,只要订阅了,以后录制相同主题的视频都是免费更新的哟。
【灵魂拷问之调度与切换十六问】有兴趣的小伙伴可以订阅奔跑吧旗舰篇视频节目,点击“阅读原文”订阅哟!
推荐阅读
- 视频更新(代码分析8之单步调试ARM64启动汇编与重定位)
- (系统级I?O)
- 运维小白成长记——第六周
- 系统cron计划任务小练习
- #yyds干货盘点#python面向对象之工厂函数调用__init__()
- 编写脚本实现登陆远程主机
- #yyds干货盘点#流媒体服务器
- 生成10个随机数保存于数组中,并找出其最大值和最小值
- 缓存技术和用户层缓存原理