0%

linux内核任务调度子系统

2020年5月4日 上午10:05

  1. 任务调度子系统可以从一下三个方面进行总结:
    1. 主要参考资料:刘超老师的趣谈操作系统
    2. 调度要解决的第一个问题是,每一个 CPU 小伙伴每过一段时间,都要想一下,白板上这么多项目,我应该干哪一个?CPU 的队列里面有这么多的进程或者线程,应该取出哪一个来执行?
      1. 有时候,进程会分优先级,如何给优先级高的进程多分时间呢?
        1. 这个简单,就相当于 N 个口袋,优先级高的袋子大,优先级低的袋子小。这样球就不能按照个数分配了,要按照比例来,大口袋的放了一半和小口袋放了一半,里面的球数目虽然差很多,也认为是公平的。
        2. vruntime:
          1. 函数 update_curr 用于更新进程运行的统计量 vruntime ,CFS 还需要一个数据结构来对 vruntime 进行排序,找出最小的那个。在这里使用的是红黑树。红黑树的节点是 sched_entity,里面包含 vruntime。
        3. 调度算法的本质就是解决下一个进程应该轮到谁运行的问题,这个逻辑在 fair_sched_class.pick_next_task 中完成。
    3. 调度要解决的第二个问题是,什么时候切换任务?也即,什么时候,CPU 小伙伴应该停下一个进程,换另一个进程运行?
      1. 方式一:主动调度
        1. A 项目做着做着,里面有一条指令 sleep,也就是要休息一下,或者等待某个 I/O 事件。那没办法了,要主动让出 CPU,然后可以开始做 B 项目。主动让出 CPU 的进程,会主动调用 schedule() 函数。
        2. 在 schedule() 函数中,会通过 fair_sched_class.pick_next_task,在红黑树形成的队列上取出下一个进程,然后调用 context_switch 进行进程上下文切换。
        3. 进程上下文切换主要干两件事情,一是切换进程空间,也即进程的内存,也即 CPU 小伙伴不能 A 项目的会议室里面干活了,要跑到 B 项目的会议室去。二是切换寄存器和 CPU 上下文,也即 CPU 将当期在 A 项目中干到哪里了,记录下来,方便以后接着干。
      2. 方式二:被动抢占
        1. 细分情形一:
          1. A 项目做着做着,旷日持久,实在受不了了。项目经理介入了,说这个项目 A 先停停,B 项目也要做一下,要不然 B 项目该投诉了。最常见的现象就是,A 进程执行时间太长了,是时候切换到 B 进程了。这个时候叫作 A 进程被被动抢占。
          2. cpu时钟Tick的中断处理函数:
            1. 抢占还要通过 CPU 的时钟 Tick,来衡量进程的运行时间。时钟 Tick 一下,是很好查看是否需要抢占的时间点。 时钟中断处理函数会调用 scheduler_tick(),他会调用 fair_sched_class 的 task_tick_fair,在这里面会调用 update_curr 更新运行时间。当发现当前进程应该被抢占,不能直接把他踢下来,而是把他标记为应该被抢占,打上一个标签 TIF_NEED_RESCHED。
        2. 细分情景二:
          1. 当一个进程被唤醒的时候。一个进程在等待一个 I_O 的时候,会主动放弃 CPU。但是,当 I_O 到来的时候,进程往往会被唤醒。这个时候是一个时机。当被唤醒的进程优先级高于 CPU 上的当前进程,就会触发抢占。如果应该发生抢占,也不是直接踢走当然进程,而也是将当前进程标记为应该被抢占,打上一个标签 TIF_NEED_RESCHED。
      3. 上面只是简单地给给进程打上了可以进行主动调度、被动调度的标签,就像是C语言中声明和初始化的区别一样
        1. 真正的抢占还是需要上下文切换,也就是需要那么一个时刻,让正在运行中的进程有机会调用一下 schedule。调用 schedule 有以下四个时机。
          1. 用户态的进程
            1. 对于用户态的进程来讲,从系统调用中返回的那个时刻,是一个被抢占的时机。
            2. 对于用户态的进程来讲,从中断中返回的那个时刻,也是一个被抢占的时机。
          2. 内核态的执行中
            1. 对内核态的执行中,被抢占的时机一般发生在 preempt_enable() 中。在内核态的执行中,有的操作是不能被中断的,所以在进行这些操作之前,总是先调用 preempt_disable() 关闭抢占。再次打开的时候,就是一次内核态代码被抢占的机会。
            2. 在内核态也会遇到中断的情况,当中断返回的时候,返回的仍然是内核态。这个时候也是一个执行抢占的时机。
  2. 上面我们了解了调度系统的主要功能,接下来说说一个灵魂拷问:调度子系统和多进程/线程之间的关系?
    1. 首先,为什么我会问这样一个问题?因为我潜意识里认为调度子系统是多线程的管理者,即是一个master-slaver的关系,这可能和中国人的思维模式有关系。那么,我就会思考这样一个问题:既然调度子系统是master,那这个master由谁来管呢?这个问题就瞬间把我难倒了!
    2. 于是我开始重新思考他们两者之间的关系。结论:调度子系统和多线程/多进程本身就是一个东西,他么两个不是依赖管理的关系,本身他们就是一个东西,共生。
      1. 一个错误的解释:如果没有多线程,没有东西供他调度;没有调度系统,多进程无法正常进行。为什么说这句话是错的:还是那句话,他们两个本身就是一个东西!!
    3. 对于这个问题,我觉得从单cpu的角度比较很好理解:
      1. 对于单个任务来说,cpu按这个任务中包含的指令一条条的执行就行了
      2. 对于多任务来说,可以有两种方式:
        1. 一种是串行:这就变成了单任务的执行
        2. 一种是并发:
          1. 多个任务ABCD之间,他们在一个cpu上进行执行,会造成各自的执行过程其实是停停走走,但由于他们公用一套cpu寄存器,那么就会涉及到多次的状态保存,这对于ABCD来说是一样的。
          2. 那么,这时候就要小心一个问题:是谁帮他们保存的,我们知道cpu就是一个傻子,他是不会自己在干什么,在他眼里没有ABCD,只有一条条无差别的指令,更不会主动的去帮助你去完成数据的保存工作,以及恢复这两个工作。也就是说这两个工作也得写成一条条的程序指令。
          3. 所以从这个角度来说,所谓的调度子系统其实就是对程序员来说屏蔽了自己编写数据保存、恢复的工作,这样会给程序员造成错误的感觉:认为cpu好智能的,他会自动的保存和恢复,以及调度ABCD的运行。
          4. 调度子系统会将上层程序员编写的任务进行重新的拆解、分析,并生成cpu这个傻子需要的一条条的指令。这中间最最最关键的一点是:调度子系统也是一个task,调度这个词虽然是一个动词,但是调度子系统并不能完成调度的工作,他只是知道如何进行调度,时候一个理论派,不是实践派,他可以总结为一条条的步骤,其实就是指令。最终的调度这个动作,还是由cpu来完成的。所以,从cpu的角度来说,也不会区分ABCD和调度子系统,cpu认为他们是一样的。
          5. 调度子系统是一个线程_进程_task,只不过他完成的工作比较特殊,首先他在linux内核中动作,不是程序员自己写的,上层程序员不容易察觉;他本身的这个工种的特殊性:自己是进程_线程_task,他处理的对象也是进程_线程_task,对他们进行安排,一般来说我们都是处理数据、计算啥的,这就会造成一种自己处理自己的感觉,这点估计就是理解调度系统和多线程/进程的关键,也是最让人不舒服的地方,容易让人忽视他的作用,或者神化他的作用。
          6. 这中间理解的关键就是要从cpu无差别的角度去分析这个问题!