Linux内核数据结构kfifo详解(2)

  为什么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值)

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/28d39759dbcdb0e55d273fcdd41171d4.html