# 硬件设备

# 存储

存储结构分为:缓存、内存、磁盘

缓存: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 锁等)