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_lock
和rcu_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.cvoid 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 spinlock
和seqlock
解决了这个问题,seqlock
倾向writer
,RW 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之间的内存访问乱序问题和处理器访问外围设备的乱序问题。文章来源:https://www.toymoban.com/news/detail-693723.html
内存屏障类型 强制性的内存屏障 SMP的内存屏障 通用内存屏障 mb ()
smp_mb ()
写内存屏障 wmb ()
smp_wmb ()
读内存屏障 rmb ()
smp_rmb ()
数据依赖屏障 read_barrier_depends()
smp_read_barrier_depends()
除数据依赖屏障之外,所有处理器内存屏障隐含编译器优化屏障。注意:
smb_mb()
、smp_rmb()
、smp_wmb()
只在SMP系统中有硬件屏障,它们在单处理器系统上产生的是软件屏障(编译器优化屏障)。文章来源地址https://www.toymoban.com/news/detail-693723.html
到了这里,关于Linux内核源码分析 (6)RCU机制及内存优化屏障的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!