一道进程相关面试题引发的思考

“ 小明同学最近贼郁闷,去年玩比特币亏得一塌糊涂,想在股市里翻盘,听信大V推荐的股票,买了康美和长生生物,被A股狠狠的收割了一把。本想今年好好工作,谁知道遇上了O记大裁员。虽然拿了N+6,但是还是要找工作养家糊口啊!只能硬着头皮去面试了,还好,面试题目都不难,因为小明认真看过《奔跑吧》”
01 面试题目
在一个双核处理器的系统中,在shell界面下运行test程序。CPU0的就绪队列上有4个进程,而CPU1的就绪队列有1个进程。假设test程序和这个5个进程的nice值都是为0。
一道进程相关面试题引发的思考
文章图片

test程序
一道进程相关面试题引发的思考
文章图片

执行test进程
问题:

  1. 请画出test程序在内核空间的执行流程图。
  2. 若干时间之后,CPU0和CPU1的就绪队列变化如何?
小明听到这题目,嘿嘿一笑,so easy,难不到窝,这题目比炒A股简单多了!于是在黑板上开始边写边画,口若悬河。
【一道进程相关面试题引发的思考】02 题目解析
站在用户空间的角度看,在shell界面下执行test程序,shell程序会调用fork系统调用来创建一个新进程,然后调用exec系统调用来装载test进程,因此新进程便开始执行test程序。
站在用户空间的角度看问题,我们只能看到test程序被执行了,但是我们是看不到新进程是如何被创建的,它会添加到哪个CPU里,它是如何执行的,以及CPU0和CPU1之间如何做负载均衡等等问题。
一道进程相关面试题引发的思考
文章图片

小明画的test进程执行流程图
从上图可知,我们把test进程在内核空间的执行过程分成了6个步骤。
(1)调用系统调用fork系统调用来创建一个新进程
(2)do_fork()创建新进程。do_fork要做好多事情,比如:
a)创建新进程进程控制块PCB,task_struct数据结构。
b)拷贝父进程的task_struct数据结构内容到新进程。
c)拷贝父进程的页表项内容到新进程。
d)设置新进程的内核栈
(3)父进程调用wake_up_new_task()尝试去唤醒新进程。
a)调用调度类的select_task_rq()方法,为新进程寻找一个负载最轻的CPU,这里选择CPU1。
b)调用调度类的enqueue_task()方法把新进程添加到CPU1的就绪队列里。
(4)CPU1重新选择一个合适的进程来运行。
a)每次时钟滴答到来时,scheduler_tick()会调用调度类的task_tick()去检查是否需要重新调度。check_preempt_tick()检查是否需要重新调度。若需要调度,则设置当前进程的thread_info中的TIF_NEED_RESCHED标志位。假设在我们这个场景里,CPU1准备调度我们的新进程P,那么就会设置当前进程curr的thread_info中的TIF_NEED_RESCHED标志位。
b)在中断返回前会检查当前进程curr的TIF_NEED_RESCHED标志位。如果需要调度的话,调用preempt_schedule_irq()来切换进程运行。
c)调度器的schedule()函数会调用调度类的pick_next_task()方法来选择下一个最合适运行的进程。在我们的场景中,选择新进程P。
d)switch_mm()切换prev进程和新进程的页表。
e)在CPU1上,switch_to()切换新进程P来执行。
(5)新进程执行。
a)新进程的第一次执行会调用ret_from_fork()函数。
b)返回用户空间执行shell程序。
c)shell程序调用exec()系统调用来执行test程序,最终新进程变成了test进程。
(6)负载均衡。
a)在每次时钟滴答到来时去检查是否需要触发软中断来做SMP负载均衡。scheduler_tick()->trigger_load_balance()。下一次做负载均衡的时间点存放在就绪队列的next_balance成员里。
b)触发SCHED_SOFTIRQ软中断。
c)在软中断处理函数run_rebalance_domains()里,从当前CPU开始遍历CPU拓扑关系图,从调度域的低层往高层遍历调度域,并寻找有负载不均匀的调度组。本例子中的CPU拓扑关系图很简单,只有一层MC级别的调度域。
d)CPU0对应的调度域是domain_mc_0,对应的调度组是group_mc_0;CPU1对应的调度域是domain_mc_1,对应的调度组是group_mc_1。CPU0的调度域domain_mc_0管辖CPU0和CPU1,其中group_mc_0和group_mc_1这两个调度组会连接到domain_mc_0的一个链表里,同理CPU1的调度域domain_mc_1也是同样管理着group_mc_1和group_mc_0这两个调度组。
e)假设当前运行的CPU是CPU1,也就是说运行到run_rebalance_domains()函数的CPU为CPU1,那么在当前MC的调度域(domain_mc_1)里去找哪个调度组是最繁忙的。在我们的场景里,很容易找到CPU0的那个调度组(group_mc_0)是最繁忙的。计算需要迁移多少的负载量到CPU1上才能保持两个调度组负载平衡。
f)迁移进程从CPU0到CPU1。
g)达到新的平衡。
一道进程相关面试题引发的思考
文章图片

笨叔点评:
这是一道很棒的面试题目,把进程方方面面的问题都考到了,比如说进程的创建,进程的调度,进程的执行,SMP负载均衡。里面任何的一点,都足以难倒很多人,比如说:
进程的本质是什么?
进程的调度本质是什么?
调度器怎么选择下一个进程?
处理器是怎么就切换和执行到下一个进程?
CFS调度器和进程优先级究竟有什么关系?
CFS调度器里的虚拟时钟是个什么鬼?
runnable和running是啥关系?
什么是进程的权重?
什么是进程的负载?
调度域和调度组是个什么鬼?
如何能快速画出一个系统的CPU拓扑关系图?
SMP负载均衡里是怎么衡量一个就绪队列的负载的?
SMP负载均衡算法是怎么玩的?
负载和权重究竟有什么关系?
Linux的负载均衡算法里说的负载是怎么计算的?
怎么理解衰减(decay)?
...
笨叔正在修改蓝色版本的《奔跑吧》,基于Linux 5.0+ARM64,欢迎大家提意见和idea!好的idea,笨叔好酒相赠!
更多精彩内容,关注笨叔的微信公众号以及笨叔收费旗舰篇视频。

    推荐阅读