一文让你彻底理解Linux内核调度器进程优先级
一、前言
本文主要描述的是进程优先级这个概念。从用户空间来看,进程优先级就是nice value和scheduling priority,对应到内核,有静态优先级、realtime优先级、归一化优先级和动态优先级等概念。我们希望能在第二章将这些相关的概念描述清楚。为了加深理解,在第三章我们给出了几个典型数据流过程的分析。
二、overview
1、蓝图
2、用户空间的视角
在用户空间,进程优先级有两种含义:nice value和scheduling priority。对于普通进程而言,进程优先级就是nice value,从-20(优先级最高)~19(优先级最低),通过修改nice value可以改变普通进程获取cpu资源的比例。随着实时需求的提出,进程又被赋予了另外一种属性scheduling priority,而这些进程被称为实时进程。实时进程的优先级的范围可以通过sched_get_priority_min和sched_get_priority_max,对于linux而言,实时进程的scheduling priority的范围是1(优先级最低)~99(优先级最高)。当然,普通进程也有scheduling priority,被设定为0。
3、内核中的实现
内核中,task struct中有若干和进程优先级有个的成员,如下:
struct task_struct {
...... int prio, static_prio, normal_prio; unsigned int rt_priority;
...... unsigned int policy;
......
}
policy成员记录了该线程的调度策略,而其他的成员表示了各种类型的优先级,下面的小节我们会一一描述。
4、静态优先级
task struct中的static_prio成员。我们称之静态优先级,其特点如下:
(1)值越小,进程优先级越高
(2)0 – 99用于real-time processes(没有实际的意义),100 – 139用于普通进程
(3)缺省值是 120
(4)用户空间可以通过nice()或者setpriority对该值进行修改。通过getpriority可以获取该值。
(5)新创建的进程会继承父进程的static priority。
静态优先级是所有相关优先级的计算的起点,要么继承自父进程,要么用户空间自行设定。一旦修改了静态优先级,那么normal priority和动态优先级都需要重新计算。
5、实时优先级
task struct中的rt_priority成员表示该线程的实时优先级,也就是从用户空间的视角来看的scheduling priority。0是普通进程,1~99是实时进程,99的优先级最高。
6、归一化优先级
task struct中的normal_prio成员。我们称之归一化优先级(normalized priority),它是根据静态优先级、scheduling priority和调度策略来计算得到,代码如下:
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;
}
这里我们先聊聊归一化(Normalization)这个看起来稍微有点晦涩的术语。如果你做过音视频定点算法的优化,应该对这个词不陌生。不同的定点数据有不同的表示,有Q31的,有Q15,这些数据的小数点的位置不同,无法进行比较、加减等操作,因此需要归一化,全部转换成某个特定的数据格式(其实就是确定小数点的位置)。在数学上,1米和1mm在进行操作的时候也需要归一化,全部转换成同一个量纲就OK了。对于这里的优先级,调度器需要综合考虑各种因素,例如调度策略,nice value、scheduling priority等,把这些factor全部考虑进来,归一化成一个数轴上的number,以此来表示其优先级,这就是normalized priority。对于一个线程,其normalized priority的number越小,其优先级越大。
调度策略是deadline的进程比RT进程和normal进程的优先级还要高,因此它的归一化优先级是负数:-1。如果采用实时调度策略,那么该线程的normalized priority和rt_priority相关。task struct中的rt_priority成员是用户空间视角的实时优先级(scheduling priority),MAX_RT_PRIO-1是99,MAX_RT_PRIO-1 - p->rt_priority则翻转了实时进程的scheduling priority,最高优先级是0,最低是98。顺便说一句,normalized priority是99的情况是没有意义的。对于普通进程,normalized priority就是其静态优先级。
7、动态优先级
task struct中的prio成员表示了该线程的动态优先级,也就是调度器在进行调度时候使用的那个优先级。动态优先级在运行时可以被修改,例如在处理优先级翻转问题的时候,系统可能会临时调升一个普通进程的优先级。一般设定动态优先级的代码是这样的:p->prio = effective_prio(p),具体计算动态优先级的代码如下:
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;
}
rt_prio是一个根据当前优先级来确定是否是实时进程的函数,包括两种情况,一种情况是该进程是实时进程,调度策略是SCHED_FIFO或者SCHED_RR。另外一种情况是人为的将该进程提升到RT priority的区域(例如在使用优先级继承的方法解决系统中优先级翻转问题的时候)。在这两种情况下,我们都不改变其动态优先级,即effective_prio返回当前动态优先级p->prio。其他情况,进程的动态优先级跟随归一化的优先级。
三、典型数据流程分析
1、用户空间设定nice value
用户空间设定nice value的操作,在内核中主要是set_user_nice函数实现的,无论是sys_nice或者sys_setpriority,在参数检查和权限检查之后都会调用set_user_nice函数,完成具体的设定。
代码如下:
void set_user_nice(struct task_struct *p, long nice)
{ int old_prio, delta, queued; unsigned long flags; struct rq *rq; rq = task_rq_lock(p, &flags); if (task_has_dl_policy(p) || task_has_rt_policy(p)) {-----------(1) p->static_prio = NICE_TO_PRIO(nice); goto out_unlock; } queued = task_on_rq_queued(p);-------------------(2) if (queued) dequeue_task(rq, p, DEQUEUE_SAVE);p->static_prio = NICE_TO_PRIO(nice);----------------(3) set_load_weight(p); old_prio = p->prio; p->prio = effective_prio(p); delta = p->prio - old_prio;if (queued) { enqueue_task(rq, p, ENQUEUE_RESTORE);------------(2) if (delta < 0 || (delta > 0 && task_running(rq, p)))------------(4) resched_curr(rq); }
out_unlock: task_rq_unlock(rq, p, &flags);
}
(1)如果是实时进程或者deadline类型的进程,那么nice value其实是没有什么实际意义的,不过我们还是设定其静态优先级,当然,这样的设定其实不会起到什么作用的,也不会实际改变调度器行为,因此直接返回,没有dequeue和enqueue的动作。
(2)在step中已经处理了调度策略是RT类和DEADLINE类的进程,因此,执行到这里,只可能是普通进程了,使用CFS算法。如果该task在run queue上(queued 等于true),那么由于我们修改了nice value,调度器需要重新审视当前runqueue中的task。因此,我们需要将该task从rq中摘下,在重新计算优先级之后,再次插入该runqueue对应的runable task的红黑树中。
(3)最核心的代码就是p->static_prio = NICE_TO_PRIO(nice);这一句了,其他的都是side effect。比如说load weight。当cpu一刻不停的运算的时候,其load是100%,没有机会调度到idle进程休息一下。当系统中没有实时进程或者deadline进程的时候,所有的runnable的进程一起来瓜分cpu资源,以此不同的进程分享一个特定比例的cpu资源,我们称之load weight。不同的nice value对应不同的cpu load weight,因此,当更改nice value的时候,也必须通过set_load_weight来更新该进程的cpu load weight。除了load weight,该线程的动态优先级也需要更新,这是通过p->prio = effective_prio(p);来完成的。
(4)delta 记录了新旧线程的动态优先级的差值,当调试了该线程的优先级(delta < 0),那么有可能产生一个调度点,因此,调用resched_curr,给当前正在运行的task做一个标记,以便在返回用户空间的时候进行调度。此外,如果修改当前running状态的task的动态优先级,那么调降(delta > 0)意味着该进程有可能需要让出cpu,因此也需要resched_curr标记当前running状态的task需要reschedule。
资料直通车:最新Linux内核源码资料文档+视频资料
内核学习地址:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈
2、进程缺省的调度策略和调度参数
我们先思考这样的一个问题:在用户空间设定调度策略和调度参数之前,一个线程的default scheduling policy是什么呢?这需要追溯到fork的时候(具体代码在sched_fork函数中),这个和task struct中sched_reset_on_fork设定相关。如果没有设定这个flag,那么说明在fork的时候,子进程跟随父进程的调度策略,如果设定了这个flag,则说明子进程的调度策略和调度参数不能继承自父进程,而是需要设定为default。代码片段如下:
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
…… p->prio = current->normal_prio; -------------------(1) if (unlikely(p->sched_reset_on_fork)) { if (task_has_dl_policy(p) || task_has_rt_policy(p)) {----------(2) p->policy = SCHED_NORMAL; p->static_prio = NICE_TO_PRIO(0); p->rt_priority = 0; } else if (PRIO_TO_NICE(p->static_prio) < 0) p->static_prio = NICE_TO_PRIO(0);p->prio = p->normal_prio = __normal_prio(p); ------------(3) set_load_weight(p); p->sched_reset_on_fork = 0; }
……
}
(1)sched_fork只是fork过程中的一个片段,在fork一开始,dup_task_struct已经复制了一个和父进程完全一个的进程描述符(task struct),因此,如果没有步骤2中的重置,那么子进程是跟随父进程的调度策略和调度参数(各种优先级),当然,有时候为了解决PI问题而临时调升父进程的动态优先级,在fork的时候不宜传递到子进程中,因此这里重置了动态优先级。
(2)缺省的调度策略是SCHED_NORMAL,静态优先级等于120(也就是说nice value等于0),rt priority等于0(普通进程)。不管父进程如何,即便是deadline的进程,其fork的子进程也需要恢复到缺省参数。
(3)既然调度策略和静态优先级已经修改了,那么也需要更新动态优先级和归一化优先级。此外,load weight也需要更新。一旦子进程中恢复到了缺省的调度策略和优先级,那么sched_reset_on_fork这个flag已经完成了历史使命,可以clear掉了。
OK,至此,我们了解了在fork过程中对调度策略和调度参数的处理,这里还是要追加一个问题:为何不一切继承父进程的调度策略和参数呢?为何要在fork的时候reset to default呢?在linux中,对于每一个进程,我们都会进行资源限制。例如对于那些实时进程,如果它持续消耗cpu资源而没有发起一次可以引起阻塞的系统调用,那么我们猜测这个realtime进程跑飞了,从而锁住了系统。对于这种情况,我们要进行干预,因此引入了RLIMIT_RTTIME这个per-process的资源限制项。但是,如果用户空间的realtime进程通过fork其实也可以绕开RLIMIT_RTTIME这个限制,从而肆意的攫取cpu资源。然而,机智的内核开发人员早已经看穿了这一切,为了防止实时进程“泄露”到其子进程中,sched_reset_on_fork这个flag被提出来。
3、用户空间设定调度策略和调度参数
通过sched_setparam接口函数可以修改rt priority的调度参数,而通过sched_setscheduler功能会更强一些,不但可以设定rt priority,还可以设定调度策略。而sched_setattr是一个集大成之接口,可以设定一个线程的调度策略以及该调度策略下的调度参数。当然,对于内核,这些接口都通过__sched_setscheduler这个内核函数来完成对指定线程调度策略和调度参数的修改。
__sched_setscheduler分成两个部分,首先进行安全性检查和参数检查,其次进行具体的设定。
我们先看看安全性检查。如果用户空间可以自由的修改调度策略和调度优先级,那么世界就乱套了,每个进程可能都想把自己的调度策略和优先级提升上去,从而获取足够的CPU 资源。因此用户空间设定调度策略和调度参数要遵守一定的规则:如果没有CAP_SYS_NICE的能力,那么基本上该线程能被允许的操作只是降级而已。例如从SCHED_FIFO修改成SCHED_NORMAL,异或不修改scheduling policy,而是降低静态优先级(nice value)或者实时优先级(scheduling priority)。这里例外的是SCHED_DEADLINE的设定,按理说如果进程本身的调度策略就是SCHED_DEADLINE,那么应该允许“优先级”降低的操作(这里用优先级不是那么合适,其实就是减小run time,或者加大period,这样可以放松对cpu资源的获取),但是目前的4.4.6内核不允许(也许以后版本的内核会允许)。此外,如果没有CAP_SYS_NICE的能力,那么设定调度策略和调度参数的操作只能是限于属于同一个登录用户的线程。如果拥有CAP_SYS_NICE的能力,那么就没有那么多限制了,可以从普通进程提升成实时进程(修改policy),也可以提升静态优先级或者实时优先级。
具体的修改比较简单,是通过__setscheduler_params函数完成,其实也就是是根据sched_attr中的参数设定到task struct相关成员中,大家可以自行阅读代码进行理解。
相关文章:

一文让你彻底理解Linux内核调度器进程优先级
一、前言 本文主要描述的是进程优先级这个概念。从用户空间来看,进程优先级就是nice value和scheduling priority,对应到内核,有静态优先级、realtime优先级、归一化优先级和动态优先级等概念。我们希望能在第二章将这些相关的概念描述清楚。…...

Java 抽象类和接口
文章目录一、抽象类1. 抽象类定义2. 抽象类成员特点二、接口1. 接口概述2. 接口成员特点3. 类和接口的关系4. 抽象类和接口的区别5. 接口案例三、形参和返回值一、抽象类 1. 抽象类定义 在 Java 中,一个没有方法体的方法应该定义为抽象方法,而类中如果…...

三行代码让你的git记录保持整洁
前言笔者最近在主导一个项目的架构迁移工作,由于迁移项目的历史包袱较重,人员合作较多,在迁移过程中免不了进行多分支、多次commit的情况,时间一长,git的提交记录便混乱不堪,随便截一个图形化的git提交历史…...

阿里巴巴内网 Java 面试 2000 题解析(2023 最新版)
前言 这份面试清单是今年 1 月份之后开始收集的,一方面是给公司招聘用,另一方面是想用它来挖掘在 Java 技术栈中,还有一些知识点是我还在探索的,我想找到这些技术盲点,然后修复它,以此来提高自己的技术水平…...

网络应用之静态Web服务器
静态Web服务器-返回固定页面数据学习目标能够写出组装固定页面数据的响应报文1. 开发自己的静态Web服务器实现步骤:编写一个TCP服务端程序获取浏览器发送的http请求报文数据读取固定页面数据,把页面数据组装成HTTP响应报文数据发送给浏览器。HTTP响应报文数据发送完…...
IndexDB 浏览器服务器
IndexDB 浏览器服务器 文章部分内容引用: https://www.ruanyifeng.com/blog/2018/07/indexeddb.html https://juejin.cn/post/7026900352968425486#heading-15 基本概念 数据库:IDBDatabase 对象对象仓库:IDBObjectStore 对象索引࿱…...

追梦之旅【数据结构篇】——详解C语言实现链队列
详解C语言实现链队列~😎前言🙌整体实现内容分析💞预备小知识🙌1.链队列头文件编写🙌2.链队列功能文件(Queue.c )编写:🙌1)初始化函数实现2)销毁函…...

SpringMVC - 13 - SpringMVC执行流程
文章目录1、SpringMVC常用组件2、DispatcherServlet初始化过程a>初始化WebApplicationContextb>创建WebApplicationContextc>DispatcherServlet初始化策略3、DispatcherServlet调用组件处理请求a>processRequest()b>doService()c>doDispatch()d>processDi…...
6091: 斐波那契数列
描述一个斐波那契序列,F(0) 0, F(1) 1, F(n) F(n-1) F(n-2) (n>2),根据n的值,计算斐波那契数F(n)。输入输入数据的第一行为测试用例的个数t,接下来为t行,每行为一个整数n(2≤n≤40)。输出…...

任何人均可上手的数据库与API搭建平台
编写API可能对于很多后端开发人员来说,并不是什么难事儿,但如果您主要从事前端功能,那么可能还是有一些门槛。 那么有没有工具可以帮助我们降低编写API的学习门槛和复杂度呢? 今天就来给大家推荐一个不错的开源工具:…...

Ubuntu(虚拟机)的Anaconda 及使用
安装Anaconda 使用firefox打开Ananconda网址Anaconda | The Worlds Most Popular Data Science Platform 下载后有.sh文件: Anaconda3-2022.10-Linux-x86_64.sh 进入所在目录打开终端并输入 $ bash Anaconda3-2022.10-Linux-x86_64.sh 然后开始安装。 对于给…...

Git ---- IDEA集成 GitHub
Git ---- IDEA集成 GitHub1. 设置 GitHub 账号2. 分享工程到 GitHub3. push 推送本地库到远程库4. pull 拉取远程库到本地库5. clone 克隆远程库到本地1. 设置 GitHub 账号 新版的 IDEA 选择之后会自动登录,就不需要设置 token 了。 如果是老版的 IDEA 的话&…...
opencv提取结构化文本总结
扫描文件表格识别 1.识别结构 situation1 有明确表格结构 1.纠正表格偏移角度(获取最大轮廓,计算最小的矩形,变换坐标截取矩形) 获取面积最大轮廓 _, contours, HIERARCHY cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2…...

JVM知识体系学习八:OOM的案例(承接上篇博文,可以作为面试中的案例)
文章目录前言一、概述二、案例二三、案例:方法区内存溢出1、代码:LambdaGC.java2、元空间内存溢出日志3、分析4、疑问*****四、案例:直接内存溢出问题(少见)(尽量不说)五、案例:栈内存溢出问题1…...

Redis的持久化方式
Redis支持两种方式的持久化,一种是RDB方式、另一种是AOF(append-only-file)方式,两种持久化方式可以单独使用其中一种,也可以将这两种方式结合使用。 •RDB:根据指定的规则“定时”将内存中的数据存储在硬…...

【unity游戏制作-mango的冒险】-4.场景二的镜头和法球特效跟随
👨💻个人主页:元宇宙-秩沅 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 本文由 秩沅 原创 收录于专栏:unity游戏制作 ⭐mango的冒险场景二——镜头和法球特效跟随⭐ 文章目录⭐mango的冒险场景二——镜…...

handwrite-1
-------------------- 实现防抖函数(debounce) 防抖函数原理:把触发非常频繁的事件合并成一次去执行 在指定时间内只执行一次回调函数,如果在指定的时间内又触发了该事件,则回调函数的执行时间会基于此刻重新开始计算…...
【一天一门编程语言】Pascal 语言程序设计极简教程
Pascal 语言程序设计极简教程 用 markdown 格式输出答案。 不少于3000字。细分到2级目录。 文章目录 Pascal 语言程序设计极简教程一、Pascal简介1.1 Pascal历史1.2 Pascal的特点1.3 Pascal的应用二、Pascal语言程序设计2.1 Pascal编程环境2.2 Pascal的基本语法2.3 Pascal程序…...

【基础篇0】Linux下ANACONDA与TF-LITE环境配置
0 写在前面:一些摸索与总结 对于Linux系统,我发现不管是电脑x86的Ubuntu还是树莓派arm的raspberry系统,在系统安装完毕后,总是自带一个特定版本的python. 例如我的ubuntu22.04自带的python版本是3.10,而高版本的py…...

TCP协议原理二
文章目录四、滑动窗口二、流量窗口三、拥塞控制四、滑动窗口 前面我们学习了 确认应答,超时重传,连接管理,这些机制都为我们TCP的可靠性提供了保证,当然在保证TCP的可靠性的同时,传输效率也受到了一定的影响ÿ…...
椭圆曲线密码学(ECC)
一、ECC算法概述 椭圆曲线密码学(Elliptic Curve Cryptography)是基于椭圆曲线数学理论的公钥密码系统,由Neal Koblitz和Victor Miller在1985年独立提出。相比RSA,ECC在相同安全强度下密钥更短(256位ECC ≈ 3072位RSA…...

Unity3D中Gfx.WaitForPresent优化方案
前言 在Unity中,Gfx.WaitForPresent占用CPU过高通常表示主线程在等待GPU完成渲染(即CPU被阻塞),这表明存在GPU瓶颈或垂直同步/帧率设置问题。以下是系统的优化方案: 对惹,这里有一个游戏开发交流小组&…...
从零实现富文本编辑器#5-编辑器选区模型的状态结构表达
先前我们总结了浏览器选区模型的交互策略,并且实现了基本的选区操作,还调研了自绘选区的实现。那么相对的,我们还需要设计编辑器的选区表达,也可以称为模型选区。编辑器中应用变更时的操作范围,就是以模型选区为基准来…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)
前言: 最近在做行为检测相关的模型,用的是时空图卷积网络(STGCN),但原有kinetic-400数据集数据质量较低,需要进行细粒度的标注,同时粗略搜了下已有开源工具基本都集中于图像分割这块,…...
【Go语言基础【13】】函数、闭包、方法
文章目录 零、概述一、函数基础1、函数基础概念2、参数传递机制3、返回值特性3.1. 多返回值3.2. 命名返回值3.3. 错误处理 二、函数类型与高阶函数1. 函数类型定义2. 高阶函数(函数作为参数、返回值) 三、匿名函数与闭包1. 匿名函数(Lambda函…...

招商蛇口 | 执笔CID,启幕低密生活新境
作为中国城市生长的力量,招商蛇口以“美好生活承载者”为使命,深耕全球111座城市,以央企担当匠造时代理想人居。从深圳湾的开拓基因到西安高新CID的战略落子,招商蛇口始终与城市发展同频共振,以建筑诠释对土地与生活的…...

免费PDF转图片工具
免费PDF转图片工具 一款简单易用的PDF转图片工具,可以将PDF文件快速转换为高质量PNG图片。无需安装复杂的软件,也不需要在线上传文件,保护您的隐私。 工具截图 主要特点 🚀 快速转换:本地转换,无需等待上…...
小木的算法日记-多叉树的递归/层序遍历
🌲 从二叉树到森林:一文彻底搞懂多叉树遍历的艺术 🚀 引言 你好,未来的算法大神! 在数据结构的世界里,“树”无疑是最核心、最迷人的概念之一。我们中的大多数人都是从 二叉树 开始入门的,它…...
【Kafka】Kafka从入门到实战:构建高吞吐量分布式消息系统
Kafka从入门到实战:构建高吞吐量分布式消息系统 一、Kafka概述 Apache Kafka是一个分布式流处理平台,最初由LinkedIn开发,后成为Apache顶级项目。它被设计用于高吞吐量、低延迟的消息处理,能够处理来自多个生产者的海量数据,并将这些数据实时传递给消费者。 Kafka核心特…...