小林Coding阅读笔记:操作系统篇之硬件结构,伪共享问题及CPU的任务执行

Posted adventure.Li

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了小林Coding阅读笔记:操作系统篇之硬件结构,伪共享问题及CPU的任务执行相关的知识,希望对你有一定的参考价值。

前言

  1. 参考/导流:
    小林coding - 2.5 CPU 是如何执行任务的?
  2. 学习意义
  • 底层基础知识,了解CPU执行过程,让上层编码有效
  • 并发问题处理、思考理解
  • 调度策略、思想借鉴分析
  1. 相关说明
    该篇博文是个人阅读的重要梳理,仅做简单参考,详细请阅读小林coding的原文!

五、CPU如何执行任务的?

前置知识

  • CPU读数据的过程
  • CacheLine的理解以及CPU存储架构设计

读数据过程

CPU 从内存中读取数据到 Cache 的时候,并不是一个字节一个字节读取,而是一块一块的方式来读取数据的,这一块一块的数据被称为 CPU Cache Line(缓存块),所以 CPU Cache Line 是 CPU 从内存读取数据到 Cache 的单位。
【而这一块是连续地址空间的一块,为什么这样设计呢?因为连续的地址空间,在下次使用到的概念就极大,例如数组遍历;有点预测未来,提前写入缓存,提升效率的思想,对比指令分支预测设计】

  • 对于数组来说,顺序去访问极大提升性能
  • 对于单个变量,例如Int、long一般都是小于CacheLine大小(一般为64字节),则会存在一定的伪共享问题(对于多核来说)

伪共享问题

根据小林的这图,极其容易理解伪共享。在多核之间去协调缓存一致性(MESI)是根据的CacheLine去标记的【因为CPU从内存的读取单位是CacheLine】,而非细粒度的单个变量。因此,虽然表面上 A、B互不干扰的两个变量,却更好因为 物理上 的连续,又更好 被在不同核心的两个线程 去读取到 CacheLine。那么A线程去修改A变量,会改变Cacheline的[状态],此时B线程再去修改则会导致CacheLine写回内存,在读取。这样来 本来独立的变量 并没有很好利用到 缓存的优势,反而增大了开销。【这样也好理解 为什么叫 共享了】

具体过程

跟着小林的步骤,再过一遍具体过程,回顾一下MESI

①. 最开始变量 A 和 B 都还不在 Cache 里面,假设 1 号核心绑定了线程 A,2 号核心绑定了线程 B,线程 A 只会读写变量 A,线程 B 只会读写变量 B。

②. 1 号核心读取变量 A,由于 CPU 从内存读取数据到 Cache 的单位是 Cache Line,也正好变量 A 和 变量 B 的数据归属于同一个 Cache Line,所以 A 和 B 的数据都会被加载到 Cache,并将此 Cache Line 标记为**「独占」**状态。

③. 接着,2 号核心开始从内存里读取变量 B,同样的也是读取 Cache Line 大小的数据到 Cache 中,此 Cache Line 中的数据也包含了变量 A 和 变量 B【本应该独立的变量不能同时在CacheLine,这也是后续解决的伪共享的突破口】,此时 1 号和 2 号核心的 Cache Line 状态变为「共享」状态。【对于 数组来说是否也存在? ,刚好控制不同数组 连续 在CacheLine范围内】

④. 1 号核心需要修改变量 A,发现此 Cache Line 的状态是「共享」状态,所以先需要通过总线发送消息给 2 号核心,通知 2 号核心把 Cache 中对应的 Cache Line 标记为**「已失效」状态,然后 1 号核心对应的 Cache Line 状态变成「已修改」**状态,并且修改变量 A。

对于修改者会将共享态 → 已修改;而其他共享者则将 共享态→已失效【被其他人修改了,该数据已非最新数据了】

⑤. 之后,2 号核心需要修改变量 B,此时 2 号核心的 Cache 中对应的 Cache Line 是已失效状态,另外由于 1 号核心的 Cache 也有此相同的数据,且状态为「已修改」状态,所以要先把 1 号核心的 Cache 对应的 Cache Line 写回到内存,然后 2 号核心再从内存读取 Cache Line 大小的数据到 Cache 中,最后把变量 B 修改到 2 号核心的 Cache 中,并将状态标记为「已修改」状态。

如何避免

  • 对于C\\C++,通过如果在多核(MP)系统里,该宏定义是 __cacheline_aligned
    【Linux内核定义】,也就是 Cache Line 的大小
  • 对于Java,「字节填充 + 继承」

关于Java的避免方式:https://blog.csdn.net/qq_37284798/article/details/126641566

CPU执行任务

在 Linux 内核中,进程和线程都是用 task_struct结构体表示的,区别在于线程的 task_struct 结构体里部分资源是共享了进程已创建的资源,比如内存地址空间、代码段、文件描述符等,所以 Linux 中的线程也被称为轻量级进程,因为线程的 task_struct 相比进程的 task_struct 承载的 资源比较少,因此以「轻」得名。

Linux 内核里的调度器,调度的对象就是 task_struct,接下来我们就把这个数据结构统称为任务

在 Linux 系统中,根据任务的优先级以及响应要求,主要分为两种,其中优先级的数值越小,优先级越高

  • 实时任务,对系统的响应时间要求很高,也就是要尽可能快的执行实时任务,优先级在 0~99 范围内的就算实时任务;
  • 普通任务,响应时间没有很高的要求,优先级在 100~139 范围内都是普通任务级别;

为了更好地执行任务,需要使用调度类进行调度执行。

  • SCHED_DEADLINE:是按照 deadline 进行调度的,距离当前时间点最近的 deadline 的任务会被优先调度;
  • SCHED_FIFO:对于相同优先级的任务,按先来先服务的原则,但是优先级更高的任务,可以抢占低优先级的任务,也就是优先级高的可以「插队」;
  • SCHED_RR:对于相同优先级的任务,轮流着运行,每个任务都有一定的时间片,当用完时间片的任务会被放到队列尾部,以保证相同优先级任务的公平性,但是高优先级的任务依然可以抢占低优先级的任务;

可以看出实时任务,都是抢占式的,实时任务需要及时性,因此需要抢占式设计。

  • SCHED_NORMAL普通任务使用的调度策略;【
  • SCHED_BATCH后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务,可以适当降低它的优先级。

而对于普通任务【平时主要的都是该种】来说,公平性最重要,在 Linux 里面,实现了一个基于 CFS 的调度算法,也就是完全公平调度(Completely Fair Scheduling

CFS

基本思想

让分配给每个任务的 CPU 时间是一样,于是它为每个任务安排一个虚拟运行时间 vruntime,如果一个任务在运行,其运行的越久,该任务的 vruntime 自然就会越大,而没有被运行的任务,vruntime 是不会变化的。在 CFS 算法调度的时候,会优先选择 vruntime 少的任务,以保证每个任务的公平性。

对于优先级的设计,则采用 权重 (weight)的方式,去解决。

vruntime += delta_exec(实际执行时间) * NICE_0_LOAD【常量】/ weight

【权重越大,vruntime越小,越容易被调到】

CPU运行队列

一个系统通常都会运行着很多任务,多任务的数量基本都是远超 CPU 核心数量,因此这时候就需要排队

事实上,每个 CPU 都有自己的运行队列(Run Queue, rq,用于描述在此 CPU 上所运行的所有进程,其队列包含三个运行队列,Deadline 运行队列 dl_rq、实时任务运行队列 rt_rq 和 CFS 运行队列 cfs_rq,其中 cfs_rq 是用红黑树来描述的,按 vruntime 大小来排序的【因为实时任务较少,一般来说简单简单队列即可?】,最左侧的叶子节点,就是下次会被调度的任务。

调整优先级

对于普通任务,可以调整任务的 nice值nice 的值能设置的范围是 -20~19, 值越低,表明优先级越高,因此 -20 是最高优先级,19 则是最低优先级,默认优先级是 0。

启动任务的时候,可以指定 nice 的值,比如将 mysqld 以 -3 优先级

nice -n -3 /usr/sbin/mysqld

对于已启动任务

renice -10 -p pid

改变任务的优先级以及调度策略,使得它变成实时任务

chrt -f 1 -p pid

设置为SCHED_FIFO,优先级为1

以上是关于小林Coding阅读笔记:操作系统篇之硬件结构,伪共享问题及CPU的任务执行的主要内容,如果未能解决你的问题,请参考以下文章

小林Coding阅读笔记:操作系统篇之硬件结构,中断问题

小林Coding阅读笔记:操作系统篇之硬件结构,CPU Cache一致性问题

小林coding阅读笔记:操作系统篇之内核设计

小林coding阅读笔记:操作系统篇之内核设计

小林coding阅读笔记:操作系统篇之内存分配与回收

小林coding阅读笔记:操作系统篇之内存分配与回收