调度策略的工作是选择什么样的任务在什么时候运行,也就是在多个任务之间完成 cpu 资源的分配。对 cpu 资源的度量主要是时间,调度策略负责给每个线程分配可以运行的时间,并且会检查线程的时间是不是已经用完,如果已经用完,那么便会把当前这个线程切走,选择下一个线程来运行。linux 中的调度策略是针对一个线程进行设置的,线程是调度的基本单位,这句话我们经常听到,调度策略当然也是针对线程进行设置的。线程是 linux 中任务的基本单位,从任务的形式来看,除了线程,linux 中的任务还包括中断,软中断,进程等。
不同的调度策略的区别主要是优先级不一样,一般来说,对实时性要求高的线程,也就是条件满足之后需要尽快执行的任务会设置较高的优先级,优先级高的线程会优先得到调度。
linux 中常见调度策略可以分为 3 类:
① 普通调度策略:SCHED_OTHER
② 实时调度策略:SCHED_FIFO, SCHED_RR
③ 截止时间调度策略:SCHED_DEADLINE
调度策略 |
特点 |
SCHED_OTHER |
1、普通调度策略,使用完全公平调度算法 2、没有优先级,有 nice 值(nice 取值范围是 [-20, 19]),nice 值越大说明这个线程越 nice,越 nice 的意思是会把 cpu 让给其它线程来运行,对比优先级就是 nice 值越小,优先级越大 |
SCHED_FIFO |
1、实时调度策略,优先级 [1, 99],数值越大,优先级越高 2、只有主动让出 cpu 的时候,其它线程才会得到调度,否则会一直占着 cpu |
SCHED_RR |
1、实时调度策略,优先级 [1, 99],数值越大优先级越高 2、时间片调度,每次时间片用完,会释放 cpu,重新调度 默认时间片是 100ms,可以通过如下文件进行查看和修改 cat /proc/sys/kernel/sched_rr_timeslice_ms 100 |
SCHED_DEADLINE |
1、DEADLINE 调度策略的优先级比普通调度策略和实时调度策略都要高,优先级是固定的,无法设置修改 |
调度策略的优先级分两个层级,第一个是调度策略层面的,比如 SCHED_DEADLINE 调度策略优先级最高,其次是 SCHED_FIFO 和 SCHED_RR,最后是 SCHED_OTHER;其次是一个调度策略之内的线程,虽然是相同的调度策略,也可以设置不同的优先级,优先级越高则会被优先调度。
SCHED_DEADLINE > SCHED_FIFO, SCHED_RR > SCHED_OTHER
在调度策略层面,实时调度策略的优先级永远大于普通调度策略,也就是说如果有一个实时线程的的优先级是 1(实时调度策略中最低优先级),另外一个普通线程的 nice 值是 -20(普通调度策略最高优先级),那么也会优先调度实时调度策略的线程。
linux 中线程的优先级在以下 3 个场合下都会用到:
① 用户态调用系统调用设置线程优先级
② top 命中显示的优先级
③ 内核中的优先级
这 3 个场合下看到的优先级并不是完全一致的,比如对于 SCHED_RR 调度策略,设置优先级为 50 的时候,top 命令显示的优先级和 linux 内核中的优先级均不是 50。
系统调用
syscall(__NR_sched_setattr, pid_t tid, struct sched_attr *attr, unsigned int flags);
struct sched_attr {
__u32 size;
__u32 sched_policy;
__u64 sched_flags;
// SCHED_NORMAL, SCHED_BATCH
__s32 sched_nice;
// SCHED_FIFO, SCHED_RR
__u32 sched_priority;
// SCHED_DEADLINE (nsec)
__u64 sched_runtime;
__u64 sched_deadline;
__u64 sched_period;
};
系统调用的第二个入参是 struct sched_attr * 类型,其中的 sched_policy 用来指定调度策略,sched_priority 用于指定优先级。
对于实时调度策略 SCHED_RR, SCHED_FIFO,优先级设置范围是 [1, 99],数值越大,优先级越高。对于普通调度策略 SCHED_OTHER,nice 值取值范围是 [-20, 19], nice 值越小,优先级越高。对 SCHED_DEADLINE 调度策略,该策略的优先级是固定的,不可修改,高于 SCHED_RR, SCHED_FIFO 以及 SCHED_OTHER。
SCHED_DEADLINE 的优先级不能更改, 如下 3 个参数是 SCHED_DEADLINE 需要配置的参数,这 3 个参数的单位均是纳秒。
sched_period: 调度周期,这个概念和定时器的定时周期有点类似,意思是多长时间,任务需要运行一次,比如有个任务每100ms 就得运行一次,就可以设置 sched_period 为 100ms。
sched_deadline: 这个参数说的是在一个调度周期内,任务运行的截止时间,比如 100ms 的调度周期,80ms 之前必须需要运行完,不过在实际使用中,常常将这个参数与 sched_period 设置成相同的。
sched_runtime: 这个参数说的是在调度周期内,至少要保证任务的运行时间。
3 个参数的大小关系如下:
sched_runtime <= sched_deadline <= sched_period
如下是使用 top 命令查看到的一个进程内的所有线程的优先级,其中 PR 列显示的是线程的优先级,NI 列显示的是 nice 值。
对于 SCHED_OTHER 调度策略的线程,nice 值取值范围是 [-20, 19], nice 值会直接显示在 top 命令的 NI 列,同时在 PR 列显示的优先级对应的是 [0, 39],nice 值越大说明这个线程越 nice,越 nice 的线程意思是越愿意把 cpu 资源让给其它线程来运行,也就是这个线程的优先级越低, nice -20 对应 PRI 为 0,nice 19 对应 PRI 为 39,PRI 值越大优先级越低。
对于实时调度策略来说,优先级的取值范围是 [1, 99], 在 top 命令中,优先级是 99 的时候,显示为 rt, 1 ~ 98 优先级显示为 -2 到 -99,即 top = -1 - priority。 优先级 1 在 top 命令中显示为 -2, 98 显示为 -99,也就是说实时调度策略在 top 中显示为负数,数值越小,优先级越高。
在 top 显示中,普通调度策略的优先级显示为 [0, 39],实时调度策略的优先级显示为 [rt, -99, -2]。在 top 命令的 PR 显示中,普通调度策略和实时调度策略达成了统一,即数值越小,优先级越高,并且实时调度策略的优先级永远大于普通调度策略。
系统调用 __NR_sched_setattr 在内核中定义如下,在内核中最终会调用到函数 __sched_setscheduler()。
SYSCALL_DEFINE3(sched_setattr, pid_t, pid, struct sched_attr __user *, uattr,
unsigned int, flags)
在函数 __sched_setscheduler() 中最终会调用函数 __normal_prio() 来将用户传入的优先级转化为内核的优先级。在内核中,也是数值越低,优先级越高,在这个规律上内核和 top 命令是一致的。
static inline int __normal_prio(int policy, int rt_prio, int nice)
{
int prio;
if (dl_policy(policy))
prio = MAX_DL_PRIO - 1; // MAX_DL_PRIO 为 0,所以在内核中 deadline 优先级规定为 -1
else if (rt_policy(policy))
prio = MAX_RT_PRIO - 1 - rt_prio; // MAX_RT_PRIO 为 100,用户可传入的范围是 [1, 99], 内核中范围是 [98, 0]
else
prio = NICE_TO_PRIO(nice); // 用户可传入的范围是 [-20, 19], 转化为内核的为 [100, 139]
return prio;
}
// deadline 这个宏的定义也说明了 deadline 调度策略的优先级永远比实时策略和普通策略要高
/*
* SCHED_DEADLINE tasks has negative priorities, reflecting
* the fact that any of them has higher prio than RT and
* NORMAL/BATCH tasks.
*/
#define MAX_DL_PRIO 0
在内核中, 调度策略的优先级大小关系如下:
SCHED_DEADLINE > SCHED_FIFO, SCHED_RR > SCHED_OTHER
优先级在用户,内核, top 中的对应关系:
调度策略 |
用户 |
top |
内核 |
SCHED_OTHER |
nice 值 [-20, 19] |
[0, 39] |
[100, 139] |
SCHED_FIFO, SCHED_RR |
priority [1, 99] |
[-2, -100], 其中 -100 在 top 命令中显示为 rt -1 - x |
[0, 98] |
SCHED_DEADLINE |
优先级不需要设置 |
显示为 rt |
-1 |
在 top 命令以及内核中,数值越小,优先级越高。
如下代码,在主线程中有一个 while(1) 循环,我们可以使用 renice 和 chrt 命令来改变线程的优先级,然后通过 top -H -p pid 来查看 top 命令的显示。
#include
#include
#include
#include
int main() {
while(1) {
sleep(1);
}
return 0;
}
renice 命令可以在进程运行起来之后,动态改变进程的 nice 值。
应用起来,线程的默认调度策略是 SCHED_OTHER,默认 nice 值是 0。
使用 renice 将 nice 值改成 -20
renice -n -20 -p 2379
之后再使用 top -H -p 2379 来查看 top 中显示的优先级,可以看到已经发生了改变。
renice -n 19 -p 2379
chrt 可以设置线程的实时调度策略优先级。
使用 chrt 将进程的调度策略设置成 SCHED_FIFO,优先级是 50。
再使用命令 top -H -p 2398 查看优先级显示为 -51。
设置优先级为 1,top 显示为 -2
设置优先级为 98,top 显示为 -99。
设置优先级 99,top 显示为 rt。
如下代码是一个 SCHED_DEADLINE 的例子, 在 linux 代码仓库的如下文件中。
Documentation/scheduler/sched-deadline.rst
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define gettid() syscall(__NR_gettid)
#define SCHED_DEADLINE 6
/* XXX use the proper syscall numbers */
#ifdef __x86_64__
#define __NR_sched_setattr 314
#define __NR_sched_getattr 315
#endif
#ifdef __i386__
#define __NR_sched_setattr 351
#define __NR_sched_getattr 352
#endif
#ifdef __arm__
#define __NR_sched_setattr 380
#define __NR_sched_getattr 381
#endif
static volatile int done;
struct sched_attr {
__u32 size;
__u32 sched_policy;
__u64 sched_flags;
/* SCHED_NORMAL, SCHED_BATCH */
__s32 sched_nice;
/* SCHED_FIFO, SCHED_RR */
__u32 sched_priority;
/* SCHED_DEADLINE (nsec) */
__u64 sched_runtime;
__u64 sched_deadline;
__u64 sched_period;
};
int sched_setattr(pid_t pid, const struct sched_attr *attr, unsigned int flags) {
return syscall(__NR_sched_setattr, pid, attr, flags);
}
int sched_getattr(pid_t pid, struct sched_attr *attr, unsigned int size, unsigned int flags) {
return syscall(__NR_sched_getattr, pid, attr, size, flags);
}
void *run_deadline(void *data) {
struct sched_attr attr;
int x = 0;
int ret;
unsigned int flags = 0;
printf("deadline thread started [%ld]\n", gettid());
attr.size = sizeof(attr);
attr.sched_flags = 0;
attr.sched_nice = 0;
attr.sched_priority = 0;
/* This creates a 10ms/30ms reservation */
attr.sched_policy = SCHED_DEADLINE;
attr.sched_runtime = 10 * 1000 * 1000;
attr.sched_period = attr.sched_deadline = 30 * 1000 * 1000;
ret = sched_setattr(0, &attr, flags);
if (ret < 0) {
done = 0;
perror("sched_setattr");
exit(-1);
}
while (!done) {
x++;
}
printf("deadline thread dies [%ld]\n", gettid());
return NULL;
}
int main(int argc, char **argv) {
pthread_t thread;
printf("main thread [%ld]\n", gettid());
pthread_create(&thread, NULL, run_deadline, NULL);
sleep(10);
done = 1;
pthread_join(thread, NULL);
printf("main dies [%ld]\n", gettid());
return 0;
}
模板设计思想在 linux 中应用广泛。定义一个结构体,结构体内声明一些函数接口,然后不同的类别可以分别定义自己的接口,类似于面向对象当中抽象,继承,多态。
网络协议有很多种(tcp, udp, icmp),但是每种协议需要实现的接口都是类似的(套接字初始化,接收报文,发送报文,设置套接字选项,获取套接字选项),struct proto 就定义了网络协议需要实现的接口,然后 tcp, udp, icmp 可以分别定义自己的接口(tcp_prot, udp_prot, ping_prot)。
一切皆文件是 linux 的一个特点,struct file_operations 在一定程度是体现了一切皆文件的思想。在用户态可以表示成 fd 的 socket, epoll 均定义了自己的结构体 struct file_operations socket_file_ops 和 struct file_operations eventpoll_fops。
调度策略类也类似,声明了模板数据结构 struct sched_class, 这个结构体中声明了一些函数接口。SCHED_OTHER, SCHED_FIFO, SCHED_RR, SCHED_DEADLINE 调度策略均实现了这些接口。这些调度策略的定义均通过宏 DEFINE_SCHED_CLASS 来声明。
如下是 rt 调度策略声明的函数,linux 中 SCHED_RR, SCHED_FIFO 均属于 rt 调度类。
DEFINE_SCHED_CLASS(rt) = {
.enqueue_task = enqueue_task_rt,
.dequeue_task = dequeue_task_rt,
.yield_task = yield_task_rt,
.check_preempt_curr = check_preempt_curr_rt,
.pick_next_task = pick_next_task_rt,
.put_prev_task = put_prev_task_rt,
.set_next_task = set_next_task_rt,
#ifdef CONFIG_SMP
.balance = balance_rt,
.pick_task = pick_task_rt,
.select_task_rq = select_task_rq_rt,
.set_cpus_allowed = set_cpus_allowed_common,
.rq_online = rq_online_rt,
.rq_offline = rq_offline_rt,
.task_woken = task_woken_rt,
.switched_from = switched_from_rt,
.find_lock_rq = find_lock_lowest_rq,
#endif
.task_tick = task_tick_rt,
.get_rr_interval = get_rr_interval_rt,
.prio_changed = prio_changed_rt,
.switched_to = switched_to_rt,
.update_curr = update_curr_rt,
#ifdef CONFIG_UCLAMP_TASK
.uclamp_enabled = 1,
#endif
};
下标是调度器类中的接口的说明:
接口 |
说明 |
enqueue_task |
将进程加入到运行队列中 |
dequeue_task |
将进程从运行队列中移除 |
pick_next_task |
选择下一个进程来运行 |
task_tick |
定时器触发,这个函数中会看进程的时间片是不是消耗完,如果消耗完,就会触发调度 |
linux 内核是用 c 语言开发,c 语言是面向过程的语言,面向过程的语言可以写出面向对象的代码,同样的面向对象的语言,也可以写出面向过程的代码。面向过程,面向对象的编程思想,并不受语言限制。
不同调度策略的优先级顺序如下:
SCHED_DEADLINE > SCHED_FIFO, SCHED_RR > SCHED_OTHER
linux 内核选择下一个进程来运行的代码如下,for_each_class 会依次遍历不同调度策略下的运行队列,具体来说就是先从 SCHED_DEADLINE 的运行队列中选择一个可运行的进程,如果有可运行的进程则运行,如果没有则从 SCHED_RT 中选择一个,以此类推。通过遍历顺序来保证的调度策略之间的先后顺序。
for_each_class(class) {
p = class->pick_next_task(rq);
if (p)
return p;
}
// 从 sched_class_highest 开始遍历,遍历到 sched_class_lowest 为止
#define for_each_class(class) \
for_class_range(class, sched_class_highest, sched_class_lowest)
#define sched_class_highest (__end_sched_classes - 1)
#define sched_class_lowest (__begin_sched_classes - 1)
下边这个宏定义了不同调度策略的先后位置,for_each_class 从下向上遍历,保证高优先调度策略的线程优先被调度到。
#define SCHED_DATA \
STRUCT_ALIGN(); \
__begin_sched_classes = .; \
*(__idle_sched_class) \
*(__fair_sched_class) \
*(__rt_sched_class) \
*(__dl_sched_class) \
*(__stop_sched_class) \
__end_sched_classes = .;
struct rq 用来管理运行态的进程,rq 全称即 run queue, 运行队列。struct rq 是一个 per_cpu 变量,每个 cpu 都有一个变量,可以减少数据访问时加锁的情况。struct rq 中的成员比较多,比较重要的有 3 个, 分别是 struct cfs_rq cfd, struct rt_rq rt 以及 struct dl_rq dl,分别管理不同调度策略的进程。也就是说不同调度策略的线程并不是都放到一个队列中进行调度,而是每个调度策略都有自己的队列。
struct rq {
...
struct cfs_rq cfs;
struct rt_rq rt;
struct dl_rq dl;
...
};
“完全公平调度算法,将所有的线程完全公平的对待。”
我一开始看到这句话的时候是有一些疑惑的,普通调度策略是有自己的 nice 值的,不同 nice 值代表着线程的优先级。如果不同 nice 值的线程都是完全公平的,那么 nice 值还能起什么用 ?
完全公平,并不是不同 nice 值的线程分配的物理时间片是完全相同的,而是虚拟时间完全相同。
虚拟时间 = 物理时间 / weight
物理时间就是线程实际占用的 cpu 时间,weight 是权重,不同 nice 值的权重是不一样的。
nice 值的范围是 [-20, 19],nice 值越小说明这个线程越不 nice,这个 cpu 占用的 cpu 就会越多。
nice 值越小,对应的 weight 就会越大,因为消耗相同的 cpu 时间,weight 值越大,那么 vruntime 就越小。cfs 调度算法就是尽量让所有的线程的 vruntime 保持一致,所以 vruntime 越小,就越容易被调度。
nice 值越小 --> 优先级越高 --> weight 越大 --> vruntime 越小 --> 越容易被调度
不同 nice 值对应的权重定义如下:
/*
* Nice levels are multiplicative, with a gentle 10% change for every
* nice level changed. I.e. when a CPU-bound task goes from nice 0 to
* nice 1, it will get ~10% less CPU time than another CPU-bound task
* that remained on nice 0.
*
* The "10% effect" is relative and cumulative: from _any_ nice level,
* if you go up 1 level, it's -10% CPU usage, if you go down 1 level
* it's +10% CPU usage. (to achieve that we use a multiplier of 1.25.
* If a task goes up by ~10% and another task goes down by ~10% then
* the relative distance between them is ~25%.)
*/
const int sched_prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
struct cfs_rq 的运行队列用红黑树来表示,红黑树的 value 是线程的 vruntime,最左侧节点的 vruntime 最小,也最先被调度。
红黑树用如下结构体表示,除了表示红黑树的 rb_root 之外,还有一个 rb_leftmost,这个指针指向红黑树最左侧的节点,这样的话,选择一个任务进行运行的时候就不需要通过遍历红黑树来找最左侧的节点,可以通过 rb_leftmost 直接获取。
struct rb_root_cached {
struct rb_root rb_root;
struct rb_node *rb_leftmost;
};
对于 SCHED_FIFO 以及 SCHED__RR 来说,用户态可以配置的优先级范围是 [1, 99], 内核态优先级 = 99 - 用户态优先级,范围是 [0, 98],数字越小,优先级越高。
struct rt_rq 中任务最终保存在 struct rt_prio_array 表示的队列中。在这个结构体中有两个属性,一个是 bitmap,一个是真正存在任务的队列。bitmap 中如果某个 bit 被 set,说明这个 bit 对应的 queue[bit] 这个队列中有待调度的任务。
struct rt_prio_array {
DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
struct list_head queue[MAX_RT_PRIO];
};
bitmap:
queue:
如果有一个内核优先级为 10 的任务放到了调度队列中,那么这个任务会加入到 queue[10] 这个队列中,同时会设置 bitmap 中的第 10 bit。
调度器从队列中选择一个任务进行执行时,从 0 到 99 分别遍历 bitmap 中的每个 bit,如果发现某个 bit 被 set,比如第 10 bit,那么就会运行 queue[10] 队列头所表示的任务。从 0 到 99 的遍历顺序,也保证了高优先级的任务优先被调度到。
如下模块是一个带回调参数的内核模块。回调参数的名字是 target_pid_int,当安装模块之后,会在 /sys/module/hello/parameters/ 中看到这个参数,并且可以使用 echo 来修改这个参数。回调参数的意思是当修改这个参数的时候会调用一个函数来设置这个参数,本模块中的回调函数是 notify_param()。在内核模块中,修改 pid 时,便会找到 pid 对应的 struct task_struct,然后打印任务的优先级信息。
比如,执行如下命令,就会打印出线程 3082 的优先级。
echo 3082 > /sys/module/hello/parameters/target_pid_int
#include
#include
#include
#include
#include
#include
int target_pid_int = 0;
int notify_param(const char *val, const struct kernel_param *kp) {
int res = param_set_int(val, kp);
if (res != 0) {
printk("set cb param failed\n");
return -1;
}
struct task_struct *task = pid_task(find_vpid(target_pid_int), PIDTYPE_PID);
if (task == NULL) {
printk("can not find task of pid %d\n", target_pid_int);
return -1;
}
printk("target pid: %d, task->pid: %d\n", target_pid_int, task->pid);
printk("prio: %d\n", task->prio);
printk("static prio: %d\n", task->static_prio);
printk("normal prio: %d\n", task->normal_prio);
printk("rt prio: %d\n", task->rt_priority);
return 0;
}
const struct kernel_param_ops my_param_ops = {
.set = ¬ify_param,
.get = ¶m_get_int,
};
module_param_cb(target_pid_int, &my_param_ops, &target_pid_int, S_IWUSR | S_IRUSR);
static int __init hello_init(void) {
printk("task priority log module installed\n");
return 0;
}
static void __exit hello_exit(void) {
printk("task priority log module removed\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wx2");
MODULE_DESCRIPTION("print task priority");
MODULE_VERSION("1.0");
obj-m += hello.o
CONFIG_MODULE_SIG=n
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
使用 top 命令找到一些不同优先级的线程,然后将 pid 设置到参数中,使用 dmesg 就可以看到如下打印:
(1)top PRI rt
实时调度策略,优先级是 99,prio 是 0,prio 用作 struct rt_rq 的下标,0 是第一个查找的队列,优先级最高。
(2)top PRI -51
用户态的优先级是 50,对应到内核是 99 - 50 = 49
(3)top PRI 0
对应的是普通调度策略的 nice 值 -20
(4)top PRI 39
对应的是普通调度策略的 nice 值 19
(5)SCHED_DEADLINE
如下是一个 SCHED_DEADLINE 的线程的打印信息,prio 是 -1。
对于 struct task_struct 中的这几个优先级相关的成员,总结如下:
① rt_priority 只有 SCHED_FIFO 和 SCHED_RR 两个调度策略有效,取值范围是 [1, 99],与我们调用系统调用设置的优先级是一样的;其它调度策略(SCHED_OTHER, SCHED_DEADLINE),rt_priority 均是 0。
② static_prio 只有 SCHED_OTHER 调度策略有效,nice 值取值范围 [-20, 19],对应 static_prio 的取值范围是 [100, 139];其它调度策略(SCHED_FIFO, SCHED_RR, SCHED_DEADLINE),static_prio 均是 120。
③ prio 和 normal_prio 对每种调度策略都是有效的,并且从上边的截图来看,两者大部分时间是相同的,SCHED_DEADLINE 优先级是 -1,SCHED_FIFO 和 SCHED_RR 的优先级是 [0, 98],SCHED_OTHER 的优先级是 [100, 139]。
SCHED_DEADLINE 的调度队列也是用红黑树实现。
调度指标是看哪个线程的 deadline 更加紧急。比如有 3 个线程 a, b,c,3 个线程的 deadline 分别是 1s, 2s, 3s,也就是说线程 a 还有 1s 就到 deadline 了,线程 b 还有 2s 到 deadline,线程 c 还有 3s 到 deadline。这个时候,线程 a 最紧急,所以优先调度线程 a。