资讯详情

资讯详情

WriteClean与普通AXI写有何不同?解析ACE缓存一致性维护技术

1. 从AXI普通写入手WriteClean不同在哪里1.1 AXI写通道的核心流程如果只看信号波形WriteClean跟一次普通的AXI写几乎长得一模一样都是AW通道发起地址W通道送数据B通道拿响应。很多刚接触ACE的工程师第一反应就是“这不就是个写操作嘛我按AXI写来做不就行了”。但如果你真的把它当成普通AXI写去实现缓存一致性系统很快就会给你一个非常惨痛的教训。先花点时间把AXI的普通写流程捋一遍。一个完整的AXI4写事务由三个独立通道完成AW通道负责携带地址、突发长度、突发大小和突发类型W通道负责搬运实际数据每拍数据都有对应的WSTRB字节选通信号最后一拍要拉高WLASTB通道是写响应通道slave处理完写请求后通过BID和BRESP告诉master这笔写到底是被接受、成功完成还是出错了。三个通道各自有独立的VALID和READY握手信号master和slave之间通过握手来完成发送和接收的同步只要VALID和READY同时拉高这一拍就算传成了。在这个过程中有一个关键点很容易被忽略AXI的写响应不代表数据一定已经落到了最终目的地。Interconnect完全可以把这笔写数据缓冲下来先把B响应返回给master之后再慢慢把数据往DDR里刷。这在AXI里是合法且合理的做法因为很多外设或者DMA发起的写并不需要严格等待数据落盘只要数据被系统正确接收就可以了。AXI把这个属性称作bufferable可缓冲反映在AWCACHE的bit0上AWCACHE[0]1表示这笔写可以被缓冲0表示必须不可缓冲数据要真正抵达终点才能返回响应。正是这个“可缓冲”的机制给后续理解WriteClean挖了一个大坑。1.2 普通写回数据时的隐患现在假设一个最简单的场景CPU的L1 D-Cache里有一个脏行CPU决定把它写回内存。如果用普通AXI写来完成会发生什么首先这个写请求在interconnect眼里就是一次普通写它可能被缓冲B响应可以提前返回。CPU收到B响应后如果立刻执行一条对同一地址的读取指令读到的可能是旧数据因为真正的数据还躺在interconnect的缓冲队列里。当然interconnect内部有仲裁和顺序机制同一个master自己发起的读写一般还能保证一定顺序但如果此时是另一个master比如DMA去读这个地址情况就复杂了。更深层的问题是一个脏cache line的写回本质上是把“还在缓存里的数据”搬到“所有agent都能看到的地方”。这个操作需要系统其他部分知道——这个缓存行正在被写回而不是一个全新的写操作。如果系统不知道就无法保证缓存行状态的一致性。第二普通AXI写可能携带部分写WSTRB不是全1也可能是一个小size的突发。一个脏行写回内存时我们希望把整个cache line的数据完整地写出去部分写会把缓存行里未修改的字节也覆盖成某些不确定的值这在缓存逐出场景下会造成数据损坏。第三一个更隐蔽的问题当CPU用普通AXI写回一个脏行时它自己cache里这个行还在吗如果CPU想保留这行写回之后它应该是clean状态。这个状态信息只存在CPU自己心里根本没法高效地告诉总线上的其他agent。其他agent可能会认为这个行是dirty的或者继续用自己本地的旧副本这就直接破坏了缓存一致性。1.3 为什么AXI对缓存维护无能为力说白了AXI协议从来没有考虑过“缓存一致性”这回事。AXI的哲学是master只管发起读写interconnect只管把读写路由到对端双方都不关心数据是否在某个master的cache里有一份副本。这个设计让AXI足够简单、高效但当系统里出现多个带cache的master时问题就来了。多核CPU场景里每个核都有自己的L1/L2两个核同时缓存同一个地址、其中一个还修改了数据如果系统完全靠普通AXI读写去同步谁先谁后根本说不清楚必须有一套协议来定义什么时候数据可见、什么时候缓存行失效、什么时候允许某个master独占修改。ACEAXI Coherency Extension就是在AXI基础上补上了这一环它保留了AXI的全部通道和事务让不带cache的普通AXI master照样能挂在同一个互联系统里同时在带cache的master之间增加了细粒度的一致性问题。而WriteClean就是ACE为了“缓存行写回但还要保留在缓存中”这个需求专门定义的一种cache maintenance操作。从AXI工程师的视角看它确实就是一次写事务但它背后所表达的一致性语义完全不是普通AXI写能覆盖的。理解了这一层下面看到的那些总线参数、状态迁移才变得有意义。2. ACE缓存状态机WriteClean到底在做什么2.1 cache line的五个状态既然要讲清楚WriteClean就绕不开ACE里定义的缓存行状态。别看ACE协议文件厚厚一大本缓存在每个master内部维护的状态其实就五种Invalid无效、UniqueClean唯一且干净、UniqueDirty唯一且脏、SharedClean共享且干净、SharedDirty共享且脏。Unique和Shared的区别在于这个cache line在当前系统里是不是只有你这一份。Unique意味着你是唯一持有者可以随便修改不需要通知任何人Shared意味着可能有多个master都持有这个行你改之前必须通过一致性机制先获得权限。Clean和Dirty则很好理解Clean表示缓存行数据和内存一致Dirty表示缓存行比内存新内存里的版本已经过期了。这五个状态可以用一个很生活化的比方来理解UniqueDirty就像你手里改了一半的文档只有你有一份别人想看你必须问你要UniqueClean就像你手里有一份和服务器完全一样的文档别人也想看的话系统可以让你把这份文档提供给别人SharedClean就是几个人手里拿着同一份标准文档谁也没改过SharedDirty则更像几个人共同维护一份文档不同的人改了不同的段落情况最复杂。2.2 WriteClean是一次状态迁移WriteClean这个操作的名字很像“干净地写”但它的真正含义是“写回去并保持干净”。一个master对自己cache里面处于UniqueDirty状态的行发起WriteClean数据会通过总线的写通道写回内存B响应回来之后这个行从UniqueDirty变成UniqueClean仍然留在cache里。这个过程在状态机上非常明确发起前你是唯一持有者你手里的数据比内存新写回之后内存里的数据和你手里的数据一致了但你依然唯一持有它所以进入UniqueClean。这个迁移路径就是WriteClean的核心价值既完成了数据写回又没有放弃自己对这个行的占用权。你可能会问既然数据已经写回去了为什么还要把行留在cache里直接丢掉不是更省事答案是性能。如果这个行接下来的短时间内还要被反复访问保留在cache里就能避免一次又一次的cache miss和内存读取。WriteClean就是专门为“我还想继续用这个行但现在得让其他人也能看到我的数据”的场景准备的。与之对应的WriteBack操作则是“写回去之后直接变成Invalid”数据交给内存自己不再保留这个后面专门展开讲。2.3 为什么必须保持Unique权限WriteClean还有一个非常关键的前置条件只有该行处于Unique状态严格说主要是UniqueDirty时才能发起WriteClean。为什么必须是Unique想一想如果行是SharedDirty会怎样你手上有一份干净的缓存副本但另一个master把同一地址的数据改成了dirty你们共同持有这个行内存里的数据已经过期。这时候你贸然发起一个写回把你自己那份数据写到内存里很可能把另一个master已经修改的内容覆盖掉。这就像两个人共同维护一个文档一个人改了一段内容后保存了另一个人却用自己未修改的旧版本去覆盖整个文档直接被改回去了。所以ACE规定WriteClean只能由当前唯一持有者发起这从协议层面堵死了“旧数据覆盖新数据”的风险。这个Unique权限可以由master自己在访问时通过一致性操作获取比如先执行一次Make Unique之类的操作把共享状态转换成唯一状态再执行WriteClean。多master并发场景下snoop过滤器也会确保在这段时间里没有其他master突然插入对这个行的操作。这样想就通了WriteClean不是一个随心所欲的写而是“我拥有独占权我自愿把数据交还内存但我还想留着这个行继续用”的一次状态宣告。3. WriteClean总线事务长什么样3.1 通道、地址与burst参数从波形上看WriteClean使用AW、W、B三个通道与普通AXI写共用一套握手机制。但在具体参数上有明确的约束验证的时候如果没留意很容易不知不觉构造出非法的WriteClean事务。首先是地址对齐和cache line对齐。WriteClean操作的地址必须是一个cache line的起始地址。如果系统里的cache line是64字节那地址低6位必须全为0。这个要求的来源很朴素写回操作搬运的是整个缓存行而不是任意一段数据地址必须落在行的边界上。然后是burst参数。WriteClean的burst类型通常要求使用INCRAWLEN根据cache line大小和总线位宽计算。举个例子内存总线是128位即16字节一拍缓存行是64字节那么AWLEN就等于64/16-13表示4拍传完整个行。AWSIZE等于总线的传输宽度这里就是0b10016字节。如果cache line是128字节总线宽度是32字节那AWLEN就是3AWSIZE对应32字节。有个容易记混的点普通AXI写可以一个事务只写一个字节可以突发只写2拍可以突发的首地址不落在任何边界上这些都合法。但WriteClean不一样它必须一次完整地覆盖整个cache line。规格上没有给你“只写半个行”或者“跨两个行写”的选项因为这本来就不是按字节寻址的普通写而是按行寻址的维护操作。3.2 WSTRB与整行写回跟burst参数同样容易忽视的是WSTRB。WriteClean要求整个cache line的所有字节都有效也就是说WSTRB必须全部为1。为什么会有这个硬性要求因为Dirty行的每个字节都可能是被修改过的写回的目的是把当前的完整数据搬到内存而不是只搬一部分。如果某个字节没有写选通interconnect和home node会认为这个字节数据无效写回之后内存里对应字节没有被更新但另一个master读取时却看到“写回已完成”的一致性视图直接导致数据不一致。我在实际验证中见过一个很典型的bug验证人员复用了普通AXI写的激励WSTRB随机生成结果跑到某个用例时DDR里的数据总是缺几字节系统偶尔出现读回旧数据定位了很久才发现WriteClean事务的WSTRB没有全1。这类问题不只在测试环境里有真实硬件里也有过因为状态机偷懒没检查WSTRB导致的数据损坏案例。数据内容和时序上WriteClean也有个小特点W通道上的数据必须和发起时cache行里的内容完全一致。协议不允许你通过WriteClean把缓存行的数据改成别的值再写回因为这会破坏缓存一致性语义。你能做的是自己想清楚要保留的行内容原样送给总线。3.3 非缓冲属性与响应顺序普通AXI写允许bufferable但WriteClean不行它必须是非缓冲的。在ACE的接口定义里这对应AWCACHE[0]必须为0以及整个事务被标记为维护操作。为什么不能缓冲因为WriteClean的B响应承载着非常重要的语义它告诉master这个行的数据已经安全到达内存其他master从这个地址读取时一定能读到最新值。如果interconnect只把这个数据放在缓冲队列里就给B响应那么master把行从UniqueDirty改成UniqueClean就是错的因为数据并没有真正落到稳定的存储点。一旦后续发生掉电或者其他异常这个缓冲中的数据就可能丢失。因此interconnect看到WriteClean事务时必须禁止自己做写缓冲优化必须保证数据真正到达Point of Coherence或者Point of Serialization之后才允许B响应返回。这个顺序约束是一条红线总线协议里所有关于数据可见性的承诺都建立在它上面。对CPU实现来说这个约束还隐含着一层意思WriteClean的完成时刻就是缓存行状态从Dirty变成Clean的时刻不能提前也不能延后。如果提前改了状态系统里就会出现“数据还没落地但状态已经是clean”的窗口期如果延后又会拖慢后续对该行的访问。因此验证工程师在检查WriteClean时序时一定要把B响应和内部状态更新关联起来审查。下面用一张表把WriteClean和普通AXI写放在一起对照对比项普通AXI写WriteClean使用通道AW/W/BAW/W/B地址对齐任意字节对齐必须对齐到cache line起始突发长度任意合法突发必须覆盖整个cache lineWSTRB任意合法组合必须全1burst类型INCR/WRAP/FIXED均可一般要求INCR缓冲属性可为bufferable必须non-bufferable数据来源CPU写端口或DMA数据cache中的脏行数据操作目的写入新数据将旧数据写回并保持clean一致性影响无特殊语义触发状态迁移UD→UC4. WriteClean与WriteBack一对容易混淆的操作4.1 一个保留、一个失效WriteClean和WriteBack是ACE里最容易被搞混的一对很多资料喜欢把这两个都叫做“写回操作”。但在缓存状态迁移的角度上它们的区别非常分明WriteClean写回之后保留该行状态从UniqueDirty变成UniqueCleanWriteBack写回之后直接丢弃该行状态从UniqueDirty变成Invalid。可以这样记Clean是把房间打扫干净人还留在房间里Back是收拾好东西走人房间直接退掉。内存数据被更新的结果是相同的但那之后你cache里还有没有这个数据是完全不同的两回事。这个区别在总线事务上体现得比较微妙。从AW/W/B通道上看两者几乎一样都是整行写回都是非缓冲的维护操作WSTRB都是全1B响应之后事务才算真正完成。但从master内部的实现看区别非常大WriteClean完成之后该行仍然占据着cache的一个位置后续访问这个地址可以直接命中WriteBack完成之后该行立刻成为Invalid如果再要访问它必须先重新发起一次读取。4.2 场景差异应用上的差异也很明显。举一个典型的缓存替换场景当CPU cache被填满需要腾出一个位置放新数据时如果被替换的行是dirty的CPU必须先把它写回内存再把行废弃。这时候用WriteBack最合适因为它省事写完不需要继续维护这个行新数据可以直接占据这个cache line。反过来看另一个场景CPU运行过程中需要频繁访问一个大数组的某一段但系统因为功耗或者其他原因要定期把修改过的数据推送到内存以保证其他一致性agent可以随时读取最新数据。此时CPU并不想放弃cache里的行因为之后马上还要继续访问。如果傻傻地WriteBack接下来每一次访问都会重新从内存load一遍性能损失巨大。正确的选择就是WriteClean数据推下去行保留着下次访问直接命中缓存。还有一个比较微妙的场景共享数据的发布。一个CPU核更新了一批数据结构后想通知其他核“可以读了”但它自己接下来还会继续读这些数据。此时用WriteClean把脏行推送到PoC其他核通过正确的缓存一致性流程读到的就是更新后的内容而发起者自己因为行还在cache里后续访问也很快。如果用WriteBack虽然效果也能达成但发起者自己下次读就需要重新加载了得不偿失。4.3 对照参考对比项WriteCleanWriteBack操作前状态UniqueDirty为主UniqueDirty为主操作后状态UniqueCleanInvalid是否保留缓存行保留不保留典型场景主动刷新、共享数据发布缓存替换、逐出后续访问代价可能命中cache必然需要重新加载总线事务形态整行写回、非缓冲整行写回、非缓冲如果你看到这里还是有点拿不准有一个很实用的判断技巧问自己一个问题“这个行写完以后我还想用吗”想用选WriteClean不想用选WriteBack。5. 真实系统里WriteClean的典型用法5.1 CPU缓存逐出与刷新在真实的SoC里CPU cache的逐出控制逻辑里会同时挂上WriteClean和WriteBack两个操作根据替换策略和后续访问预测来选择。比如经典的随机替换策略中CPU根本不知道某一行之后还会不会被用到有些实现会保守地选择WriteClean因为万一之后要访问还能从cache里直接命中。另外一个非常典型的场景是cache刷新指令。很多处理器架构里提供了一条类似“clean cache line”的指令它做的事情就是把指定地址的脏行写回内存同时保留这个行在缓存中。这个指令背后的硬件实现走的就是WriteClean事务。你可以在Linux内核里看到大量这种用法当设备驱动需要把CPU缓存里的数据同步给DMA设备时先执行clean操作再启动DMA确保DMA读到的数据是最新的。这种用法下有个容易被忽略的细节WriteClean只保证数据到PoC不一定到主存。如果系统里还有第三级缓存没有写回DMA和CPU看到的可能是PoC一级的同一份数据但主存未必是最新的。对大多数一致性场景来说只要全局所有agent都认可PoC这个点就足够了不需要强制刷到DDR。5.2 多核场景下的缓存推送多核处理器里WriteClean还有一个非常漂亮的用法用于高效地让数据从私有缓存走向共享空间。考虑两个CPU核共享一个变量核0更新了这个变量希望核1之后能读到最新值。核0如果只是把新值写到自己L1里核1在自己的L1里可能还保留着旧副本此时核1想读会从自己的L1返回旧数据根本不触发任何一致性交互。如果核0对这个地址发起WriteClean数据会被推送到PoC同时核0这边变成clean。此时核1如果恰好有这个行的shared副本一致性系统会通过失效等手段保证它下一次读到的是新数据。当然真正的多核场景里这种同步还会配合各种内存屏障指令使用但WriteClean在这里扮演的角色就是把脏数据从“私有”变成“公开”。在一致性互联内部WriteClean事务到达home node后通常还会触发对系统中其他cache副本的清理。这也就是为什么ACE要专门定义这套操作而不让所有Cache直接发普通AXI写互联需要知道“这是一个整行回写”才能正确地推进内部目录状态、维护其他副本的一致视图。5.3 加速器与DMA的一致性配合要把WriteClean推荐给做DMA和加速器的人。一个不带cache的DMA master挂在ACE平台上时它通常作为AXI slave或者普通AXI master存在。CPU往DMA缓冲区写入一批数据后如果缓冲区之前在CPU cache里是dirty的直接启动DMA就可能读到过期数据。正确流程是CPU先对缓冲区所在的cache行执行一次WriteClean或者WriteBack把脏数据推到PoC再启动DMA。因为WriteClean之后缓存行还保留着如果之后CPU还要频繁访问这些数据就不用重新加载。对比WriteBack最大的好处在这里体现出来了缓冲区很大如果每个cache行都WriteBackDMA跑完之后CPU访问缓冲区全都要miss性能衰减非常明显。用WriteClean的话数据又新鲜缓存命中率又保留两全其美。6. 验证与实现里容易踩的坑6.1 激励构造size/burst/strobe最容易错在验证环境里构造WriteClean最容易出错的不是握手时序而是三个看起来不起眼的参数AWSIZE、AWLEN和WSTRB。很多从AXI转过来的验证工程师会随手填一个普通的burst配置结果就跑出非法的WriteClean。首先确认cache line size和总线宽度的换算关系。如果cache line是64B总线宽度是32B那AWLEN就是1AWSIZE0b10132字节。如果总线宽度是16BAWLEN就是3AWSIZE0b100。如果你想当然地填了AWLEN15那这个事务实际上会覆盖512字节至少跨越8个cache line完全不符合WriteClean的定义。其次WSTRB必须全1。这是最容易在随机化激励里漏掉的一点。我建议在约束里专门写死这个条件不要让它随机否则一个用例跑上几万次才触发一次WSTRB不全的错误定位起来非常折磨。还有WLAST的位置必须在最后一个有效数据拍上这个和普通AXI是一致的但因为WriteClean的len计算一旦出错WLAST的位置也会跟着错数据分析时可以把这两项放在一起检查。6.2 检查点清单验证WriteClean时我习惯把检查项分成协议层和一致性层两层。协议层检查眼前的问题AWADDR是不是cache line对齐AWLEN和AWSIZE是否覆盖完整行WSTRB是否全1burst类型是否合规AWCACHE[0]是否被写0B响应是否在数据真正到达终点后才返回。这一层相对机械但非常重要很多低级bug就藏在里面。一致性层检查更深的问题事务完成后发起master的cache line是否从UniqueDirty迁移到UniqueClean期间是否有其他master对同一地址发起了读请求数据的返回顺序是否满足协议要求如果是多master并发场景snoop响应和WriteClean的数据是否可能出现交叉会不会出现“先写后读”的顺序颠倒。这一层的验证难点在于你无法只靠检查某一根信号来确认正确性必须把一致性状态模型也纳入检查。我用过一个很土但很有效的办法在验证环境里建一个参考模型单独跟踪每个cache line在master内部的状态。每发起一笔WriteClean就在参考模型里把对应行的状态从UD改成UC。DUT的行为如果和参考模型不一致那不管波形多漂亮都是错的。6.3 并发snoop和多master场景多master并发是WriteClean验证里最深的水域。一个master正在发起WriteClean另一个master同时对同一地址发起读取或写入此时总线上会出现snoop请求和WriteClean数据同时存在的场面ACE协议里面对这种race condition有一整套规则比如snoop响应和给home node的数据要按照特定顺序组合。很多验证平台最初只测单master的WriteClean跑得很顺一旦接入第二个ACE master开始做随机并发用例就不断冒出一致性错误。解决思路是在验证环境中引入一个内存模型对所有master发起的读写和维护操作统一排序每笔操作都检查最终内存数据和所有master的缓存状态是否一致。这个模型不一定需要实现全套ACE协议只需要精确建模数据可见性和状态迁移就能把绝大多数据一致性bug挖出来。对底层实现来说这里最常踩的坑是snoop过滤器和WriteClean请求对同一cache line的状态更新发生了竞争。比如home node刚收到WriteClean还没更新自己的目录状态另一个master的读取请求就到了按旧状态处理了请求导致读到的不是最新数据。解决思路常见的是在home node对同一地址的请求做序列化处理让同地址上的多个操作按到达顺序依次生效。6.4 关于缓冲属性的常见误解AWCACHE[0]这一位是常见误解的重灾区。很多AXI工程师习惯了给普通写事务设置bufferable1因为这样性能好。到了ACE里如果复用旧的激励模板极有可能把WriteClean的bufferable位也顺手写成1。一旦WriteClean变成bufferableinterconnect就有权在缓冲队列里保留数据并提前返回B响应。master看到B响应后把缓存行改成clean但数据实际还没到达PoC。此时另一个master发起读取会看到旧数据直接造成不可接受的一致性错误。更恶心的是这类bug的触发条件往往需要很微妙的总线时序普通测试用例跑不到得靠长时间的随机验证才能复现一旦在真实系统里出现基本就是偶发数据错误难查得要命。所以做ACE系统的验证时我建议专门写一个断言检查所有标记为维护操作的写事务的AWCACHE[0]必须为0任何时候触发这个断言都直接报错。这个断言成本极低回报极高能拦住一大批从AXI迁移过来的惯性错误。还有一点值得提醒AWCACHE的编码在AXI和ACE里虽然都叫AWCACHE但ACE针对cache maintenance操作有更严格的编码要求。如果你们家的一致性互联做了一些实现简化可能只允许特定的AWCACHE值随手填一个“看起来差不多”的编码可能在AXI部分没问题但在ACE协议检查里直接fail。写在最后当初我刚从AXI切到ACE时最大的困惑就是总线上都是一样的写波形为什么非要区分普通写和WriteClean。后来我把思路从“总线上有什么信号”切换到“缓存状态要怎么变”一切就顺了普通AXI写对缓存状态无感WriteClean则是一个完整的状态迁移动作。记这两个操作有一个特别有效的土办法Clean是打扫完还住在屋里Back是退房走人。最后再提醒一句工程上大家都在关注复杂的一致性算法但最致命的问题往往出在AWCACHE[0]、WSTRB和burst长度这种三秒钟就能写完的检查上多花点时间在这些基础约束上比去研究深奥的协议边界划算得多。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →