[Linux]——Linux下的进程调度

进程调度

在多进程的操作系统中,进程调度是一个全局性、关键性的问题,他对系统的总体设计、系统的实现和功能设置以及各个方面的性能都有着决定性的作用。本博客笔者就对进程的调度这一知识点进行总结,不过为了简单起见,我们先从Linux内核2.4中调度介绍起,将原理说清楚后介绍更高级的Linux2.6内核调度算法。

什么是一个好的调度算法?

一个好的调度算法说明白点其实就是能够更合理的分配系统资源,所以调度算法应该具备以下特点:

  • 公平:保证每个进程能够得到合理的CPU执行时间
  • 高效:使CPU总是保持忙碌状态,也就是总有进程在CPU上执行
  • 响应时间:使交互用户的响应时间尽可能短
  • 周转时间:使批处理用户等待的时间尽可能短
  • 吞吐量:使单位时间内处理的进程数尽可能多

主要的几种调度算法

时间片轮转算法

时间片就是分配给进程运行的一段时间,而此算法中就是让进程排成一个队列,处于队列首部的进程能够被CPU执行。执行一个时间片后系统发出信号通知调度程序终止此进程的运行,并将它送到队列的尾部进行等待。时间片的大小为几毫秒到几百毫秒不等。此算法能保证在某一个特定时间内所有的进程都能被执行,这个时间能被人接受。
在这里插入图片描述

优先权调度算法

时间片轮转算法能保证每一个进程都能得到合理的CPU运行时间,但是某种常见下为了让紧迫性进程得到优先处理,引入了优先权调度算法。

  • 非抢占式优先权算法:也称为不可剥夺算法,在这种方式下,系统一旦将CPU分配给运行队列中优先级最高的进程后,那么该进程就会一直执行下去,直到执行完毕。或者因为该进程放弃CPU时,系统将CPU分配给另一个优先权高的进程。这种算法一般用于实时性不严的实时系统中。
  • 抢占式优先权算法:也称为可剥夺算法,该算法的本质就是当前CPU运行的程序永远是那个优先级最高的进程。这种算法下,系统同样把CPU分配给优先级最高的进程。但是只要出现另一个优先权更高的进程,调度程序就会暂停原最高优先权进程的执行,而将CPU分配给新出现的优先级最高的进程。这种方式的优先权调度算法,能够更好的满足紧迫型进程的要求,常用于实时性要求比较严格的系统中,Linux目前也采用这种调度算法。

在这里插入图片描述

多级反馈队列调度

这是一种折中的办法。其本质将时间片轮转算法和抢占式优先权调度算法结合。也就是让优先级高的进程先执行给定的时间片,相同优先权的进程轮流运行给定的时间片。

据笔者了解,当前BSD下使用的就是多级反馈队列的调度算法。

实时调度

实时调度也叫实时系统中的调度。实时系统就是对外部时间有求必应、尽快响应。在实时系统中存在若干个实时进程和任务,他们用来反应或控制某个外部事件。一般使用抢占式的调度方式。

Linux下的调度时机

Linux的调度程序是一个名叫schedule的函数,这个函数我们在之前的进程切换中已经提到过了,这个函数被调用的频率特别高。这个函数用来决定是否进行进程的切换,如果要切换那么应该切换到哪个进程。我们来大体列出Linux下需要调度的时机

  • 进程状态转换的时刻:进程终止、进程睡眠
  • 当前进程的时间片用完时会主动放弃CPU,会主动调用调度程序
  • 设备驱动程序运行时,在反复的循环中,驱动程序检查调度标识,如果有必要则会主动放弃CPU调用调度程序
  • 从内核态返回到用户态的时候为了提高效率会进行调度,他需要在转化前将内核该处理的事全部做完。

进程调度的依据

调度程序运行时,要选择最值得投入运行的程序。那么选择这些最值得投入程序时的依据是什么呢?在test_strut结构体中有这么几个和进程调度相关的域。

域名描述
need_resched决定是否调用schedule函数
counter剩余的时钟节拍数,每次时钟中断来到时,这个值就会减一
nice进程的基本优先级,她觉得counter的初值,这个值范围为-20~19,这个值越底进程优先级越高
policySCHED_FIFO:先入先出的实时进程 SCHED_RR:时间片轮转实时进程 SCHED_OTHER:普通分时进程
rt_priority实时进程的优先级

值得强调的是counter时钟节拍其实就时间片。每一个节拍如果为10ms,那么如果节拍为2的话就代表这个进程的时间片长度为20ms

Linux中使用一个名叫goodness的函数来计算当前进程的优先级别:

static inline int goodness(struct task_struct * p, int this_cpu,\
 												struct mm_struct *this_mm)
{
	int weight;
	if (p->policy != SCHED_OTHER) {//说明不是普通分时进程
		weight = 1000 + p->rt_priority;
		goto out;
	}
	//将counter的值赋给weight,这就给了进程一个大概的权值,
	//counter中的值表示进程在一个时间片内,剩下要运行的时间.
	weight = p->counter;
	
	if (!weight) //weight==0,表示该进程的时间片已经用完,则直接转到标号out
		goto out;
		
	#ifdef __SMP__
	/*在SMP情况下,如果进程将要运行的CPU与进程上次运行的CPU是一样的,
	则最有利,因此,假如进程上次运行的CPU与当前CPU一致的话,
	权值加上PROC_CHANGE_PENALTY,这个宏定义为20。*/
	if (p->processor == this_cpu)
		weight += PROC_CHANGE_PENALTY;
	#endif
	
	if (p->mm == this_mm)
	 //进程p与当前运行进程,是同一个进程的不同线程,
	 //或者是共享地址空间的不同进程,优先选择,权值加1
		weight += 1;
		weight += p->priority; //权值加上进程的优先级
	out:
		return weight; //返回值作为进程调度的唯一依据,谁的权值大,就调度谁运行
}

Linux 2.6 调度程序的改进

在这之前我们谈到进程调度的算法还是基于内核2.4版本的,但是随着处理器的增多,我们之前的算法会存在这样的问题。

  • 单就绪队列问题:不管时间片是否用完,都会放在一个就绪队列中,这就使得时间片耗完的情况下,还参与调度。并且调度算法与进程的数量密切相关,队列越长,选中一个进程的时间就越长。
  • 多处理器问题:多个处理器上的进程都放在一个就绪队列中,这就使得这个队列成为了临界资源,各个处理器因为等待进入就绪队列而降低了系统效率。
  • 内核态不可抢占问题:只要有一个进程进入了内核态,即使有一个非常紧迫的任务到来,他也只能等着。只有那个进程从内核返回到用户态时,紧迫任务才能占有处理器。

O(1)算法的runqueue

针对上面的问题,2.6调度算法给出了这样的一个结构:

在这里插入图片描述
针对多处理器的问题,给每个CPU都设置一个就绪队列。针对单就绪队列的问题,设置俩个队列组:active队列组和时间片到期expried队列组。每个队列组中以优先级再进行分类,相同优先级的进程为一个队列,最多有140个优先级,也就对应140个队列

如上图,没有毫完时间片的进程位于活动队列数组中,耗完时间片的进程存放到expried队列组中,从而节省空转的时间。当一轮调度结束后,活动队列为空,交换俩个队列组的指针进行下一轮的调度。上图中队列的数据结构定义如下:

struct runqueue
{
	...
	prio_array *active, *expired, array[2];
	...
}

prio_array定义为:

struct prio_array
{
	unsigned int nr_active;//进程总个数
	struct list_head queue[MAX_PRIO];//进程链表头指针数组
	unsigned long bitmap[BITMAP_SIZE];//进程就绪位图
}

观察这个数据结构时我们发现了一个位图,还记的我们算法的名字??O(1)算法的runqueue。从结构上来看,一个CPU上的就绪队列最多可达280个。如何快速的选出要运行的进程成了系统性能的一个关键性因素。而这个位图设置为1时表示进程以调度就绪,从最高优先级开始,位图第一个被置为1的队列将被取出调度。

而为了提高进程的响应时间,O(1)算法同样使用时间片轮转算法的动态调整思想,每次时钟中断进程的时间片就减一,当counter减为0如果是交互式或者实时进程就重置时间片并再次加入active组中,如果是普通进程就加入到expired组中等待下一次的调度。不过如果实时进程一直被调度就会发生其他进程调度的饥饿现象,所以即便是实时进程在调度一定的CPU时间后也会被插入到expired组中。

楼梯算法

为了解决动态调整的问题,大量的复杂代码被加入到2.6的调度模块中,这就导致代码的可维护度极大的降低。而在2004年,大佬们又提出了一个新的方法——楼梯算法。

楼梯算法并没有完全抛弃O(1)算法的框架,而是保留了active数组,丢弃了expired数组。现在一个进程用完时间片后并不是被移到expired组中,而是被加入到低一优先级队列中。这里注意只是将任务插入低一优先级的队列中,任务本身的优先级并没有发生变化。当时间片再次被用完时,就加入更低一级的队列中。

当任务到达最低一级楼梯时,如果时间片再次用完,他会回到初始化优先级下一任务队列中。如果优先级为3,那么下次就加入到优先级为4的队列。不过将该任务的时间片变为原来的两倍。 如果再一次的到达最低一级队列时,下一次就再放到优先级为5的队列中,不过这次的时间片与最初的时间片相同。

以上对于楼梯算法的描述是普通进程的调度算法,实时进程还是采用原来的调度策略,即FIFO或者轮询算法。可以看出楼梯算法使用不一样的思路大大优化了代码。并且事实证明楼梯算法比O(1)算法更好。

总结

博客中并未给出调度函数schedule,函数本身不结合上下文难以理解,所以有兴趣的同学可以自己看看源代码。本博客重点理解最重要的O(1)算法和楼梯算法,如果有什么不懂小伙伴们可以尽管指出。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值