在用户空间,或者应用编程领域 ,Linux提供了一些API或者系统调用来影响Linux的内核调度器,或者是获取内核调度器的信息。比如可以获取或者设置进程的调度策略、优先级,获取CPU时间片大小的信息。
这些接口一旦在应用程序中调用,就像给湖面扔进一颗石子,对内核带来了那些的影响,其实这是我内心很激动很想分享的东西,但是内容好没有组织好,所以本文的主题暂不深入涉及这些系统调用及对应的内核层的代码。
严格地说,对于优先级对于实时进程和普通进程的意义是不一样的。
1 在一定程度上,实时进程优先级高,实时进程存在,就没有普通进程占用CPU的机会,(但是前一篇博文也讲过了,实时组调度出现在内核以后,允许普通进程占用少量的CPU时间,取决于配置)。
2 对于实时进程而言,高优先级的进程存在,低优先级的进程是轮不上的,没机会跑在CPU上,所谓实时进程的调度策略,指的是相同优先级之间的调度策略。如果是FIFO实时进程在占用CPU,除非出现以下事情,否则FIFO一条道跑到黑。
a)FIFO进程良心发现,调用了系统调用sched_yield 自愿让出CPU
b) 更高优先级的进程横空出世,抢占FIFO进程的CPU。有些人觉得很奇怪,怎么FIFO占着CPU,为啥还能有更高优先级的进程出现呢。别忘记,我们是多核多CPU ,如果其他CPU上出现了一个比FIFO优先级高的进程,可能会push到FIFO进程所在的CPU上。
c)FIFO进程停止(TASK_STOPPED or TASK_TRACED状态)或者被杀死(EXIT_ZOMBIE or EXIT_DEAD状态)
d) FIFO进程执行了阻塞调用并进入睡眠(TASK_INTERRUPTIBLE OR TASK_UNINTERRUPTIBLE)。
如果是进程的调度策略是时间片轮转RR,那么,除了前面提到的abcd,RR实时进程耗尽自己的时间片后,自动退到对应优先级实时队列的队尾,重新调度。
下面我们就是来探究FIFO策略和RR策略的特点。为了降低理解的难度,我将我们启动的实时进程绑定到同一个核上。
- #include<stdio.h>
- #include<stdlib.h>
- #include<unistd.h>
- #include<sys/time.h>
- #include<sys/types.h>
- #include<sys/sysinfo.h>
- #include<time.h>
- #define __USE_GNU
- #include<sched.h>
- #include<ctype.h>
- #include<string.h>
- #define COUNT 300000
- #define MILLION 1000000L
- #define NANOSECOND 1000
- void test_func()
- {
- int i = 0;
- unsigned long long result = 0;;
- for(i = 0; i<8000 ;i++)
- {
- result += 2;
- }
- }
- int main(int argc,char* argv[])
- {
- int i;
- struct timespec sleeptm;
- long interval;
- struct timeval tend,tstart;
- struct tm lcltime = {0};
- struct sched_param param;
- int ret = 0;
- if(argc != 3)
- {
- fprintf(stderr,"usage:./test sched_method sched_priority\n");
- return -1;
- }
- cpu_set_t mask ;
- CPU_ZERO(&mask);
- CPU_SET(1,&mask);
- if (sched_setaffinity(0, sizeof(mask), &mask) == -1)
- {
- printf("warning: could not set CPU affinity, continuing...\n");
- }
- int sched_method = atoi(argv[1]);
- int sched_priority = atoi(argv[2]);
- /* if(sched_method > 2 || sched_method < 0)
- {
- fprintf(stderr,"sched_method scope [0,2]\n");
- return -2;
- }
- if(sched_priority > 99 || sched_priority < 1)
- {
- fprintf(stderr,"sched_priority scope [1,99]\n");
- return -3;
- }
- if(sched_method == 1 || sched_method == 2)*/
- {
- param.sched_priority = sched_priority;
- ret = sched_setscheduler(getpid(),sched_method,¶m);
- if(ret)
- {
- fprintf(stderr,"set scheduler to %d %d failed %m\n");
- return -4;
- }
- }
- int scheduler = sched_getscheduler(getpid());
- fprintf(stderr,"the scheduler of PID(%ld) is %d, priority (%d),BEGIN time is :%ld\n",
- getpid(),scheduler,sched_priority,time(NULL));
- sleep(2);
- sleeptm.tv_sec = 0;
- sleeptm.tv_nsec = NANOSECOND;
- for(i = 0;i<COUNT;i++)
- {
- test_func();
- }
- interval = MILLION*(tend.tv_sec - tstart.tv_sec)
- +(tend.tv_usec-tstart.tv_usec);
- fprintf(stderr," PID = %d\t priority: %d\tEND TIME is %ld\n",getpid(),sched_priority,time(NULL));
- return 0;
- }
上面这个程序有几点需要说明的地方 1 为了降低复杂度,绑定到了同一个核上,我做实验的机器是四核(通过cat /proc/cpuinfo可以看到)
2 sleep(2),是给其他进程得到调度的机会,否则无法模拟出多个不同优先级的实时进程并行的场景。sleep过后,就没有阻塞性的系统调用了,高优先级的就会占据CPU(FIFO),同等优先级的进程轮转(RR)
- struct sched_param {
- /* ... */
- int sched_priority;
- /* ... */
- };
int sched_setscheduler (pid_t pid,
int policy,
const struct sched_param *sp);
sched_setscheduler函数的第二个参数调度方法 :
- #define SCHED_OTHER 0
- #define SCHED_FIFO 1
- #define SCHED_RR 2
- #ifdef __USE_GNU
- # define SCHED_BATCH 3
- #endif
SCHED_OTHER表示普通进程,对于普通进程,第三个参数sp->sched_priority只能是0 SCHED_FIFO 和SCHED_RR表示实时进程的调度策略,第三个参数的取值范围为[1,99]。
如果sched_setscheduler 优先级设置的值和调度策略不符合的话,会返回失败的。
内核中有这么一段注释:
- /*
- * Valid priorities for SCHED_FIFO and SCHED_RR are
- * 1..MAX_USER_RT_PRIO-1, valid priority for SCHED_NORMAL,
- * SCHED_BATCH and SCHED_IDLE is 0.
- */
LINUX系统提供了其他的系统调用来获取不同策略优先级的取值范围:
- #include <sched.h>
- int sched_get_priority_min (int policy);
- int sched_get_priority_max (int policy);
- static const int 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,
- };
假如有1台电脑,10个人玩,怎么才公平。
1 约定好时间片,每人玩1小时,玩完后记账,张XX 1小时,谁玩的时间短,谁去玩
2 引入优先级的概念,李四有紧急情况,需要提高他玩电脑的时间,怎么办,玩1个小时,记账半小时,那么同等情况下,李四会比其他人被选中玩电脑的频率要高,就体现了这个优先级的概念。
3 王五也有紧急情况,但是以考察,不如李四的紧急,好吧,玩1个小时,记账45分钟。
4 情况有变化,听说这里有电脑,突然又来了10个人,如果按照每人玩1小时的时间片,排在最后的那哥们早就开始骂人了,怎么办?时间片动态变化,根据人数来确定时间片。人越多,每个人玩的时间越少,防止哥们老捞不着玩,耐心耗尽,开始骂人。
这个记账就是我们prio_to_weight的作用。我就不多说了,prio_to_weight[20]就是基准,玩一小时,记账一小时,数组20以前的值是特权一级,玩1小时记账20分钟之类的享有特权的,数组20之后是倒霉蛋,玩1小时,记账1.5小时之类的倒霉蛋。 CFS这种调度好在大家都能捞着玩。
扯到优先级多说了几句,现在回到正题。我将上面的C程序编译成可执行程序test,然后写了一个脚本comp.sh。
- [root@localhost sched]# cat comp.sh
- #/bin/sh
- ./test $1 99 &
- usleep 1000;
- ./test $1 70 &
- usleep 1000;
- ./test $1 70 &
- usleep 1000;
- ./test $1 70 &
- usleep 1000;
- ./test $1 50 &
- usleep 1000;
- ./test $1 30 &
- usleep 1000;
- ./test $1 10 &
因为test进程有sleep 2秒,所以可以给comp.sh启动其他test的机会。可以看到有 99级(最高优先级)的实时进程,3个70级的实时进程,50级,30级,10级的各一个。 对于FIFO而言,一旦sleep过后,高优先级运行,低优先级是没戏运行的,同等优先级的进程,先运行的不运行完,后运行的也没戏。
对于RR而言,高优先级的先运行,同等优先级的进程过家家,你玩完,我玩,我玩完你再玩,每个进程耗费一个时间片的时间。对于Linux,RR时间片是100ms:
- #define DEF_TIMESLICE (100 * HZ / 1000)
下面我们验证: 我写了两个观察脚本,来观察实时进程的调度情况:
第一个脚本比较简单,观察进程的CPU 占用的time,用ps工具就可以了:
- [root@localhost sched]# cat getpsinfo.sh
- #!/bin/sh
- for((i = 0; i < 40; i++))
- do
- ps -C test -o pid,pri,cmd,time,psr >>psinfo.log 2>&1
- sleep 2;
- done
第二个脚本比较复杂是systemtap脚本,观察名字为test的进程相关的上下文切换,谁替换了test,或者test替换了谁,同时记录下test进程的退出: