Linux kernel 内存屏障在RCU上的应用

内存屏障主要解决的问题是编译器的优化和CPU的乱序执行。

 

编译器在优化的时候,生成的汇编指令可能和c语言程序的执行顺序不一样,在需要程序严格按照c语言顺序执行时,需要显式的告诉编译不需要优化,这在linux下是通过barrier()宏完成的,它依靠volidate关键字和memory关键字,前者告诉编译barrier()周围的指令不要被优化,后者作用是告诉编译器汇编代码会使内存里面的值更改,编译器应使用内存里的新值而非寄存器里保存的老值。

 

同样,CPU执行会通过乱序以提高性能。汇编里的指令不一定是按照我们看到的顺序执行的。linux中通过mb()系列宏来保证执行的顺序。简单的说,如果在程序某处插入了mb()/rmb()/wmb()宏,则宏之前的程序保证比宏之后的程序先执行,从而实现串行化。

 

即使是编译器生成的汇编码有序,处理器也不一定能保证有序。就算编译器生成了有序的汇编码,到了处理器那里也拿不准是不 是会按照代码顺序执行。所以就算编译器保证有序了,程序员也还是要往代码里面加内存屏障才能保证绝对访存有序,这倒不如编译器干脆不管算了,因为内存屏障 本身就是一个sequence point,加入后已经能够保证编译器也有序。

 

处理器虽然乱序执行,但最终会得出正确的结果,所以逻辑上讲程序员本不需要关心处理器乱序的问题。但是在SMP并发执行的情况下,处理器无法知道并发程序之间的逻辑,比如,在不同core上的读者和写者之间的逻辑。简单讲,处理器只保证在单个core上按照code中的顺序给出最终结果。这就要求程序员通过mb()/rmb()/wmb()/read_barrier_depends来告知处理器,从而得到正确的并发结果。内存屏障、数据依赖屏障都是为了处理SMP环境下的数据同步问题,UP根本不存在这个问题。


下面分析下内存屏障在RCU上的应用:

#define rcu_assign_pointer(p, v)        ({ \

                                                                        smp_wmb();\

                                                                        (p)= (v); \

                                                })


#define rcu_dereference(p)     ({ \

                                                typeof(p)_________p1 = p; \

                                                smp_read_barrier_depends();\

                                                (_________p1);\

                                                }) 

        

rcu_assign_pointer()通常用于写者的发布,rcu_dereference()通常用于读者的订阅。


写者:

1 p->a = 1;
2 p->b = 2;
3 p->c = 3;
4 rcu_assign_pointer(gp, p);


读者:

1 rcu_read_lock();
2 p = rcu_dereference(gp);
3 if (p != NULL) {
4 do_something_with(p->a, p->b, p->c);
5 }
6 rcu_read_unlock();

rcu_assign_pointer()是说,先把那块内存写好,再把指针指过去。这里使用的内存写屏障是为了保证并发的读者读到数据一致性。在这条语句之前的读者读到旧的指针和旧的内存,这条语句之后的读者读到新的指针和新的内存。如果没有这条语句,很有可能出现读者读到新的指针和旧的内存。也就是说,这里通过内存屏障刷新了p所指向的内存的值,至于gp本身的值有没有更新还不确定。实际上,gp本身值的真正更新要等到并发的读者来促发。

rcu_dereference() 原语用的是数据依赖屏障,smp_read_barrier_dependence,它要求后面的读操作如果依赖前面的读操作,则前面的读操作需要首先完成。根据数据之间的依赖,要读p->a, p->b, p->c, 就必须先读p,要先读p,就必须先读p1,要先读p1,就必须先读gp。也就是说读者所在的core在进行后续的操作之前,gp必须是同步过的当前时刻的最新值。如果没有这个数据依赖屏障,有可能读者所在的core很长一段时间内一直用的是旧的gp值。所以,这里使用数据依赖屏障是为了督促写者将gp值准备好,是为了呼应写者,这个呼应的诉求是通过数据之间的依赖关系来促发的,也就是说到了非呼应不可的地步了。


下面看看kernel中常用的链表操作是如何使用这样的发布、订阅机制的:

写者:

static inline void list_add_rcu(struct list_head *new, struct list_head *head)
{
__list_add_rcu(new, head, head->next);
}

static inline void __list_add_rcu(struct list_head * new,
struct list_head * prev, struct list_head * next)
{
new->next = next;
new->prev = prev;
smp_wmb();
next->prev = new;
prev->next = new;
}


读者:

#define list_for_each_entry_rcu(pos, head, member) \

            for(pos = list_entry((head)->next, typeof(*pos), member); \

                        prefetch(rcu_dereference(pos)->member.next),\

                                    &pos->member!= (head); \

                        pos= list_entry(pos->member.next, typeof(*pos), member))


写者通过调用list_add_rcu来发布新的节点,其实是发布next->prev, prev->next这两个指针。读者通过list_for_each_entry_rcu来订阅这连个指针,我们将list_for_each_entry_rcu订阅部分简化如下:

pos = prev->next;

prefetch(rcu_dereference(pos)->next);

 

读者通过rcu_dereference订阅的是pos,而由于数据依赖关系,又间接订阅了prev->next指针,或者说是促发prev->next的更新。


下面介绍下其他相关链表操作的函数:

safe版本的iterate的函数?为什么就safe了?

 

#define list_for_each_safe(pos,n, head) \

            for(pos = (head)->next, n = pos->next; pos != (head); \

                        pos= n, n = pos->next)

 

#define list_for_each(pos, head)\

            for(pos = (head)->next; prefetch(pos->next), pos != (head); \

            pos= pos->next)

 

当在iterate的过程中执行删除操作的时候,比如:

 list_for_each(pos,head)

      list_del(pos)

这样会断链,为了避免这种断链,增加了safe版本的iterate函数。另外,由于preftech的缘故,有可能引用一个无效的指针LIST_POISON1。这里的safe是指,为避免有些cpu的preftech的影响,干脆在iterate的过程中去掉preftech。

 

 

还有一个既有rcu+safe版本的iterative函数:

#definelist_for_each_safe_rcu(pos, n, head) \

            for(pos = (head)->next; \

                        n= rcu_dereference(pos)->next, pos != (head); \

                        pos= n)

 

只要用这个版本的iterate函数,就可以和多个_rcu版本的写操作(如:list_add_rcu())并发执行。



<think>嗯,用户让我介绍一下LinuxRCU锁。首先,我需要回想一下RCU的基本概念。RCU全称是Read-Copy-Update,是一种同步机制,主要用于读多写少的场景。对吧?那它和传统的锁有什么不同呢?传统锁比如互斥锁或者读写锁,可能在读的时候也会有锁的开销,而RCU据说读操作不需要加锁,这样性能会更好。 接下来,我应该解释RCU的工作原理。记得RCU的核心思想是延迟回收内存。当写者要修改数据时,不是直接修改原来的数据,而是复制一份副本,修改副本,然后再替换指针,让后续的读操作看到新数据。旧数据的回收则要等到所有之前的读操作都完成之后。这部分需要详细说明一下,比如如何跟踪读者的访问,如何确定何时可以安全回收内存。 可能用户还想知道RCU的优势在哪里。比如,读操作无锁,所以性能高;适合读多写少的情况。但也要提到缺点,比如写者的开销较大,因为要复制和延迟释放,还有内存开销的问题,因为旧数据不能立即回收。此外,适用场景也很重要,比如在链表操作中常用RCU,因为替换指针比较容易。 用户可能还会问到RCU的实现机制,比如如何跟踪读者的临界区。这时候需要提到Linux内核中的Preemptible RCU和Tree RCU,不同的实现方式,但核心都是通过某种机制确保所有读者完成访问后再回收资源。还有宽限期(Grace Period)的概念,也就是写者需要等待的时间,直到所有读者都退出临界区。 另外,应该举一个例子,比如链表删除节点的操作。用代码或者伪代码说明写者如何复制、修改、替换指针,然后安排旧节点的回收。这样用户能更直观地理解。 需要注意的是,用户可能对RCU的使用场景和限制不太清楚,所以需要明确说明适用的场景和不适用的地方。例如,频繁写入的情况可能不适合RCU,因为写者的开销会累积,导致性能下降。 最后,要确保整个解释结构清晰,分点说明,让用户能够逐步理解。同时,检查是否有术语使用不当的地方,或者有没有遗漏关键点。比如,是否需要提到内存屏障(Memory Barrier)的作用,或者RCU API的基本函数?不过作为介绍,可能不需要太深入,保持基础概念的解释即可。</think>### Linux RCU 锁详解 #### 1. **RCU 是什么?** **RCU(Read-Copy-Update)** 是 Linux 内核中一种高效的同步机制,主要用于**读多写少**的场景。与传统锁(如互斥锁、读写锁)不同,RCU 允许**读操作无锁访问共享数据**,而写操作通过“副本替换+延迟回收”保证一致性。 --- #### 2. **核心思想** - **读操作**:直接访问数据,**无需加锁**,但需在“临界区”标记读操作(通过 `rcu_read_lock()`/`rcu_read_unlock()`)。 - **写操作**: 1. **复制**:创建数据的副本。 2. **修改**:更新副本内容。 3. **替换**:通过原子操作将指针指向新副本。 4. **延迟回收**:等待所有旧读操作退出后,安全回收旧数据。 --- #### 3. **关键机制** - **宽限期(Grace Period)** 写操作提交后,需等待所有正在执行的读操作完成,确保旧数据不再被引用后才能回收。 **示例代码片段**: ```c // 删除链表节点 struct node *old = head; head = head->next; // 原子替换指针 synchronize_rcu(); // 等待宽限期结束 kfree(old); // 安全回收旧节点 ``` - **内存屏障(Memory Barriers)** 确保写操作的替换指针操作对其他 CPU 可见,避免乱序执行问题。 --- #### 4. **RCU 的优势** - **读性能极高**:读操作无锁,无竞争。 - **无死锁风险**:读操作不阻塞写操作。 - **适用场景**:链表、哈希表等数据结构,频繁读、偶尔写的场景。 --- #### 5. **RCU 的局限性** - **写开销大**:复制数据、等待宽限期增加延迟。 - **内存占用**:旧数据需延迟释放,短期内存消耗较高。 - **适用性限制**:不适合频繁写操作或实时性要求极高的场景。 --- #### 6. **典型使用场景** 1. **内核链表操作** 如 `list_for_each_entry_rcu()` 遍历链表。 2. **路由表更新** 网络协议栈中快速更新路由信息。 3. **虚拟文件系统(VFS)** 文件系统元数据(如 dentry、inode)的并发访问。 --- #### 7. **与读写锁的对比** | 特性 | RCU | 读写锁 | |---------------|-------------------|--------------------| | **读开销** | 无锁 | 需加锁 | | **写开销** | 高(复制+等待) | 低(直接修改) | | **内存安全** | 延迟回收 | 即时释放 | | **适用场景** | 读多写极少 | 写操作较频繁 | --- #### 8. **实现原理(简化版)** - **读者标记**:通过 `rcu_read_lock()` 进入临界区,标记当前线程为“正在读”。 - **写者同步**:调用 `synchronize_rcu()` 触发宽限期,等待所有活跃读者退出。 - **回调机制**:通过 `call_rcu()` 注册回收函数,宽限期结束后自动释放旧数据。 --- #### 9. **代码示例(内核风格)** ```c // 读者 rcu_read_lock(); struct node *p = rcu_dereference(head); // 安全获取指针 // 使用 p 读取数据... rcu_read_unlock(); // 写者 struct node *new_node = kmalloc(sizeof(*new_node), GFP_KERNEL); *new_node = *old_node; // 复制数据 new_node->value = new_value; // 修改副本 rcu_assign_pointer(head, new_node); // 原子替换指针 call_rcu(&old_node->rcu_head, free_node); // 延迟回收旧数据 ``` --- #### 10. **总结** RCU 通过**无锁读**和**延迟回收**,在特定场景下显著提升并发性能,但需权衡写操作的开销。理解其核心思想(宽限期、副本替换)和适用场景,是正确使用 RCU 的关键。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值