Linux tiny RCU

Linux rcu主要由两部分功能:

  • 作为一种类锁功能,避免读、写竞争资源,功能上类似读写锁;
  • rcu也是一个垃圾回收器;可以利用这个特性debug kernel, 著名的log: "RCU stall ..."
    注:这篇文章直接讲解代码,讲解RCU的设计。 rcu基础概念需自行了解。

Tiny rcu 基础

内核有针对单核mcu开发tiny rcu, 是非常基础的版本,适合学习rcu。

代码:kernel/rcu/tiny.c

核心结构体:

c 复制代码
31 struct rcu_ctrlblk {
 | 32     struct rcu_head *rcucblist; /* List of pending callbacks (CBs). */
 | 33     struct rcu_head **donetail; /* ->next pointer of last "done" CB. */
 | 34     struct rcu_head **curtail;  /* ->next pointer of last CB. */
~| 35     unsigned long gp_seq;       /* Grace-period counter. */
~| 36 };
~| 37 
~| 38 /* Definition for rcupdate control block. */
~| 39 static struct rcu_ctrlblk rcu_ctrlblk = {
~| 40     .donetail   = &rcu_ctrlblk.rcucblist,
~| 41     .curtail    = &rcu_ctrlblk.rcucblist,
~| 42     .gp_seq     = 0 - 300UL,
~| 43 };
~| 44 
  1. rcucblist: 链表的头,指向第一个回调。
  2. donetail: 指向"已完成宽限期"的回调段的末尾指针的地址(即最后一个已完成 CB 的 next 指针)。
  3. curtail: 指向"整个链表"的末尾指针的地址(即链表最后一个 CB 的 next 指针)。
  4. 链表初始状态:
c 复制代码
链表是空的,donetail 和 curtail 都指向头指针 rcucblist 的地址。
donetail == &rcucblist
curtail == &rcucblist
结果:donetail == curtail(没有待处理的回调)。 

tiny rcu 回收

rcu 作为一个垃圾回收器,会定期回收 old value 占用的内存,而且回收方式有两种:同步、异步。

  1. 同步回收:synchronize_rcu:阻塞只到所有cpu上报qs;
    因为synchronize_rcu同步操作是不允许在rcu_read_lock() 内部调用的,所以在单cpu时,调用synchronize_rcu的位置一定在qs阶段,所以synchronize_rcu 本身不用做任何事;多cpu时synchronize_rcu要阻塞操作,等待所有cpu上报qs。
  • 典型场景:
c 复制代码
     rcu_dereference(global_ptr);                                                
     rcu_assign_pointer(global_ptr, new_obj); // 1. 指针指向新数据                      
     synchronize_rcu();                       // 2. 等待(在 UP 上直接返回)             
     kfree(old_obj);                          // 3. 直接手动释放内存!         

同步回收是谁调用synchronize_rcu, 谁去主动kfree旧值,rcu不负责垃圾回收。

  1. 异步回收:rcu_call
    在某些场景下, 代码是不能阻塞的(比如网络协议栈、vfs查找等频繁快操作),也就是说旧值不是立刻回收,回收动作是异步的。
  • 典型场景:
c 复制代码
     void writer_delete_rcu(struct my_data __rcu **pptr)        
     {  
         struct my_data *old_p;                                                       
         // 假设写者持有某种锁(如 my_lock)
         spin_lock(&my_lock);   
         // rcu_replace_pointer(目标指针, 新值, 检查条件)
         // 它是 RCU 语义下的"替换并返回旧值"                                             
         old_p = rcu_replace_pointer(*pptr, NULL, lockdep_is_held(&my_lock)); 
         spin_unlock(&my_lock);                                                       
         if (old_p)                                                                   
             call_rcu(&old_p->rcu, my_data_reclaim_callback);  // 调用call_rcu注册hook, 异步等待所有cpu上报qs后,调用my_data_reclaim_callback 释放旧值

          return; //直接return, 不阻塞
     }       

看一下tiny RCU 是如何实现call_rcu的:

c 复制代码
|171 void call_rcu(struct rcu_head *head, rcu_callback_t func)        
  |172 {
  |173     static atomic_t doublefrees;
  |174     unsigned long flags;
  |175 
  |176     if (debug_rcu_head_queue(head)) {
  |177         if (atomic_inc_return(&doublefrees) < 4) {
  |178             pr_err("%s(): Double-freed CB %p->%pS()!!!  ", __func__, head, head->func);
  |179             mem_dump_obj(head);
  |180         }
  |181 
  |182         if (!__is_kvfree_rcu_offset((unsigned long)head->func))
  |183             WRITE_ONCE(head->func, tiny_rcu_leak_callback);
  |184         return;
  |185     }
  |186 
~ |187     head->func = func;
~ |188     head->next = NULL;
~ |189 
~ |190     local_irq_save(flags);
~ |191     *rcu_ctrlblk.curtail = head;
~ |192     rcu_ctrlblk.curtail = &head->next;
~ |193     local_irq_restore(flags);
~ |194 
~ |195     if (unlikely(is_idle_task(current))) {
~ |196         /* force scheduling for rcu_qs() */
~ |197         resched_cpu(0);
~ |198     }
~ |199 }
~ |200 EXPORT_SYMBOL_GPL(call_rcu);

核心代码:187~191:将要释放的内存,放入链表,当所有cpu 上报qs后,遍历列表释放内存。

c 复制代码
当我们调用 call_rcu 加入一个新的回调 A 时:                                            
A 被挂在 curtail 指向的位置。
curtail 指向 A 的 next 指针
注意:此时 donetail 还没有动,依然指向 rcucblist。                                         
结果:donetail != curtail。这说明有回调(A)新加入,但还没经过宽限期。

195:如果此时是idle task,直接resched, 因为处于idle task本身就代表在qs,可以立刻回收旧值了。

tiny rcu 上报qs

Rcu 会在某些时刻,上报qs(quiescent state),代表着此时cpu已经脱离了rcu read context, 如果每个cpu都是上报了qs状态 ,意味着所有cpu都脱离了rcu read context,那么此时释放旧内存是安全的。

rcu上报qs的点:

  1. 进程切换
  2. 软中断退出rcu_softirq_qs
  3. CPU 进入idle task
  4. tick到来时发现cpu在用户态el0
    以上四点中,任意一点发生时,此cpu一定是脱离rcu read context的,可以上报qs,
    所以这就是为什么rcu会被用来debug task, 如果发生 "RCU stall log" 一定是cpu长时间没上报qs,一定是cpu上时间没调度、或者没tick等。

看一下tiny rcu 如何实现上报qs的,拿tick举例:

timer中断到来

->update_process_times

->rcu_sched_clock_irq

c 复制代码
 71 void rcu_sched_clock_irq(int user)
 72 {
 73     if (user) {
 74         rcu_qs();                                                                 
 75     } else if (rcu_ctrlblk.donetail != rcu_ctrlblk.curtail) {
 76         set_tsk_need_resched(current);
 77         set_preempt_need_resched();
 78     }
 79 }

如果是cpu 处于user el0, 此cpu一定是脱离rcu read context的,直接调用rcu_qs上报qs;

如果处于el1,且rcu_ctrlblk.donetail != rcu_ctrlblk.curtail, 意味之前有人调用过call_rcu,有旧值待释放,要set_tsk_need_resched,设置重新调度标记。当然cpu能不能重新调度是另外一回事。

rcu_qs:

c 复制代码
51 /* Record an rcu quiescent state.  */
 52 void rcu_qs(void)                                                  
 53 {
 54     unsigned long flags;
 55 
 56     local_irq_save(flags);
 57     if (rcu_ctrlblk.donetail != rcu_ctrlblk.curtail) {
 58         rcu_ctrlblk.donetail = rcu_ctrlblk.curtail;
 59         raise_softirq_irqoff(RCU_SOFTIRQ);
 60     }
 61     WRITE_ONCE(rcu_ctrlblk.gp_seq, rcu_ctrlblk.gp_seq + 2);
 62     local_irq_restore(flags);
 63 }

57~58:判断是否有旧值待释放,如果有就激活软中断(rcu软中断初始化时已经注册好hook)

RCU hook 函数如下:

bash 复制代码
107 /* Invoke the RCU callbacks whose grace period has elapsed.  */
108 static __latent_entropy void rcu_process_callbacks(struct softirq_action *unused)     
109 {
110     struct rcu_head *next, *list;
111     unsigned long flags;
112 
113     /* Move the ready-to-invoke callbacks to a local list. */
114     local_irq_save(flags);
115     if (rcu_ctrlblk.donetail == &rcu_ctrlblk.rcucblist) {
116         /* No callbacks ready, so just leave. */
117         local_irq_restore(flags);
118         return;
119     }
120     list = rcu_ctrlblk.rcucblist;
121     rcu_ctrlblk.rcucblist = *rcu_ctrlblk.donetail;
122     *rcu_ctrlblk.donetail = NULL;
123     if (rcu_ctrlblk.curtail == rcu_ctrlblk.donetail)
124         rcu_ctrlblk.curtail = &rcu_ctrlblk.rcucblist;
125     rcu_ctrlblk.donetail = &rcu_ctrlblk.rcucblist;
126     local_irq_restore(flags);
127 
128     /* Invoke the callbacks on the local list. */
129     while (list) {
130         next = list->next;
131         prefetch(next);
132         debug_rcu_head_unqueue(list);
133         local_bh_disable();
134         rcu_reclaim_tiny(list);
135         local_bh_enable();
136         list = next;
137     }
138 }

120~126:

c 复制代码
// 1. 摘取已处理段                                                                 
     list = rcu_ctrlblk.rcucblist;                   // list 指向链表头                 
     rcu_ctrlblk.rcucblist = *rcu_ctrlblk.donetail;  // 头指针跳过已处理段,指向第一个"未处理"节点                                                                                
     *rcu_ctrlblk.donetail = NULL;                   // 断开 list 和剩余链表的联系,使其成为独立链表 
// 2. 修正指针  
     if (rcu_ctrlblk.curtail == rcu_ctrlblk.donetail)                                 
         rcu_ctrlblk.curtail = &rcu_ctrlblk.rcucblist; // 如果整个链表都被取走了,重置 curtail                                                                                
     rcu_ctrlblk.donetail = &rcu_ctrlblk.rcucblist;    // 重置 donetail 到头部    
     local_irq_restore(flags); 
// 3. 执行回调
     while (list) { ... f(list); ... }    

假设链表中有回调 A -\> B -\> C -\> D,其中 A, B 已过宽限期(donetail 指向 B->next),C, D 是新加入的。

  1. 摘取:
    1. list 拿到 A。
    2. rcucblist 被设置为 *donetail,即 B->next,也就是 C。现在 rcucblist 指向 C。
    3. *donetail = NULL 即 B->next = NULL。现在 list 变成了 A -\> B -\> NULL
  2. 重置:
    1. donetail 回到 &rcucblist(现在指向 C 的前驱)。
    2. 如果刚才把 D 也取走了(即 curtail == donetail),则 curtail 也会回到 &rcucblist。
  3. 总结链表分段模型:
    1. [rcucblist ... *donetail) : 已成熟区(宽限期已过,等待软中断执行)。
    2. [*donetail ... *curtail) : 孵化区(刚 call_rcu 加入,正在等静止状态)。

129~136:循环调用rcu_reclaim_tiny

c 复制代码
 85 static inline bool rcu_reclaim_tiny(struct rcu_head *head)                                                                                                                                                                                                                  
 86 {
 87  ....
102     f(head);
103   ...
105 }

102:这个函数非常简单,就是调用之前注册的f函数,释放内存。

为什么 gp_seq 要 + 2

在tiny代码中,rcu_qs\synchronize_rcu函数中会执行:

c 复制代码
 WRITE_ONCE(rcu_ctrlblk.gp_seq, rcu_ctrlblk.gp_seq + 2);

这是配合start_poll_synchronize_rcu、poll_state_synchronize_rcu使用的。

典型场景:

c 复制代码
     cookie = start_poll_synchronize_rcu(); // "登记"一下,拿到当前版本号                                 
     while (!poll_state_synchronize_rcu(cookie)) {                                     
         // 宽限期还没过,但我不想睡觉                                                    
         do_something_else();                                                         
     }                                                                                                                                                            
     // 执行到这里,说明 gp_seq != cookie,即宽限期已过                                    
     free_my_data();                                                                         
                             

当判断出gp已经变化,代表宽限期结束。才会退出循环。

Q: 为什么需要"典型场景", 看上去rcu_call完全可以代替?

A: 主要是为了性能,RCU_CALL的性能比较差,而且链表占用内存多,且回收内存的hook函数是软中断的callback, 不能休眠、不能获取复杂的锁,且运行时间不能太长。start_poll_synchronize_rcu + poll_state_synchronize_rcu 解决了这件事。

rcu_barrier

确保在调用 rcu_barrier() 之前所有已提交的 RCU 回调函数(callbacks)都已经执行完毕。

在tiny rcu里,rcu_barrier的实现非常简洁:

c 复制代码
void call_rcu_hurry(struct rcu_head *head, rcu_callback_t func)
 {
  __call_rcu_common(head, func, false);
 }

void rcu_barrier(void)
{
    wait_rcu_gp(call_rcu_hurry);                                                                                                                                                                                                                                        
}
  • wait_rcu_gp 是一个宏,最终会调用 __wait_rcu_gp。它会在栈上创建一个 struct rcu_synchronize(包含一个 rcu_head 和一个 completion 信号量),然后:
  1. 调用传入的函数(这里是 call_rcu_hurry,在 Tiny RCU 中通常等同于 call_rcu)。
  2. 将该 rcu_head 注册到 RCU 链表的末尾,回调函数设为 wakeme_after_rcu(该函数仅负责 complete() 信号量)。
  3. 当前进程进入睡眠,等待 completion 信号量。
  • 在 Tiny RCU 中,所有的回调函数都排在唯一的单向链表 rcucblist 中。
  1. 由于 RCU 回调是在同一个 CPU 上按顺序执行的。
  2. rcu_barrier 注册的"屏障回调"被放在了链表的最后。
  3. 因此,当 RCU 软中断(rcu_process_callbacks)执行到这个"屏障回调"并唤醒调用者时,排在它之前的每一个回调函数(即在 rcu_barrier 调用前提交的所有回调)都已经执行完毕了。
相关推荐
wuminyu2 小时前
虚拟线程底层ForkJoinPool的工作窃取算法机制
java·linux·c语言·jvm·c++
测试运维日常笔记4 小时前
tcpdump(Linux服务器)配合 Wireshark(本地分析)使用手顺与常用过滤语法详解
linux·服务器·tcpdump
jason_renyu5 小时前
Linux Docker 服务管理与排查实战指南(新手速查版)
linux·docker·新手docker学习·docker日志查询·docker服务管理
测试运维日常笔记5 小时前
Unix/Linux 系统软件包管理笔记
linux·笔记·unix
雾时之林6 小时前
Linux--介绍及管理
linux·运维·服务器
盐焗鹌鹑蛋6 小时前
【Linux】重定向
linux
Forever Nore6 小时前
LeetCode 5 最长回文子串 - 中心扩散
linux·算法·leetcode
呱呱巨基6 小时前
Docker 基础概念学习
linux·c++·学习·docker
Dlrb12117 小时前
Linux驱动---Linux 中断系统及其上与下半部的介绍与阻塞IO实现按键检测
linux·嵌入式硬件·imx6ull·中断·阻塞io