任务调度器:从入门到放弃(二)

article/2025/6/8 22:23:22

理想与现实的差异

在上文中,我们提到,一个优先级100的线程,跟一个优先级为120的线程进行公平调度的资源抢占的话。

从抓取的一份systrace上简单的对比来看,

其份额大概是24:1856  (systrace上随便框的,数据不够准确,看个大概就行了)。大概有个概念就行了,这里不准备深入分析优先级到weight的换算,以及weight到资源占比的换算。

而当两个线程的优先级都是120的话,我们可以看到。这两个线程的资源占比在1:1. 每间隔4ms轮转一次。

那么问题来了,为什么是4ms,而不是3ms或者5ms 呢?

答案是因为linux的时钟tick的周期为4ms,也就是250HZ。

上面提到的“proportional scheduling”,是有一整套理论数学模型的,最近kernel6.6上引入的eevdf,其最早可以追溯到一篇1996年的论文。但是从理论模型到工程实现,却有着种种限制。就跟原子弹的爆炸原理大家都知道,但是能造出原子弹的没几个。

当CFS的调度器在选取任务的时候,pick_next_task从红黑树的拿到最左节点。调度器是知道这个线程1的virtual runtime的(简称V1),同时也是知道左次节点的线程2的virtual runtime(简称V2),随着线程1的在CPU上运行,线程1的virtual runtime V1一直在递增。是一旦V1超过V2的时候,线程1就立马让出CPU给线程2吗?

答案是否定的。因为从工程实现上不可取。从理论模型上,我们希望有一个“上帝视角”在监测线程的虚拟时间的变化,同时把它跟红黑树上最小虚拟时间进行比较。一旦超过,就立马触发进程切换。 这虽然可以用定时器来进行控制,但是会带来额外的开销。毕竟CPU最终是为了提供计算能力,在一定时间内提供的计算能力越高,效果越好,比如make -j32进行编译。工作量是一定的,我们希望越快结束越好。这就提出了另外一个要求,就是throughout。throughout越高越好。

但是任务调度器自身的代码逻辑并不产生throughout,反而是在额外的消耗CPU的资源。从throughout的角度,其实期望调度器越少的参与越好。

调度器的事件驱动

调度器没有上帝视角,也没有一个监视器在时时刻刻监视系统中进程的情况变化。因此大部分的调度器都折中地选择了通过事件来驱动。

那么都是哪些事件呢?这里我们借用一下高通walt代码里面的内容。

52  const char *task_event_names[] = {

53         "PUT_PREV_TASK",

54         "PICK_NEXT_TASK",

55         "TASK_WAKE",

56         "TASK_MIGRATE",

57         "TASK_UPDATE",

58         "IRQ_UPDATE"

59  };

由于我们上述的例子里面讲到的一个特殊的情况,即2个不会主动终止的死循环线程的调度轮换与配额问题。因此我们实际遇到的最多的两个事件是:

1.task wake

2.irq update

第一个task wake是指当一个线程A(也可能是隐藏在某个进程里面的中断)唤醒线程B的时候,需要检查一下,这个被唤醒的线程是否需要抢占当前CPU上正在运行的任务。

第二个irq update也比较好理解,最典型的就是我们上面提到的kernel tick。tick是一个arch timer的irq。每间隔4ms触发一次中断,进行线程状态的更新与判断。

这些event事件,有一种“过了这个村就没那个店”的感觉。

以task wake为例,因为CFS是每次都是选取虚拟时间最小的线程进行运行。假如被唤醒的线程B的虚拟时间,跟CPU上当前运行的线程的虚拟时间相比,要小一些,但是这个差异可以忽略不计。那么调度器就判断,抢占不成立。也就是被唤醒的线程B无法抢占当前正在CPU上运行的线程。当前线程继续运行。

那么什么时候线程B才会被调度出去呢?答案是“不确定”,依赖于事件, 最长要等下一个tick的发生。如果不凑巧,当前线程处于关中断状态,比如调用了spinlock_irqsave关闭了CPU的local irq呢? 这时候tick中断也不会发生,需要一直等下去。因此正在运行的线程B有可能并不是系统中“实际”虚拟时间最小的,只是没有事件来驱动更新数据而已。 

虽然理论模型是比较完善的,但是在工程实现过程中进行各种取舍,大部分调度器(包括freebsd以及ios)都是基于事件触发的。就给调度器的实际运行结果带来了一些不确定性,它并不是一台完美无瑕的精准的机器。

上图是一张示意图,横坐标是实际时间,纵坐标是一些负载或者虚拟时间的统计变量。黑色斜线是理论模式里面,虚拟时间随着时间增加的理想情形。但是因为整个调度器是基于事件触发的,实际情况是红色折线。每次事件发生时,虚拟时间发生统计上的跳变。理论上的连续,变成了实际的离散。

这些离散事件引起的“跳变”让整个调度器的运作并不是非常精准(当然,如果事件足够密集的话,可以构成一条近似的连续线条)。因此很多时候,tick周期4ms就成为托底的机制。因此这个tick的周期成为调度器的一个精度保障(关抢占、关中断这两种情况比较特殊,暂不在讨论范围内)。

如果我们将linux的心跳从250HZ改成1000HZ,那么整个调度器的精度会得到相应的提升,但是也会带来更多的开销,导致系统的throughout一定程度上的降低。

在上面我们引入了2个调度器的指标,其中一个是throughout吞吐量,我们当然希望吞吐量越大越好。因为吞吐量才是实际的输出,而调度器本身内部统计跟决策逻辑产生的是损耗。就跟红十会一样,会收“管理费”,所以大家才把钱捐给韩红的基金会。

另外一点是调度器的精度,或者latency延迟。这个从事performance分析的同学比较熟悉。 

图:线程1932从被唤醒(被线程1983唤醒)到获取到CPU资源过程中,有873us的延迟

钱捐出去了,我们希望这些钱或者物资更快的达到需要的人手里,而不是在红十会的仓库里。我们希望调度器更好的发挥分时复用的能力,让各个进程看起来都是得到“实时”的调度。如果延迟比较大的话,就会产生”不实时“的感觉。 

事实上这两个指标之间是矛盾的,因为throughout要求调度器尽量少的干预运行。但是低延迟要求调度器进行系统的跟踪监视,并且精度尽可能的高,尽快的做出响应,这些都要求调度器尽量多的运行。

到目前为止,我们介绍了任务调度器,以及cgroup,event-driven scheduler等等概念。都是在围绕着CPU的任务调度器。很多年前,当时手机处理的CPU还是单核的,慢慢的变成双核,四核直到今天的八核甚至十核。以前所有的CPU都是同构的,也就是每个CPU都是一样的(SMP)。但是随着处理器性能的提升,人们发现已经无法同时满足高性能与低功耗了。为了提升CPU单核处理器的性能,不得不把流水线做的更深,频率更高,乱序执行、多发射等等。高频性能与低频功耗不可兼得。

似乎进入了死胡同,但是人的想象力是无止尽的。“天空一声巨响,big-little闪亮登场”。在SOC上同时存在big大核跟little小核。大核可以提供高性能,给一些需要高算力的任务运行,little小核可以做到低功耗,给一些轻载任务来运行。似乎问题得到了完美的解决,要性能有性能,要功耗有功耗。其实不然,潘多拉之盒才刚刚开启,软件苦难的日子才开始。因为B-L成功的将一个硬件难题转变了软件难题,而软件似乎是无所不能的,而且也是“不值钱的”。

回到这个设计的最初设想,大核给需要性能的大任务用,满足性能;小核给轻载的小任务节省功耗。那么多大的任务是大任务呢?

多大的任务是大任务,多小的任务是小任务呢?

      一下子傻眼了吧。因为在big-little出现之前,处理器基本都是同构的,也就是SMP-对称多处理"(Symmetrical Multi-Processing)技术,而且由于这些CPU公用同一个时钟源(clock source)来进行驱动,因此他们也是同频的。所以在任务选择的时候,只需要做到多个CPU之间任务分布的均衡或者算力需求的均衡就可以了。 因此SMP只要做好任务的均衡就行了,避免“一核有难、九核围观”的惨剧就行。

再次借用一下这张图,因为太经典了

但是在big-little架构下(HMP,heterogeneous multi-processor),大核跟小核的同频率性能存在较大的差异,而且频率也是不同的时钟源,并不同频(小核跟大核不同频, 每个cluster内部还是同频的),问题一下子复杂化。 

回到原始的问题,那么多大的任务是大任务的呢? 这个问题最初一看,以为很好回答,但是如果细想一下,其实非常复杂。就跟人眼看一眼图片中的猫,就能认出这是猫一样,让计算机程序去识别图片中的猫,却非常困难,直到最近十年因为处理器算力提升重新捡起来的各种神经网络,这个问题才初步得到一定程度的解释,但是还没有根本解决。

一些人脑很容易判断的事实,计算机却非常难以判断。

要回答多大的任务才是大任务,就不得不回归到对于任务的量化度量上。

当前在linux生态中,存在两种任务的负载量化的模型,也就是pelt(per-entity load tracking)跟walt (Window Assisted Load Tracking)。这两种算法虽然不一样,但是都是基于历史信息来预估。就跟银行会基于你的“征信报告”来觉得是否给你发放贷款一样。这些方法有一定的局限性,因为有些人没征信记录(不用担心办信用卡,正确的使用信用卡是个加分项),有些人故意刷记录。

PELT(per-entity load tracking)

网上关于pelt(http://www.wowotech.net/process_management/450.html)以及walt的资料很多,这里不详细讲解了,只讲一下基本原理。从pelt的角度,任务在过去运行过的任何一段时间(注意是所有),都对于负载造成影响贡献,只是距离现在的时间不一样,它们的贡献权重不一样而已。时间近的,贡献权重大;时间远的,贡献权重小。

进程运行的时间横轴,以1ms为一个period周期,Li是一个调度实体在i个时间周期内的负载,虽然进程已经又运行了i个周期,它对当前系统负载仍有贡献 Li*y^i,y是衰减系数, y^32=0.5,距离当前时间点越远,衰减系数越大,对调度实体总的负载影响越小。为什么 y^32=0.5呢?这个是经验值,可以调整修改https://lwn.net/Articles/906375/。

pelt带来了一个很奇怪的问题,就是当任务不运行时候,基于pelt算法的理论,计算出来的负载是不为0的(因为衰减需要一定的时间)。因此,当一个CPU即使进入idle,CPU上的pelt负载也是不为0的(奇怪吧!),而且也参与到了整个cluster上的调频。

WALT(Window Assisted Load Tracking)

当移动android终端继续采用pelt的时候,大家发现个问题,就是pelt的负载累积过慢。以一个死循环的while(1)程序为例,横坐标是时间,纵坐标是通过pelt计算出来的负载(最大值是1024)。当这个进程从刚开始创建,到pelt能够正确的表示它的最大负载的时候,已经过去100+ms。

图:pelt时间负载曲线示意图

而我们的60HZ刷新率的屏幕的整个单帧绘制周期只有16.7ms。等到100ms的话,黄花菜都凉了。因此pelt的这种基于历史运行状态进行累计负载的做法,并不太适应于交互式的设备。

因此QUALCOMM在其最初的HMP架构的处理器上设计了walt算法。https://lwn.net/Articles/704903/

其核心思想就是把时间分成一个个对齐的窗口周期,通过统计线程在窗口周期的运行时间占比来计算负载。

图:walt窗口统计原理

听起来是不是很合理?!可惜,被社区给拒绝了。因此高通一直在单独维护walt的所有相关代码。相比pelt,walt确实可以大幅提升负载累积的速度,但是窗口周期虽然对周期性的绘制比较有利,但是android系统里面除了这些线程之外,还有很多其他的非周期性运行的线程。 

比如说,当一个线程正好跨窗口,以下图为例,进程刚好被窗口边界(T1+T)分割成两部分,前一部分属于(T1,T1+T)的窗口周期,后一部分属于(T1+T, T1+2T)的窗口周期。但是这个线程很可能是因为丢帧才落到了2个周期内,是一个完整的负载,并不应该做切割。

图:进程被walt窗口切割 

这时候pelt的理论优势就出来了。只要线程运行的pattern是一定的,它的负载值就是唯一的,而walt会受到窗口的影响。

但是不管是pelt,还是walt,都需要一定的统计时长(启发式统计,或许未来AI预测是个good idea),对于任务计算量突然发生变化的场景,依然显得无能为力。这一点以后再详细展开,可以看下图的一个实例。

图: 因为三方APK实现问题,线程负载突变,导致的负载跟踪不及时,导致的卡顿

上面不管pelt,还是walt。都是针对单个任务的负载量化方式,基于这些统计出来的量化负载,以及我们对于所谓“大任务”设置的各种threshold阈值,大体可以来粗略的来界定一个任务是不是大任务。

大任务跑大核,小任务就一定跑小核吗?

我猜大家肯定会下意识的回答是(苹果跟intel一般把小核称之为efficiency core),但是细想一下好像哪里不对。大家可能想到,如果都是小任务,而且小核堆积满了,这时候应该把多余的任务放到大核上。

那如果小核没有堆积满呢?? 

针对单个CPU,其算力与功耗的曲线图大概长成下面这个样子。

图:性能功耗能效示意图(网上随便搜的)

这是一条单调递增的,频率越高,功耗越高,斜率越大,能效比越差。这似乎是一条铁律。因此在SMP系统中,只需要考虑负载均衡(load balance)就行了,把负载尽可能均分到不同的CPU上。因为负载越均衡,系统的频率就会越低,功耗就会越低。 

但是B-L引入之后,由于大核与小核之间是非对称异构的。情况瞬间发生了变化。

图:A53、A75能效曲线示意图

假如大小核的能效曲线如上图所示的话,我们依然可以认为任务在小核上功耗最优。那么如果情况变成下面这样呢。

当两条能效曲线出现了交叉的时候,到底任务跑在哪个CPU上更加省电呢?

这些都是B-L引入之后带来的问题,篇幅有点长,下一次我们继续介绍EAS。

任务调度器:从入门到放弃(一)

ebpf工作原理介绍——ebpf指令集及虚拟机

自适应流媒体(ABR)技术与算法解析

图片


http://www.hkcw.cn/article/vpJFpMaBLX.shtml

相关文章

linux 1.0.6

PMS和软件安装的介绍 软件安装不能放在前面 你连linux自带的命令都整不会,你还去使用人家自己写的软件 整一个类似于手机商城的概念,直接进商城就可以下载软件 package management system 也就是PMS 叫做包管理器,作用是用来进行软件的安装&…

3D Gaussian splatting 02: 快速评估

目录 3D Gaussian splatting 01: 环境搭建3D Gaussian splatting 02: 快速评估3D Gaussian splatting 03: 用户数据训练和结果查看3D Gaussian splatting 04: 代码阅读-提取相机位姿和稀疏点云3D Gaussian splatting 05: 代码阅读-训练整体流程3D Gaussian splatting 06: 代码阅…

【系统配置与部署类】linux系统下的desktop图标文件配置

相关文章已经在个人博客网站上更新,欢迎访问: linux系统下的desktop图标文件配置http://www.turnin-blog.online/articles/%E7%B3%BB%E7%BB%9F%E9%85%8D%E7%BD%AE%E4%B8%8E%E9%83%A8%E7%BD%B2/linux%E7%B3%BB%E7%BB%9F%E4%B8%8B%E7%9A%84desktop%E5%9B%…

法国为何此时回应阵风是否被击落 服役20年首损?

法国军方首次回应“阵风”战机疑被击落事件,称如果消息属实,这将是该机服役以来首次在实战中损毁。印度空军引进了这款战斗机。5月27日,在例行记者会上,法国国防部军方发言人强调,若相关信息属实,这将是“阵风”战斗机服役20年来首次在实战中损毁。这也是法国军方就这一话…

从零开始的二三维CAD|CAE软件: 解决VTK,DICOM体素化-失效问题.

背景: 在从零开始的二三维软件开发中, 需要加载CT的dicoms影像文件, 并将其序列化之后的数据,体素化 可惜..vtk的c#库,将其体素化的时候,竟然失败... 使用vtkDicomReader ,设置 Dicom文件夹读取,竟然不停的失败...从网上找了一些版本.也没啥可用的资料... 解决办法: 直接…

Cesium快速入门到精通系列教程一

一、打造第一个Cesium应用 1、官方渠道下载Cesium(可选择历史版本) ​​GitHub Releases页面​​ 访问 Cesium GitHub Releases,此处列出了所有正式发布的版本。 通过标签(如 v1.95.0)选择目标版本,下载…

【多线程初阶】synchronized -监视器锁monitor lock

文章目录 🌅synchronized关键字🌊 synchronized 的互斥🌊 synchronized 的变种写法🏄‍♂️synchronized 修饰代码块 :明确指定锁哪个对象🏄‍♂️synchronized 修饰方法 🌊 synchronized 的可重入性&#…

C# 面向对象特性

面向对象编程的三大基本特性是:封装、继承和多态。下面将详细介绍这三大特性在C#中的体现方式。 封装 定义:把对象的数据和操作代码组合在同一个结构中,这就是对象的封装性。 体现方式: 使用访问修饰符控制成员的可见性 通过属…

[LitCTF 2024]浏览器也能套娃?

打开题目在线环境: 这里尝试填写一个网站回车之后是: 这里想到了ssrf漏洞:ssrf漏洞通常是由于应用程序在处理用户输入时缺乏严格的安全检查,错误地信任外部输入,或者使用的网络请求库配置不当等原因导致的。攻击者可…

明远智睿SSD2351开发板:视频监控领域的卓越之选

随着安全防范意识的提高,视频监控在各个领域得到了广泛应用。明远智睿SSD2351开发板凭借其出色的性能和特性,成为视频监控领域的卓越之选,为视频监控系统的升级和发展提供了有力支持。 SSD2351开发板的四核1.4GHz处理器在视频监控数据处理方面…

threejs加载外部三维模型(gltf)

1. 建模软件绘制3D场景(Blender) 这节课主要给大家科普一些三维模型创建、美术和程序员协作的相关问题。 三维建模软件作用 对于简单的立方体、球体等模型,你可以通过three.js的几何体相关API快速实现,不过复杂的模型,比如一辆轿车、一栋房…

【从零开始系列】Qwen2.5 Llama-Factory:开源语言大模型+训练平台——(超详细、最新版)一篇文章解决:环境搭建 => 微调训练 => 本地部署

目录 一、简介 1.Qwen2.5:开源模型 2. LLaMA-Factory:微调工具 二、环境搭建 1.Python和Pytorch版本 2.llamafactory项目克隆安装 3.其他重要库安装 三、模型微调 1.预训练模型下载 2.训练数据集 ①创建对话文本数据 ②配置dataset_info 3.配置文件与…

git 代码提交规范,feat,fix,chore都是什么意思?

写到前面 经常看到别人提交的代码记录里面包含一些feat、fix、chore等等,而我在提交时也不会区分什么,直接写下提交信息,今天就来看一下怎么个事,就拿 element-plus 举例来看一下 其实这么写是一种代码提交规范,当然…

Gemma 3模型:Google 开源新星,大语言模型未来探索

🐇明明跟你说过:个人主页 🏅个人专栏:《深度探秘:AI界的007》 🏅 🔖行路有良友,便是天堂🔖 目录 一、引言 1、快速发展的AI世界:为何关注Gemma 3&#x…

2024.11最新Hexo+GitHub搭建个人博客

2024.11最新HexoGitHub搭建个人博客 一、Hexo介绍 Hexo 是一个快速、简洁且高效的博客框架,有丰富的主题和插件可供使用。 Hexo 使用 Markdown(或其他标记语言)解析文章,在几秒内,即可利用靓丽的主题生成静态网页。这…

完美解决 git 报错 “fatal: unable to access ‘https://github.com/.../.git‘: Recv failure Connection was rese

文章目录 方法一:取消代理设置方法二:设置系统代理结语 🎉欢迎来到Java学习路线专栏~探索Java中的静态变量与实例变量 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒🍹✨博客主页:IT陈寒的博客🎈该系列文章专栏&#xf…

git 拉取Github时报错【 Recv failure: Connection was reset】

问题 当我们电脑能够正常访问Github时,但是git拉取代码出现 : Recv failure: Connection was reset原因 这是因为使用了特殊上网方法,电脑能够正常访问,但是git通过底层访问需要配置代理才能正常访问 解决办法 配置方法如下&…

【PostgreSQL系列】PostgreSQL性能优化

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

如何使用 TortoiseGit 将本地代码回退到指定版本

如何使用 TortoiseGit 将本地代码回退到指定版本 在使用 Git 进行版本控制时,我们可能会遇到需要回退到某个旧版本的情况,比如发现最近的修改引入了问题,或者需要恢复到某个特定的稳定状态。TortoiseGit 是一款非常流行的 Git 图形化工具&am…

航空安全警钟须长鸣 充电宝冒烟再敲警钟

5月31日,CZ6850杭州飞往深圳的航班上,一名旅客携带的相机电池和充电宝突然冒烟。乘务组迅速采取措施,排除了安全风险。为确保安全,机组决定立即返航,航班在起飞15分钟后安全降落。这次事件虽然没有造成人员伤亡,但再次提醒人们航空安全的重要性。任何微小的安全隐患都可能…