未完待续…
研究的源码主要是2.6版本。
1. 调度策略概述
1.1 调度
调度时机和调度是不同的,
什么时候阻塞进程,什么时候唤醒进行,什么时候执行调度,这属于调度时机;
阻塞和唤醒进程时候相应调度变量如何改变,应该从可执行队列中挑选哪个进程运行,这属于调度;
调度大致提供这些主要功能:
- 从可执行队列中,选出合适进程执行;
- 阻塞和唤醒进程;
- 多CPU间等方面的负载平衡;
1.2 进程类型
-
类型
-
批处理进程
一般为后台进程,不需要和用户交互,处理上受慢待。
例如编译程序,数据库搜索引擎,科学计算等。
-
交互式进程
需要和用户交互的程序,反应太慢会影响用户体验。
例如shell,文本编辑器,图形桌面等。
-
实时进程
有很强的调度需要的程序,甚至在某种调度策略下允许一直占据CPU。
例如音频视频程序,机器人控制程序。
-
-
批处理进程和交互式进程的区分
批处理进程和交互式进程,实则都属于普通进程,内核无法直接区分这两者。
内核是通过统计平均睡眠时间等数据来对它们进行区分的。
-
实时进程和普通进程的区分
实时进程和普通进程,内核是可以直接区分的。
原因在于优先级,实时进程的优先级要比所有普通进程都高。
1.3 进程状态
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4
#define EXIT_ZOMBIE 8
#define EXIT_DEAD 16
-
TASK_RUNNING,可执行状态,属于这个状态的有:
可执行但未执行的进程,可执行但已经耗尽时间片的进程,正在执行的进程;
这个状态的进程被放在可执行队列中,是调度算法要处理的主要对象。
在ps输出中被标识为R;
-
TASK_INTERRUPTIBLE,可中断的睡眠状态。
表示进程在等待时间片段或者某个特定的事件,一旦事件发生,进程会从可中断之睡眠状态中退出。
这个状态可以接受信号,也就是可以用kill -9杀死。
在ps输出中被标识为S;
-
TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。
不可中断之睡眠状态的进程不会处理任何信号,而仅在其等待的资源可用或超时时退出(前提是设置了超时时间)。
不可中断之睡眠状态通常和设备驱动等待磁盘或网络 I/O 有关。
关键就在于不会处理信号,所以kill -9也杀不死!反正除了满足它,和重启外,没办法了。
在ps输出中被标识为D;
-
EXIT_ZOMBIE ,僵尸状态。
进程调用exit()后,会清理相应的数据结构,但是进程表占着的槽位,需要父进程来释放。
因此正常子进程结束后,是会发SIGCHLD信号,让父进程收尸的。
在儿子死了,到父亲来收尸这段时间内,就会进入ZOMBIE状态。
更惨的是,可能父亲比儿子挂得早,这种情况的处理我还未了解。
在ps输出中被标识为Z;
1.4 优先级
-
静态优先级
本质上了决定了程序获得的时间片大小,实时和普通进程都一样。
在普通进程中,动态优先级是在静态优先级基础上计算的;
在实时进程中,静态优先级只影响时间片大小;
-
动态优先级
调度时实际使用的进程优先级,无论普通还是实时进程。
普通进程的动态优先级,与静态优先级、进程过去的平均睡眠时间有关,会动态变化;
(并不是简单的绝对睡眠时间求平均,可中断睡眠和不可中断睡眠在计算上地位不同)
实时进程的动态优先级,与实时优先级有关,除非用系统调用设置,否则不变;
普通进程和实时进程在动态优先级上取值范围不同,也正是以此区分两种进程;
(大于MAX_USER_RT_PRIO为普通进程)
-
实时优先级
决定实时进程的动态优先级;
1.5 时间片
众所周知,进程运行一般是受时间片限制的(FIFO策略下的实时进程除外),运行一段时间后还没运行就会切换。
但这两点需要注意:
- 进程被分配的时间片是不同的,和静态优先级有关;
- 进程只是耗光了时间片的话,进程仍然是可执行状态,但会放到expired可执行队列中去;
如上优先级介绍中所说,时间片由静态优先级所决定。
1.6 active和expired可执行队列
这个设计在于解决优先级低进程的饥饿问题。
目标是一个高优先级的进程用完了时间片,应该让较低优先级的进程执行。
两个队列:
-
active队列,尚未消耗完时间片的可执行进程,可以执行;
-
expired队列,已经消耗完时间片的课执行进程,知道active队列全部清空才可执行,
此时只需要将两个队列的指针交换就行;
如果进程间没有区别,那么情况就会非常简单:
- 不断从active队列选出进程执行,执行完就放到expired队列;
- acitve队列空了之后,交换两个队列的指针;
实际还要考虑交互式进程,expired中等待最久的进程,实时进程等等之间的复杂关系。
1.7 进程抢占
当某个进程通过中断处理进入TASK_RUNNING状态时,内核会对比这个进程与current的动态优先级,
如果动态优先级比current要高,则会设置thread_info中flags的TIF_NEED_RESCHED位。
经过这番操作后,中断处理结束后,将会current的执行而重新进行调度。
1.8 交互式进程的区分
和动态优先级一样,和平均睡眠时间相关,在重新计算动态优先级顺带一起计算。
由interactive_credit记录,初始为0视情况++或者–,
一旦大于CREDIT_LIMIT,将被认定为交互式进程,并且数值不再改变。
1.9 实时进程的调度
实时进程的特点:
- 实时进程的动态优先级,不会随着运行自动改变;
- 实时进程,会一直处于active队列中;
- SCHED_RR和SCHED_FIFO这些作用于周期调度中(时钟中断);
每个实时进程都有一与其相关的实时优先级,取值范围0-MAX_RT_PRIO-1。
其大小可以通过sched_setscheduler()和sched_setparam()来改变。
实时进程被一个进程替换,可能的情况有:
-
进程被拥有更高优先级的进程抢占。
-
进程发生阻塞进入睡眠状态。
-
进程被终止(状态为TASK_STOPPED OR TASK_TRACED)或者被杀死(EXIT_DEAD OR EXIT_ZOMBIE)。
-
进程通过调用sched_yield()自愿放弃处理器。
-
进程是轮回实时(SCHED_RR)且其时间片执行完毕。
(但是并不会移出active队列)
当在SCHED_RR时调用nice()和set_priority()函数并不影响实时优先级,只会影响静态优先级(从而影响基时间片)。
2. 相关数据结构
3. 杂项
虽然感觉内容实在太多,暂时还没想研究详细调度过程的代码,但是有些东西不结合代码,还是弄不清。
3.1 实时进程和普通进程在调度上如何统一
-
ULK用到的linux 2.6
这版本的内核代码中,不同SCHED_XX策略下如何选取next并没有进行区分,
普通进程和实时进程都在混杂地存在于同一个可执行队列中,
只不过实时进程会一直呆在active队列中,不会被扔到expired而已。
那么SCHED_XX在何时用到呢?在scheduler_tick()中会用到。
scheduler_tick()是周期性调度(时钟中断),在进程fork的时候也会调用。
/**
* 调度函数。
*/
asmlinkage void __sched schedule(void)
{
...
/**
* 现在开始在活动集合中搜索一个可运行的进程。
* 首先搜索第一个非0位,并找到对应的链表。
*/
idx = sched_find_first_bit(array->bitmap);
queue = array->queue + idx;
/**
* 将下一个可运行进程描述符放到next中
*/
next = list_entry(queue->next, task_t, run_list);
...
}
/*
* This function gets called by the timer code, with HZ frequency.
* We call it with interrupts disabled.
*
* It also gets called by the fork code, when changing the parent's
* timeslices.
*/
/**
* 维持当前最新的time_slice计数器
* 每次时钟节拍到来时,scheduler_tick函数将被调用,以执行与调度相关的操作。
*/
void scheduler_tick(void)
{
...
}
-
在更新的内核版本中,会用采用pick_next_task()函数选取next。
这时候各种调度策略已经封装成类了,成为调度类,每个调度类管理属于自己的可执行队列等等。
这时是这样在不同策略中,选取出next的:
// 其实就很简单,就是按优先级遍历所有调度类, // p就是next,在某个优先级中选取p成功就返回。 // 策略优先级大致就是实时 > 普通 > 空闲 class = sched_class_highest; for ( ; ; ) { p = class->pick_next_task(rq); if (p) return p; class = class->next; }
参考
Liam0205 程序员的自我修养(⑨):Linux 系统里的进程状态
杨沙洲 Linux 2.6 调度系统分析
visayafan 【内核】非实时进程、实时进程、静态优先级、动态优先级、实时优先级]
taocr pick_next_task与红黑树浅析
《深入理解Linux内核》