嵌入式系统Linux内核开发实战指南(ARM平台)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第6章 ARM处理器存储访问一致性问题

6.1 存储访问一致性问题介绍

当存储系统中引入了cache和写缓冲区(Write Buffer)时,同一地址单元的数据可能在系统中有多个副本,分别保存在cache、Write Buffer及主存中,如果系统采用了独立的数据cache和指令cache,同一地址单元的数据还可能在数据cache和指令cache中有不同的版本。位于不同物理位置的同一地址单元的数据可能会不同,使得数据读操作可能得到的不是系统中“最新的数值”,这样就带来了存储系统中数据的一致性问题。在ARM存储系统中,数据不一致的问题则需要通过程序设计时遵守一定的规则来保证,这些规则说明如下。

6.1.1 地址映射关系变化造成的数据不一致性

当系统中使用了MMU时,就建立了虚拟地址到物理地址的映射关系,如果查询cache时进行的相连比较使用的是虚拟地址,则当系统中虚拟地址到物理地址的映射关系发生变化时,可能造成cache中的数据和主存中数据不一致的情况。

在虚拟地址到物理地址的映射关系发生变化前,如果虚拟地址A1所在的数据块已经预取到cache中,当虚拟地址到物理地址的映射关系发生变化后,如果虚拟地址A1对应的物理地址发生了改变,则当CPU访问A1时再使用cache中的数据块将得到错误的结果。

同样当系统中采用了Write Buffer时,如果CPU写入Write Buffer的地址是虚拟地址,也会发生数据不一致的情况。在虚拟地址到物理地址的映射关系发生变化前,如果CPU向虚拟地址为A1的单元执行写操作,该写操作已经将A1以及对应的数据写入到Write Buffer中,当虚拟地址到物理地址的映射关系发生变化后,如果虚拟地址A1对应的物理地址发生了改变,当Write Buffer将上面被延迟的写操作写到主存中时,使用的是变化后的物理地址,从而使写操作失败。

为了避免发生这种数据不一致的情况,在系统中虚拟地址到物理地址的映射关系发生变化前,根据系统的具体情况,执行下面操作序列中的一种或几种:

1)如果数据cache为write back类型,清空该数据的cache;

2)使数据cache中相应的块无效;

3)使指令cache中相应的块无效;

4)将Write Buffer中被延迟的写操作全部执行;

5)有些情况可能还要求相关的存储区域被设置成非缓冲的。

6.1.2 指令cache的数据不一致性问题

当系统中采用独立的数据cache和指令cache时,下面的操作序列可能造成指令不一致的情况:

1)读取地址为A1的指令,从而包含该指令的数据块被预取到指令cache中。

2)与A1在同一个数据块中的地址为A2的存储单元的数据被修改,这个数据写操作可能影响数据cache、Write Buffer和主存中地址为A2的存储单元的内容,但是不影响指令cache中地址为A2的存储单元的内容。

3)如果地址A2存放的是指令,当该指令执行时,就可能发生指令不一致的问题。如果地址A2所在的块还在指令cache中,系统将执行修改前的指令。如果地址A2所在的块不在指令cache中,系统将执行修改后的指令。

为了避免这种指令不一致情况的发生,在上面第1)步和第2)步之间插入下面的操作序列:

1)对于使用统一的数据cache和指令cache的系统,不需要任何操作;

2)对于使用独立的数据cache和指令cache的系统,使指令cache的内容无效;

3)对于使用独立的数据cache和指令cache的系统,如果数据cache是write back类型的,清空数据cache。

当数据操作修改了指令时,最好执行上述操作序列,保证指令的一致性。下面是上述操作序列的一个典型应用场合。当可执行文件加载到主存中后,在程序跳转到入口点处开始执行之前,先执行上述的操作序列,以保证下面指令的是新加载的可执行代码,而不是指令中原来的旧代码。

6.1.3 DMA造成的数据不一致问题

DMA操作直接访问主存,而不会更新cache和Write Buffer中相应的内容,这样就可能造成数据的不一致。

如果DMA从主存中读取的数据已经包含在cache中,而且cache中对应的数据已经被更新,这样DMA读到的将不是系统中最新的数据。同样DMA写操作直接更新主存中的数据,如果该数据已经包含在cache中,则cache中的数据将会比主存中对应的数据“老”,也将造成数据的不一致。

为了避免这种数据不一致情况的发生,根据系统的具体情况,执行下面操作序列中的一种或几种:

1)将DMA访问的存储区域设置成非缓冲的,即uncachable及unbufferable;

2)将DMA访问的存储区域所涉及数据cache中的块设置成无效,或者清空数据cache;

3)清空Write Buffer(执行Write Buffer中延迟的所有写操作);

4)在DMA操作期间限制处理器访问DMA所访问的存储区域。

6.1.4 指令预取和自修改代码

在ARM中允许指令预取,在CPU执行当前指令的同时,可以从存储器中预取其后若干条指令,具体预取多少条指令,不同的ARM实现中有不同的数值。

当用户读取PC寄存器的值时,返回的是当前指令下面第2条指令的地址。比如当前执行的是第N条指令,当用户读取PC寄存器的值时,返回的是指令N+2的地址。对于ARM指令来说,读取PC寄存器的值时,返回当前指令地址值加8个字节;对于Thumb指令来说,读取PC寄存器的值时,返回当前指令地址值加4个字节。

6.2 Linux中解决存储访问一致性问题的方法

在Linux中,是用barrier()宏来解决以上存储访问一致性问题的,barrier()的定义如下所示:

        #define barrier() __asm__ __volatile__("": : :"memory")

另外在barrier()的基础上还衍生出了很多类似的定义,如:

        #define mb() __asm__ __volatile__ ("" : : : "memory")
        #define rmb() mb()
        #define wmb() mb()
        #define smp_mb()        barrier()
        #define smp_rmb()       barrier()
        #define smp_wmb()       barrier()

barrier是内存屏障的意思,CPU越过内存屏障后,将刷新自己对存储器的缓冲状态。barrier()宏定义这条语句实际上不生成任何代码,但可使gcc在barrier()之后刷新寄存器对变量的分配。具体分析如下。

概括起来说barrier()起到两个作用:

1)告诉编译器不要优化这部分代码,保持原有的指令执行顺序;

2)告诉CPU执行完barrier()之后要进行同步操作,更新registers、cache、写缓存和内存中的内容,全部重新从内存中取数据。