【INT的内核笔记】调度研究(一)

未完待续…
研究的源码主要是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内核》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值