Linux内核源码分析 (A.6)RCU机制及内存优化屏障

Linux内核源码分析 (A.6)RCU机制及内存优化屏障

一、RCU机制

  • 问题:RCU英文全称为Read-Copy-Update,顾名思义就是读-拷贝-更新,是Linux内核中重要的同步机制。Linux内核已有原子操作、读写信号量等锁机制,为什么要单独设计一个比较复杂的新机制?

1、RCU的原理和特点

  • RCU的原理
    RCU记录所有指向共享数据的指针的使用者,当要修改该共享数据时,首先创建一个副本, 在副本中修改。所有读访问程都离开读临界区之后,指针指向新的修改后副本的指针,并且删除旧数据。
  • RCU的特点
    • RCU写者修改对象的过程是:首先复制生成一个副本,然后更新这个副本,最后使用新的对象替换旧的对象。在写者执行复制更新的时候读者可以读数据。
    • 写者删除对象,必须等到所有访问被删除对象的读者访问结束,才能够执行销毁操作。RCU关键技术是怎么判断所有读者已经完成访问。等待所有读者访问结束的时间称为宽限期(grace period) 。
    • RCU读者不并不需要直接与写者进行同步,读者与写者也能并发的执行。RCU目标最大程序来减少读者的开销。因为也经常使用于读者性能要求高的场景。
    • RCU优点:读者开销少,不需要获取任何锁,不需要执行原子指令或内存屏障;没有死锁问题;没有优先级反转的问题;没有内存泄露的危险问题;很好的实时延迟操作。
    • RCU缺点:写者的同步开销比较大的,写者之间需要互斥处理;使用其它同步机制复杂。

2、核心API(例中使用RCU保护指针)

  • 假定指针ptr指向一个被RCU保护的数据结构。直接反引用指针是禁止的,首先必须调用rcu_dereference(ptr),然后反引用返回的结果。此外,反引用指针并使用其结果的代码,需要用rcu_read_lockrcu_read_unlock调用保护起来

    rcu_read_lock(); 
    /*被反引用的指针不能在rcu_read_lock()和rcu_read_unlock()
    保护的代码范围之外使用,也不能用于写访问。*/
    p = rcu_dereference(ptr); 
    if (p != NULL) { 
    	awesome_function(p); 
    } 
    rcu_read_unlock();
    
  • 如果必须修改ptr指向的对象,则需要使用rcu_assign_pointer()

    struct super_duper *new_ptr = kmalloc(...); 
    
    new_ptr->meaning = xyz; 
    new_ptr->of = 42; 
    new_ptr->life = 23; 
    
    rcu_assign_pointer(ptr, new_ptr);
    

    按RCU的术语,该操作公布了这个指针,后续的读取操作将看到新的结构,而不是原来的如果更新可能来自内核中许多地方,那么必须使用普通的同步原语防止并发的写操作,如自旋锁。尽管RCU能保护读访问不受写访问的干扰,但它不对写访问之间的相互干扰提供防护!

  • 在新值已经公布之后,旧的结构实例会怎么样呢?在所有的读访问完成之后,内核可以释放该内存,但它需要知道何时释放内存是安全的。为此,RCU提供了另外两个函数。

    • synchronize_rcu()等待所有现存的读访问完成。在该函数返回之后,释放与原指针关联的内存是安全的。
    • call_rcu可用于注册一个函数,在所有针对共享资源的读访问完成之后调用。这要求将一个rcu_head实例嵌入(不能通过指针)到RCU保护的数据结构。
      struct super_duper {
      	struct rcu_head head; 
      	int meaning, of, life; 
      };
      
      该回调函数可通过参数访问对象的rcu_head成员,进而使用container_of机制访问对象本身。
      kernel/rcupdate.c
      void fastcall call_rcu(struct rcu_head *head, void (*func)(struct rcu_head *rcu))
      

3、链表操作

  • RCU不仅能保护一般的指针,还能保护内核提供的双链表和散列表。以链表为例,仍然可以使用标准的链表元素。只有在遍历链表、修改和删除链表元素时,必须调用标准函数的RCU变体(附加_rcu后缀)。
    <list.h>

    /*将新的链表元素new添加到表头为head的链表头部*/
    static inline void list_add_rcu(struct list_head *new, struct list_head *head) 
    /*将新的链表元素new添加到表头为head的链表尾部*/
    static inline void list_add_tail_rcu(struct list_head *new, struct list_head *head) 
    /*从链表删除链表元素entry*/
    static inline void list_del_rcu(struct list_head *entry) 
    /*将链表元素old替换为new*/
    static inline void list_replace_rcu(struct list_head *old, struct list_head *new)
    

    此外,list_for_each_rcu允许遍历链表的所有元素。而list_for_each_rcu_safe甚至对于删除链表元素也是安全的。这两个操作都必须通过一对rcu_read_lock()rcu_read_unlock()包围。

4、RCU应用场景

  • 每种锁都有自己适合场景:spinlock不分区reader/writer,对于些读写强度不对称的是不适合的, RW spinlockseqlock解决了这个问题,seqlock倾向writerRW spinlock倾向reader。RCU适用于需要频繁的读取数据,而相应修改数据并不多的场景。比如:文件系统中,搜索定位目录,而对目录修改相对来讲基本没有。
    • RCU只能保护动态分配的数据结构,并且必须是通过指针访问该数据结构;
    • 受RCU保护的临界区不能sleep;
    • 读写不对称,对writer的性能没有特别的要求,但是reader性能要求极高;
    • reader端对新旧数据不敏感。

二、内存和优化屏障

1、优化屏障

  • 在编程时,指令一般不按照源程序顺序执行,原因是为提高程序执行性能,会对它进行优化,主要为两种:

    • 编译器优化:提高系统的性能,编译器在不影响逻辑的情况下会调整指令的顺序。
    • CPU执行优化:提高流水线的性能,CPU的乱序执行可能会让后面的没有寄存器冲突和汇编指令先于前面的指令完成。
  • Linux使用barrier()函数实现优化屏障,如其编译器的优化屏障源码为:

    static inline void barrier(void)
    {
    	/*asm表示插入汇编语言程序;volatile表示阻止编译对该值进行优化,确保变量使用了用
    	户定义的精确地址,而不是装有同意信息的一些别名。memory表示修改了内存单元。*/
    	asm volatile("" : : : "memory");
    }
    
  • 优化屏障的一个特定应用是内核抢占机制。要注意,preempt_disable对抢占计数器加1因而停用了抢占,preempt_enable通过对抢占计数器减1而再次启用抢占。这两个命令之间的代码,可免受抢占的影响。看一看下列代码:

    preempt_disable(); 
    function_which_must_not_be_preempted(); 
    preempt_enable();
    

    如果编译器决定将代码重新排序,那就会出现问题:

    function_which_must_not_be_preempted(); 
    preempt_disable(); 
    preempt_enable();
    

    另一种重排也会出现问题

    preempt_disable(); 
    preempt_enable(); 
    function_which_must_not_be_preempted();
    
  • 上述的错误时间不会发生,因为preempt_disable在抢占计数器加1之后插入一个内存屏障,preempt_enable在再次启用抢占之前插入一个优化屏障:
    <preempt.h>

    #define preempt_disable() \ 
    do { \ 
    	inc_preempt_count(); \ 
    	barrier(); \ 
    } while (0)
    

    这防止了编译器将inc_preempt_count()与后续的语句交换位置。
    <preempt.h>

    #define preempt_enable() \ 
    do { \ 
    ... 
    	barrier(); \ 
    	preempt_check_resched(); \ 
    } while (0)
    

    这种措施可以防止上文给出的第二种错误的重排。

2、内存屏障

  • 内存屏障是一种保证内存访问顺序的方法,解决内存访问乱序问题:

    • 编译器编译代码时可能重新排序汇编指令,使编译出来的程序在处理器上执行速度更快,但是有的时候优化结果可能不符合软件开发工程师意图。
    • 新式处理器采用超标量体系结构和乱序执行技术,能够在一个时钟周期并行执行多条指令。一句话总结为:顺序取指令,乱序执行,顺序提交执行结果。
    • 多处理器系统当中,硬件工程师使用存储缓冲区等机制实现高效性能,引入处理器之间的内存访问乱序问题。|
  • 处理器内存屏障解决CPU之间的内存访问乱序问题和处理器访问外围设备的乱序问题。

    内存屏障类型强制性的内存屏障SMP的内存屏障
    通用内存屏障mb ()smp_mb ()
    写内存屏障wmb ()smp_wmb ()
    读内存屏障rmb ()smp_rmb ()
    数据依赖屏障read_barrier_depends()smp_read_barrier_depends()

    除数据依赖屏障之外,所有处理器内存屏障隐含编译器优化屏障。注意:smb_mb()smp_rmb()smp_wmb()只在SMP系统中有硬件屏障,它们在单处理器系统上产生的是软件屏障(编译器优化屏障)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elec Liu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值