Linux内核学习笔记(四)——进程调度

  进程调度程序是在可运行态进程之间分配有限的处理器时间资源的内核子系统。
  
  调度程序完成的基本工作是,在一组处于可运行状态的进程中选择一个来执行。


1、多任务

  多任务操作系统是能同时并发地交互执行多个进程的操作系统。
  
  多任务系统分为两类:非抢占式多任务抢占式多任务
  
  抢占式多任务模式下,由调度程序来决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会。这个强制的挂起动作叫做抢占
  进程在被抢占之前能够运行的时间是预先设置好的,叫做进程的时间片
  
  非抢占式多任务模式下,除非进程自己主动停止运行,否则它会一直执行。进程主动挂起自己的操作称为让步
  缺点:调度程序无法对每个进程执行多长时间做出统一规定;不做出让步的悬挂进程能使系统崩溃。


2、Linux的进程调度

  在Linux的2.4内核之前,调度程序相当简陋。
  2.5内核采用O(1)调度程序的新调度程序,包括静态时间片算法和针对每一处理器的运行队列。但是该算法对于交互进程(响应时间敏感的程序)表现不佳。
  2.6内核采用完全公平调度算法(CFS)。其中包含“反转楼梯最后期限调度算法(RSDL)”,吸取了队列理论。


3、策略

  策略决定调度程序在何时让什么进程运行。

3.1 I/O消耗型和处理器消耗型的进程

  进程可以被分为I/O消耗型处理器消耗型
  I/O消耗型进程的大部分时间用来提交或等待I/O请求,经常处于可运行状态,但运行时间短。
  处理器消耗型进程把时间大多用在执行代码上,除非被抢占。调度策略尽量降低它们的调度频率,延长运行时间。
  进程可以同时表现为两种类型。
  
  调度策略要在进程响应迅速和最大系统利用率中间寻找平衡。
  Unix的调度程序更倾向于I/O消耗型程序,以提供更好的程序响应速度。Linux缩短响应时间,更倾向于优先调度I/O消耗型进程。

3.2 进程优先级

  Linux采用了两种不同的优先级范围。
  
  nice值
  范围为从-20到+19,默认值为0。越大的nice值意味着更低的优先级,第nice值的进程可以获得更多的处理器时间。Linux系统中,nice值代表时间片的比例。
  
  实时优先级
  默认情况下变化范围是从0到99,越高的数值意味着进程优先级越高。任何实时进程的优先级都高于普通进程。使用命令ps-eo state,uid,pid,ppid,rtprio,time,comm.查看进程列表以及对应的实时优先级。如果进程对应列显示-,说明不是实时进程。

3.3 时间片

  时间片是一个数值,表明进程在被抢占前能持续运行的时间。时间片过长会导致系统对交互的响应表现欠佳,太短会明显增大进程切换带来的处理器消耗。
  
  Linux的CFS调度器将处理器的使用比例划分给了进程。
  关于抢占时机,如果新进程消耗的处理器使用比比当前进程小,则立刻投入运行,抢占当前进程;否则推迟其运行。


4、调度算法

4.1 调度器类

  Linux调度器是以模块方式提供的,允许不同类型的进程可以有针对性地选择调度算法。这种模块化结构称为调度器类,允许多种不同的可动态添加的算法并存,调度属于自己范畴的进程。
  每个调度器都有一个优先级,定义在kernel/sched.c文件中,按照优先级遍历调度类,拥有可执行进程的最高优先级的调度器类胜出。
  CFS是针对普通进程的调度类,定义在kernel/sched_fair.c文件中。

4.2 Unix进程调度

  Unix系统的nice值问题。

  • 若要将nice值映射到时间片,需要将nice单位值对应到处理器的绝对时间。进程切换无法最优化进行。
  • 相对nice值。nice值减小1带来的效果不同。
  • 时间片必须是定时器节拍(10ms或1ms)的整倍数。
  • 使给定进程打破公平原则,获得更多处理器时间。

  
  分配绝对的时间片引发的固定的切换频率,给公平性造成了很大变数。

4.3 公平调度

  CFS允许每个进程运行一段时间、循环轮转、选择运行最少的进程作为下一个运行进程,而不再采用分配给每个进程时间片的做法。CFS在所有可运行进程总数上计算出一个进程应该运行多久。
  nice值在CFS中被作为进程获得的处理器运行比的权重。
  
  CFS为完美多任务中的无限小调度周期的近似值设立了一个目标,称作目标延迟
  CFS引入每个进程获得的时间片底线,称为最小粒度。默认值为1秒。
  
  进程的处理器时间是由它自己和其他所有可运行进程nice值的相对差值决定的。nice值对应的绝对时间不再是一个绝对值,而是处理器的使用比。


5、Linux调度的实现

5.1 时间记账

  Unix系统分配时间片给进程,当系统时钟节拍发生时,时间片减少一个节拍周期,时间片减少到0时被其他可运行进程抢占。

5.1.1 调度器实体结构

  CFS使用调度器实体结构追踪进程运行记账:

struct sched_entity {
    struct load_weight         load;
    struct rb_node             run_node;
    struct list_head           group_node;
    unsigned int               on_rg;
    u64                        exec_start;
    u64                        sum_exec_runtime;
    u64                        vruntime;
    u64                        prev_sum_exec_runtime;
    u64                        last_wakeup;
    u64                        avg_overlap;
    u64                        nr_migrations;
    u64                        start_runtime;
    u64                        avg_wakeup;
}

  在进程描述符中是名为se的成员变量

5.1.2 虚拟实时

  vruntime变量存放进程的虚拟运行时间,单位为ns。CFS使用vruntime变量记录一个程序运行了多长时间以及它还应该再运行多久。
  
  kernel/sched_fair.c中的update_curr()函数实现记账功能。
  update_curr()计算当前进程的执行时间,存放在变量delta_exec中,传递给__update_curr(),根据当前可运行进程总数对运行时间进行加权计算。最终将权重值与当前运行进程的vruntime相加。
  update_curr()由系统定时器周期性调用,所以vruntime可以测量给定进程的运行时间,和下一个被运行的进程。

5.2 进程选择

  CFS调度算法核心:选择具有最小vruntime的任务。CFS使用红黑树组织可运行进程队列,并利用其迅速找到最小vruntime值的进程。

5.2.1 挑选下一个任务

  CFS算法运行rbtree树中最左边叶子节点所代表的进程。这一过程通过函数__pick_next_entity()实现。

5.2.2 向树中加入进程

  在进程变为可运行状态或者fork()第一次创建进程时,CFS将进程加入rbtree,通过函数enqueue_entity()实现。
  
  enqueue_entity()函数更新运行时间和其他一些统计数据,然后调用__enqueue_entity()进行插入操作。

5.2.3 从树中删除进程

  删除动作发生在进程堵塞(变为不可运行态)或者终止时。通过函数dequeue_entity()实现,调用函数__dequeue_entity()。

5.3 调度器入口

  进程调度的主要入口点是函数schedule(),它会调用pick_next_task()依次检查每一个调度器类,从最高优先级的调度类中选择最高优先级的进程。

5.4 睡眠和唤醒

  休眠(被阻塞)的进程处于一个特殊的不可执行状态,进程把自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行其他进程。唤醒时,进程被设置为可执行状态,从等待队列中移到可执行红黑树中。

5.4.1 等待队列

  等待队列是由等待某些事件发生的进程组成的简单链表。内核用wake_queue_head_t来代表等待队列,可以静态创建也可以动态创建。
  
  加入等待队列的步骤:

  • 调用宏DEFINE_WAIT()创建一个等待队列的项;
  • 调用add_wait_queue()把自己加入到队列中。事件发生时,对等待队列执行wake_up()操作;
  • 调用prapare_to_wait()将进程状态变更为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE;
  • 检查并处理信号。信号唤醒进程是伪唤醒,不是因为事件的发生唤醒;
  • 进程被唤醒时,再次检查条件是否为真,如果不是,再次调用schedule()并重复这步操作;
  • 条件满足后,设置为TASK_RUNNING并调用finish_wait()将自己移出等待队列。

  
  inotify_read()函数负责从通知文件描述符中读取信息。

5.4.2 唤醒

  函数wake_up()唤醒指定的等待队列上的所有进程。调用函数try_to_wake_up()将进程设置为TASK_RUNNING,调用enqueue_task()将进程放入红黑树,被唤醒的进程比当前进程优先级高还要设置need_resched标志。


Linux内核学习笔记(四)——进程调度_第1张图片


6、抢占和上下文切换

  上下文切换是从一个可执行进程切换到另一个可执行进程,由context_switch()函数处理,该函数由schedule()函数调用,完成工作:

  • 调用switch_mm(),把虚拟内存从上一个进程映射切换到新进程中。
  • 调用switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。包括保存、恢复栈信息和寄存器信息,还有其他任何与体系结构相关的状态信息。

  
  内核提供need_resched标志表明是否需要重新执行一次调度。内核检查该表示确认其被设置(比如返回用户空间以及从中断返回时),调用schedule()切换到新的进程。
  每个进程都包含一个need_resched标志。

6.1 用户抢占

  内核即将返回用户空间时,如果need_resched标志被设置,会导致schedule()被调用。此时发生用户抢占。
  
  用户抢占发生在从系统调用返回用户空间时,或从中断处理程序返回用户空间时。

6.2 内核抢占

  不支持内核抢占的系统,内核代码一直执行到完成或明显的阻塞为止。
  
  如果没有持有锁,正在执行的代码就是可重新导入的,也就是可以抢占的。
  每个进程的thread_info引入preempt_count计数器。使用锁的时候加1,释放锁的时候减1。当数值为0时,内核可执行抢占。从中断返回内核空间时,内核会检查need_resched和preempt_count的值。
  
  内核抢占发生在:

  • 中断处理程序正在执行,且返回内核空间之前;
  • 内核代码再一次具有可抢占性的时候;
  • 内核中的任务显式地调用schedule();
  • 内核中的任务被阻塞,调用schedule();

7、实时调度策略

  Linux提供两种实时调度策略:SCHED_FIFO和SCHED_RR。普通的非实时调度策略是SCHED_NORMAL。
  实时策略不被完全公平调度器管理,而是被一个特殊的实时调度器管理。

7.1 SCHED_FIFO

  SCHED_FIFO实现了一种简单的、先入先出的调度算法,不使用时间片。

  • 处于可运行状态的SCHED_FIFO进程比SCHED_NORMAL进程先得到调度。
  • 不基于时间片。SCHED_FIFO进程处于可执行状态,会一直执行,直到自己受阻塞或显式地释放处理器。
  • 只有优先级更高的SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务。
  • 多个同优先级的SCHED_FIFO进程会轮流执行。
  • 其他级别较低的进程只能等待SCHED_FIFO进程变为不可运行态后才有机会执行。

7.2 SCHED_RR

  SCHED_RR是带有时间片的SCHED_FIFO,是一种实时轮流调度算法,在耗尽实现分配给它的时间后就不能再继续执行。
  
  这两种实时算法实现的是静态优先级,内核不为实时进程计算动态优先级。

7.3 软实时

  Linux的实时调度算法提供软实时工作方式:内核调度进程,尽力使进程在它的限定时间到来前运行,但内核不保证总能满足这些进程的要求。硬实时系统保证在一定条件下,可以满足任何调度的要求。
  
  实时优先级范围从0到MAX_RT_PRIO减1,默认MAX_RT_PRIO为100。
  SCHED_NORMAL进程的nice值范围从MAX_RT_PRIO到(MAX_RT_PRIO+40)。nice值从-20到+19对应实时优先级从100到139。

8、与调度相关的系统调用

  与调度相关的系统调用

系统调用 描述
nice() 设置进程的nice值
sched_setscheduler() 设置进程的调度策略
sched_getscheduler() 获取进程的调度策略
sched_setparam() 设置进程的实时优先级
sched_getparam() 获取进程的实时优先级
sched_get_priority_max() 获取实时优先级的最大值
sched_get_priority_min() 获取实时优先级的最小值
sched_rr_get_interval() 获取进程的时间片值
sched_setaffinity() 设置进程的处理器的亲和力
sched_getaffinity() 获取进程的处理器的亲和力
sched_yield() 暂时让出处理器

8.1 与调度策略和优先级相关的系统调用

  • sched_setscheduler()和sched_getscheduler(),读取或改写进程tast_struct的policy和rt_priority的值。
  • sched_setparam()和sched_getparam(),获取封装在sched_param结构体中的rt_priority。
  • sched_get_priority_max()和sched_get_priority_min()。
  • nice(),只有超级用户才能使用负值,设置进程task_struct的static_prio和prio的值。

8.2 和处理器绑定有关的系统调用

  Linux调度程序提供强制的处理器绑定机制。它尽力通过一种软的亲和性使进程尽量在同一个处理器上运行,但也允许强制指定进程运行的处理器。
  强制的亲和性保存在进程task_struct的cpus_allowed的位掩码标志中,每一位对应一个处理器。默认所有位都被设置。
  使用sched_setaffinity()和sched_getaffinity()函数可以设置和获取位掩码。
  
  进程第一次创建时继承父进程的掩码,和父进程运行在相同处理器上。当处理器绑定关系改变时,内核采用移植线程把任务对到合法处理器上。最后,加载平衡器把任务拉倒允许的处理器上。

8.3 放弃处理器时间

  通过sched_yield()让进程显式地将处理器时间让给其他等待执行的进程。将进程从活动队列移到过期队列中,在一段时间内它不会再被执行。
  实时进程例外,不会过期,只是被移动到其优先级队列的最后面。
  
  内核代码调用yield()确定给定进程处于可执行状态,然后在调用sched_yield()。用户空间程序直接调用sched_yield()。

你可能感兴趣的:(Linux)