前言之前看了一篇讨论顺序一致性和缓存一致性的文章,感觉豁然开朗。我想对linux内核中出现的各种同步和障碍做一个总结。缓存一致性我以前以为linux里面很多东西都是用来保证缓存一致性的,其实不然。缓存一致性大部分是由硬件机制实现的,只有在执行带有锁前缀的指令时才与缓存有一点关系。(这是一个绝对的说法,但在我看来到目前为止)我们更多的时候是为了保证顺序的一致性。-所谓缓存一致性,就是在多处理器系统中,每个cpu都有自己的L1缓存。很有可能是同一块内存的内容分别缓存在两个不同cpu的L1缓存中。如果一个cpu改变了自己缓存的内容,它必须保证另一个cpu在读取数据的时候,读取的也是这条数据。.不过不用担心,这项复杂的工作完全由硬件来完成。通过实现一个MESI协议,硬件可以轻松完成缓存一致性工作。不要说一个读一个写,就是多个同时写就可以了。一个cpu在读取的时候总能读到最新的数据,不管是在自己的缓存中,在其他cpu的缓存中,还是在内存中,这就是缓存一致性。顺序一致性所谓顺序一致性,指的是与缓存一致性完全不同的概念,虽然它们都是处理器发展的产物。随着编译器技术的不断发展,它可能会更改某些操作的执行顺序以优化您的代码。多启动和乱序执行的概念早就存在于处理器中。因此,实际执行的指令顺序将与代码编写的顺序略有不同。这在单处理器下当然不算什么。毕竟只要自己的代码不插手,就没人插手。编译器和处理器打乱执行顺序,同时保证自己的代码不会被发现。但这不是多处理器的情况。指令在一个处理器上的完成顺序可能对在其他处理器上执行的代码有很大影响。于是就有了顺序一致性的概念,即保证线程在一个处理器上的执行顺序从其他处理器上的线程来看是相同的。这个问题的解决不能单靠处理器或编译器来解决,而是需要软件的介入。内存屏障软件介入的方式也很简单,就是插入一个内存屏障。其实memorybarrier这个词是处理器人为造成的,让我们很难理解。内存屏障很容易导致我们缓存一致性,甚至怀疑这样做是否能让其他CPU看到修改后的缓存,这是错误的。所谓内存屏障,从处理器的角度看,是用来序列化读写操作的,从软件的角度看,是用来解决顺序一致性问题的。难道编译器不想打乱代码执行的顺序吗?处理器不想乱序执行吗?当你插入内存屏障时,就相当于告诉编译器,屏障前后的指令顺序不能颠倒。告诉处理器只有屏障之前的指令可以被反转。指令执行后,屏障后面的指令就可以开始执行了。当然,内存屏障可以防止编译器乱来,但处理器还是有办法的。处理器不是有多次启动、乱序执行、顺序完成的概念吗?它只需要确保在内存屏障期间必须先完成前面指令的读写操作,然后才能完成后面指令的读写操作。因此,内存屏障对应三种类型:读屏障、写屏障和读写屏障。比如在x86之前,写操作是保证顺序完成的,所以不需要写屏障,但是现在有些ia32处理器的写操作是乱序完成的,所以也需要写屏障。事实上,除了特殊的读写屏障指令外,还有很多指令是用读写屏障函数执行的,比如带有锁前缀的指令。在特殊的读写屏障指令出现之前,Linux是靠锁生存的。至于在那里插入读写屏障,就看软件的需要了。读写屏障不能完全实现顺序一致性,但是多处理器上的线程不会一直盯着你的执行顺序,只要保证它看你的时候认为你是符合顺序一致性的,而且执行时不会出现你代码中意想不到的情况。所谓意外情况,比如你的线程先给变量a赋值,然后给变量b赋值。结果跑在其他处理器上的线程看了看,发现b已经被赋值了,而a却没有。(注意这个不一致不是cache不一致造成的,而是处理器写操作完成顺序不一致造成的)那么a的赋值和b的赋值之间必须加一个writebarrier。多个处理器之间的同步使用SMP,线程同时开始在多个处理器上运行。只要是线程,就有通信和同步的要求。幸运的是,SMP系统是共享内存,即所有处理器看到的内存内容相同。虽然有独立的L1缓存,但是缓存一致性处理的问题还是由硬件来完成。如果不同处理器上的线程想要访问相同的数据,则它们需要临界区和同步。通过什么同步?之前在UP系统中,我们在上层依赖信号量,在下层关闭中断和读写指令。现在在SMP系统中,禁止中断已经被废除。虽然仍然需要在同一个处理器上同步线程,但仅仅依靠它已经不够了。读取、修改和写入命令?它也不起作用。当你指令中的读操作完成而写操作还没有执行时,另一个处理器可能会执行读或写操作。缓存一致性协议是先进的,但它还不够先进,无法预测是哪条指令发出了这个读操作。于是x86又发明了带锁前缀的指令。该指令执行时,所有包含指令中读写地址的缓存行都将失效,内存总线将被锁定。这样,如果其他处理器要对同一地址或同一缓存行上的地址进行读写,既不能从缓存中读取(缓存中的相关行已经失效),也不能从缓存中读取。内存总线(整个内存总线被阻塞)。Locked),终于达到了原子执行的目的。当然,从P6处理器开始,如果加锁前缀的指令要访问的地址已经在缓存中,就不需要加锁内存总线,可以完成原子操作(虽然我怀疑这是因为增加了多处理器内部通用二级缓存的缘故)。因为内存总线会被加锁,未完成的读写操作会在以lock为前缀的指令执行之前完成,这也起到了内存屏障的作用。现在多处理器之间的线程同步,上层使用自旋锁,下层使用锁前缀的读、改、写指令。当然,实际的同步还包括处理器任务调度的禁止,任务关闭和中断的增加,以及信号量外套的增加。Linux中这种自旋锁的实现经历了四代的发展,变得更加高效和强大。内存屏障的实现#endifCONFIG_SMP用于支持多处理器。如果是UP(单处理器)系统,就会翻译成barrier()。#definebarrier()__asm____volatile__(""::::"memory")barrier()的作用是告诉编译器内存中的变量值发生了变化,之前存放在寄存器中的变量的副本为无效的。要访问变量,需要再次访问内存。这样做足以满足UP中的所有内存屏障。#ifdefCONFIG_X86_32/**Somenon-Intelclonesssupportoutoforderstore.wmb()ceasestobea*nopforthese.*/#definemb()alternative("lock;addl$0,0(%%esp)","mfence",X86_FEATURE_XMM2)#definemb()alternative("lock;addl$0,0(%%esp)","lfence",X86_FEATURE_XMM2)#definewmb()alternative("lock;addl$0,0(%%esp)","sfence",X86_FEATURE_XMM)#else#definemb()asmvolatile("mfence"::::"memory")#definermb()asmvolatile("lfence"::::"memory")#definewmb()asmvolatile("sfence"::::"memory")#endif中SMP系统,内存屏障会被翻译成对应的mb()、rmb()和wmb()。这里的CONFIG_X86_32表示这是一个32位的x86系统,否则就是64位的x86系统。目前的Linux内核在同一个x86目录下集成了32位x86和64位x86,所以需要增加这个配置选项。可以看出,如果是64位x86,肯定有mfence,lfence,sfence这三个指令,但是32位x86系统就不一定了,所以还需要进一步查看cpu是否支持这三个新的指令,如果没有,则使用加锁的方法增加内存屏障。SFENCE、LFENCE和MFENCE指令提供了一种有效的方法来确保读写内存的顺序。该操作发生在生成弱排序数据的程序和读取该数据的程序之间。SFENCE-序列化发生在SFENCE指令之前但不影响读取操作的写入操作。LFENCE-序列化发生在SFENCE指令之前但不影响写操作的读操作。MFENCE-序列化在MFENCE指令之前发生的读取和写入。sfence:sfence指令之前的写操作必须在sfence指令之后的写操作之前完成。lfence:lfence指令之前的读操作必须在lfence指令之后的读操作之前完成。mfence:mfence指令之前的读写操作必须在mfence指令之后的读写操作之前完成。对于带锁的内存操作,前面的读写操作会在内存总线被锁之前完成。功能相当于mfence,但是执行效率当然差一些。说起来,现在写一些底层的代码还真是不容易。需要注意SMP问题、cpu乱序读写问题、缓存问题、设备DMA问题等等。
