【INT的内核笔记】梳理sleep

    技术2022-07-12  73

    1. sleep_avg

    1.1 sleep_avg简介

    sleep_avg处在task_struct数据结构中,

    sleep其实和平均没有什么关系,是一个睡眠时间评估值,命名可能有历史原因。

    直接关系到进程动态优先级prio的计算:

    动态优先级prio = 静态优先级static_prio - CURRENT_BONUS(p) + 5 CURRENT_BONUS(p)和p->sleep_avg是正相关的,prio越小越好,因此: sleep_avg越大,调用effective_prio(p)重新计算动态优先级时,动态优先级提高得越多。

    1.2 sleep_avg相关数学关系

    涉及到的一些数学关系(都是经验公式,不必太深究):

    平均睡眠时间sleep_avg ↑,CURRENT_BONUS§ ↑,prio的增量 ↑;

    平均睡眠时间sleep_avg ↓,CURRENT_BONUS§ ↓,

    sleep_avg增长量相关值MAX_BONUS - CURRENT_BONUS§ ↑;

    动态优先级prio = 静态优先级static_prio - CURRENT_BONUS§ + MAX_BONUS / 2,

    MAX_BONUS / 2 = 5,

    动态优先级prio = 静态优先级static_prio - CURRENT_BONUS§ + 5;

    CURRENT_BONUS§就是ULK中所说的bonus,与sleep_avg正相关;

    1.3 sleep_avg的更新

    sleep_avg一般在recalc_task_prio()中被更新,也会在schedule中被直接更新。

    sleep_avg的更新如下:

    被唤醒时,经由以下调用链在recalc_task_prio()中更新:

    ... --> 上层wake_up相关封装 --> try_to_wake_up() --> activate_task() --> recalc_task_prio()

    主要是在sleep_avg中加上睡眠时间sleep_time

    (会乘以一些倍率,也有不同的上限,具体见recalc_task_prio());

    从TASK_INTERRUPTIBLE被唤醒后,在正式被调度到时,经由以下调用链在recalc_task_prio()中更新:

    ... --> schedule() --> recalc_task_prio()

    主要是在sleep_avg中加上被唤醒之后,到被真正被调度到这一段时间delta

    (会根据是被中断等等唤醒,还是被系统调用等等唤醒(最典型就是信号唤醒),乘以一些倍率);

    当进程主动放弃CPU或被剥夺CPU,也就是在schedule()作为prev时,经由以下调用链在schedule()中更新:

    ... --> schedule()

    主要是减去运行时间run_time

    (会根据由sleep_avg计算出的CURRENT_BONUS,乘以一些倍率, sleep_avg越大,倍率越小,减掉的run_time越小)

    在sched_exit()时候,更新其父进程的平均睡眠时间p->parent->sleep_avg,还未研究清楚;

    2. prio

    2.1 prio简介

    prio一般称作动态优先级或者实际优先级,

    实际优先级其实更贴切,因为在实时进程中prio是不会随着进程运行时间而改变的,

    不过之前的笔记都称之为动态优先级,因此还是继续沿用算了。

    动态优先级prio正是用于在发生调度时挑选进程的。

    如上一节所述,普通进程的prio计算公式为:

    动态优先级prio = 静态优先级static_prio - CURRENT_BONUS(p) + 5;

    2.2 prio的更新

    伴随着sleep_avg,一起在recalc_task_prio()中更新,可参考上一节;

    在scheduler_tick()中,普通进程在消耗完时间片时直接调用effective_prio()进行更新。

    我之前很疑惑,为什么在scheduler_tick()要进行更新prio的操作?

    现在大致有了一点思路,分析schedule()函数可以得知,schedule()在

    prev->sleep_avg -= run_time;

    这一步之后,并没有任何关于更新prio的操作了,而schedule()关于prio更新的操作,也只限于next是被唤醒的。

    所以对于在TASK_RUNNING状态被切换的prev进程,重新切换回来之后,要怎么更新prio呢?

    我觉得scheduler_tick()中的这个prio更新步骤,就是为了解决这个问题的;

    在wake_up_new_task()中,直接调用effective_prio()进行更新。

    这个看名字就知道,是进程被创建之后计算prio了;

    在set_user_nice(),直接执行p->prio += delta进行更新。

    用户直接更新static_prio的一个系统调用,static_prio更新了,prio自然要一起更新;

    3. activated

    3.1 activated简介

    activated表达的是从什么状态被唤醒,所以这是在什么时候设置的呢?

    那当然是在被唤醒,也就是try_to_wake_up()函数中被设置的。

    我之前混淆了这两个概念:

    从什么状态进入睡眠;从什么状态被唤醒;

    activated有以下几个值:

    0,进程处于TASK_RUNNING状态;

    1,进程处于TASK_INTERRUPTIBLE或者TASK_STOPPED状态,

    而且正在被系统调用服务例程或内核线程唤醒。

    常见例子就是被信号唤醒;

    2,进程处于TASK_INTERRUPTIBLE或者TASK_STOPPED状态,

    而且正在被ISR或者可延迟函数唤醒。

    常见例子就是IO中断;

    -1,表示从UNINTERRUPTIBLE状态被唤醒。

    据实验和源码,普通文件read()进入睡眠后,就是UNINTERRUPTIBLE状态;

    3.2 activated的更新

    在try_to_wake_up()中设置,

    如果当前进程是被从TASK_UNINTERRUPTIBLE状态唤醒,则直接设置activated = -1;

    否在会在调用activate_task()后,在activated_task()中设置,具体如下:

    recalc_task_prio(p, now); // 为何在recalc_task_prio()才设置呢? // 因为在recalc_task_prio()依靠0和-1来区分 // TASK_UNINTERRUPTIBLE和TASK_INTERRUPTIBLE if (!p->activated) { if (in_interrupt()) // 如被中断处理等等唤醒 p->activated = 2; else { // 如被系统调用等等唤醒 p->activated = 1; } } p->timestamp = now;

    在schedule()中设置,

    当选定next进程后,也就是即将占用CPU的进程,会将其activated值置为0。

    /** * 正式被选择了,所以activated清零。 */ next->activated = 0;

    4. timestamp, last_ran

    4.1 timestamp, last_ran简介

    timestamp,时间戳,主要用于now - timestamp求出睡眠时长、运行时长等;

    last_ran,最近被在schedule()作为prev进程被切换出去的时间,如何起作用还未详细研究,

    关于last_ran的更新不单独讲了,暂时只发现下面代码段中和timestamp一起被设置;

    4.2 timestamp的更新

    在try_to_wake_up() --> activate_task()被设置,在recalc_task_prio()之后设置。

    这里更新timestamp作用之一是,之后在schedule()中计算唤醒后到被调度到的等待时间;

    在schedule()中,prev在被切换出去时会设置timestamp,如下:

    switch_tasks: ... /** * 更新prev的时间戳,暂时发现了以下两点作用,可能不全: * 1.schedule()其中一个步骤是把TASK_INTERRUPTIBLE的进程移出可执行队列, * 但是,我们会发现在那个代码段中timestamp没有设置,而陷入睡眠的时间戳是需要设置的, * 答案就是,在这步中统一设置。 * 内核的一点小优化,内核中不少代码在不同情境有种不同语义。 * * 2.注意下面prev != next这个判断,当不满足条件也就是next == prev时, * 这时候的代码段中是没有对于next->timestamp的设置的。 * 原因也是在于prev->timestamp这步,在这种情况下相当于设置next->timestamp。 * 一句多义,感觉是挺巧妙的。 */ prev->timestamp = prev->last_ran = now; sched_info_switch(prev, next); if (likely(prev != next)) {/* prev和next不同,需要切换 */ next->timestamp = now; ... } else /* 如果prev和next是同一个进程,就不做进程切换。 当prev仍然是当前活动集合中的最高优先级进程时,这是有可能发生的。 */ spin_unlock_irq(&rq->lock)

    在schedule()中,next在被切换进来时会设置timestamp,如上;

    在新创建进程时的相关函数wake_up_new_task()和sched_fork()中被设置,还未详细研究;

    在多CPU间负载均衡的相关函数__migrate_task()和pull_task()中被设置,还未详细研究;

    参考

    baoyou.xie Linux 2.6.12代码注释 《深入理解linux内核》

    Processed: 0.018, SQL: 10