透过 Linux 内核看无锁编程(6)

3. Lock -free 应用场景三 —— RCU

在 2.6 内核中,开发者还引入了一种新的无锁机制 -RCU(Read-Copy-Update),允许多个读者和写者并发执行。RCU 技术的核心是写操作分为写和更新两步,允许读操作在任何时候无阻碍的运行,换句话说,就是通过延迟写来提高同步性能。RCU 主要应用于 WRRM 场景,但它对可保护的数据结构做了一些限定:RCU 只保护被动态分配并通过指针引用的数据结构,同时读写控制路径不能有睡眠。以下数组动态增长代码摘自 2.4.34 内核:

清单 7. 2.4.34 RCU 实现代码

其中 ipc_lock 是读者,grow_ary 是写者,不论是读或者写,都需要加 spin lock 对被保护的数据结构进行访问。改变数组大小是小概率事件,而读取是大概率事件,同时被保护的数据结构是指针,满足 RCU 运用场景。以下代码摘自 2.6.10 内核:


清单 8. 2.6.10 RCU 实现代码
#define rcu_read_lock() preempt_disable() #define rcu_read_unlock() preempt_enable() #define rcu_assign_pointer(p, v) ({ \ smp_wmb(); \ (p) = (v); \ }) struct kern_ipc_perm* ipc_lock(struct ipc_ids* ids, int id) { …… rcu_read_lock(); entries = rcu_dereference(ids->entries); if(lid >= entries->size) { rcu_read_unlock(); return NULL; } out = entries->p[lid]; if(out == NULL) { rcu_read_unlock(); return NULL; } …… return out; } static int grow_ary(struct ipc_ids* ids, int newsize) { struct ipc_id_ary* new; struct ipc_id_ary* old; …… new = ipc_rcu_alloc(sizeof(struct kern_ipc_perm *)*newsize + sizeof(struct ipc_id_ary)); if(new == NULL) return size; new->size = newsize; memcpy(new->p, ids->entries->p, sizeof(struct kern_ipc_perm *)*size +sizeof(struct ipc_id_ary)); for(i=size;i<newsize;i++) { new->p[i] = NULL; } old = ids->entries; /* * Use rcu_assign_pointer() to make sure the memcpyed contents * of the new array are visible before the new array becomes visible. */ rcu_assign_pointer(ids->entries, new); ipc_rcu_putref(old); return newsize; }  

纵观整个流程,写者除内核屏障外,几乎没有一把锁。当写者需要更新数据结构时,首先复制该数据结构,申请 new 内存,然后对副本进行修改,调用 memcpy 将原数组的内容拷贝到 new 中,同时对扩大的那部分赋新值,修改完毕后,写者调用 rcu_assign_pointer 修改相关数据结构的指针,使之指向被修改后的新副本,整个写操作一气呵成,其中修改指针值的操作属于原子操作。在数据结构被写者修改后,需要调用内存屏障 smp_wmb,让其他 CPU 知晓已更新的指针值,否则会导致 SMP 环境下的 bug。当所有潜在的读者都执行完成后,调用 call_rcu 释放旧副本。同 Spin lock 一样,RCU 同步技术主要适用于 SMP 环境。

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

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