首页 / 资讯中心 / 文章详情

CS-Base 图解系统:CPU 如何读写数据与调度任务——从 Cache 伪共享到 Linux 完全公平调度

CS-Base 图解系统:CPU 如何读写数据与调度任务——从 Cache 伪共享到 Linux 完全公平调度 ★ FEATURED ARTICLE
文档教程知识库【免费下载链接】CS-Base图解计算机网络、操作系统、计算机组成、数据库共 1000 张图 50 万字破除晦涩难懂的计算机基础知识让天下没有难懂的八股文 在线阅读https://xiaolincoding.com项目地址https://gitcode.com/GitHub_Trending/cs/CS-Base点击查看免费下载导读本文以 CS-Base 仓库 os/1_hardware/how_cpu_deal_task.md 为核心脉络系统讲解两个开发者绕不开的 CPU 主题CPU 如何以 Cache Line 为单位读写数据以及多核场景下 Cache 伪共享的成因与规避同时深入 Linux 内核的任务调度体系讲清task_struct、调度类、CFS 完全公平调度与nice/renice/chrt的实战调优。读完后你将能写出缓存命中率更高的代码、在项目中有意识地规避伪共享并能对高响应要求的任务做优先级干预。一、CPU 是如何读写数据的要理解 CPU 如何读写数据前提是先理解 CPU 的架构。对于一个现代 CPU通常包含多个 CPU 核心每个核心都拥有自己私有的L1 Cache和L2 Cache其中 L1 Cache 又细分为dCache数据缓存与iCache指令缓存而L3 Cache 则是多个核心共享的这就是 CPU 典型的缓存层次结构。把视角拉大到 CPU 外部还有内存与硬盘这些存储设备共同构成了金字塔存储层次从上往下存储设备的容量越来越大访问速度越来越慢CPU 访问 L1 Cache 的速度比访问内存快约100 倍因此 L1~L3 Cache 存在的意义就是充当 CPU 与内存之间的缓存层降低 CPU 对内存的访问频率。关于各级缓存的访问耗时差异与为什么有了内存还需要 Cache可进一步参阅仓库姊妹篇 如何写出让 CPU 跑得更快的代码其中给出了一次内存访问约需200~300个时钟周期、L1 Cache 仅需2~4个时钟周期等量化对比正是这数量级的差距催生了 CPU 内部的多级缓存。1.1 CPU Cache LineCPU 读取数据的基本单位CPU 从内存读取数据到 Cache 时并不是一个字节一个字节地读取而是一块一块地读取这一块块的数据被称为CPU Cache Line缓存块。也就是说CPU Cache Line 是 CPU 从内存读取数据到 Cache 的单位。在 Linux 系统上可以通过以下方式查看本机 Cache Line 的大小一般常见值为 64 字节# 查看 L1 Cache Line 大小单位为字节 cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size若 L1 Cache Line 大小为 64 字节即意味着L1 Cache 一次载入数据的大小是 64 字节。Cache Line 的内部结构索引 Index、有效位 Valid、组标记 Tag、数据块 Data Block以及直接映射 Cache 的寻址逻辑均已在 如何写出让 CPU 跑得更快的代码 中做了完整剖析本文不再赘述但下面两个结论与本文主题直接相关访问数组时按内存地址顺序遍历由于 CPU 会一次性把连续 64 字节载入 Cache因此按照物理内存地址分布的顺序访问元素Cache 命中率会非常高能大幅减少从内存读取数据的频率从而提升程序性能访问独立变量时则要小心伪共享当我们操作的不是数组而是普通变量且处于多核 CPU 环境时就可能触发Cache 伪共享——这是一个性能杀手需要主动规避。二、Cache 伪共享多核场景下的隐形性能杀手2.1 伪共享是如何发生的——MESI 协议视角的完整推演先构造一个场景假设有一颗双核心 CPU两个核心并行运行着两个不同线程它们分别从内存读取两个long类型的变量 A 和 B。这两个变量的物理地址连续Cache Line 大小为 64 字节且变量 A 恰好位于 Cache Line 的开头位置。那么变量 A 和 B 落在同一个 Cache Line中因为 CPU Cache Line 是读取单位这两个数据会同时被读入两个 CPU 核心各自的 Cache中。表面上看这没什么问题但接下来如果两个核心的线程分别修改不同的变量1 号核心只改 A2 号核心只改 B缓存一致性协议就要开始打架了。这里需要借助MESI 协议Modified 已修改 / Exclusive 独占 / Shared 共享 / Invalidated 已失效来说明完整的协议状态机与写直达/写回两种写策略详见仓库文档 CPU 缓存一致性此处只推演伪共享发生的五个关键步骤① 初始状态变量 A 和 B 都不在任何 Cache 中。假设 1 号核心绑定线程 A只读写变量 A2 号核心绑定线程 B只读写变量 B。② 1 号核心读取变量 A由于读取单位是 Cache Line而 A 与 B 恰好在同一个 Cache Line 中因此 A、B 的数据会一起被加载进 1 号核心的 Cache该 Cache Line 被标记为「独占Exclusive」状态。③ 2 号核心读取变量 B同样以 Cache Line 为单位读取载入的数据同样包含 A 和 B。此时两个核心都缓存了同一份数据两边的 Cache Line 状态都变为「共享Shared」状态。④ 1 号核心修改变量 A发现该 Cache Line 是「共享」状态不能直接改必须先通过总线广播消息通知 2 号核心把其对应的 Cache Line 标记为「已失效Invalidated」随后 1 号核心的 Cache Line 变为「已修改Modified」状态并修改变量 A。⑤ 2 号核心修改变量 B此刻 2 号核心的 Cache Line 已是失效状态且 1 号核心存在该数据的「已修改」副本因此必须先把 1 号核心的 Cache Line 写回内存再重新从内存读入 Cache Line 数据最后才修改变量 B 并标记为「已修改」。结论如果 1 号和 2 号核心持续交替地分别修改 A 和 B就会反复执行 ④ 和 ⑤Cache 完全起不到缓存的效果。虽然变量 A 和 B 之间毫无逻辑关系仅仅因为归属于同一个 Cache Line任意一方被修改都会牵连另一方失效。于是得到伪共享的定义伪共享False Sharing多个线程同时读写同一个 Cache Line 内的不同变量导致 CPU Cache 反复失效的现象。值得注意的是MESI 协议的存在正是为了多核缓存一致性写传播 事务串行化但在不同变量同处一个 Cache Line这种场景下协议本身反而放大了总线交互开销——这就是伪共享成为性能杀手的根本原因。2.2 避免伪共享的方法一Cache Line 字节对齐内核宏对于多个线程共享的热点数据即频繁被修改的数据应当避免它们恰好落在同一个 Cache Line 中。Linux 内核对此提供了现成的解决方案——__cacheline_aligned_in_smp宏/* 内核 include/linux/cache.h 中的定义示意 */ #ifdef CONFIG_SMP #define __cacheline_aligned_in_smp __cacheline_aligned /* 多核对齐到 Cache Line */ #else #define __cacheline_aligned_in_smp /* 单核空定义 */ #endif从定义可以看出多核SMP系统下该宏展开为__cacheline_aligned即对齐到 Cache Line 大小单核系统下该宏是空的因为单核不存在跨核心缓存一致性问题无需付出对齐的空间代价。举例来说假设有下面这个结构体成员a和b在物理内存上连续可能落在同一个 Cache Line 中struct test { long a; long b; /* 与 a 相邻极可能与 a 位于同一 Cache Line */ };为避免伪共享可以给b加上__cacheline_aligned_in_smp宏把b的地址强制对齐到 Cache Line 边界struct test { long a; long b __cacheline_aligned_in_smp; /* 对齐到 Cache Line与 a 分离 */ };这样a和b就不会再共享同一个 Cache Line。这种规避方式的本质是以空间换时间浪费一部分 Cache 空间换取性能提升。2.3 避免伪共享的方法二字节填充Disruptor 的经典实践再看一个应用层的经典方案Java 并发框架Disruptor采用「字节填充 继承」的方式来规避伪共享。Disruptor 中的RingBuffer类经常被多个线程并发使用其代码结构如下示意// Disruptor 源码结构示意 abstract class RingBufferPad { protected long p1, p2, p3, p4, p5, p6, p7; // 前置填充7 个无用的 long } abstract class RingBufferFieldsE extends RingBufferPad { // 实际业务字段如 cursor 等位于 7 个前置 long 与 7 个后置 long 之间 } public final class RingBufferE extends RingBufferFieldsE { protected long p1, p2, p3, p4, p5, p6, p7; // 后置填充7 个无用的 long }这些 7 个long变量看似毫无作用却对性能起着至关重要的作用。推导如下一般 64 位 CPU 的 Cache Line 大小是 64 字节一个long是 8 字节因此一个 Cache Line 恰好能容纳 8 个long根据 JVM 对象继承关系父类成员与子类成员的地址是连续排列布局的因此RingBufferPad中的 7 个long作为 Cache Line 的前置填充RingBuffer末尾的 7 个long作为 Cache Line 的后置填充这 14 个long变量没有任何实际用途也从不会被读写RingBufferFields中的业务字段都是final修饰的第一次加载后不会再被修改。于是无论 Cache 如何加载整个 Cache Line 里都没有会发生更新操作的数据。只要数据被频繁读取访问就不会因其他核心的写入而被换出 Cache也就自然杜绝了伪共享问题。三、CPU 是如何选择线程的搞清楚 CPU 读写数据的过程后另一个核心问题是CPU 根据什么来选择当前要执行的线程3.1 调度对象task_struct 任务在 Linux 内核中进程和线程统一用task_struct结构体表示。二者的区别在于线程的task_struct中部分资源共享了进程已创建的资源如内存地址空间、代码段、文件描述符等因此 Linux 中的线程也被称为轻量级进程LWP——因为它比进程的task_struct承载的资源少故以轻得名。一般来说没有创建线程的进程只有单个执行流称为主线程想让进程处理更多事情可以创建多个线程分别去处理但无论多少线程对应到内核里都是一个个task_struct。所以Linux 内核里的调度器调度对象就是task_struct下文中统称为任务。关于进程/线程的完整知识PCB 进程控制块、线程模型、上下文切换等可延伸阅读仓库文档 进程、线程基础知识关于进程的五种状态、就绪/阻塞队列如何组织是理解排队等待 CPU的重要铺垫。3.2 任务的分类与优先级范围在 Linux 系统中根据任务的优先级以及响应要求主要分为两类优先级数值越小优先级越高任务类型优先级范围特点实时任务0~99对系统响应时间要求很高需要尽可能快地被执行普通任务100~139响应时间没有很高要求3.3 调度类Scheduling Class为了保障高优先级任务能尽早执行Linux 把调度器划分为多种调度类其整体优先级顺序为Deadline Realtime Fair也就是说选择下一个任务执行时会先从dl_rqDeadline 运行队列中选择然后从rt_rq实时任务队列中选择最后才轮到cfs_rqCFS 普通任务队列。因此实时任务总是比普通任务优先被执行。Deadline 与 Realtime 调度类应用于实时任务二者的调度策略合计有三种调度策略工作机制SCHED_DEADLINE按照 deadline 调度距离当前时间点最近的 deadline 任务优先被调度SCHED_FIFO相同优先级任务按先来先服务原则执行更高优先级任务可以抢占低优先级任务即插队SCHED_RR相同优先级任务轮流运行每个任务都有时间片用完时间片的任务被放到队列尾部保证同级公平高优先级任务依然可以抢占低优先级任务Fair 调度类应用于普通任务统一由 CFS 调度器管理包含两种调度策略调度策略适用场景SCHED_NORMAL普通任务使用的默认调度策略SCHED_BATCH后台任务调度策略不与终端交互可在不影响其他交互任务的前提下适当降低其优先级3.4 完全公平调度CFS与 vruntime我们平日里遇到的几乎都是普通任务对普通任务来说公平性最重要。Linux 为此实现了基于CFSCompletely Fair Scheduling完全公平调度的调度算法。CFS 的理念是让分配给每个任务的 CPU 时间尽量相等具体手段是为每个任务安排一个虚拟运行时间 vruntime一个任务在运行时运行得越久它的vruntime就越大没有被运行的任务vruntime不会变化CFS 调度时会优先选择 vruntime 少的任务从而保证公平。打个比方把一桶奶茶平均分到 10 个杯子里你看哪杯少就多倒一些哪杯多了就先不倒——经过多轮操作虽然不能保证每杯完全一样多但至少是公平的。上面的比喻没考虑优先级。虽然都是普通任务但普通任务之间仍有优先级区分因此计算vruntime时还要考虑普通任务的权重值weight。注意权重值并不等于优先级数值内核中维护着一张nice 级别与权重值的转换表nice 级别越低权重值越大。于是有了 vruntime 的核心公式vruntime vruntime (实际运行时间 × NICE_0_LOAD) / 权重值其中NICE_0_LOAD可理解为常量。可以直观推导在同样的实际运行时间里高权重任务的 vruntime 增长得比低权重任务慢更少。而 CFS 优先选择 vruntime 少的任务所以高权重任务会被优先调度获得的实际运行时间自然更多。3.5 CPU 运行队列任务如何排队一个系统通常运行着大量任务数量远超 CPU 核心数因此需要排队。每个 CPU 都有自己的运行队列Run Queuerq用于描述在该 CPU 上运行的所有进程由三个子队列组成子队列数据结构说明dl_rq—Deadline 运行队列rt_rq—实时任务运行队列cfs_rq红黑树CFS 运行队列按 vruntime 大小排序最左侧叶子节点就是下次会被调度的任务调度类之间的优先级决定了选择顺序Deadline Realtime Fair即先看dl_rq再看rt_rq最后看cfs_rq——实时任务总是先于普通任务执行。3.6 调整优先级nice / renice / chrt 实战如果没有特意指定优先级任务默认都是普通任务由 CFS 调度器管理目标是公平分配 CPU 时间。如果你希望某个普通任务获得更多执行时间可以调整它的nice 值。nice 值要点设置范围-20 ~ 19值越低优先级越高-20最高19最低默认值为 0nice 值并非优先级本身而是优先级的修正数值二者关系为priority(new) priority(old) nice内核中 priority 范围为0~139值越低优先级越高其中0~99提供给实时任务nice 值映射到100~139这个范围供普通任务使用因此 nice 调整的是普通任务的优先级由于 nice 值越低 → 权重值越大 → 计算出的 vruntime 越少 → CFS 越优先调度所以nice 值越低任务优先级越高。实战操作示例启动任务时指定 nice 值例如让mysqld以-3优先级启动nice -n -3 mysqld如果想修改正在运行的任务的优先级使用renice调整 nice 值PID为目标进程号renice -n -3 -p PID注意nice 调整的是普通任务的优先级所以不管怎么缩小 nice 值任务永远都是普通任务不可能越级成为实时任务。如果某些任务对实时性要求很高可以考虑同时改变任务的调度策略与优先级使其变成实时任务使用chrt命令# 将 PID 设置为 SCHED_FIFO 实时调度策略优先级设为 900~99 内 chrt -f -p 90 PID # 将 PID 设置为 SCHED_RR 实时调度策略优先级设为 90 chrt -r -p 90 PID关于抢占/非抢占、时间片、优先级调度、多级反馈队列等经典调度算法FCFS、SJF、HRRN、RR、HPF、MFQ以及对应的典型场景与时间片取值建议如20ms~50ms的折中值仓库文档 进程调度/页面置换/磁盘调度算法 有系统化图解可作为调度体系学习的延伸。四、总结理解 CPU 如何执行任务本质上是理解两条主线主线一CPU 如何读写数据CPU 内部多个 Cache 外部内存和磁盘构成金字塔存储结构越往下容量越大、速度越慢CPU 读写数据以CPU Cache Line通常 64 字节为单位而非逐字节操作数组时按内存地址顺序访问能充分利用 Cache、提升命中率操作普通变量且处于多核环境时须警惕Cache 伪共享多个线程读写同一个 Cache Line 内的不同变量会导致 Cache 反复失效规避伪共享的常见手段包括Cache Line 大小字节对齐如内核__cacheline_aligned_in_smp宏与字节填充如 Disruptor 的 77 个 long 填充本质是以空间换时间。主线二CPU 如何选择线程Linux 中进程与线程统一用task_struct表示调度对象就是它任务按优先级分为实时任务0~99与普通任务100~139调度类优先级为Deadline Realtime Fair普通任务由CFS管理按vruntime最小者优先调度并通过权重把 nice 值折算进公平性计算每个 CPU 有自己的运行队列dl_rq/rt_rq/cfs_rq普通任务队列由红黑树按 vruntime 排序若任务对响应要求很高可通过nice/renice调整普通任务优先级或用chrt切换调度策略为实时任务。当系统中运行的线程数远超 CPU 核心数时排队等待必然带来延迟如果你的任务对延迟容忍度很低就可以通过上述手段人为干预 Linux 的默认调度策略与优先级让关键任务获得更快、更稳定的 CPU 时间。延伸阅读本主题的相邻知识点均收录于本仓库os/目录CPU 缓存命中率与直接映射 Cache 的完整原理见 如何写出让 CPU 跑得更快的代码MESI 协议状态机、写直达/写回策略见 CPU 缓存一致性CPU 执行程序与指令周期见 CPU 是如何执行程序的进程/线程基础与 PCB 见 进程、线程基础知识经典调度算法体系见 进程调度/页面置换/磁盘调度算法。赞分享文档教程知识库【免费下载链接】CS-Base图解计算机网络、操作系统、计算机组成、数据库共 1000 张图 50 万字破除晦涩难懂的计算机基础知识让天下没有难懂的八股文 在线阅读https://xiaolincoding.com项目地址https://gitcode.com/GitHub_Trending/cs/CS-Base点击查看免费下载相关推荐Hoppscotch 实时通信测试实战指南WebSocket 与 SSE 从建连到排障Hoppscotch 实时通信测试实战指南WebSocket 与 SSE 从建连到排障 验证推送通知是否按时送达、聊天消息是否实时同步这类实时接口联调用 H开发工具接口测试前端后端CLI终极指南Aceso热修复安全防护策略与代码签名验证机制终极指南Aceso热修复安全防护策略与代码签名验证机制 Aceso是一款基于Instant Run Hot Swap技术的Android热修复库能够在不重新终极指南使用Scarab轻松管理《空洞骑士》Mods的10个技巧终极指南使用Scarab轻松管理《空洞骑士》Mods的10个技巧 Scarab是一款专为《空洞骑士》游戏设计的现代化Mod管理器采用Avalonia框架开发人工智能AI 应用AI Agent桌面应用插件系统DeepSeekdsh-plugin上一篇MiniCPM-V多模态训练数据构建深度解析从数据标注到模型微调实战指南下一篇推荐开源项目Laravel Doctrine ORM创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
阅读完成 · 觉得有帮助?
咨询建站