前言:
今天你们对“linux线程调度算法”可能比较关心,咱们都想要剖析一些“linux线程调度算法”的相关知识。那么小编也在网上搜集了一些对于“linux线程调度算法””的相关内容,希望各位老铁们能喜欢,我们快快来了解一下吧!Linux线程调度是一种策略,在计算机资源相对固定的情况下通过某种规则动态的调度线程的运行,使其最大程度的满足程序运行的要求。内核定义了五种调度策略,通过五个调度器(结构体实例)加以实现,优先级从高到低分别是:stop_sched_class、dl_sched_class、rt_sched_class、fair_sched_class、idle_sched_class。本文讲解优先级最高的调度器stop_sched_class。
在讲调度器之前,让我们先粗略地了解一下Linux调度系统的设计(如上图所示)。众所周知,线程(task)想运行得先调度到CPU上才行,要调度必须符合两个条件,第一,task须获得运行所需的全部资源,如在等待锁,或者内存页面时,则不能调度。第二,task具备条件后须加入到目标CPU的rq中。rq是run queue的缩写,内核给每个CPU配备了一个rq,用于管理所有可以调度运行的task,一个rq并不是一个具体的队列,只是一个总括性的对象,具体的队列操作在其子元素中。说简单点,调度就是关于task围绕rq的入队列,出队列等一系列操作,CPU在调度时候从rq中抓一个优先级最高的task运行,至于怎么抓,那就是调度器的事了。
stop_sched_class 调度器的定义
stop_sched_class是sched_class结构体的一个实例,实现了sched_class若干个接口函数,包括enqueue_task,dequeue_task,pick_next_task等。有位大佬曾经说过,给代码命名是一件很困难的事情,在这里不得不吐槽一下stop调度器的命名相当蹩脚,完全做不到顾名思义水平。定义如下图所示:
别看stop_sched_class优先级最高,但实现却相当简单,因为整个Linux内核,只有migration
线程会用到此调度器,调度器内部并不管理task队列,所以本文的重点将放在task的唤醒及出入队列的流程上。
设置stop调度器
我们看一下task如何使用此调度器。
sched_set_stop_task函数设置task的调度器为stop_sched_class,并把rq中的stop变量设为task,如果之前rq中的stop变量已经赋值了,那么就把old_stop的调度器设置为rt_sched_class,也就是说,一个CPU只能有一个优先级最高的task,那就是stop。搜索整个内核源码,发现只有cpu_stop_threads会调用sched_set_stop_task函数,相当于stop_sched_class就是为它而建的。cpu_stop_threads为每个CPU创建一个线程,用于处理CPU的热插拔及 task的迁移工作(一旦CPU插入或拔掉,都需要把task迁移进去或迁移出来),详细的内容我们另文再聊。
现在,假设一个task所有的运行条件都满足了,它从睡眠转入运行需要经历那些步骤呢?答案如下:
唤醒task
一个task如果运行条件一直保持不变,哪怕时间片已经用完,或者被其他更高优先级的task切换出去,它都不会被移出rq,只有当它需要的某种资源不足时,比如内存需要调页,或者无法进入锁(自旋锁除外)时,才会把状态设为TASK_INTERRUPTIBLE或 TASK_UNINTERRUPTIBLE,然后主动执行调度例程(schedule)进入睡眠。反过来,当资源已准备好,睡眠的task就会被唤醒,获得CPU时间而调度运行。唤醒的操作在调度系统看来就是外部程序调用wake_up_process()函数的操作,wake_up_process进而又调用try_to_wake_up唤醒task。
在这里提一下为什么函数要用try_to_xxx命名呢?因为task可能在各种状态中,并不一定会唤醒成功,只能try一下,故此而名。来看看代码:
在内核中,task的调度是频繁发生的,调用try_to_wake_up的时候指定state参数为TASK_NORMAL唤醒状态为TASK_INTERRUPTIBLE 和TASK_UNINTERRUPTIBLE的task,如果task不是处在这两种状态中,则在1977行的地方跳出转到out处,不再继续唤醒。
如果一个task已经在rq中(p->on_rq=1)并且正在迁往别的CPU,这样的情况也要也要排除在外,代码2007行跳出转到stat处,只做task的统计操作。
如果一个task在唤醒时已经在调度了,但因某些原因还没完成,这样的情况会体现在p->on_cpu中,所以在唤醒前先判断p->on_cpu是否设置,如果设置了则在2033和2034行处循环,直到上一次调度完成为止。
一个task,从上一次调度到这次到唤醒,不知道要经历多少的CPU时间,有可能转瞬即来,也有可能经过了漫长的睡眠。唤醒的时候需要先执行select_task_rq选择适合运行的CPU,特别是在支持CPU热插拔的系统中更是如此。在stop_sched_class调度器中只简单的返回task当前的CPU,因为stop线程与CPU是强绑定的,不会发生迁移操作,但别的task或调度器就不一定了。如果task需要迁移,先设置WF_MIGRATED标志,接着执行set_task_cpu,往别的CPU上迁移。
ttwu_queue
ttwu_queue接着调用ttwu_do_activate,这两个函数名的前缀都是ttwu是try_to_wak_up的缩写。
ttwu_do_activate指定ENQUEUE_WAKEUP | ENQUEUE_WAKING参数调用ttwu_activate函数。有些线程调度的时候并不是由睡眠唤醒而来,而是由时间片用完或别的什么原因调度而来的,这时有这些参数就会重新计算运行时间等,但stop_sched_class并没有用到它。
ttwu_do_activate调用ttwu_activate执行具体的唤醒操作,在Linux中,有这样一种习惯,函数名前面如果多了一个do表示函数功能更广泛一些,没有do则更具体一些,另外前面有没有下划线也是如此,有下划线或者下划线越多的表示执行的操作越具体。
ttwu_activate除了调用activate_task外,更重要的是设置task的on_rq属性。task的on_rq如果为0表示它还没有在rq中,那无论如何也不会被执行的,只有on_rq大于0它才会得到CPU的调度。有些书上说,task状态设置为TASK_INTERRUPTIBLE就不会调度运行,其实这说得还不够具体,因为设置为TASK_INTERRUPTIBLE的那一刻,线程还是在CPU中执行着的,只有等到task调用了schedule()并切换出去了才会进入睡眠。而且有时候task会被信号唤醒,那时也不在TASK_RUNNING状态中,所以从严格意义上来讲,线程的调度和task的状态(state)没有必然关系。
activate_task负责task的入队列的操作,通过调用与task绑定的调度器的enqueue_task接口函数,把task加入到具体的队列中。
由于stop_sched_class只管理一个stop线程,所以不会接受多余的task,也没有入队列的行为,只简单的把rq的task运行数量加1,并增加一下WALT的参数即可。可能有同学会问WALT是什么?它是Windows-Assist Load Tracing的缩写,一种计算方法,用数据来表现CPU当前的loading情况,用于后续任务调度、迁移、负载均衡等功能,在这里就不多说了。
CPU获取调度的task
Linux调度的策略主要体现在如何选择下一个待切换的线程上,每一个调度器都有不同的实现。schedule从最高优先级的调度器开始逐个执行pick_next_task接口函数,一旦找到了待调度的线程,则从for_each_class循环中返回,如果一直没有找到符合条件的,最后的idle调度器将返回idle线程的指针。
stop_sched_class是优先级最高的调度器,在pick的过程中第一个被调用,如果检查到stop线程存在并且已经在队列中,则返回stop的指针,否则返回NULL。stop线程在没有工作要做的时候处会在睡眠状态中,不占用CPU时间,而一旦唤醒则会第一时间调度运行。在返回task之前调度器会先调用put_prev_task函数,把前一个task做个了结,该计算时间片的计算时间片,该清理资源的清理资源,但此函数并不会把task踢出队列,甚至还会为下一次调度准备好下一个task。
线程在运行的过程中只有再次遭遇资源不足,才会把自己的状态设为TASK_INTERRUPTIBLE 或TASK_UNINTERRUPTIBLE,然后调用schedule切换出去(进入睡眠)。这时,调度例程发现task的状态不为TASK_RUNNING会调用deactivate_task函数把task移出rq。
从代码中可以看到deactivate_task的逻辑比较简单,最终调用与task绑定的调度器的enqueue_task函数而已。
和enqueue_task_stop一样,stop_sched_class的enqueue_task接口的功能也非常简单,只是简单减少rq的task运行数及更新一下WALT参数而已。
task移出rq后去了哪里?
task移出rq后问题来了,被移出的task去了哪里?其实Linux已经封装好了许多有睡眠功能的对象,比如等待队列就是例子,task先创建或加入一个等待队列,然后把自己切换出去不再运行,自己一直待在等待队列中成为睡眠task,直到等待的条件满足了,由唤醒task把睡眠task再度加入rq中,从而睡眠task就可以调度运行了。所以,就算task移出了运行队列,它并不会凭空消失的,只是暂时由某种队列管理起来了而已。
task迁移
最后,我们看一下把task迁移到另一个CPU的代码:
set_task_cpu函数首先判断调度器的migrate_task_rq接口有没有实现,如果实现了就调用它,这个接口功能是通知调度器task要迁移CPU了,但接口并不会影响迁移的过程,主要是做一些时间清零操作等,stop调度器没有实现此接口。
最后__set_task_cpu函数设置task的属性
task的rq及cpu的属性设置完成后说明task已经加入到新的rq中了,这样迁移工作基本完成了,随后只要CPU发生调度,调度器就能pick出此线程运行了。
以上就是stop_sched_class调度器的代码分析(基于ARMv8,Kernel4.4)。
标签: #linux线程调度算法