首页 > 其他分享 >【CPU】缓存一致性、CPU基础

【CPU】缓存一致性、CPU基础

时间:2022-10-11 16:23:01浏览次数:45  
标签:dma 缓存 Cache 内存 一致性 数据 CPU

PU的位宽、寻址总线、数据总线

CPU的位宽和寄存器一次能够存储的大小相关,比如一个大于4字节的数据,我们用32位的CPU显然是无法存储它的,自然就无法一次性处理它

而总线位宽和处理器的寻址能力有关,总线宽度越大,能够寻的地址就越大

CPU执行程序的一般步骤

  • 第一步,CPU 读取「程序计数器」的值,这个值是指令的内存地址,然后 CPU 的「控制单元」操作「地址总线」指定需要访问的内存地址,接着通知内存设备准备数据,数据准备好后通过「数据总线」将指令数据传给 CPU,CPU 收到内存传来的数据后,将这个指令数据存入到「指令寄存器」。
  • 第二步,CPU 分析「指令寄存器」中的指令,确定指令的类型和参数,如果是计算类型的指令,就把指令交给「逻辑运算单元」运算;如果是存储类型的指令,则交由「控制单元」执行;
  • 第三步,CPU 执行完指令后,「程序计数器」的值自增,表示指向下一条指令。这个自增的大小,由 CPU 的位宽决定,比如 32 位的 CPU,指令是 4 个字节,需要 4 个内存地址存放,因此「程序计数器」的值会自增 4;

简单总结一下就是,一个程序执行的时候,CPU 会根据程序计数器里的内存地址,从内存里面把需要执行的指令读取到指令寄存器里面执行,然后根据指令长度自增,开始顺序读取下一条指令。

CPU 从程序计数器读取指令、到执行、再到下一条指令,这个过程会不断循环,直到程序执行结束,这个不断循环的过程被称为 CPU 的指令周期

 

cache的写回机制

  • 如果当发生写操作时,数据已经在 CPU Cache 里的话,则把数据更新到 CPU Cache 里,同时标记 CPU Cache 里的这个 Cache Block 为脏(Dirty)的,这个脏的标记代表这个时候,我们 CPU Cache 里面的这个 Cache Block 的数据和内存是不一致的,这种情况是不用把数据写到内存里的;
  • 如果当发生写操作时,数据所对应的 Cache Block 里存放的是「别的内存地址的数据」的话,就要检查这个 Cache Block 里的数据有没有被标记为脏的,如果是脏的话,我们就要把这个 Cache Block 里的数据写回到内存,然后再把当前要写入的数据,先从内存读入到 Cache Block 里,然后再写入的数据写入到 Cache Block,最后也把它标记为脏的;如果 Cache Block 里面的数据没有被标记为脏,则就直接将数据写入到这个 Cache Block 里,然后再把这个 Cache Block 标记为脏的就好了。

可以发现写回这个方法,在把数据写入到 Cache 的时候,只有在缓存不命中,同时数据对应的 Cache 中的 Cache Block 为脏标记的情况下,才会将数据写到内存中,而在缓存命中的情况下,则在写入后 Cache 后,只需把该数据对应的 Cache Block 标记为脏即可,而不用写到内存里。

这样的好处是,如果我们大量的操作都能够命中缓存,那么大部分时间里 CPU 都不需要读写内存,自然性能相比写直达会高很多。

 

缓存一致性问题

问题:多核CPU在访问内存时,由于cache的写回机制,部分数据没有及时更新到内存,那么在不同线程访问同一个变量的时候就会出现读到错误的值

那么,要解决这一问题,就需要一种机制,来同步两个不同核心里面的缓存数据。要实现的这个机制的话,要保证做到下面这 2 点:

  • 第一点,某个 CPU 核心里的 Cache 数据更新时,必须要传播到其他核心的 Cache,这个称为写传播(Write Propagation
  • 第二点,某个 CPU 核心里对数据的操作顺序,必须在其他核心看起来顺序是一样的,这个称为事务的串行化(Transaction Serialization

要实现事务串行化,也就是让不同核心看到的其他核心对于代码的执行顺序是一致的

要实现事务串行化,要做到 2 点:

  • CPU 核心对于 Cache 中数据的操作,需要同步给其他 CPU 核心;
  • 要引入「锁」的概念,如果两个 CPU 核心里有相同数据的 Cache,那么对于这个 Cache 数据的更新,只有拿到了「锁」,才能进行对应的数据更新。

问题:DMA与cache的缓存一致性问题

DMA和CPU可以异步地操作memory,解决访问冲突主要从cache与memory的两种映射机制入手

  • 一致性映射,代表函数:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *handle, gfp_t gfp);  void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t handle); 
 

一般驱动使用多,申请一片uncache mem ,这样无需考虑data 一致性。代码流程:对page property,也就是是kernel页管理的页面属性设置成uncache,在缺页异常填TLB时,该属性就会写到TLB的存储属性域中。保证了dma_alloc_coherent映射的地址空间是uncached的。

dma_alloc_coherent首先对分配到的缓冲区进行cache刷新,之后将该缓冲区的页表修改为uncached,以此来保证之后DMA与CPU操作该块数据的一致性

问题:cache与外设寄存器的一致性问题

外设寄存器和CPU往往也是异步的,有时候某个外设的状态寄存器或者数据寄存器,内容已经改变了,此时如果CPU还是从cache中读值,就会出现不一致问题了

所以对于硬件和外设寄存器的地址空间,操作往往是uncached,保证CPU总是直接读写硬件寄存器。IO空间通过ioremap进行映射到内核空间。ioremap在映射寄存器地址时页表是配置为uncached的。数据不走cache,直接由地址空间中读取。保证了数据一致性。此外,在bootup的过程,在最初阶段的bootrom,系统还会关dcache,具体见笔者对于mboot源码分析的wiki

  • 流式DMA映射

一致性映射会导致性能下降,因为直接ban掉了cache,另外一方面上层传下来的数据包,到了底层用的不是用dma_alloc_coherent申请的一致性内存。

那么流式映射就可以很好的解决问题

dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir)  void dma_unmap_single(struct device *dev, dma_addr_t handle, size_t size, enum dma_data_direction dir)     void dma_sync_single_for_cpu(struct device *dev, dma_addr_t handle, size_t size, enum dma_data_direction dir)  void dma_sync_single_for_device(struct device *dev, dma_addr_t handle, size_t size, enum dma_data_direction dir)     int dma_map_sg(struct device *, struct scatterlist *, intenum dma_data_direction);  void dma_unmap_sg(struct device *, struct scatterlist *, intenum dma_data_direction); 

 

在每发起一次DMA之前都先调用一次dma_map_sg,并指定DMA和外设之间的数据流方向,CPU是可以控制cache的,那么当指定内存流向外设时,则此时CPU把cache中的内容flush回内存,保证DMA出去的数据是最新的;当方向为外设流向内存,则相反,标记cache内容为invalid,此时CPU从内存中读取最新的值

总线嗅探与MESI协议

写传播的原则就是当某个 CPU 核心更新了 Cache 中的数据,要把该事件广播通知到其他核心。最常见实现的方式是总线嗅探(Bus Snooping

我还是以前面的 i 变量例子来说明总线嗅探的工作机制,当 A 号 CPU 核心修改了 L1 Cache 中 i 变量的值,通过总线把这个事件广播通知给其他所有的核心,然后每个 CPU 核心都会监听总线上的广播事件,并检查是否有相同的数据在自己的 L1 Cache 里面,如果 B 号 CPU 核心的 L1 Cache 中有该数据,那么也需要把该数据更新到自己的 L1 Cache。

可以发现,总线嗅探方法很简单, CPU 需要每时每刻监听总线上的一切活动,但是不管别的核心的 Cache 是否缓存相同的数据,都需要发出一个广播事件,这无疑会加重总线的负载。

另外,总线嗅探只是保证了某个 CPU 核心的 Cache 更新数据这个事件能被其他 CPU 核心知道,但是并不能保证事务串行化。

 

MESI 协议其实是 4 个状态单词的开头字母缩写,分别是:

  • Modified,已修改
  • Exclusive,独占
  • Shared,共享
  • Invalidated,已失效

这四个状态来标记 Cache Line 四个不同的状态。

「已修改」状态就是我们前面提到的脏标记,代表该 Cache Block 上的数据已经被更新过,但是还没有写到内存里。而「已失效」状态,表示的是这个 Cache Block 里的数据已经失效了,不可以读取该状态的数据。

「独占」和「共享」状态都代表 Cache Block 里的数据是干净的,也就是说,这个时候 Cache Block 里的数据和内存里面的数据是一致性的。

「独占」和「共享」的差别在于,独占状态的时候,数据只存储在一个 CPU 核心的 Cache 里,而其他 CPU 核心的 Cache 没有该数据。这个时候,如果要向独占的 Cache 写数据,就可以直接自由地写入,而不需要通知其他 CPU 核心,因为只有你这有这个数据,就不存在缓存一致性的问题了,于是就可以随便操作该数据。

另外,在「独占」状态下的数据,如果有其他核心从内存读取了相同的数据到各自的 Cache ,那么这个时候,独占状态下的数据就会变成共享状态。

那么,「共享」状态代表着相同的数据在多个 CPU 核心的 Cache 里都有,所以当我们要更新 Cache 里面的数据的时候,不能直接修改,而是要先向所有的其他 CPU 核心广播一个请求,要求先把其他核心的 Cache 中对应的 Cache Line 标记为「无效」状态,然后再更新当前 Cache 里面的数据。

总结:CPU中的缓存数据在规则的限制下互相通信,通过不同状态的转换,保持缓存的一致性

伪共享

问题:两个核心对同一cacheline的不同变量进行修改,造成这两个变量所在的cacheline不断在共享和已修改以及已失效,于是交替发生从内存读回缓存的操作,降低了缓存的性能(甚至让cache失效)

在 Linux 内核中存在 __cacheline_aligned_in_smp 宏定义,是用于解决伪共享的问题,它会使得变量在cacheline中对齐,从而避免了同一cacheline中出现伪共享,属于空间换时间的思想

提高CPU执行效率的代码

CPU缓存层面:

  • 提升数据缓存的命中率
  • 提升指令缓存的命中率

核心思想都是能够一次读入更多数据或者指令进入CPU,从而减少对于内存的访问,对于数据,比如数组,尽量按照内存分布顺序进行读取,减少跳跃;

对于指令,利用分支预测工具,把更大可能执行的语句加载到内存,C语言中可以利用likely等宏

  • 提升多核CPU的缓存命中率

在单核 CPU,虽然只能执行一个线程,但是操作系统给每个线程分配了一个时间片,时间片用完了,就调度下一个线程,于是各个线程就按时间片交替地占用 CPU,从宏观上看起来各个线程同时在执行。

而现代 CPU 都是多核心的,线程可能在不同 CPU 核心来回切换执行,这对 CPU Cache 不是有利的,虽然 L3 Cache 是多核心之间共享的,但是 L1 和 L2 Cache 都是每个核心独有的,如果一个线程在不同核心来回切换,各个核心的缓存命中率就会受到影响,相反如果线程都在同一个核心上执行,那么其数据的 L1 和 L2 Cache 的缓存命中率可以得到有效提高,缓存命中率高就意味着 CPU 可以减少访问 内存的频率。

当有多个同时执行「计算密集型」的线程,为了防止因为切换到不同的核心,而导致缓存命中率下降的问题,我们可以把线程绑定在某一个 CPU 核心上,这样性能可以得到非常可观的提升。

在 Linux 上提供了 sched_setaffinity 方法,来实现将线程绑定到某个 CPU 核心这一功能。

 

操作系统层面:

  • 减少系统调用,减少内核态和用户态的切换
  • 减少IO操作,减少不必要的拷贝操作

CPU 是如何执行任务的?

假设PC指向0x200,首先控制单元操纵地址总线根据PC的值寻址,接着控制单元操纵数据总线将地址所在的数据(这个数据可能是指令也可能是变量)传回指令寄存器(注意PC指针只是保存指令的地址,指令寄存器另有其器),接着根据指令的类型,将指令分配给逻辑运算单元(各种运算指令)或控制单元(存取寻址类指令)执行,这个指令寄存器以及不同指令的执行的时序以及工作原理与arm流水线架构有关,这个稍后分析

 

标签:dma,缓存,Cache,内存,一致性,数据,CPU
From: https://www.cnblogs.com/jiayu-code/p/16779616.html

相关文章

  • vue3中tab详情页多开keepalive根据key去动态缓存与清除缓存
    一.场景由于存在tab栏,当从查询页面点击列表进入详情时,需求是详情页都会新开一个tab,并缓存,tab中的切换不会重新加载详情页数据,但是关闭一个详情tab,再次从查询页点击这条详......
  • 记一次某制造业ERP系统 CPU打爆事故分析
    一:背景1.讲故事前些天有位朋友微信找到我,说他的程序出现了CPU阶段性爆高,过了一会就下去了,咨询下这个爆高阶段程序内部到底发生了什么?画个图大概是下面这样,你懂的。按......
  • mysql CPU过高排查
    最近发现mysqlCPU使用率高,将排查步骤记录一下一、top命令找到PID为24319二、vmstat1查看一下CPU使用率三、pidstat-u查看一下CPU使用率​四、pidstat-t-p2431......
  • redis本地缓存
    我不想带给你负面情绪,但又想让你知道我的不开心。为了系统性能的提升,一般会将部分数据放入缓存中,加快访问速度。而db承担数据罗盘工作。哪些数据适合放在缓存及时性......
  • mpstat -P ALL 10 不但能查看所有CPU的平均状况信息,而且能够查看特定CPU的信息
        ......
  • linux top 查看CPU命令 top输出详解
    [root@localhost~]$top//动态查看进程使用资源的情况,每三秒刷新一次[root@localhost~]$top-c//动态查看进程使用资源的情况,但会详细地显示进程的命......
  • 英伟达 | 推出适用于AI和高性能计算的NVIDIA GRACE CPU
    计算机视觉研究院专栏作者:Edison_GNVIDIA发布其首款基于Arm架构的数据中心CPU处理器,在最复杂的AI和高性能计算工作负载下,可实现10倍于当今最快服务器的超高性能。4月12日晚,......
  • 19岁天才少年自制CPU!1200个晶体管,纯手工打造
    一个人,到底能“肝”到什么程度?19岁“天才少年”或许给了这个问题一个完美的诠释:耗时整整3年,纯手工自制CPU!这位叫做SamZeloof的美国大学生,最终打造出1200个晶体管的CPU!在1......
  • 什么是缓存雪崩?服务器雪崩的场景与解决方案
    目录什么是应用服务雪崩雪崩效应产生的几种场景缓存雪崩的解决方案雪崩的整体解决方案熔断设计隔离设计超时机制设计如何提前发现雪崩 什么是应用服务雪......
  • 浏览器缓存
    1.由后端通过response响应头设置的缓存1.1强缓存1.1.1什么是强缓存所谓的强缓存是指将由后台返回过来的文件存储到本地,当再次请求相同文件时,不用再去向服务器......