linux kernel rcu 读复制更新 并发控制机制 简介

本文详细介绍了Linux内核中的RCU(ReadCopyUpdate)机制,包括RCU的基本原理、不同变种如普通RCU、可睡眠RCU(SRCU)的特点以及它们在并发控制中的应用。RCU适用于读多写少的场景,通过异步回收机制保证数据有效性,而SRCU则允许读者在临界区睡眠,适合进程上下文。此外,文章还探讨了RCU的各种API及其使用示例。
摘要由CSDN通过智能技术生成

目录

RCU原理

普通RCU

变种RCU的特点

可睡眠RCU(SRCU)


RCU原理

RCU特点:RCU全称Read Copy Update 读复制更新,是一种完全不同于锁的并发控制机制。

主要保护的对象:动态申请的内存,即指针变量。

是扩展性最好的并发控制机制。

RCU主要适用于读者多,写着少的场景

不像锁那样将进程串行化来保护资源,是允许进程并发的。在一些必须串行化才能保护临界区的场景RCU是不适合使用的。

RCU只能保证每个读者读到的数据是有效的,但是不能保证每个数据是最新的。

大量的copy数据,占用了大量的内存,极端情况下可能会出现OOM(out of memory)的场景。

RCU允许读读并发,读写并发,甚至是写写并发(写着之间需要用锁或者原子操作之类的同步控制)

读写自旋锁和读写信号量只允许读读并发,不允许读写并发,顺序锁允许读写并发,但是写者迫使读者做出了牺牲,并要求读者临界区可重复执行。

RCU可分为普通RCU和SRCU(可睡眠RCU)

普通RCU

普通RCU类似于自旋锁,可用于中断上下文和进程上下文,但是在读者临界区内不允许调度和睡眠。在4.4的内核中出现了三个变种,分别文标准RCU,调度版RCU ,快速版RCU。标准版RCU在非抢占内核里面成为RCU,在抢占内核里面成为rcu_preempt,调度版RCU称为rcu_sched (进程名为 rcu_sched),快速版称为rcu_bh。

普通RCU原理:

读者用rcu_read_lock()和rcu_read_unlock()包围临界区,临界区内不不允许直接引用共享指针的数据,二是用rcu_dereference§获取被保护的指针,RCU临界区和自旋锁临界区一样不允许调度和睡眠。

写者:写者先复制一份共享数据的副本,然后根据需要来修改副本的数据,修改之后用rcu_assign_pointer(p,v)给被保护的指针从新赋值,这样后面读者再读取的内容就是新指针的内容。然后写者通过call_rcu()注册一个回调函数在适当的时机来释放旧的副本。释放函数大多都是kfree的包装。

何时回收副本:从call_rcu注册回调函数开始到真正的执行回调函数去回收旧副本的这段时间被称为宽限期(Grace Period,简称GP),回收的合适时间就是指所有读者都退出了临界区,也就是所有读请求都读完了。

异步回收:传统上RCU 回调函数是tasklet里面执行的,具体函数是rcu_tasklet,从2.6开始专门设计了一种RCU_SOFTIRQ软中断,从此rcu回调函数就在RCU_SOFTIRQ软中断处理函数rcu_process_callback中执行,但是3.0开始也可以在内核线程rcu_cpu_kthread中执行。

同步回收:上面的回收机制叫做异步回收,当然也有同步回收写着使用syncchronize_rcu进行同步回收,注意这个函数会阻塞当前进程,指导所有RCU的读者都退出了临界区,才开始回收副本。注意这种回收机制只能用于进程上下文,不能用在中断上下文。

异步回收的数据结构:异步回收的情况下,每个被保护的指针式通过链表的方式被组织在一起的。通过一下结构实现:

struct callback_head {
      struct callback_head *next;
      Void (* func) (struct callback_head*head);
}
#define rcu_head callback_head

在异步回收中每一个被RCU保护的数据结构都包含一个rcu_head,指向下一个被保护的数据结构。func是用来回收内存的回调函数。

普通RCU经常使用的API如下:

rcu_read_lock()/rcu_read_lock_shced()/rcu_read_lock_bh() 
(读者进入RCU临界区,需要使用的锁)
rcu_read_unlock()/rcu_read_unlock_sched()/rcu_read_unlock_bh() (读者退出临界区需要释放的锁)
rcu_dereference(p)/rcu_dereference_sched(p)/rcu_dereference_bh(p) 
(获取被RCU保护的指针变量)
rcu_assign_pointer(p,v) 
(给RCU保护的指针变量赋值V)
call_rcu(head,func)/call_rcu_sched(head,func)/call_rcu_bh(head,func) 
(注册一个异步rcu回收内存的回调函数func)
synchronize_rcu()/synchronize_rcu_shced()/synchronize_rcu_bh()/
(这里需要注意以sched结尾的是调度版,以bh结尾的是快速版)
(同步回收内存的rcu函数,改函数要等待所有对着退出临界区采取释放内存,可能导致进程睡眠,不可用于中断上下文) 

变种RCU的特点

RCU:非抢占标准版RCU其读者临界区是禁止抢占的,保护函数rcu_read_lock()/rcu_read_unlock()等价于preempt_disable()/preempt_enable()

RCU_SCHED:调度版RCU等同于非抢占标准本RCU,即和上面的是一样的,保护函数rcu_read_lock_sched()/rcu_read_unlock_sched()同样等价于preempt_disable()/preempt_enable()

RCU_BH:快速版RCU其读者临界区不但禁止抢占,还禁止软中断。其保护函数rcu_read_lock_bh()/rcu_read_unlock_bh()分别等效于local_bh_disable()/local_bh_enable()

RCU_PREEMPT:可抢占标准版RCU,其读者临界区既不禁止抢占也不禁止软中断,因此允许读者在临界区内发生抢占(但不允许进程发生睡眠,抢占调度是一种强制调度,被调度出去的进程依旧保持可运行状态,孤儿让宽限期无限延长)。通过嵌套计数来记录进程的状态,其保护函数rcu_read_lock()和rcu_read_unlock()分别相当于计数自增和计数自减函数。由于其允许抢占,因此不能用在中断上下文。

RCU_BOOST:优先级提升RCU,可以提升RCU读者的优先级,保证其快速退出临界区。

普通RCU在Linux内核发展史上有果很多种实现,早期实现的叫做经典RCU,在数据结构设计上有较多的全局共享数据,因此CPU数比较大的时候扩展性不好,后来引入了层次树RCU(Tree RCU)层次树结构在全局共享上实现了最小化,可以解决大规模处理器的扩展性问题。2.6.32以后全面移除了经典RCU广泛使用层次树RCU,后来又引入了专门为单CPU设计的微小RCU(Tiny RCU)因此在Linux 4.4中启用多核处理器的时候使用Tree RCU 单核处理器使用Tiny RCU。

可睡眠RCU(SRCU)

普通RCU其读者临界区是不允许调度和睡眠的,虽然可抢占RCU允许强制调度,但依然不允许睡眠。但是内核里确实需要允许睡眠的RCU,便有了可睡眠RCU的产生即SRCU,很显然SRCU只能用于进程上下文,不能用于中断上下文。

SRCU原理

在设计上普通RCU与SRCU最大的区别在于普通RCU只有一个全局的RCU域,所有的RCU都归全局域管理,而SRCU包含多个域,每个不同的子系统都含有自己的RCU域,一个SRCU域用一个struct srcu_struct来描述。就类似于普通RCU只有一个全局锁,而SRCU相当于每一个子系统都有一个自己的局部的锁。

SRCU经常使用的API

定义一个SRCU域的描述符 
DEFINE_SRCU(name)/DEFINE_STATIC_SRCU(name)
动态初始化一个SRCU域的描述符
Int init_srcu_struct(struct srcu_struct *srcu)
动态清理一个销毁的srcu域的描述符
Void cleanup_srcu_struct( struct srcu_struct *srcu)
SRCU进入读者临界区要使用的锁的函数,返回值为临界区ID
Int srcu_read_lock(struct srcu_struct *srcu)
SRCU推出临界区使用的函数,第二个参数为临界区Id
Int srcu_read_unlock(struct srcu_struct *srcu, int Id)
SRCU注册一个回收旧副本的异步回收函数
Void call_srcu(struct srcu_struct *srcu,struct rcu_head *head,void (*fun)(struct rcu_head *head))
SRCU等待所有读者都退出临界区然后回收副本的同步回收函数
Void synchronize_srcu(struct srcu_struct *srcu)
SRCU读者在临界区获取一个被保护的指针
srcu_dereference(p,srcu)
SRCU写着给被保护的指针赋值
srcu_assign_pointer(p,v)

注意:每一个SRCU域都包含一个每CPU数组,其中包含计数器,srcu_read_lock()/srcu_read_unlock(),其作用就是增加或者减少这些计数器。

RCU的代码样例

这里假设有两个读者,一个写者的情况为例

同步回收
struct foo {
  int a;
  long b;
};

struct foo __rcu *gbl_foo;

int initializa(void)
{
  gbl_foo = kmalloc(sizeof(struct foo),GFP_KERNEL);
  gbl->a = 0;
  gbl->b = 0;

  return 0;
}

int foo_a_reader(void)
{
  int ret;
   //加锁保护
   rcu_read_lock();
   ret = rcu_dereference(gbl_foo)->a;
   //释放锁
   rcu_read_unlock();

   return ret;
}



异步回收
struct foo {
  int a;
  long b;
  struct rcu_head rcu;
};

struct foo __rcu *gbl_foo;

int initializa(void)
{
  gbl_foo = kmalloc(sizeof(struct foo),GFP_KERNEL);
  gbl->a = 0;
  gbl->b = 0;

  return 0;
}

int foo_a_reader(void)
{
  int ret;
   //加锁保护
   rcu_read_lock();
   ret = rcu_dereference(gbl_foo)->a;
   //释放锁
   rcu_read_unlock();

   return ret;
}

注意:更详细的原理请看内核Documentation/RCU/下面的内核文档 。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值