# 硬件设备
# 存储
存储结构分为:缓存、内存、磁盘
缓存:L1 cache、L2 cache、L3cache。其中,L1 和 L2 每个 CPU 都有,L3 被所有 CPU 共享。
缓存中,以行为单位,即缓存行(cache line),每一行分为有效位、头标志 Tag、数据块 Data Block。一般来说,cache line 大小为 64 字节,任何一个字节上的变量发生变化,就会导致缓存行失效,所以设计程序时应该注意伪共享问题。
CPU 如何知道缓存行中是否有需要的内存数据?
内存的数据和缓存行的数据存在映射关系,不同的映射算法会导致不同的映射规则。
直接映射:内存地址【Tag+Index+Offset】,内存地址的 Index 取模看落到哪个 cache line,然后查看 cache line 的有效位是否有效,再比较 Tag 是否相同,最后通过 Offset 拿到该数据在缓存行中的数据。
# CPU
分支预测器:如果分支预测可以预测到接下来要执行 if 里的指令,还是 else 指令的话,就可以「提前」把这些指令放在指令缓存中,这样 CPU 可以直接从 Cache 读取到指令,于是执行速度就会很快。
对于代码:
int arr[N]; | |
for(int i = 0; i < N; i++) { | |
arr[i] = rand() % 100; // 赋值 0~100 | |
} | |
// 操作 A | |
for(int i = 0; i < N; i++) { | |
if(arr[i] > 50) arr[i] = 0; | |
} | |
// 操作 B | |
sort(arr) |
执行操作 A 再执行操作 B 速度比执行操作 B 再执行操作 A 慢,因为一开始 arr 是随机的,分支预测器不能很好工作,反而会出现一些无效预测,先排序再赋值,分支预测器能更好工作。
CPU 存在缓存一致性问题,这就引来两个需要处理的事情:写传播和事务串行化。写传播就是将修改的数据传给其他 CPU,而事务串行化就是其他 CPU 拿到修改的顺序相同。
MESI 协议在总线嗅探机制上,实现了 CPU 缓存一致性(单纯使用总线嗅探无法保证事务串行化)。
MESI 含义为:
- Modified,已修改,更新了 cache line 数据后的状态,此时 cache line 数据和内存中的数据不同。之后如果该 cache line 会被替换成其他数据,就需要先同步到内存中。
- Exclusive,独占,数据只在该 CPU cache 中
- Shared,共享,数据在多个 CPU cache 中
- Invalidated,已失效,更新共享的数据时,先广播请求,使得其他 CPU 对应的 cache line 设置为无效,再更新 cache line 数据。
表示缓存行的四种状态,具体操作必须等待缓存行成为具体状态才可以执行,假设:A 操作将缓存行状态设为 invalid,此时 B 操作也是对该缓存行的写入操作,B 操作就需要等待 A 操作完成,缓存行状态被设置为 modified 才可以执行 B 操作(设置缓存行无效等等)。
因为一个缓存行的大小一般是 64 字节,所以就会存在伪共享问题:假设 AB 两个对象占用内存比较小,位于同一个缓存行,那么当改变了 A 时,整个缓存行数据都会失效,这也导致 B 本来没有修改,但是也会失效。如果其他 CPU 的也是将 AB 放到了同一个缓存行,这个问题更严重。一般的解决方法就是填充对象,使得一个对象大小为 64 字节。DIsruptor 框架就是用了字节填充 + 继承的方式。在 Linux 中,还有一种宏,可以使的字节对齐,本质还是空间换时间。
线程和进程在 Linux 内核中都是 task_struct
结构体表示的,只不过线程的部分资源共享了进程的资源,因此承载的资源更少。但是本质都是 task_struct
,Linux 中的 CPU 调度器,调度的对象就是 task_struct
。
硬中断和软中断:硬件发出中断(硬中断),CPU 就停下当前任务并处理中断,此时会屏蔽中断(硬中断),快速处理完必要任务后,剩余的长任务(IO 操作等),通过软件指令发出软中断交给内核线程(每个 CPU 都对应一个内核线程)运行。软中断其实还包括一些内核自定义事件(内核调度、RCU 锁等)