RCU在功耗和性能场景下的扩展

RCU概念

    ReadCopy Update (RCU) 是Linux的一种同步机制,把写数据看成一个update操作,可分为两个阶段removal和reclamation。removal阶段将旧数据的指针替换为新数据的指针,可以与 reader 并发,reclamation阶段在等待旧数据的读操作完成后将其回收或直接释放,两个阶段之间的间隔也被称之为grace period。从代码的实现上来说grace period是所有CPU都经历一次quiescent state所需要的等待时间,CPU发生一次上下文切换称为经历一个quiescent state,采用这样的实现方式是因为CPU能够切换上下文的话,必然已经运行过rcu_read_unlock或者根本就没有rcu_read_lock。

                  

    RCU可以理解为改进版的rwlock,读者不需要锁,并且read端除alpha外的所有架构都不需要加上memorybarrier,因此几乎没有读者的同步开销。与rwlock不同的是RCU允许读写并发,但是多个写者更新相同临界区时还是需要用锁保护的。如下左图为临界区长度为零时RCU相较于读写锁的性能优势,横轴为CPU个数纵轴为CPU时间开销。右图为临界区长度增加时RCU和读写锁的开销比较,横轴为临界区长度(毫秒)纵轴为CPU时间开销。

 

 

    在CPU数目众多的场景下,由于每等待一个CPU经历一次quiescent state都需要操作一个全局的cpumask字段,因此扩展出了分级RCU,另外在realtime 系统下也有Preemptible RCU,用户态还有URCU,随着业务场景的需求变化,RCU也在逐渐扩展。

 

RCU主要API

rcu_read_lock() //读临界区开始前关抢占

rcu_read_unlock() //读临界区结束后开抢占

synchronize_sched() //同步等待之前的读访问完成然后回收内存

call_rcu() //注册内存回收函数,异步等待之前的读访问完成后运行回调函数。

rcu_assign_pointer() //更新临界区数据并执行memorybarrier,让其他 CPU 知晓已更新的指针值。

rcu_dereference() //获取RCU保护的数据

 

RCU适用场景

(1)RCU只能保护一个指针,该指针指向动态分配的数据结构。RCU读者要么读到旧值要么读到新值,如果不是一个32bit(32位处理器)数据的话在读写并发的时候可能会读到中间态数据。

(2)写者不能改变临界区的数据类型。

(3)读多写少的场景,对writer的性能没有特别要求而对reader的性能要求极高。

(4)reader端对新旧数据不敏感。

 

CPU隔离的performance场景下RCU的扩展

    以8核心CPU为例,BootLoader给Kernel传入的cmdline包含”isolcpus=1-7”也就是在SMP均衡调度算法中将CPU1-7孤立出来,同时依然可以将进程按需绑定到 “孤立CPU”运行。这样做的好处是,CPU1-7可以只运行一个业务相关的用户态进程而尽量不被内核调度打扰以保证实时性。

 

      ① RCU_NOCB

    RCU调用的rcu_process_callbacks是在timer软中断里触发的RCU_SOFTIRQ,对于实时需求来说,写端的RCUcallback不可避免地会增加OS jitter和schedulinglatency。如果是在smart phone上,也有可能在一定程度上减小cpu的idle rate从而略微影响功耗。于是,RCU_NOCB的设计就被引入了,将CPU1-7上的callbackoffload到kthread并绑定到CPU0上(bootloader传入"rcu_nocbs=1-7"),也就是有些平台上看到的线程"rcuox/N",“x”对应RCU-bh,RCU-preempt,RCU-sched 分别为"b", "p", “s”,N为具体某个CPU。天下没有免费的午餐,NOCB的使用只是将call back的负载转移到别的CPU而已。

 

    ② Expedited quiescent state

    前面讲到经历一次quiescent state是用CPU切换一次上下文来判断的,那么某些业务场景下CPU不关抢占也不切换上下文(CPU idle或者 CPU只运行一个用户态业务进程),这种情况下RCU写端等待超时后经典RCU的做法是直接调用force_quiescent_state再调用smp_send_reschedule函数向其他所有cpu发送一次resched  ipi强制调度,这就影响了功耗和性能。

    RCU引入了dynticks变量并将处于该场景的CPU状态称谓expedited quiescentstate, 在per-cpu 字段rcu_data里加入包含dynticks变量的rcu_dynticks结构体,该值初始化为1,每次进入用户态的时候调用rcu_user_enter加1,出用户态的时候调用rcu_user_exit加1,这样当dynticks为偶数时就认为该CPU处于expedited quiescent state不需要等待该CPU切换上下文,CPU进入idle模式也同理,不过真实的场景没这么简单,会考虑到很多种情况以优化代码。需要注意的是,user模式下的expedited quiescent state必须打开宏CONFIG_NO_HZ_FULL,该宏起到stop tick的作用,忽略tick的条件依次为当前运行的进程为SCHED_FIFO,或者当前运行的进程为SCHED_RR并且与其相同优先级的runlist里只有一个进程,或者当前CPU的运行队列进程数不多于1个。

 

参考文献

perfbook

https://github.com/urcu/userspace-rcu

https://wiki.linuxfoundation.org/realtime/documentation/technical_details/rcu

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux 内核的 RCU(Read-Copy Update)机制在内存管理方面有一些特殊的考虑和实现。下面是关于 Linux 内核 RCU 内存管理的详细介绍: 1. RCU 内存回收:RCU 机制允许在没有锁的情况下进行并发读取,这意味着在进行内存回收时需要特殊处理。内核使用了一种称为“延迟回收”的技术来处理内存回收。当某个对象不再被引用时,RCU 并不立即释放它,而是延迟一段时间,以确保没有正在执行的读取操作仍然可以访问该对象。只有在延迟期间所有的读取操作都完成后,该对象才会被安全地释放。 2. RCU 保护的数据结构:在使用 RCU 时,需要特殊的数据结构来管理共享数据。常见的结构包括 rcu_head、struct rcu_head、rcu_node 等。这些结构用于追踪需要进行延迟回收的对象,并在适当的时机释放它们。 3. RCU 的内存屏障:为了确保并发读取的正确性,RCU 引入了内存屏障(memory barrier)来保证读取操作的顺序。在读取共享数据之前和之后,需要使用适当的内存屏障指令来确保读取操作的正确顺序。 4. RCU 的内存分配:在使用 RCU 时,需要特殊的内存分配函数来分配 RCU 保护的数据结构。Linux 内核提供了一些 RCU 特定的内存分配函数,例如 rcu_alloc() 和 rcu_free(),这些函数确保分配和释放的对象能够正确地与 RCU 机制配合使用。 5. RCU 的内存同步:在使用 RCU 时,需要进行适当的内存同步操作来确保数据的一致性。常见的同步操作包括 synchronize_rcu() 和 call_rcu()。synchronize_rcu() 用于等待所有当前正在执行的 RCU 读取操作完成,而 call_rcu() 用于注册一个回调函数,在所有当前正在执行的读取操作完成后执行。 总体而言,Linux 内核的 RCU 机制在内存管理方面提供了一种高效且无锁的并发读取解决方案。通过合理使用延迟回收、特殊数据结构、内存屏障和内存分配函数,以及适当进行内存同步操作,RCU 确保了并发读取的正确性和性能。这些特性使得 RCU 成为内核中重要的并发编程机制之一。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值