一次脑残的记录: Linux 中实时任务调度与优先级
背景知识:Linux 调度策略
关于进程的调度策略,不同的操作系统有不同的整体目标,因此调度算法也就各不相同。
这需要根据进程的类型(计算密集型?IO密集型?)、优先级等因素来进行选择。
对于 Linux x86 平台来说,一般采用的是 CFS
:完全公平调度算法。
之所以叫做完全公平,是因为操作系统以每个线程占用 CPU
的比率来进行动态的计算,操作系统希望每一个进程都能够平均的使用 CPU
这个资源,雨露均沾。
我们在创建一个线程的时候,默认就是这个调度算法 SCHED_OTHER
,默认的优先级为 0
。
PS: 在 Linux 操作系统中,线程的内核对象与进程的内核对象(其实就是一些结构体变量)是很类似的,所以线程可以说是轻量级的进程。
在本文中,可以把线程约等于进程,有的地方也可能称为任务,在不同的语境下有一些不同的惯用说法。
可以这么理解:如果系统中一共有 N
个进程,那么每个进程会得到 1/N
的执行机会。每个进程执行一段时间之后,就被调出,换下一个进程执行。
如果这个 N
的数量太大了,导致每个进程刚开始执行时,分给它的时间就到了。如果这个时候就进行任务调度,那么系统的资源就耗费在进程上下文切换上去了。
因此,操作系统引入了最小粒度,也就是每个进程都有一个最小的执行时间保证,称作时间片。
除了 SCHED_OTHER
调度算法,Linux
系统还支持两种实时调度策略:
SCHED_FIFO:根据进程的优先级进行调度,一旦抢占到 CPU 则一直运行,直达自己主动放弃或被被更高优先级的进程抢占;
SCHED_RR:在 SCHED_FIFO 的基础上,加上了时间片的概念。当一个进程抢占到 CPU 之后,运行到一定的时间后,调度器会把这个进程放在 CPU 中,当前优先级进程队列的末尾,然后选择另一个相同优先级的进程来执行;
本文想测试的就是 SCHED_FIFO
与普通的 SCHED_OTHER
这两种调度策略混合的情况。
背景知识:Linux 线程优先级
在 Linux
系统中,优先级的管理显得比较混乱,先看下面这张图:
这张图表示的是内核中的优先级,分为两段。
前面的数值 0-99
是实时任务,后面的数值 100-139
是普通任务。
数值越低,代表这个任务的优先级越高。
数值越低,代表这个任务的优先级越高。
数值越低,代表这个任务的优先级越高。
再强调一下,以上是从内核角度来看的优先级。
好了,重点来了:
我们在应用层创建线程的时候,设置了一个优先级数值,这是从应用层角度来看的优先级数值。
但是内核并不会直接使用应用层设置的这个数值,而是经过了一定的运算,才得到内核中所使用的优先级数值(0-139
)。
1. 对于实时任务
我们在创建线程的时候,可以通过下面这样的方式设置优先级数值(0-99
):
struct sched_param param;
param.__sched_priority = xxx;
当创建线程函数进入内核层面的时候,内核通过下面这个公式来计算真正的优先级数值:
kernel priority = 100 - 1 - param.__sched_priority
如果应用层传入数值 0
,那么在内核中优先级数值就是 99
,在所有实时任务中,它的优先级是最低的。
如果应用层传输数值 99
,那么在内核中优先级数值就是 0
,在所有实时任务中,它的优先级是最高的。
因此,从应用层的角度看,传输人优先级数值越大,线程的优先级就越高;数值越小,优先级就越低。
与内核角度是完全相反的!
2. 对于普通任务
调整普通任务的优先级,是通过 nice
值来实现的,内核中也有一个公式来把应用层传入的 nice
值,转成内核角度的优先级数值:
kernel prifoity = 100 + 20 + nice
nice
的合法数值是:-20 - 19。
如果应用层设置线程 nice
数值为 -20
,那么在内核中优先级数值就是 100
,在所有的普通任务中,它的优先级是最高的。
如果应用层设置线程 nice
数值为 19
,那么在内核中优先级数值就是 139
,在所有的普通任务中,它的优先级是最低的。
因此,从应用层的角度看,传输人优先级数值越小,线程的优先级就越高;数值越大,优先级就越低。
与内核角度是完全相同的!
背景知识交代清楚了,终于可以进行代码测试了!
测试代码说明
注意点:
#define _GNU_SOURCE
必须在#include <sched.h>
之前定义;
#include <sched.h>
必须在#include <pthread.h>
之前包含进来;
// filename: test.c
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <pthread.h>// 用来打印当前的线程信息:调度策略是什么?优先级是多少?
void get_thread_info(const int thread_index)
{int policy;struct sched_param param;printf("\n====> thread_index = %d \n", thread_index);pthread_getschedparam(pthread_self(), &policy, ¶m);if (SCHED_OTHER == policy)printf("thread_index %d: SCHED_OTHER \n", thread_index);else if (SCHED_FIFO == policy)printf("thread_index %d: SCHED_FIFO \n", thread_index);else if (SCHED_RR == policy)printf("thread_index %d: SCHED_RR \n", thread_index);printf("thread_index %d: priority = %d \n", thread_index, param.sched_priority);
}// 线程函数,
void *thread_routine(void *args)
{// 参数是:线程索引号。四个线程,索引号从 1 到 4,打印信息中使用。int thread_index = *(int *)args;// 为了确保所有的线程都创建完毕,让线程睡眠1秒。sleep(1);// 打印一下线程相关信息:调度策略、优先级。get_thread_info(thread_index);long num = 0;for (int i = 0; i < 10; i++){for (int j = 0; j < 5000000; j++){// 没什么意义,纯粹是模拟 CPU 密集计算。float f1 = ((i+1) * 345.45) * 12.3 * 45.6 / 78.9 / ((j+1) * 4567.89);float f2 = (i+1) * 12.3 * 45.6 / 78.9 * (j+1);float f3 = f1 / f2;}// 打印计数信息,为了能看到某个线程正在执行printf("thread_index %d: num = %ld \n", thread_index, num++);}// 线程执行结束printf("thread_index %d: exit \n", thread_index);return 0;
}void main(void)
{// 一共创建四个线程:0和1-实时线程,2和3-普通线程(非实时)int thread_num = 4;// 分配的线程索引号,会传递给线程参数int index[4] = {1, 2, 3, 4};// 用来保存 4 个线程的 id 号pthread_t ppid[4];// 用来设置 2 个实时线程的属性:调度策略和优先级pthread_attr_t attr[2];struct sched_param param[2];// 实时线程,必须由 root 用户才能创建if (0 != getuid()){printf("Please run as root \n");exit(0);}// 创建 4 个线程for (int i = 0; i < thread_num; i++){if (i <= 1) // 前2个创建实时线程{// 初始化线程属性pthread_attr_init(&attr[i]);// 设置调度策略为:SCHED_FIFOpthread_attr_setschedpolicy(&attr[i], SCHED_FIFO);// 设置优先级为 51,52。param[i].__sched_priority = 51 + i;pthread_attr_setschedparam(&attr[i], ¶m[i]);// 设置线程属性:不要继承 main 线程的调度策略和优先级。pthread_attr_setinheritsched(&attr[i], PTHREAD_EXPLICIT_SCHED);// 创建线程pthread_create(&ppid[i], &attr[i],(void *)thread_routine, (void *)&index[i]);}else // 后两个创建普通线程{pthread_create(&ppid[i], 0, (void *)thread_routine, (void *)&index[i]);}}// 等待 4 个线程执行结束for (int i = 0; i < 4; i++)pthread_join(ppid[i], 0);for (int i = 0; i < 2; i++)pthread_attr_destroy(&attr[i]);
}
编译成可执行程序的指令:
gcc -o test test.c -lpthread
首先说一下预期结果,如果没有预期结果,那其他任何问题都压根不用谈了。
一共有 4
个线程:
线程索引号 1和2:是实时线程(调度策略是 SCHED_FIFO,优先级是 51,52);
线程索引号 3和4:是普通线程(调度策略是 SCHED_OTHER, 优先级是 0);
我的测试环境是:Ubuntu16.04
,是一台安装在 Windows10
上面的虚拟机。
我期望的结果是:
首先打印 1 号和 2 号这两个线程的信息,因为它俩是实时任务,需要优先被调度;
1 号线程的优先级是 51,小于 2 号线程的优先级 52,因此应该是 2 号线程结束之后,才轮到 1 号线程执行;
3 号和 4 号线程是普通进程,它俩需要等到 1 号和 2 号线程全部执行结束之后才开始执行,并且 3 号和 4 号线程应该是交替执行,因为它俩的调度策略和优先级都是一样的。
我满怀希望的在工作电脑中测试,打印结果如下:
====> thread_index = 4
thread_index 4: SCHED_OTHER
thread_index 4: priority = 0 ====> thread_index = 1
thread_index 1: SCHED_FIFO
thread_index 1: priority = 51 ====> thread_index = 2
thread_index 2: SCHED_FIFO
thread_index 2: priority = 52
thread_index 2: num = 0
thread_index 4: num = 0 ====> thread_index = 3
thread_index 3: SCHED_OTHER
thread_index 3: priority = 0
thread_index 1: num = 0
thread_index 2: num = 1
thread_index 4: num = 1
thread_index 3: num = 0
thread_index 1: num = 1
thread_index 2: num = 2
thread_index 4: num = 2
thread_index 3: num = 1后面打印内容不用输出了,因为前面已经出现了问题。
问题很明显:为什么 4 个线程为什么被同时执行了?
1
号和 2
号这两个线程应该被优先执行啊,因为它俩是实时任务!
怎么结果是这个样子?彻底凌乱了,一点都不符合预期!
想不出个所以然,只能求助网络!但是没有找到有价值的线索。
其中有一个信息涉及到 Linux
系统的调度策略,这里记录一下。
Linux
系统中,为了不让实时任务彻底占据 CPU
资源,会让普通任务有很小的一段时间缝隙来执行。
在目录 /proc/sys/kernel
下面,有 2
个文件,用来限制实时任务占用 CPU
的时间:
sched_rt_runtime_us: 默认值 950000
sched_rt_period_us: 默认值 1000000
意思是:在 1000000
微秒(1秒)的周期内,实时任务占用 950000
微秒(0.95秒),剩下的 0.05
秒留给普通任务。
如果没有这个限制的话,假如某个 SCHED_FIFO
任务的优先级特别高,恰巧出了 bug
:一直占据 CPU
资源不放弃,那么我们压根就没有机会来 kill
掉这个实时任务,因为此时系统无法调度其他的任何进程来执行。
而有了这个限制呢,我们就可以利用这 0.05
秒的执行时间,来 kill
掉有 bug
的那个实时任务。
回到正题:资料上说,如果实时任务没有被优先调度,可以把这个时间限制删掉就可以了。方法是:
sysctl -w kernel.sched_rt_runtime_us=-1
我照做之后,依旧无效!
换一台虚拟机,继续测试
难道是电脑环境的问题吗?于是,把测试代码放到另一台笔记本里的虚拟机 Ubuntu14.04
里测试。
编译的时候,有一个小问题,提示错误:
error: ‘for’ loop initial declarations are only allowed in C99 mode
只要把编译指令中添加 C99 标准就可以了:
gcc -o test test.c -lpthread -std=c99
执行程序,打印信息如下:
====> thread_index = 2 ====> thread_index = 1
thread_index 1: SCHED_FIFO
thread_index 1: priority = 51
thread_index 2: SCHED_FIFO
thread_index 2: priority = 52
thread_index 1: num = 0
thread_index 2: num = 0
thread_index 2: num = 1
thread_index 1: num = 1
thread_index 2: num = 2
thread_index 1: num = 2
thread_index 2: num = 3
thread_index 1: num = 3
thread_index 2: num = 4
thread_index 1: num = 4
thread_index 2: num = 5
thread_index 1: num = 5
thread_index 2: num = 6
thread_index 1: num = 6
thread_index 2: num = 7
thread_index 1: num = 7
thread_index 2: num = 8
thread_index 1: num = 8
thread_index 2: num = 9
thread_index 2: exit ====> thread_index = 4
thread_index 4: SCHED_OTHER
thread_index 4: priority = 0
thread_index 1: num = 9
thread_index 1: exit ====> thread_index = 3
thread_index 3: SCHED_OTHER
thread_index 3: priority = 0
thread_index 3: num = 0
thread_index 4: num = 0
thread_index 3: num = 1
thread_index 4: num = 1
thread_index 3: num = 2
thread_index 4: num = 2
thread_index 3: num = 3
thread_index 4: num = 3
thread_index 3: num = 4
thread_index 4: num = 4
thread_index 3: num = 5
thread_index 4: num = 5
thread_index 3: num = 6
thread_index 4: num = 6
thread_index 3: num = 7
thread_index 4: num = 7
thread_index 3: num = 8
thread_index 4: num = 8
thread_index 3: num = 9
thread_index 3: exit
thread_index 4: num = 9
thread_index 4: exit
1
号和 2
号线程同时执行,完毕之后,再 3
号和 4
号线程同时执行。
但是这同样也不符合预期:2
号线程的优先级比 1
号线程高,应该优先执行才对!
不知道应该怎么查这个问题了,想不出思路,只好请教 Linux
内核的大神,建议检查一下内核版本。
这时,我才想起来在 Ubuntu16.04
这台虚拟机上因为某种原因,降过内核版本。
往这个方向去排查了一下,最后确认也不是内核版本的差异导致的问题。
比较结果,寻找差异
只好再回过头来看一下这两次次打印信息的差异:
工作电脑里的 Ubuntu16.04 中:4 个线程同时调度执行,调度策略和优先级都没有起作用;
笔记本里的 Ubuntu14.04 中:1 号和 2 号实时任务被优先执行了,说明调度策略起作用了,但是优先级没有起作用;
突然, CPU
的亲和性从脑袋里蹦了出来!
紧接着立马感觉到问题出在哪里了:这TMD大概率就是多核引起的问题!
于是我把这 4
个线程都绑定到 CPU0 上去,也就是设置 CPU 亲和性。
在线程入口函数 thread_routine
的开头,增加下面的代码:
cpu_set_t mask;
int cpus = sysconf(_SC_NPROCESSORS_CONF);
CPU_ZERO(&mask);
CPU_SET(0, &mask);
if (pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) < 0)
{printf("set thread affinity failed! \n");
}
然后继续在 Ubuntu16.04
虚拟机中验证,打印信息很完美,完全符合预期:
====> thread_index = 1 ====> thread_index = 2
thread_index 2: SCHED_FIFO
thread_index 2: priority = 52
thread_index 2: num = 0
。。。
thread_index 2: num = 9
thread_index 2: exit
thread_index 1: SCHED_FIFO
thread_index 1: priority = 51
thread_index 1: num = 0
。。。
thread_index 1: num = 9
thread_index 1: exit ====> thread_index = 3
thread_index 3: SCHED_OTHER
thread_index 3: priority = 0 ====> thread_index = 4
thread_index 4: SCHED_OTHER
thread_index 4: priority = 0
thread_index 3: num = 0
thread_index 4: num = 0
。。。
thread_index 4: num = 8
thread_index 3: num = 8
thread_index 4: num = 9
thread_index 4: exit
thread_index 3: num = 9
thread_index 3: exit
至此,问题真相大白:就是多核处理器导致的问题!
而且这两台测试的虚拟机,安装的时候分配的 CPU
核心是不同的,所以才导致打印结果的不同。
真相大白
最后,再确认一下这 2
个虚拟机中的 CPU
信息:
Ubuntu 16.04
中 cpuinfo
信息:
$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 158
model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
stepping : 10
cpu MHz : 2807.996
cache size : 9216 KB
physical id : 0
siblings : 4
core id : 0
cpu cores : 4
。。。其他信息processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 158
model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
stepping : 10
cpu MHz : 2807.996
cache size : 9216 KB
physical id : 0
siblings : 4
core id : 1
cpu cores : 4
。。。其他信息processor : 2
vendor_id : GenuineIntel
cpu family : 6
model : 158
model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
stepping : 10
cpu MHz : 2807.996
cache size : 9216 KB
physical id : 0
siblings : 4
core id : 2
cpu cores : 4
。。。其他信息processor : 3
vendor_id : GenuineIntel
cpu family : 6
model : 158
model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
stepping : 10
cpu MHz : 2807.996
cache size : 9216 KB
physical id : 0
siblings : 4
core id : 3
cpu cores : 4
。。。其他信息
在这台虚拟机中,正好有 4
个核心,而我的测试代码正好也创建了 4
个线程,于是每个核心被分配一个线程,一个都不闲着,同时执行。
因此打印信息中显示 4
个线程是并行执行的。
这个时候,什么调度策略、什么优先级,都不起作用了!(准确的说:调度策略和优先级,在线程所在的那个 CPU
中是起作用的)
如果我在测试代码中,一开始就创建 10
个线程,很可能会更快发现问题!
再来看看笔记本电脑里虚拟机 Ubuntu14.04
的 CPU
信息:
$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 142
model name : Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
stepping : 9
microcode : 0x9a
cpu MHz : 2304.000
cache size : 4096 KB
physical id : 0
siblings : 2
core id : 0
cpu cores : 2
。。。其他信息processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 142
model name : Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
stepping : 9
microcode : 0x9a
cpu MHz : 2304.000
cache size : 4096 KB
physical id : 0
siblings : 2
core id : 1
cpu cores : 2
。。。其他信息
在这台虚拟机中,有 2
个核心,于是 2
个实时任务 1
号和 2
号被优先执行(因为是 2
个核心同时执行,所以这 2
个任务的优先级也就没什么意义了),结束之后,再执行 3
号和 4
号线程。
再思考一下
这一圈测试下来,真的想用键盘敲自己的脑袋,怎么就没有早点考虑到多核的因素呢?!
深层的原因:
之前的很多项目,都是 ARM、mips、STM32等单核情况,思维定式让我没有早点意识到多核这个屏体因素;
做过的一些 x86 平台项目,并没有涉及到实时任务这样的要求。一般都是使用系统默认的调度策略,这也是 Linux x86 作为通用电脑,在调度策略上所关注的重要指标:让每一个任务都公平的使用 CPU 资源。
随着 x86
平台在工控领域的逐渐应用,实时性问题就显得更突出、更重要了。
所以才有了 Windows
系统中的 intime
,Linux
系统中的 preempt
、xenomai
等实时补丁。
------ End ------
推荐阅读
【1】C语言指针-从底层原理到花式技巧,用图文和代码帮你讲解透彻
【2】一步步分析-如何用C实现面向对象编程
【3】原来gdb的底层调试原理这么简单
【4】内联汇编很可怕吗?看完这篇文章,终结它!
【5】都说软件架构要分层、分模块,具体应该怎么做
一次脑残的记录: Linux 中实时任务调度与优先级相关推荐
- Linux 中实时查看日志的3种方法
Linux 中实时查看日志的3种方法 最近我从cnaaa.com购买了云服务器. 我们大家应该都知道如何在 Linux 中查看文件,比如可以使用 cat 或者 less 命令. 这对于查看静态文件来说 ...
- 查询linux上调度命令,浅析Linux中crontab任务调度
一.创建调度任务 指令 crontab -e 进入当前用户编辑界面 crontab -u 用户名 -e 进入指定用户编辑界面 进入crontab任务编辑界面 任务编写格式 #每分钟执行查看一次/ect ...
- linux中pri=100,浅析Linux中PRI和NI的关系
Linux中: 所有的优先级范围为0-139,一共140个优先级,数值越低优先级越高; 优先级范围100-139称为静态优先级,这个范围里的进程属于非实时进程,调度方式是:SCHED_OTHER,也就 ...
- linux进程调度算法,关于嵌入式Linux系统实时进程调度算法系统详解
1 嵌入式Linux系统分析 1.1 嵌入式系统 嵌入式系统(Embedded Systems)是以应用为中心,以计算机技术为基础,软件硬件可剪裁(可编程.可重构),适用于应用系统对功能.可靠性.成本 ...
- linux中文件记录的时间参数,【Linux】stat命令查看文件的三个时间参数
在Windows中创建一个文件都会有相应的创建时间,修改时间,访问时间来记录文件的一些属性.在Linux中也不例外,文件也有三个时间来记录文件的变动,这三个时间分别是Modification t ...
- Linux 中多终端同步 history 记录
很多文章都是第一时间在语雀进行了更新和发布,公众号的文章只是在语雀平台的基础上进行同步的,而且更新频率也会比语雀要慢.最近弃坑简书,入驻语雀,欢迎阅读原文来语雀一起交流学习. 基本认识 Linux 默 ...
- 2020-11-30 脑残记录
现象 内网基于云桌面进行开发,每周日云桌面都会自动重启,重启后所有程序会被关闭,周一打开时发现,IDEA针对不同项目竟然有不同效果,如图: 项目一: 项目一的状态信息全部正常,可以看到git分支信息, ...
- linux中打开gif图片命令,Terminalizer – 记录Linux终端活动并生成GIF动画
Terminalizer是一个免费的,开源的,简单的,高度可定制的跨平台程序,用于记录您的Linux终端会话并生成动画gif图像或共享网络播放器. 它带有自定义:窗框,字体,颜色,CSS样式; 支持水 ...
- 脑残式网络编程入门(八):你真的了解127.0.0.1和0.0.0.0的区别?
本文由"小姐姐养的狗"原创发布于"小姐姐味道"公众号,原题<127.0.0.1和0.0.0.0地址的区别>,收录时有优化和改动.感谢原作者的分享. ...
最新文章
- 点评主流软件开发技术
- Oracle游标的使用
- ajax 同步和异步
- figma 安装插件_彩色滤光片Figma插件,用于色盲
- Git基础学习(黑马程序员笔记)
- bootdo图片上传
- CentOS配置静态IP
- java gc什么意思_对Java GC的简单理解
- 日语学习之——学习技巧
- MapperReduce初学附加自定义输出的NameWordCount统计
- LaTeX常用表格绘制入门
- 申请免费的域名并且通过FTP上传及通过自己的域名访问自己的网站
- JAVA支付宝蚂蚁金服开放平台沙箱
- vue插槽的理解 slot slot-scop,三种插槽方式,默认插槽,具名插槽,作用域插槽
- 洗礼灵魂,修炼python(80)--全栈项目实战篇(8)—— 计算器
- 终端插双电信卡都能打电话么?
- 【Android车载系列】第5章 AOSP开发环境配置
- 网站加速的【5大因素】
- C# 删除指定Windows端口的进程
- 1650显卡能带动144hz吗_神舟又有一款真香游戏本:不到5K,144Hz屏+1650显卡