暑假读书笔记第三天
今日文章:
小林coding:CPU 缓存一致性
小林coding:CPU 是如何执行任务的?
目录
- CPU 缓存一致性
- 写直达(Write Through)
- 写回(Write Back)
- 多核心缓存一致性
- CPU读写与调度问题
- CPU Cache **伪共享(False Sharing)**导致缓存失效
- 避免伪共享
- 线程调度
- 任务
- 调度类种类
- 完全公平调度(Completely Fair Scheduling)
其他:
往期打卡
CPU 缓存一致性
数据写入 Cache 之后,内存与 Cache 相对应的数据将会不同,需要将数据同步写回内存才能保证数据的一致性
下面介绍两种针对写入数据的方法:
写直达(Write Through)
写直达是保证Cache和内存数据一致最简单的方式,写入数据到Cache时,把数据同时写入内存和 Cache 中,如果是新数据直接写入内存
写回(Write Back)
写直达频繁访问内存影响性能,写回机制中,写操作新数据仅仅写入Cache,只在替换数据时判断脏数据并写回内存
特殊情况为写未命中,写回策略对于该情况并没有做无意义操作
-
为什么写回未命中需要从内存中先读取回来再访问而不是直接写cache ?
因为需要保证整个 cache line 块和内存数据一致,毕竟写入一般不会刚好是 cache line 块长
-
为什么写回未命中不能直接写内存还得写入cache再等写回去?
因为数据改写后一般有用,用到了还得读,而且可能还会多次修改,必要时再写回内存有助于减少内存访问次数。
多核心缓存一致性
写直达只需要在修改内存时广播事件通知各核心即可,主要看写回策略
写回策略是替换时写回,要实现各核心数据一致性,需要做到两点:
- 第一点,某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(Write Propagation);
- 第二点,某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,需要做到隔离性,事务隔离级别一般为串行化(Transaction Serialization)。
写操作一定要互斥,所以需要互斥锁
事务四大隔离级别:
写传播和事务串行化的具体实现技术
- 总线嗅探
- 写传播最常见的实现方式是总线嗅探(*Bus Snooping*),也就是总线广播,CPU核心监听总线上的广播事件
- MESI 协议,四种状态标记Cache Line
- Modified,已修改,脏标记,更新未写回内存,和独占的区别是其他核心发起读取请求时要先写回内存(因为会丢失已修改状态,不写回内存数据就不一致了)
- Exclusive,独占,不用多核同步,直接修改
- Shared,共享,相同的数据在多个 CPU 核心的 Cache 里都有,先通过广播让其他核心中该Cache Line 块无效,再更新当前Cache Line块
- Invalidated,已失效,Cache Block 里的数据已经失效了,不可以读取该状态的数据。
具体例子:
- 当 A 号 CPU 核心从内存读取变量 i 的值,数据被缓存在 A 号 CPU 核心自己的 Cache 里面,此时其他 CPU 核心的 Cache 没有缓存该数据,于是标记 Cache Line 状态为「独占」,此时其 Cache 中的数据与内存是一致的;
- 然后 B 号 CPU 核心也从内存读取了变量 i 的值,此时会发送消息给其他 CPU 核心,由于 A 号 CPU 核心已经缓存了该数据,所以会把数据返回给 B 号 CPU 核心。在这个时候, A 和 B 核心缓存了相同的数据,Cache Line 的状态就会变成「共享」,并且其 Cache 中的数据与内存也是一致的;
- 当 A 号 CPU 核心要修改 Cache 中 i 变量的值,发现数据对应的 Cache Line 的状态是共享状态,则要向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后 A 号 CPU 核心才更新 Cache 里面的数据,同时标记 Cache Line 为「已修改」状态,此时 Cache 中的数据就与内存不一致了。
- 如果 A 号 CPU 核心「继续」修改 Cache 中 i 变量的值,由于此时的 Cache Line 是「已修改」状态,因此不需要给其他 CPU 核心发送消息,直接更新数据即可。如果 A 号 CPU 核心的 Cache 里的 i 变量对应的 Cache Line 要被「替换」,发现 Cache Line 状态是「已修改」状态,就会在替换前先把数据同步到内存。
CPU读写与调度问题
CPU Cache **伪共享(False Sharing)**导致缓存失效
①. 假设 1 号核心绑定了线程 A,2 号核心绑定了线程 B,线程 A 只会读写变量 A,线程 B 只会读写变量 B,但变量 A 和 变量 B 的数据位于同一块
②. 1 号核心读取变量 A,A 和 B 的数据都会被加载到 Cache,并将此 Cache Line 标记为「独占」状态。
③. 2 号核心开始从内存里读取变量 B,此时 1 号和 2 号核心的 Cache Line 状态变为「共享」状态。
④. 1 号核心修改变量 A,核心2Cache Line失效,核心A状态变为「已修改」状态
⑤. 之后如果A,B线程交替修改A、B变量,就会发生伪共享,每次修改都要写回内存,重新读取,Cache 并没有起到缓存的效果
多个线程同时读写同一个 Cache Line 的不同变量时,而导致 CPU Cache 失效的现象称为伪共享(False Sharing)。
避免伪共享
在 Linux 内核中存在 __cacheline_aligned_in_smp
宏定义,是用于解决伪共享的问题。
比如对于存在a、b两个变量的结构体,可以将 b 的地址设置为 Cache Line 对齐地址
Java 并发框架 Disruptor 使用「字节填充 + 继承」的方式,来避免伪共享的问题。
根据 JVM 对象继承关系中父类成员和子类成员,内存地址是连续排列布局的,因此 RingBufferPad 中的 7 个 long 类型数据作为 Cache Line 前置填充,而 RingBuffer 中的 7 个 long 类型数据则作为 Cache Line 后置填充,这 14 个 long 变量没有任何实际用途,更不会对它们进行读写操作。
经过前后填充,无论怎么加载RingBufferFields都不会和其他RingBufferFields加载到一个Cache,所以多线程间不会出现伪共享问题
一句话总结就是要保证独占占据的全部Cache Line
线程调度
任务
在 Linux 内核中,进程和线程都是用 task_struct 结构体表示的,区别在于线程的 task_struct 结构体里部分资源是共享了进程已创建的资源,比如内存地址空间、代码段、文件描述符等
没有创建线程的进程,是只有单个执行流,它被称为是主线程。如果想让进程处理更多的事情,可以创建多个线程分别去处理,但不管怎么样,它们对应到内核里都是 task_struct。
Linux 内核里的调度器,调度的对象就是 task_struct,文章中把这个数据结构称为任务。
任务有不同的优先级以及响应要求,优先级数值越小越优先,在 Linux 系统中主要分为两种:
- 实时任务,对系统的响应时间要求高,优先级小于100算实时任务
- 普通任务,响应时间没有很高的要求,优先级在100~139范围算普通任务
nice表示优先级的修正数值,它与优先级(priority)的关系是:
priority(new) = priority(old) + nice
nice 值是映射到 100~139,这个范围是提供给普通任务用的,因此 nice 值调整的是普通任务的优先级。
指定nice值启动任务
调整运行中任务的nice值
改变任务的优先级以及调度策略
调度类种类
Deadline 和 Realtime 这两个调度类,都是应用于实时任务的,这两个调度类的调度策略合起来共有这三种,它们的作用如下
- SCHED_DEADLINE:是按照 deadline 进行调度的,距离当前时间点最近的 deadline 的任务会被优先调度;(执行最快要到期的任务)
- SCHED_FIFO:同优先级先来先服务,高优先级抢占
- SCHED_RR:同优先级时间片轮转,高优先级抢占
而 Fair 调度类是应用于普通任务,都是由 CFS 调度器管理的,分为两种调度策略:
- SCHED_NORMAL:普通任务使用的调度策略;
- SCHED_BATCH:后台任务的调度策略,不和终端进行交互,因此在不影响其他需要交互的任务,可以适当降低它的优先级。
每个 CPU 都有自己的运行队列(Run Queue, rq),队列中包含三种调度类的运行队列
调度类有优先级Deadline > Realtime > Fair
先从 dl_rq 里选择任务,然后从 rt_rq 里选择任务,最后从 cfs_rq 里选择任务。因此,实时任务总是会比普通任务优先被执行。
完全公平调度(Completely Fair Scheduling)
这个算法的理念是想让分配给每个任务的 CPU 时间是一样,于是它为每个任务安排一个虚拟运行时间 vruntime,如果一个任务在运行,其运行的越久,该任务的 vruntime 自然就会越大,而没有被运行的任务,vruntime 是不会变化的。
当然为了区分优先级,需要考虑权重值
内核中会有一个 nice 级别与权重值的转换表,nice 级别越低的权重值就越大
其中NICE_0_LOAD可以视为常量
往期打卡
暑假读书笔记第二天
暑假读书笔记第一天