万字讲解linux kermel RCU以及读写锁
信号量有一个很明显的缺点,没有区分临界区的读写属性,读写锁允许多个线程进程并发的访问临界区,但是写访问只限于一个线程,在多处理器系统中允许多个读者访问共享资源,但是写者有排他性,读写锁的特性如下:允许多个读者同时访问临界区,但是同一时间不能进入;同一时刻只允许一个写者进入临界区;读者和写者不能同时进入临界区。读写锁也有关闭中断和下半部的版本。
RCU:read-copy-update 。。。。。。。。。。。。。。。。。。。。
问题:rcu相比读写锁,解决了什么问题? rcu的基本原理?
1、由于内核中spinlock mutex 等都使用了原子操作指令,即原子的访问内存,但是当多cpu 竞争访问临界区时会让cpu的cache命中率下降,性能下降。同时读写锁有个缺陷,读者和写者不能同时存在。
rcu实现的目标就是要解决这个问题,为了使线程同步开销小。不需要原子操作以及内存屏障而访问数据,把同步的问题交给写者线程,写者线程等待所有的读者线程完成后才会吧旧数据销毁。当有多个写者线程存在时,需要额外的保护机制。
【文章福利】小编推荐自己的Linux内核技术交流群:【891587639】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!(含视频教程、电子书、实战项目及代码)


原理
RCU原理:简单理解为 记录了所有指向共享数据的指针使用者,当要修改共享数据时,先创建一个副本,在副本中修改。所有读者离开临界区后,指针指向新的修改副本后的地方,并且删除旧数据。
官方描述:RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。
写者的同步开销比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。
读者必须提供一个信号给写者以便写者能够确定数据可以被安全地释放或修改的时机。
有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。
目前在内核中链表使用RCU较多。
在经典RCU中,RCU读侧临界部分由rcu_read_lock() 和rcu_read_unlock()界定,它们可以嵌套。
对应的同步更新原语为synchronize_rcu(),还有同义的synchronize_net(),等待当前正执行的RCU读侧闻临界部分运行完成。等待的时间称为“宽限期”。
异步更新侧原语call_rcu()在宽限期之后触发指定的函数,如:call_rcu(p,f)调用触发回调函数f(p)。有些情况,如:当卸载使用call_rcu()的模块,必须等待所有RCU回调函数完成,原语rcu_barrier()起该作用。 在“RCU BH”列中,rcu_read_lock_bh() 和rcu_read_unlock_bh()界定读侧临界部分,call_rcu_bh()在宽限期后触发指定的函数。注意:RCU BH没有同步接口synchronize_rcu_bh(),如果需要,用户很容易添加同步接口函数。
直接操作指针的原语rcu_assign_pointer()和rcu_dereference()用于创建RCU保护的非链表数据结构,如:数组和树
NOTE:读者 在访问被RCU保护的共享数据期间不能被阻塞,这是RCU机制得以实现的一个基本前提,也就说当读者在引用被RCU保护的共享数据期间,读者所在的CPU不能发生上下文切换,spinlock和rwlock都需要这样的前提。 写者 在访问被RCU保护的共享数据时不需要和读者竞争任何锁,只有在有多于一个写者的情况下需要获得某种锁以与其他写者同步。写者修改数据前首先拷贝一个被修改元素的副本,然后在副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数以便在适当的时机执行真正的修改操作。等待适当时机的这一时期称为宽限期(grace period),而CPU发生了上下文切换称为经历一个quiescent state,grace period就是所有CPU都经历一次quiescent state所需要的等待的时间。垃圾收集器就是在grace period之后调用写者注册的回调函数来完成真正的数据修改或数据释放操作的。
在使用RCU时,对共享资源的访问在大部分时间应该是只读的,写访问应该相对较少,因为写访问多了必然相对于其他锁机制而已更占系统资源,影响效率。其次是读者在持有rcu_read_lock(RCU读锁定函数)的时候,不能发生进程上下文切换,否则,因为写者需要等待读者完成方可进行,则此时写者进程也会一直被阻塞,影响系统的正常运行。再次写者执行完毕后需要调用回调函数,此时发生上下文切换,当前进程进入睡眠,则系统将一直不能调用回调函数,更槽糕的是,此时其它进程若再去执行共享的临界区,必然造成一定的错误。最后一点是受RCU机制保护的资源必须是通过指针访问。因为从RCU机制上看,几乎所有操作都是针对指针数据的;
同步函数最为重要,即synchronize_rcu()。读者函数的实质其实很简单:禁止抢占,也就是说在RCU期间不允许发生进程上下文切换,原因上述已提及,即是写者需要等待读者完成方可进行,则此时写者进程也会一直被阻塞,影响系统的正常运行等,故而不允许在RCU期间发生进程上下文切换
关于写者函数,主要就是call_rcu和call_rcu_bh两个函数。其中call_rcu能实现的功能是它不会使写者阻塞,因而它可在中断上下文及软中断使用,该函数将函数func挂接到RCU的回调函数链表上,然后立即返回,读者函数中提及的synchronize_rcu()函数在实现时也调用了该函数。而call_rcu_bh函数实现的功能几乎与call_rcu完全相同,唯一的差别是它将软中断的完成当作经历一个quiescent state(静默状态,本节一开始有提及这个概念), 因此若写者使用了该函数,那么读者需对应的使用rcu_read_lock_bh() 和rcu_read_unlock_bh()。
使用rcu_read_lock_bh() 和rcu_read_unlock_bh()函数的原因是由于call_rcu_bh函数不会使写者阻塞,可在中断上下文及软中断使用。这表明此时系统中的中断和软中断并没有被关闭。那么写者在调用call_rcu_bh函数访问临界区时,RCU机制下的读者也能访问临界区。此时对于读者而言,它若是需要读取临界区的内容,它必须把软中断关闭,以免读者在当前的进程上下文过程中被软中断打断(上述内容提过软中断可以打断当前的进程上下文)。而rcu_read_lock_bh() 和rcu_read_unlock_bh()函数的实质是调用local_bh_disable()和local_bh_enable()函数,显然这是实现了禁止软中断和使能软中断的功能。
另外在Linux源码中关于call_rcu_bh函数的注释中还明确说明了如果当前的进程是在中断上下文中,则需要执行rcu_read_lock()和rcu_read_unlock(),结合这两个函数的实现实质表明它实际上禁止或使能内核的抢占调度,原因不言而喻,避免当前进程在执行读写过程中被其它进程抢占。同时内核注释还表明call_rcu_bh这个接口函数的使用条件是在大部分的读临界区操作发生在软中断上下文中,原因还是需从它实现的功能出发,相信很容易理解,主要是要从执行效率方面考虑。
static inline void rcu_read_lock_bh(void); static inline void rcu_read_unlock_bh(void);
这个变种只在修改是通过 call_rcu_bh进行的情况下使用,因为 call_rcu_bh将把 softirq 的执行完毕也认为是一个 quiescent state,因此如果修改是通过 call_rcu_bh 进行的,在进程上下文的读端临界区必须使用这一变种
每一个 CPU 维护两个数据结构 rcu_sched_data,rcu_bh_data,它们用于保存回调函数。函数call_rcu和函数call_rcu_bh用于注册回调函数,前者把回调函数注册到rcu_sched_data,而后者则把回调函数注册到rcu_bh_data,在每一个数据结构上,回调函数被组成一个链表,先注册的排在前头,后注册的排在末尾;时钟中断处理函数(update_process_times)调用函数rcu_check_callbacks
函数rcu_check_callbacks首先检查该CPU是否经历了一个quiescent state,如果(或):
当前进程运行在用户态;
当前进程为idle且当前不处在运行softirq状态,也不处在运行IRQ处理函数的状态;
该CPU已经经历了一个quiescent state,因此通过调用函数rcu_sched_qs和rcu_bh_qs标记该CPU的数据结构rcu_sched_data和rcu_bh_data的标记字段passed_quiesc,以记录该CPU已经经历一个quiescent state。
否则,如果当前不处在运行softirq状态,那么,只标记该CPU的数据结构rcu_bh_data的标记字段passed_quiesc,以记录该CPU已经经历一个quiescent state。注意,该标记只对rcu_bh_data有效。
然后,函数rcu_check_callbacks将调用开启RCU_SOFTIRQ。
synchronize_rcu()在RCU中是一个最核心的函数,它用来等待之前的读者全部退出。
在完整的宽限期结束后,即在所有当前正在执行的RCU读取端临界区完成之后,控制权会在一段时间后返回给调用者。

原文作者:codestacklinuxer
