进程的调度

        多任务操作系统是同时并发交互执行多个进程的操作系统。多任务系统可以划分为两类:抢占式多任务和非抢占式多任务。Linux提供了抢占式多任务模式。在此模式下由调度程序决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会。每个进程在抢占前能够运行的时间是预先设置好的,称之为时间片(timeslice)

        进程可以被分为I/O消耗型处理器消耗型。前者的大部分时间用来提交I/O请求或等待I/O请求。这样的进程经常处于可运行状态,但通常都是运行短短一会。处理器消耗型进程把时间大多用在执行代码上。除非被强占,否则一直不停地运行,因为其没有太多的I/O需求。

        Linux采用两种不同的优先级范围。第一种用nice值,其范围从-20到+19,默认值为0;越大的nice值意味着更低的优先级。Linux中nice值代表时间片的比例,可通过ps -el 查看系统中进程列表,其中NI一列就是进程对应的nice值。第二种范围是实时优先级,其值是可配置的,范围是[0, 99]。越高的实时优先级意味着进程优先级越高。任何实时进的优先级都高于普通进程,也就是说实时优先级和nice优先级处于互不相交的两个范畴。

可通过ps -eo state,uid,pid,ppid,rtpric,time,comm,ni查看系统进程列表,实时优先级位于RTPRIO列下。RTPRIO列下为"-"说明不是实施进程,NI列下为"-"说明不是普通进程。

Linux调度算法

        Linux2.6.23内核中使用了一个名为“完全公平调度算法”,简称为CFS。Linux的CFS调度器不直接分配时间片到进程,而是将处理器的使用比划分给进程。这样一来,进程所获得的处理器时间其实是和系统负载密切相关的。这个比例会受nice值影响,nice值越高的进程被赋予越低的权重。CFS调度器中,其抢占时机取决于新的可运行程序消耗了多少处理器使用比。如果消耗的使用比比当前进程小,则新进程立刻投入运行,抢占当前进程。否则,将推迟其运行。

        例如有文本编辑程序和视频编辑程序,前者需要及时响应,但是又不需要太多处理器,后者需要大量处理器时间。CFS使用处理器使用比进行处理器的调度,例如两个进程都分配了50%的处理器使用比。一旦文本编辑器被唤醒,但CFS注意到给它的处理器使用比是50%但其实使用的却很少。CFS发现文本编辑器比视频编辑器运行的时间短得多。这种情况下,他会立刻抢占视频编辑器进程,让文本编辑器投入运行。在文本编辑器运行后,立即处理器用户输入后又进入睡眠等待用户输入。因为文本编辑器没有消耗50%处理器使用比,因此CFS会毫不犹豫地让文本编辑器在需要时投入运行。

        Linux调度器是以模块方式提供的,允许不同类型的进程可以有针对性地选择调度算法。这种模块化结构称为调度器类,他允许多种不同的可动态添加的调度算法存在,调度属于自己范畴的进程。每个调度器都有一个优先级,基础的调度器代码定义在kernel/sched.c,它会按照优先级遍历调度类,拥有一个可执行的最高优先级的调度器生活才,去选择要执行的进程。

        目前Linux支持三种进程调度策略,分别是SCHED_FIFO 、 SCHED_RR和SCHED_NORMAL;而Linux支持两种类型的进程,实时进程和普通进程。实时进程可以采用SCHED_FIFO 和SCHED_RR调度策略;普通进程则采用SCHED_NORMAL调度策略。从Linux2.6.23内核版本开始普通进程(采用调度策略SCHED_NORMAL的进程)采用了绝对公平调度算法,不再跟踪进程的睡眠时间,也不区分是否为交互式进程,它将所有的进程都统一对待,这就是完全公平的含义。

CFS基本原理概述

cfs定义了一种新调度模型,它给cfs_rq(cfs的run queue)中的每一个进程都设置一个虚拟时钟-virtual runtime(vruntime),以ns为单位,不再跟定时器节拍相关。CFS使用vruntime变量来记录一个程序到底运行了多长时间以及它还应该再运行多久。。如果一个进程得以执行,随着执行时间的不断增长,其vruntime也将不断增大,没有得到执行的进程vruntime将保持不变。
而调度器将会选择最小的vruntime那个进程来执行。这就是所谓的“完全公平”。不同优先级的进程其vruntime增长速度不同,优先级高的进程vruntime增长得慢,所以它可能得到更多的运行机会。

CFS算法设计核心

时间记账

CFS根据各个进程的权重分配进程运行时间。
进程的运行时间计算公式为:
分配给进程的运行时间 = 调度周期 * 当前进程权重 / 所有进程权重总和 
备注:调度周期:将所有处于TASK_RUNNING态进程都调度一遍的时间,在O(1)调度算法中就是运行队列中进程运行一遍的时间。所以进程权重与分配给进程的运行时间成正比。

vruntime的计算公式为:
vruntime = 实际运行时间 * NICE_0_LOAD/ 当前进程权重 (公式3.2)

如果分配给进程的运行时间等于实际运行的时间时,将推到出另一vruntime计算公式。把公式3.2中的分配给进程的运行时间 与公式3.1中实际运行时间替换,将得出以下结果:
vruntime = (调度周期 * 当前进程权重 / 所有进程权重总和) * NICE_0_LOAD/ 当前进程权重

= 调度周期 * NICE_0_LOAD/ 所有进程权重总和
初步结论:当分配给进程的运行时间等于实际运行的时间时,虽然每个进程的权重不同,但是它们的 vruntime增长速度均相同,与权重无关。上文已述用vruntime来选择将要运行的进程,vruntime值较小表明它以前占用cpu的时间较短,受到了“不公平”对待,因此下一个运行进程就是它。如此一来既能公平选择进程,又能保证高优先级进程获得较多的运行时间。

如果分配给进程的运行时间不等于实际运行的时间时:CFS的思想就是让每个调度实体的vruntime增加速度不同,权重越大的增加的越慢,这样高优先级进程就能获得更多的cpu执行时间,而vruntime值较小者也得到执行。

每一个进程或者调度组都对应一个调度的实体,每一个进程都通过调度实体与CFS运行队列建立联系,每次进行CFS调度的时候都会在CFS运行队列红黑树中选择一个进程(vruntime值较小者)。cfs_rq代表CFS运行队列,它可以找到对应的红黑树。进程task_struct ,可以找到对应的调度实体。调度实体sched_entity对应运行队列红黑树上的一个节点。

        CFS使用struct_sched_entity来追踪进程运行的时间记账。而struct_sched_entity作为一个名为se的成员变量,嵌入在进程描述符struct task_struct内。

struct sched_entity{
    struct load_weight load;
    struct rb_node     run_node;
    struct list_head   group_node;
    unsigned int       on_rq;
    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;
....
}

       内核通过update_curr()函数实现记账功能。其由系统定时器周期性调用,无论进程处于可运行态还是被堵塞处于不可运行态。根据这种方式vruntime都可以准确测量给定进程的运行时间,而且可知道谁应该是下一个被运行的进程。

/*
 * Update the current task's runtime statistics.
 */
static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	u64 now = rq_clock_task(rq_of(cfs_rq));
	u64 delta_exec;
 
	if (unlikely(!curr))
		return;
    /* (3.2.1.1)  计算cfs_rq->curr se的实际执行时间 */ 
	delta_exec = now - curr->exec_start;
	if (unlikely((s64)delta_exec <= 0))
		return;
	curr->exec_start = now;
    
	schedstat_set(curr->statistics.exec_max, max(delta_exec, curr->statistics.exec_max));
    // (1) 累计当前进程的实际运行时间
	curr->sum_exec_runtime += delta_exec;
    // 更新cfs_rq的实际执行时间cfs_rq->exec_clock
	schedstat_add(cfs_rq, exec_clock, delta_exec);
    /* (3.2.1.2)  计算cfs_rq->curr se的虚拟执行时间vruntime */
	curr->vruntime += calc_delta_fair(delta_exec, curr);// (2) 累计当前进程的vruntime
	update_min_vruntime(cfs_rq);
    /* (3.2.1.3)  如果se对应的是task,而不是task_group,
        更新task对应的时间统计
    */
	if (entity_is_task(curr)) {
		struct task_struct *curtask = task_of(curr);
    	trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
        // 更新task所在cgroup之cpuacct的某个cpu运行时间ca->cpuusage[cpu]->cpuusage
	    cpuacct_charge(curtask, delta_exec);
        // 统计task所在线程组(thread group)的运行时间:
        // tsk->signal->cputimer.cputime_atomic.sum_exec_runti	me
	    account_group_exec_runtime(curtask, delta_exec);
	}
    /* (3.2.1.4)  计算cfs_rq的运行时间,是否超过cfs_bandwidth的限制:
        cfs_rq->runtime_remaining
     */
	account_cfs_rq_runtime(cfs_rq, delta_exec);
}

进程选择

        CFS使用红黑树来组织可运行进程队列,并利用其迅速找到最小vruntime值的进程。只需运行rbtree树中最左边叶子节点所代表的的那个进程即可。CFS将最左叶子节点放在rb_leftmost字段中。

调度器入口

        进程调度的主要入口是函数schedule(),其是内核其他部分用于调度进程调度器的入口:选择哪个进程可以运行,合适将其投入运行。schedule()通常跟一个具体的调度类相关联,其找到一个最高优先级的调度类,后者需要有自己的可运行队列,然后schedule()想后者询问下一个该运行的进程。

睡眠和唤醒

        休眠(被阻塞)的进程处于一个特殊的不可执行状态。进程休眠有很多原因,但肯定都是为了等待一些事件。无论什么事件,内核的操作都相同:进程吧自己标记成休眠状态,从可执行红黑树中移出,放入等待队列,然后调用chedule()选择和执行一个其他进程。唤醒过程刚好相反:进程被设置为可执行状态,然后从等待队列中移到可执行红黑树中。

        等待队列是由等待某些事件发生的进程组成的简单链表。内核用wake_queue_head_t代表等待队列。其可通过DECLARE_WAITQUEUE()静态创建,也可由init_wairqueue_head()动态创建。 

//创建一个等待队列的项
DEFINE_WATE(wait)
//把自己加入到队列中,队列会在进程等待的条件满足时唤醒它
add_wait_queue(q, &wait);
while(!condition) {
//将进程状态变更为TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE
    prepare_to_wait(&q, &wait, TASK_INTERRUPTIBLE);
    if(signal_pending(current))
        /*处理信号*/
    schedule();
}
finish_wait();

唤醒

        唤醒操作通过函数wake_up()进行,它会唤醒指定等待队列上的所有进程。他调用函数try_to_wake_up(),其辅助将进程设置为TASK_RUNNING状态,调用enqueue_task()将此进程放入红黑树中,如果被唤醒的进程优先级比当前正在执行的进程优先级高,还要设置need_resched标志。通常哪段代码促使等待条件达成,他就要负责随后调用wake_up()函数。例如当磁盘数据到来时,VFS就要负责对等待队列调用wake_up(),以唤醒队列中等待这些数据的进程。

抢占和上下文切换

         上下文切换,即从一个可执行进程切换到另一个可执行进程,由定义于kernel/sched.c中的context_switch()函数负责处理。每当一个新的进程被选出来准备投入运行的时候,schedule()就会调用该函数。他完成两项工作:

        1、调用声明在<asm/mmu_context.h>中的switch_mm(),该函数负责把虚拟内存从上一个进程映射切换到新进程中。

        2、调用声明在<asm/system.h>中的switch_do(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态。这包括保存、恢复栈信息和寄存器信息,还有其他任何与体系结构相关的状态信息,都必须以每个进程为对象进程管理和保存。

       而什么时候调用schedule()? 

        内核提供了一个need_resched标志来表明是否需要重新执行一次调度。当某个进程应该被抢占时,scheduler_tick()就会设置这个标志;当一个优先级高的进程进入可执行状态的时候,try_to_wake_up也会设置这个标志。内核检查该标志,确认其被设置,调用schedule()来切换到一个新的进程。内核提供set_tsk_need_resched()来设置进程的need_resched,提供clear_tsk_need_resched来清除进程中的need_resched,提供need_resched()来检查进程的need_resched标志。在返回用户空间以及从中断返回的时候,内核会检查need_resched标志,如果已被设置,内核会在继续执行前调用调度程序。

用户抢占:内核即将返回用户空间时,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占。用户抢占在一下情况产生:1、从系统调用返回用户空间时。2、从中断处理程序返回用户空间时。

内核抢占:Linux完整地支持内核抢占。只要调度是安全的,内核就可以在任何时间抢占正在执行的任务(只要没有持有锁,内核就可以进行抢占)。thread_info引入preempt_count计数器,从中断返回内核空间时检查need_resched和preempt_count的值。如果need_resched被设置,且preempt_count为0的话,说明有一个更重要的任务需要执行并且可以安全抢占,此时调度程序就会被调用。如果进程持有的所有锁都被释放了,释放锁的代码也会检查need_resched是否被设置,如果是的话,就调用调度程序。

内核抢占发生在:1、中断处理程序正在执行,且返回内核空间之前。2、内核代码再一次具有可抢占性的时候。3、如果内核中的任务显示的调用schedule()。4、如果内核中的任务阻塞。

实时调度策略

Linux提供了两种实时调度策略:SCHED_FIFO和SCHED_RR。而普通的、非实时的调度策略是SCHED_NORMAL。实时策略由一个特殊的实时调度器管理。具体实现定义在文件kernel/sched.h中。

SCHED_FIFO不使用时间片。处于可运行状态的SCHED_FIFO进程比任何SCHED_NORMAL级的进程都先得到调度。一旦一个SCHED_FIFO进程处于可执行状态,就会一直执行,直到自己阻塞或显示的释放处理器。只有更高优先级的SCHED_FIFO或者SCHED_RR任务才能抢占SCHED_FIFO任务。如果有两个或更多同优先级的SCHED_FIFO进程,他们会轮流执行,但依然只有他们愿意让出处理器时才会退出。

SCHED_RR的进程在耗尽实现分配给它的时间后就不再继续执行了。即SCHED_RR是带有时间片的SCHED_FIFO。当耗尽时间片时,同一优先级的其他实时进程被轮流调度。同一优先级的其他实时进程被轮流调度。时间片只用来重新调度同一优先级的进程。对于SCHED_FIFO进程,高优先级总是立即抢占低优先级,但低优先级进程决不能抢占SCHED_RR任务,即使它的时间片耗尽。

Linux的实时调度算法提供了一种软实时工作方式。虽然不能保证硬实时工作方式,但Linux的实时调度算法的性能还是很不错的。

实时优先级范围为[0, MAX_RT_PRIO - 1],MAX_RT_PRIO为100,而SCHED_NORMAL级进程的nice值取值范围是通过[MAX_RT_PRIO, MAX_RT_PRIO + 40]到[-20, 19]进行映射得到。

 Linux通过sched_yield()系统调用,让进程将处理器时间让给其他进程,并将其放到优先级队列的最后面。而实时进程不会过期,只能放在其优先级队列的后面

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值