一、进程的特点
1、有一段程序供其执行
2、有进程专用的系统空间堆栈
3、有一个 task_struct 数据结构记录进程的信息
4、独立的存储空间,意味着除了专用的系统空间堆栈外还要有用户空间堆栈
如果以上条件,只是缺少用户空间堆栈,完全没有被称为“内核线程”,共享用户空间被成为“用户线程”。
Linux 系统中,进程 process 和 任务 task 是同一个意思,因此描述进程的结构体叫 task_struct 。
每一个进程都有一个 task_struct 结构 和 一片用作系统堆栈的存储空间,内核在为每一个进程分配一个 task_struct 时,实际上分配两个连续的物理页面 8192 字节。(使用 alloc_task_struct 分配)
系统堆栈不像用户空间那样可以在运行时扩展,而是静态确定的,因此在中断服务程序、内核软中断服务程序、其它设备驱动程序中,不应让函数嵌套太深、不应有较大较多的局部变量。
宏:current 返回当前进程的 task_struct 结构体指针。
二、task_struct 中关于进程调度的成员
1、task_struct.state
TASK_RUNNING,并不是表示一个进程正在执行(占有CPU),而是表示这个程序处于就绪太,位于“运行队列”,可以被调度器调度执行。
TASK_UNINTERRUPTIBLE 和 TASK_INTERRUPTIBLE ,表示进程处于休眠或将要进入休眠状态,不同的是 TASK_UNINTERRUPTIBLE 的休眠可以被信号所打断唤醒。
TASK_ZOMBIE,表示进程 exit 而未 注销 task_struct
TASK_STOPPED,主要用于调试
2、task_struct.counter
与调度有关
3、task_struct.need_resched
与调度有关
4、task_struct.priority 和 task_struct.rt_priority
优先级别和“实时”优先级别
5、task_struct.pid
进程号
6、task_struct.policy
适用于本进程的调度策略
三、关于进程调度的三个问题
1、调度的时机:在什么情况下调度,什么时候调度
2、调度的策略:根据什么准则挑选下一个运行的进程
3、调度的方式:“抢占” 还是 “不可抢占”
首先来回答第一个,调度时机:
自愿调度:随时都可以进行,在内核里,一个进程可以通过 schedule() 启动一次调度。当然也可以在 schedule() 之前,将当前进程的状态设置成 TASK_UNINTERRUPTIBLE 、TASK_INTERRUPTIBLE ,暂时放弃运行而睡眠,也可以给这种自愿放弃运行加上一个时间限制,在内核中用 schedule_timeout(),时间到达自动唤醒。
非自愿调度:非自愿的调度,即强制地发生在每次由系统调用返回的前夕,以及每次从中断或异常处理函数返回的前夕。这意味着,只有在用户空间(CPU在用户空间运行)时发生的中断或异常才会引起调度。
注意:在用户空间(CPU在用户空间运行)时发生的中断或异常才会引起调度只是必要条件,还需要满足 task_struct.need_resched 非 0 ,后面再说。
第二个问题,调度策略:
内核有三种不同的调度策略,每一个进程都可以选择一个作为自己的调度策略。
SCHED_FIFO ,适用于“实时”进程,且每次运行时间较短的进程。
SCHED_RR ,适用于每次运行时间较长的“实时”进程
SCHED_OTHER , 传统的调度策略,适用于“普通”进程
SCHED_FIFO:
进程一旦收到调度开始运行之后,就要自愿让出,或者被高“资格”的进程抢占而停止,对于每次运行时间较短的进程使用 SCHED_FIFO 是恰当的。
如果有一个进程使用 SCHED_FIFO 运行起来没完没了会怎么样呢,高“资格”的当然可以抢占它,低“资格”的需要等待,但是对于同等“资格”运行时间较短的进程是不公平的。这就好比,有一台公共电脑,大家每人用个3、5分钟百度个问题是合适的,但是有个人非要拿它来看电影,那么对于那些只用3、5分钟的人来说就是不公平的。
SCHED_RR:
这种调度算法就是针对上面所说运行时间较长的进程。SCHED_RR 在 SCHED_FIFO 的基础上,通过 task_struct.counter “时间配额”,来实现一种同“资格”的轮换调度。什么意思呢,对于 SCHED_RR 的进程,通过task_struct.nice 计算出一个初始的“时间配额”给 task_struct.counter ,task_struct.counter 会在每次的时钟中断的处理函数里减少,减少到 0 的时候,task_struct.need_resched 被置 1 ,且该进程就会被移动到“运行队列”的尾部,当返回用户空间的时候自然就要进行调度了。调度器的规则是按照进程在“运行队列”的顺序,比较进程的“资格”,相同资格的,排在前面的先调度。那么刚刚那个被移动到“运行队列”尾部的进程就要等到前面所有同“资格”的进程运行一遍从而被放在它后边它才有机会再次运行。当然,这里只是“时间配额”减少,进程在“运行队列”里位置的移动,对于“资格”来说是不变的,因此低“资格”的进程还是没有机会运行,除非“运行队列”里高“资格”的进程都去睡眠了~
还是打个比方来理解一下,这里有1台电脑,大家都可以用,老师具有高“资格”可以抢电脑,同学们的“资格”都一样。同学们有的用电脑看电影,一个电影2H,有的听歌10min,有的百度3mini,大家完全运行的时间不一样可能偏差很大。那么电脑管理员说了,看电影的同学,电影虽然2个小时,但是你每次只能看5分钟,看完到队伍后面去排队,轮到你再继续看。听歌的也是,每次听5分钟。百度的,每次3分钟,百度完去后面排队。这样,给各个“同资格”同学们分配一个差不多的“时间配额”,保证了公平。
SCHED_OTHER:
这种调度算法适用于普通进程,它们的“资格”很低很低,低到只要有“实时”进程在,它们就永远没有运行的可能。为什么这么说?
对于实时进程,它们的“资格”=1000+task_struct.pt_priority , 0 <= pt_priority <= 99,因此实时进程的“资格”至少也是1000,而且,它们的“资格”不会随时间增长而降低。
对于普通进程,它们的“资格”跟 task_struct.nice 和 task_struct.counter 有关系,我们前面说了 task_struct.counter 决定了“时间配额”会随着时钟滴答而减少,在 task_struct.counter 未减少到 0时,“资格”= counter += 20 - nice 。nice (-20 ~ 19),因此一个普通进程的“资格”会随时间滴答而降低。还有,在 task_struct.counter 减少到 0时,它的“资格”直接为 0 。这里先透露一下,当普通进程的“资格”降低为0,它的 task_struct.need_resched 被置 1 ,因此会发生抢占。
第三个问题,调度的方式
前边,我们再说调度时机的时候,提到了自愿调度和非自愿调度,非自愿调度那必然是抢占了。只不过这个抢占是有条件的。在用户空间(CPU在用户空间运行)时发生的中断或异常才会引起调度只是必要条件,还需要满足 task_struct.need_resched 非 0 。
自愿调度:
随时都可以进行,在内核里,一个进程可以通过 schedule() 启动一次调度。当然也可以在 schedule() 之前,将当前进程的状态设置成 TASK_UNINTERRUPTIBLE 、TASK_INTERRUPTIBLE ,暂时放弃运行而睡眠,也可以给这种自愿放弃运行加上一个时间限制,在内核中用 schedule_timeout(),时间到达自动唤醒。
如果当前进程的状态被设置为:TASK_INTERRUPTERABLE , 在 schdule() 时,schdule() 函数会判断当前进程是否有信号要处理,如果有,把状态改回 TASK_RUNNING ,继续去处理信号。如果没有信号要处理,那么移除“运行队列”,休眠,调度器挑选下一个“资格”最高的运行。
如果当前进程的状态被设置为:TASK_UNINTERRUPTERABLE ,直接移除“运行队列”,休眠,调度器挑选下一个“资格”最高的运行。
如果当前进程的状态被设置为:TASK_RUNNING ,有意思,想让出 CPU ,但是你还想在“运行队列”里有机会被调度。这让跟不让一个样啊...为啥这么说呢
当前进程状态 TASK_RUNNING ,然后又 schdule(),如果没有更高“资格”的进程等待执行,那么调度器会满足这个进程继续执行的意愿,让它继续执行。(不管在运行队列的位置是否较同“资格”进程靠前)。如果有资格较高的进程出现,那么在返回用户空间的时候会发生抢占,你不让也是不行的。
也就是说,当前进程状态 TASK_RUNNING ,然后又 schdule() 唯一有意义的一点是,使突然唤醒的高“资格”进程立刻执行,无需等到返回用户空间时抢占。
强制性调度(抢占):
当前进程为实时进程:
实时进程的“资格”不会因为时钟滴答而降低,因此也就不会出现想普通进程那样随时钟滴答降低,降低为 0 时,need_resched 被置 1 的情况。
但是,如果有一个进程突然唤醒,那么会比较它俩的“资格”,如果新唤醒的进程“资格”更高,当前进程的 need_resched 被置 1 ,返回用户空间时发生抢占。
当前进程为普通进程:
前面提到了,普通进程的“资格”会随时钟滴答而降低,而且当它的时间配额为 0 ,“资格”会被设成 0,那么 need_resched 会被置 1 ,返回用户空间时发生抢占。
如果有一个进程唤醒,和实时进程是一样的。
抢占就是当前进程状态为TASK_RUNNING 然后被迫 schdule(),只不过要么当前进程“资格”降低了,要么有高“资格”出现了,不会满足这个进程继续执行的意愿。
本文大多数内容来自:《Linux内核源代码情景分析》