天天看点

Linux内核链表 内存屏障,从cache一致性到理解内存屏障

本帖最后由 blake326 于 2013-03-07 00:12 编辑

http://bbs.chinaunix.net/forum.p ... =4070325&extra=

http://bbs.chinaunix.net/forum.p ... ;page=6#pid23786513

参考上面两个帖子。纯属个人总结理解,具体实现应该更复杂或者以此为基础。

先说cache 一致性的理解。

假设有cpu0, cpu1。一个虚拟地址addr。

则cpu0, cpu1都有一个addr对应的cache line, 状态有四种:有效E, 修改M,共享S, 无效I。I实际上就是说该addr在cache中没有缓存的意思。根据这四种状态,组合分析一下。

cpu0,cpu1的cache状态一样:

都是I状态:

这种最简单,cpu0读addr的话,则分配一个cache line并且从ddr把addr的值读取过来,状态变成E。cpu0写的话,状态会变成M。

都是S状态:

也很简单,读的话直接从cache读出来。写的话复杂一点,cpu0写addr,修改本地cache,状态变成M。并且发送一个inv消息给cpu1通知cpu1失效响应的cache line。

都是E,M的状态是不存在的。

cpu0,cpu1的cache状态不一样:

cpu0 I,  cpu1 E:

cpu0 读addr,发现cpu1有对应的E cache,新建一个本地的cache line将cpu1的cache line拷贝过来,并且设置大家的状态为S。

cpu0 写addr,发现cpu1有对应的E cache,发送一个inv消息告诉cpu1,然后本地新建一个cache line将ddr读进来,之后修改本地cache line,并且设置状态M。

cpu1 读addr,直接读cache。

cpu1 写addr,直接写cache,设置状态为M。

cpu0 I, cpu1 M:

cpu0 读addr,发现cpu1有对应的M cache,发送一个flush消息告诉cpu1并且等待cpu1刷新cache到ddr,然后新建一个本地cache line将ddr从内存读进来,并且设置状态为E。

cpu0 写addr,发现cpu1有对应的M cache,发送一个flush消息告诉cpu1并且等待cpu1刷新cache到ddr,然后新建一个本地cache line将ddr从内存读进来并且写,并且设置状态为M。

cpu1 读addr,直接读。

cpu1 写addr,直接写。

内存屏障,一个经典的场景,插入链表一个节点。

cpu0

new->next = next;

wmb();

prev->next = new;

cpu1

读 prev->next

rmb();

读 new->next

如果没有内存屏障,cpu1读到最新的prev->next的时候,可能还没有读到最新的new->next。内核就要挂了。

换一种舒服的例子:

cpu0

a = 1;

wmb();

b = 2;

cpu1

读 b;

rmb();

读 a;

通过内存屏障能够保证cpu1假设读取到了b=2,那么cpu1读取的a肯定等于1。

看一看实现:

假设现在a和b在不同的cache line,并且不管在哪个cpu,他们的状态都是S的。(其他状态可以一样的分析)

cpu0三条指令,通过wmb()保证 先发送inv a,在发送inv b。或者说是,cpu1先收到inv a,后收到inv b的消息。

cpu1三条指令,rmb()保证在执行后面的指令之前,rmb前面所有的inv 指令都必须执行完成。假设读b的时候,inv b已经收到并且已经执行,则b获取到了最新的值。那么在执行读a的指令之前,一定会吧inv a执行了,因为inv a在inv 本就收到了(这个是cpu0的wmb保证的)

所以wmb,rmb都是缺一不可的。

补充改一下L2 cache,arm架构经常会有l2 cache, 不同与L1cahce, L2 cache是smp共享的一个cache。上面说的cache刷新到ddr,从ddr读取到cache,其实都是经过这个L2 cache的,比如cache刷新ddr,如果l2cache也有对应的cache的话则这个cache知会刷新到l2 cache, l2 cache会等待适当的时机或者主动调用api被刷新到真正的ddr。读的话也类似。

一般来说,L2 cache对软件来说是很透明的,除了启动的时候要初始化一下,并且需要将cache刷新真正的ddr的时候可以调用响应的操作方法来刷新。比如dma操作之前,可能需要clean或者inv L2 cache。