没想到,直到写完才发现,这一篇就写了一个优先级,就可以写这么多,CFS又要往后推。
16.1 分配给进程的时间linux也是一种抢占式时间片的系统,不过linux系统的时间片分配跟其他的不一样,使用的是一种动态时间片算法,它给每个进程分配了使用CPU的时间比例。
linux为了保证调度算法在异常情况下,有所取舍,定义了两个参数:
调度延迟,sysctl_sched_latency,记录在/proc/sys/kernel/sched_latency_ns中。
调度延迟指每一个可运行的进程都至少运行一次的时间间隔。
调度最小粒度,sysctl_sched_min_granularity,记录在/proc/sys/kerbel/kernel/sched_min_granularity_ns中
调度最小粒度指任一进程运行的最小时间,除了阻塞或者主动让出CPU。
这两个参数结合起来看就有点意思了,如果就绪进程比较少,是可以满足调度延迟的,如果进程太多,linux调度算法会做一个取舍,尽量去满足调度最小粒度。
那我们来看看这个进程的个数是多少:
root@ubuntu:/proc/sys/kernel# cat sched_latency_ns (单位ns) 12000000 root@ubuntu:/proc/sys/kernel# cat sched_min_granularity_ns (单位ns) 1500000
s c h e d _ n r _ l a t e n c y = s y s c t l _ s c h e d _ l a t e n c y s y s c t l _ s c h e d _ m i n _ g r a n u l a r i t y = 12000000 1500000 = 8 sched_nr_latency = frac {sysctl_sched_latency}{sysctl_sched_min_granularity} = frac {12000000}{1500000} = 8 sched_nr_latency=sysctl_sched_min_granularitysysctl_sched_latency=150000012000000=8
两个怎么大的数,竟然算出来是8,
也就是说在就绪进程小于等于8的时候,调度周期等于延迟周期。
如果大于8的时候,调度周期等于进程个数*最小粒度。
那我们就可以算出分配给进程的时间了:
分
配
给
进
程
的
时
间
=
调
度
周
期
∗
1
就
绪
进
程
个
数
分配给进程的时间 = frac {调度周期 * 1}{就绪进程个数}
分配给进程的时间=就绪进程个数调度周期∗1
我们上面介绍的都是在同等优先级的情况下,下面我们来看看引入优先级后的情况。
linux系统是支持优先级的,不过这个优先级在linux系统下是叫nice,该值的取值范围是[-20, 19],其中nice越高,表示优先级越低。默认优先级是0。
16.2.1 优先级的内核表示nice越高,优先级越低,我就一直很纳闷,结果看到是怎么翻译的,nice的英文意思是友好,nice值越高,表示越友好,越谦让,即优先级越低。(真的是好人卡啊)
也不知道这种诡异的范围是怎么取出来的,但是在内核中,也有一个优先级的范围,取值0-139:
用户设置的nice值对应的内核中的是100-139,内核中也是值越低,优先级越高。
可以来看看内核代码中的转换:
// include/linux/sched/prio.h #ifndef _SCHED_PRIO_H #define _SCHED_PRIO_H #define MAX_NICE 19 #define MIN_NICE -20 #define NICE_WIDTH (MAX_NICE - MIN_NICE + 1) #define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH) #define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2) #define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO) // 直接加上偏移,真是简单粗暴 #define PRIO_TO_NICE(prio) ((prio) - DEFAULT_PRIO) #define USER_PRIO(p) ((p)-MAX_RT_PRIO) #define TASK_USER_PRIO(p) USER_PRIO((p)->static_prio) #define MAX_USER_PRIO (USER_PRIO(MAX_PRIO))16.2.2 计算优先级
通过上面的注释,我们是不是也注意到了一个单词:static priority。没错,nice设置的就是进程的静态优先级。在进程的控制块中(进程控制块详细描述再后面吧,这个内容是真的多,先把里面的细节分开介绍),有3个优先级的变量,分别是:
int prio , static_prio, normal_prio; unsigned int rt_priority ;
这么多个优先级,那之间是怎么联系的呢?我们来看看代码
// 文件:kernel/sched/core.c
static int effective_prio(struct task_struct *p)
{
p->normal_prio = normal_prio(p);
if (!rt_prio(p->prio))
return p->normal_prio;
return p->prio;
}
我们现在先不管实时进程,所以只要了解normal_prio()函数里面是做啥的,就知道普通优先级是啥了。
static inline int normal_prio(struct task_struct *p)
{
int prio;
if (task_has_dl_policy(p))
prio = MAX_DL_PRIO-1;
else if (task_has_rt_policy(p))
prio = MAX_RT_PRIO-1 - p->rt_priority;
else
prio = __normal_prio(p);
return prio;
}
接下来看看普通进程是怎么赋值优先级的:
static inline int __normal_prio(struct task_struct *p)
{
return p->static_prio;
}
是不是惊呆了所有小伙伴,直接把静态优先级赋值。
综上所述:普通进程的优先级其实还是等于静态优先级,也就是nice的值。
吹了这么多水,才得出这么个结论,哎。
注意:在进程分出子进程时,子进程的静态优先级继承自父进程。子进程的动态优先级会设置为父进程的普通优先级。
16.2.3 计算负荷权重我们接下来看一个nice是怎么影响进程的负荷权重的,先来看看进程负荷权重保存在哪?
// task_struct->se.load task_struct是进程的控制块
struct load_weight {
unsigned long weight;
u32 inv_weight;
};
内核中定义了一个数组,来表达每个不同nice值对应的权重:
// kernel/sched/sched.h
static const int prio_to_weight[40] = {
88761, 71755, 56483, 46273, 36291,
29154, 23254, 18705, 14949, 11916,
9548, 7620, 6100, 4904, 3906,
3121, 2501, 1991, 1586, 1277,
1024, 820, 655, 526, 423,
335, 272, 215, 172, 137,
110, 87, 70, 56, 45,
36, 29, 23, 18, 15,
};
英语厉害的,直接看上面的介绍也能明白了。下面我在用中文介绍一波:
一般的概念是这样的,进程每降低一个nice值,将多获得10%的CPU时间。我们来举一个例子,如果运行队列中有两个进程,一个nice值为0的进程A,一个nice值为1的进程B,那么按约定,进程B获取45%的CPU,进程A获取55%的CPU,是怎么算的:
进
程
A
=
1024
1024
+
820
=
0.55
进程A = frac {1024}{1024+820} = 0.55
进程A=1024+8201024=0.55
进 程 B = 820 1024 + 820 = 0.45 进程B = frac {820}{1024+820} = 0.45 进程B=1024+820820=0.45
就是这么算出来的,上面的数组之间的乘数因子是1.25。
接下来看看代码是怎么给他赋值的:
// kernel/sched/core.c
static void set_load_weight(struct task_struct *p)
{
int prio = p->static_prio - MAX_RT_PRIO; // 映射到数组坐标中
struct load_weight *load = &p->se.load; // 获取指针,后面用来保存
// 这里没有实时进程了,难道内核改版了?
if (idle_policy(p->policy)) { // 空闲进程SCHED_IDLE,填入很少的值
load->weight = scale_load(WEIGHT_IDLEPRIO);
load->inv_weight = WMULT_IDLEPRIO;
return;
}
// 这两个就是把值填入负载中,啥时候用呢?下面再分析
load->weight = scale_load(prio_to_weight[prio]);
load->inv_weight = prio_to_wmult[prio];
}
16.2.4 应用设置nice值
都深入内核,还回到应用,这种玩法确实有点不太习惯啊。
linux提供了如下函数来获取和修改进程的nice值:
#include#include int getpriority(int which, int who); int setpriority(int which, int who, int prio);
which和who是有关系的,我们一起来看看这两个参数:
| which | who |
|---|---|
| PRIO_PROCESS | 操作进程ID |
| PRIO_PGRP | 操作进程组ID的所有成员 |
| PRIO_USER | 操作所有真实用户ID的进程 |
getpriority返回nice值,如果有多个进程符合条件,那么返回优先级最高的那个nice。(也是nice值最小的)
nice的范围是[-20,19],所以不能直接判断返回值是否等-1,需要结合errno来判断。
常见的errno介绍:
| errno | 说明 |
|---|---|
| EACCESS | 尝试获取更高的优先级(更低的prio值),但是没有CAP_SYS_NICE权限 因为之前的系统是不能自己调整优先级的 |
| EINVAL | which的值不对的时候 |
| ESRCH | which和who指定的进程不存在 |
| EPERM | 指定进程的有效用户ID和调用进程的有效用户ID不一致,且调用进程没有CAP_SYS_NICE权限 |
看着上面这么复杂就知道是新办法。
下面看一下老方法,nice函数也可以更改优先级。
#includeint nice(int incr);
incr参数被增加到调用进程的nice值上。如果incr太大,系统直接把它降到最大合法值,不给出提示。
类似地,如果incr太小,系统也会无声息地把它提高到最小合法值。由于-1是合法的成功返回值,在调用nice函数之前需要清除errno,在nice函数返回-1时,需要检查它的值。nice的范围是[-20,19],所以只有返回值=-1并且error不为0时,nice调用失败。
写了一个测试程序:
// 编译命令:gcc test_nice.c -o test_nice -lm #define _GNU_SOURCE #include#include #include #include #include int heavy_work() { double sum = 0.0; unsigned long long i =0; while(1) { sum = sum + sin(i++); } return 0; } int main() { // 设置CPU亲和度 cpu_set_t set; CPU_ZERO(&set); CPU_SET(0, &set); sched_setaffinity(0, sizeof(cpu_set_t), &set); // 设置亲和度 int m_nice = nice(0); printf("nice = %dn", m_nice); int prio = getpriority(PRIO_PROCESS, getpid()); printf("prio = %dn", prio); // 设置优先级 setpriority(PRIO_PROCESS, getpid(), 5); prio = getpriority(PRIO_PROCESS, getpid()); printf("prio = %dn", prio); m_nice = nice(0); printf("nice = %dn", m_nice); m_nice = nice(-4); // nice的值是在原优先级上增加或减少的值 printf("nice = %dn", m_nice); int pid = fork(); if(pid < 0) { return -1; } if(pid == 0) { prio = getpriority(PRIO_PROCESS, getpid()); printf("son prio = %dn", prio); setpriority(PRIO_PROCESS, getpid(), 0); prio = getpriority(PRIO_PROCESS, getpid()); printf("son prio = %dn", prio); } heavy_work(); return 0; }
因为现在的计算机都是多核的,所以需要设置CPU的亲和度,这个等到多核的时候,会详细讲,我们来编译执行看看:
root@ubuntu:~/c_test/16# ./test_nice nice = 0 prio = 0 prio = 5 nice = 5 nice = 1 son prio = 1 son prio = 0
几个api测试的结果也符合预期,nice的参数是incr在原优先级上增加或减少incr的值。
最后父进程优先级设为1, 子进程的优先级设为2。
我们可以用ps来查看一下:
root@ubuntu:~# ps -C test_nice -o pid,ppid,cmd,etime,nice,pri,psr PID PPID CMD ELAPSED NI PRI PSR 21700 1670 ./test_nice 00:04 1 18 0 21701 21700 ./test_nice 00:04 0 19 0
NI表示的是优先级,PRI是折算后内核的优先级-100的表示,要还原就要加上100的基数。
这样是不是还看不出,两个进程运行时间的比例,我们可以在/proc/PID/sched中查看se_sum_exec_runtime,这个值的意义是累计运行的物理时间:
21700 :se.sum_exec_runtime : 8023.416966
21701:se.sum_exec_runtime : 10010.900838
10010
8023
≈
1.25
frac {10010}{8023} approx 1.25
802310010≈1.25
刚刚好,是1.25,这个调度算法设计的还是很不错的。
注意:绝对的nice值并不影响调度决策,而是nice的相对值,影响了CPU时间的分配。如果进程A的nice是5,进程B的nice是6,这两个进程的运行时间比例也是1.25。
总结:引入优先级后,分配给进程的时间的公式:
分
配
给
进
程
的
时
间
=
调
度
周
期
∗
进
程
权
重
就
绪
进
程
权
重
之
和
分配给进程的时间 = frac {调度周期 * 进程权重}{就绪进程权重之和}
分配给进程的时间=就绪进程权重之和调度周期∗进程权重



