为什么kfifo实现的单生产/单消费模式的共享队列是不需要加锁同步的呢?天底下没有免费的午餐的道理人人都懂,下面我们就来看看kfifo实现并发无锁的奥秘。
我们知道 编译器编译源代码时,会将源代码进行优化,将源代码的指令进行重排序,以适合于CPU的并行执行。然而,内核同步必须避免指令重新排序,优化屏障(Optimization barrier)避免编译器的重排序优化操作,保证编译程序时在优化屏障之前的指令不会在优化屏障之后执行。
举个例子,如果多核CPU执行以下程序:
1: a = 1; 2: b = a + 1; 3: assert(b == 2);
假设初始时a和b的值都是0,a处于CPU1-cache中,b处于CPU0-cache中。如果按照下面流程执行这段代码:
1 CPU0执行a=1;
2 因为a在CPU1-cache中,所以CPU0发送一个read invalidate消息来占有数据
3 CPU0将a存入store buffer
4 CPU1接收到read invalidate消息,于是它传递cache-line,并从自己的cache中移出该cache-line
5 CPU0开始执行b=a+1;
6 CPU0接收到了CPU1传递来的cache-line,即“a=0”
7 CPU0从cache中读取a的值,即“0”
8 CPU0更新cache-line,将store buffer中的数据写入,即“a=1”
9 CPU0使用读取到的a的值“0”,执行加1操作,并将结果“1”写入b(b在CPU0-cache中,所以直接进行)
10 CPU0执行assert(b == 2); 失败
软件可通过读写屏障强制内存访问次序。读写屏障像一堵墙,所有在设置读写屏障之前发起的内存访问,必须先于在设置屏障之后发起的内存访问之前完成,确保内存访问按程序的顺序完成。Linux内核提供的内存屏障API函数说明如下表。内存屏障可用于多处理器和单处理器系统,如果仅用于多处理器系统,就使用smp_xxx函数,在单处理器系统上,它们什么都不要。
smp_rmb 适用于多处理器的读内存屏障。smp_wmb 适用于多处理器的写内存屏障。
smp_mb 适用于多处理器的内存屏障。
如果对上述代码加上内存屏障,就能保证在CPU0取a时,一定已经设置好了a = 1:
1: void foo(void) 2: { 3: a = 1; 4: smp_wmb(); 5: b = a + 1; 6: }
这里只是简单介绍了内存屏障的概念,如果想对内存屏障有进一步理解,请参考我的译文《为什么需要内存屏障》。
四、kfifo的入队__kfifo_put和出队__kfifo_get操作__kfifo_put是入队操作,它先将数据放入buffer中,然后移动in的位置,其源代码如下:
1: unsigned int __kfifo_put(struct kfifo *fifo, 2: const unsigned char *buffer, unsigned int len) 3: { 4: unsigned int l; 5: 6: len = min(len, fifo->size - fifo->in + fifo->out); 7: 8: /* 9: * Ensure that we sample the fifo->out index -before- we 10: * start putting bytes into the kfifo. 11: */ 12: 13: smp_mb(); 14: 15: /* first put the data starting from fifo->in to buffer end */ 16: l = min(len, fifo->size - (fifo->in & (fifo->size - 1))); 17: memcpy(fifo->buffer + (fifo->in & (fifo->size - 1)), buffer, l); 18: 19: /* then put the rest (if any) at the beginning of the buffer */ 20: memcpy(fifo->buffer, buffer + l, len - l); 21: 22: /* 23: * Ensure that we add the bytes to the kfifo -before- 24: * we update the fifo->in index. 25: */ 26: 27: smp_wmb(); 28: 29: fifo->in += len; 30: 31: return len; 32: }
6行,环形缓冲区的剩余容量为fifo->size - fifo->in + fifo->out,让写入的长度取len和剩余容量中较小的,避免写越界;
13行,加内存屏障,保证在开始放入数据之前,fifo->out取到正确的值(另一个CPU可能正在改写out值)