可强占RCU

可强占RCU

前言

RCU机制属于无锁编程的一种,在访问读端临界区时不需要加锁,故不存在上面提到的锁的问题,所以拥有极好的扩展性,且可以多个读者同时存在。那为什么不用RCU锁取代其他锁呢?前面说到读端不用加锁,但是写端是需要加锁的,用来保证写者之间的同步,这一特点限制RCU的适用场景一定是读多写少的,否则相对其他锁优势就没那么明显了。且由于读端无锁写端才需要加锁,RCU又是可以读者写者共存的,即多个读者一个写者同时存在。
关于RCU,我对它的理解是它将对数据块的保护转化成了对指向数据块的指针的保护,极大的简化了对被保护资源的保护的难易程度,而对指针的保护则通过封装原子操作和各种屏障就可以确保,这样就用不到锁了。(所以RCU通常用来保护链表资源。)

当然,将对数据块的保护转化成对指向数据块的指针的保护是要付出额外的代价的,毕竟用户真正要操作的还是数据块中的数据。所以写者在update动作前,需要先copy一份数据块的副本,在副本中修改完成后,再将新的副本再重新assign到链表中替代旧的数据块。

那旧的数据块怎么办呢?当然是要回收了,那么什么时候回收呢?当然是要在没人访问它时再回收了,那怎么如何确定没人访问它了呢?当然是在宽限期(grace period)结束之后就可以确定了,那宽限期什么时候会结束呢?当然是在所有的CPU都经过一次静止态(Quiescent state)时就结束了,那CPU怎么才算度过一次静止态呢?那就在下文寻找答案吧。
注 :不可抢占RCU度过宽限期的标准是所有CPU都经过一次QS,可抢占的RCU不仅要每个CPU经过一次QS,还需要每个rcu node中所记录的属于当前宽限期的blocked进程都为空。本文讲可抢占RCU
宽限期(Grace period):

普通宽限期:用户调用call_rcu()或synchronize_rcu()所开启的宽限期。
加速宽限期:用户调用synchronize_rcu_expedited()会触发一个加速宽限期或是将进行中的普通宽限期标记为一个加速宽限期。所谓加速宽限期主要的执行流程跟普通宽限期是一样的,只是会比普通宽限期多出一些QS的上报点,用来减少宽限期中不必要的延时。此接口会为系统带来一些额外的overhead,作者Paul建议少用。
静止态(Quiescent state):

普通的QS: CPU发生进程切换,或是运行在用户态都标志着它进入了QS

Extended QS:CPU执行idle进程、进入中断、运行在用户态的tickless模式下标志着它进入EQS。参考链接:RCU邮件列表中经常活跃的Joel大佬的博客
Deferred QS:调用rcu_read_unlock退出最外层RCU读临界区时若抢占是关的、或软中断是关的、或中断是关的三种情况之一都会导致读端临界区被延长,致使当前CPU的QS推迟上报。被推迟的QS会在读临界区以外的中断、软中断、进程切换中被上报。举例:若unlock时抢占是关的,在抢占开之前再次进入另一个临界区,那QS的上报将在退出第二个读临界区以后开抢占之后。deferred QS的提出是为了解决scheduler中的一个死锁问题。参考链接:
Tick中断:

Tick中断即我们熟悉的周期性调度器。它的中断处理程序不仅包含任务调度、定时器、时间管理等相关操作,也包含了RCU的部分任务,由于位于中断处理程序中,而中断处理程序通常被要求耗时短,所以这里只是主要的工作是检查是否存在待处理的事情(例如:需要上报QS、Callback过载),有的话则唤醒RCU软中断进行实质性的工作。

我们熟知的CPU stall的检查也是在tick中断中进行的,检测中若发现宽限期在规定的时间内(默认是21秒)未结束,则会打出cpu stall的警告,以各个cpu的QS情况。
RCU软中断:

Linux系统专门为RCU留了一个名为RCU_SOFTIRQ的软中断,并在系统启动阶段通过rcu_init()为它注册对应的回调函数rcu_core_si()。RCU软中断主要有两个大的作用:

  1. 处理本CPU上已经进入DONE状态的callback
  2. 检测并更新本CPU的gp编号;检测并上报本CPU的静止态;若有需要则唤醒gp线程。

小知识点:1. RCU_SOFTIRQ软中断是所有软中断中优先级最低的。2. 在RT-Linux中并未使用软中断,而是为每个CPU都注册了一个rcu_cpu_kthread线程作为替代。
GP内核线程:

这里所谓的gp线程即rcu_gp_kthread(),主要功能是处理宽限期相关的事务,包括:开启新的宽限期、处理forcing QS、清理已结束的宽限期。

此线程的名字保存在rcu_state->name中,可抢占RCU命名是rcu_preempt;不可抢占RCU的命名是rcu_sched。我们可以通过ps -aux看到。
RCU数据结构
Tree RCU通过rcu_data、rcu_node、rcu_state 三种数据结构组成了整棵RCU tree,它们分别代表树叶、树干、树根。下面对其中重要成员做了注释

struct rcu_data {
         /* cpu当前的gp编号,以及低两位表示gp的状态 */
        unsigned long   gp_seq;        
        unsigned long   gp_seq_needed;  
        /* 表示cpu是否未度过norm或exp静止态,例:进程切换时会调用union成员rcu_qs设为false*/
        union rcu_noqs  cpu_no_qs;      
        /* 表示RCU是否需要此cpu上报QS状态*/
        bool            core_needs_qs;
        /*cpu最近的一次状态是online*/
        bool            beenonline; 
        /* gp_seq溢出后,此变量会置为true */
        bool            gpwrap; 
        /*当前cpu是否在等待一个deferred QS*/
        bool            exp_deferred_qs;
        bool            cpu_started; 
        /* 指向cpu对应的rcu_node*/
        struct rcu_node *mynode;        
        /* 此cpu对应rcu_node中的掩码位,rcu_node->qsmask表示node中的cpu的qs情况 */
        unsigned long grpmask;       
        /*当前gp已经历过多少个tick中断,每个tick中断中都会对它加1*/   
        unsigned long   ticks_this_gp;  
        struct irq_work defer_qs_iw;    /* Obtain later scheduler attention. */
        bool defer_qs_iw_pending;       /* Scheduler attention pending? */
        struct work_struct strict_work; /* Schedule readers for strict GPs. */
        /* 分段链表,存放当前cpu上的callback */
        struct rcu_segcblist cblist;    
        long            qlen_last_fqs_check;
        ......
        /* 3) dynticks interface. */
        int dynticks_snap;              /* Per-GP tracking for dynticks. */
        long dynticks_nesting;          /* Track process nesting level. */
        long dynticks_nmi_nesting;      /* Track irq/NMI nesting level. */
        atomic_t dynticks;              /* Even value for idle, else odd. */
        bool rcu_need_heavy_qs;         /* GP old, so heavy quiescent state! */
        bool rcu_urgent_qs;             /* GP old need light quiescent state. */
        bool rcu_forced_tick;           /* Forced tick to provide QS. */
        bool rcu_forced_tick_exp;       /*   ... provide QS to expedited GP. */
        .......
        int cpu;                        /* 本cpu的编号 */
};
struct rcu_node {
        /*保护此node的spinlock*/
        raw_spinlock_t __private lock;
        /* 本节点当前宽限期的编号,低两位表示状态 */
        unsigned long gp_seq;              
        unsigned long gp_seq_needed;  
        /*当前node已经度过所有的QS,且gp_tasks指针为NULL*/
        unsigned long completedqs;
        /* 记录当前node中的cpu或子node的QS情况,经历过为0*/
        unsigned long qsmask;              
        /* 每个GP开始时的初值,从qsmaskinitnext中获取,用来赋为qsmask */
        unsigned long qsmaskinit;   
        /* 下一个宽限期开始时的静止态位图,因为可能有些cpu下线了*/
        unsigned long qsmaskinitnext; 
        unsigned long ofl_seq;
        /* 类似于qsmask,用来记录加速宽限期的位图*/
        unsigned long expmask;   
        /*此node对应父node的qsmask的哪个bit*/
        unsigned long grpmask; 
         /* node中最小的处理器编号 */
        int     grplo;  
         /* node中最大的处理器编号 */
        int     grphi;  
         /* node在上一层node中的编号 */
        u8      grpnum;
        /* node在tree中的level,root node为0 */
        u8      level;                     
        /*指向父node*/
        struct rcu_node *parent;    
        /*读临界区中被抢占的进程,都会被挂如此链表*/
        struct list_head blkd_tasks;
        /*指向blkd_tasks中阻塞当前宽限期的第一个进程,当前node度过qs时,此成员必须为NULL*/
        struct list_head *gp_tasks;
        ......
}
struct rcu_state {
        /* 存放了系统中所有rcu_node实体 */
        struct rcu_node node[NUM_RCU_NODES];    
        /* 树中每level的其实rcu_node指针(+1是为了消除编译警告) */
        struct rcu_node *level[RCU_NUM_LVLS + 1]; 
        /* 总的cpu的数量 */
        int ncpus;      
        /* online的cpu数量 */
        int n_online_cpus;                      
        /* The following fields are guarded by the root rcu_node's lock. */
        /* 当前宽限期的编号,低两位表示状态 */
        unsigned long gp_seq;   
        /* 目前系统上经历过的最长的gp的时间(jiffies) */
        unsigned long gp_max;           
        /* 指向rcu_gp_kthread内核线程 */
        struct task_struct *gp_kthread;    
        /*wait queue数据结构,gp线程中会用到*/
        struct swait_queue_head gp_wq;   
        /* 用来控制gp线程行为的标志位,可以通知进行FQS或开启新的GP */
        short gp_flags;       
        /* rcu_gp_kthread线程的状态 */
        short gp_state;                         
        /*当前系统的callback是否堆积过多*/
        u8 cbovld;      
        ......
};

RCU主要接口
本文主要讲RCU的同步原理,故这里只罗列和静止态(QS)以及宽限期(GP)有关的接口。关于指针引用和指针替换的接口这里不展开。

/*注册回调函数,并开启一个宽限期*/
call_rcu()

/*阻塞并等待宽限期结束,内部实现仍是通过调用call_rcu()实现*/
synchronize_rcu()

/*开启一个加速宽限期或标记当前宽限期为加速宽限期*/
synchronize_rcu_expedited()

/*标记一个读临界区的开始*/
rcu_read_lock()

/*标记一个读临界区的结束*/
rcu_read_unlock()

注意:rcu_read_unlock虽然表示一个读临界区的结束,但并不代表执行完它,本CPU就一定上报了QS,
     若此时抢占是关的、或软中断是关的、或中断是关的,都将导致一个deferred QS,在前面所述状
     态打开后才去上报一个推迟的QS

CPU如何经历静止态:
下面我们来说下CPU度过静止态的几种情况

进程切换: 包括读临界区内发生抢占 和 读临界区外的进程切换
(读临界区内只允许被抢占,禁止主动睡眠或调度,否则触发warning)

schedule()
  __schedule
    ->rcu_note_context_switch

void rcu_note_context_switch(bool preempt)
{
        /*若处于rcu读临界区且不是抢占,那么说明是临界区主动调度,直接打警告*/
        WARN_ON_ONCE(!preempt && rcu_preempt_depth() > 0);
        /*此进程在读临界区且当前临界区是首次被抢占*/
        if (rcu_preempt_depth() > 0 &&
            !t->rcu_read_unlock_special.b.blocked) {

                raw_spin_lock_rcu_node(rnp);
                /*标记当前进程的rcu读临界区被抢占,并记录目前进程属于哪个rcu node*/
                t->rcu_read_unlock_special.b.blocked = true;
                t->rcu_blocked_node = rnp;

                /*将被抢占的进程加入rcu node中的被抢占进程链表*/
                rcu_preempt_ctxt_queue(rnp, rdp);
        } else {
                /*检测是否有适合且存在pending的deferred QS需要上报,有则上报*/
                rcu_preempt_deferred_qs(t);
        }
        /*记录本CPU已经度过QS,但只是记录,上报会在后续的RCU软中断中进行*/
        rcu_qs();
        if (rdp->exp_deferred_qs)
                rcu_report_exp_rdp(rdp);
        /*rcu tasks相关的部分,非本文内容*/
        rcu_tasks_qs(current, preempt);
        trace_rcu_utilization(TPS("End context switch"));
}

通过函数注释我们看到了当抢占发生在读临界区以内,会将被抢占的进程记录在rcu_node的gp_tasks成员中,若此进程是当前GP下第一个被抢占的进程则使用blkd_tasks成员指向它。挂在gp_tasks链表中的进程当执行rcu_read_unlock()时会自己把自己从gp_tasks链表中删除。

可抢占RCU和不可抢占RCU的判断渡过静止态的区别就在这里,可抢占的rcu node若是要度过宽限期不仅要检查qsmask==0,还要检查blkd_tasks == NULL,同时满足才可以说整个rcu node度过了宽限期。
2. CPU进入EQS(extended QS):包括CPU运行idle进程 和 返回用户空间

CPU运行Idle进程的情况:
do_idle
  ...
    ->rcu_idle_enter
      ->rcu_eqs_enter

CPU返回用户空间的情况(arm64):
ret_to_user
  ->user_enter_irqoff
    ->__context_tracking_enter
      ->rcu_user_enter
        ->rcu_eqs_enter

具体实现:
rcu_eqs_enter
  ->rcu_prepare_for_idle    //为进入eqs前做准备。1.判断nohz是否被sysfs修改过,并记录进rdp
                              2.若当前cpu上仍有callback则触发软中断来处理
  ->rcu_preempt_deferred_qs //判断是否有deferred qs需要上报,有则上报。
  ->rcu_dynticks_eqs_enter  //重点:rdp->dynticks加0x10,gp线程的fqs处理中通过判断bit1的奇偶来
                              判断eqs,用bit1的原因是,bit0被另做它用了

为什么要有EQS呢?主要是为了动态时钟的场景,若系统开启了NO_HZ_IDLE,在运行idle进程的情况下CPU是不响应tick中断的;若开启NO_HZ_FULL,不仅CPU Idle时不响应tick,在CPU只有一个running状态的进程时也不响应tick中断。前面我们讲tick中断时已经说到它的功能包括检查QS,以及唤醒RCU软中断。那在没有的tick的情况下CPU如何上报QS呢?

看上面的函数分析,进入EQS时会调用rcu_eqs_enter()->rcu_dynticks_eqs_enter()将当前CPU的rdp->dynticks的bit1加1变为奇数,表示处于动态时钟模式,而gp线程在处理强制静止态force qs的操作中会搜集所有的处于EQS的CPU,并替它们上报QS。

既然有进入,那就会有退出,退出EQS模式时会将rdp->dynticks的bit1减1,变为偶数。对应函数rcu_eqs_exit() ,场景包括退出idle进程、进入中断。
宽限期的创建、处理、结束
概念澄清一段已经介绍了宽限期的处理由内核线程rcu_gp_kthread全权负责,现在一起来分析下此线程的逻辑。

此线程是个for循环,逻辑清晰,分为三大块:

此线程主逻辑是一个死循环,主要是对几种情况的处理:
for(;;){
    1.等待开启新的宽限期
    2.睡眠并等待被事件唤醒,并处理对应的事件。事件包括:强制静止态事件、或callback
      过载事件、或gp超时事件、或发现gp已经结束(结束依据root rcu_node的qsmask为0
      且gp_tasks指针为NULL)
    3.结束已完成的宽限期
}

rcu_gp_kthread()线程详解:

阶段1 开启新宽限期:
->将rcu状态设为RCU_GP_WAIT_GPS
->swait_event_idle_exclusive  //线程进入idle状态等待RCU_GP_FLAG_INIT被设置
->将rcu状态设为RCU_GP_DONE_GPS  //此时线程已被唤醒,准备开启新的gp
->rcu_gp_init                 //初始化成功则跳入阶段2(等待FQS)
  ->rcu_state.gp_seq + 1                    //创建一个新的gp_seq即加1,通过for循环从root node向leaf node传递。
  ->rcu_preempt_check_blocked_tasks         //可抢占rcu如果在开启一个新的gp时rcu_node中仍有blockded的线程,报警告!
  ->rnp->qsmask = rnp->qsmaskinit           //初始化qsmask(qsmaskinit只cover online的cpu)
  ->WRITE_ONCE(rnp->gp_seq, rcu_state.gp_seq)//给node设置新的gp_seq
  ->__note_gp_changes(rnp, rdp)             //若当前cpu恰巧属于此node,则初始化rcu_data

阶段2 处理强制静止态:  //通过下文我们可以看到真正的qsmask=0且无blocked进程的情况直接return,当过载被设置FQS事件或gp超时都会进行一次fqs任务。
->rcu_gp_fqs_loop
  ->swait_event_idle_timeout_exclusive  //睡眠。唤醒情况:1.callback过载 2.FQS flag被设置 3.root rcu node的qsmask为0且无block进程
  ->若是根node的qsmask和blocked tasks为NULL的唤醒,直接进入阶段3,开始gp cleanup
  ->若不为0和NULL,可能是callback过载设置了FQS或是gp超时,gp未结束,调用rcu_gp_fqs进行一次fqs
    ->force_qs_rnp
      ->若qsmask为0,但是blocked task不为NULL则通过rcu boost机制提升blocked reader task的优先级。通常有实时app的系统会启用rcu boost功能
      ->若存在dyntick-idle或offline的cpu,则调用rcu_report_qs_rnp上报同时清除对应的rnp->qsmask。
    ->清除RCU_GP_FLAG_FQS标志
  ->若是等待事件超时,则调整睡眠时长重新进入swait_event等待,若经过若干次的超时都未结束gp,进行一次强制静止态force_qs_rnp
      
阶段3 清理结束的宽限期:
->将rcu状态设置为RCU_GP_CLEANUP
->rcu_gp_cleanup
  ->rcu_state.gp_max           //若此次的gp持续时间是最长的,则更新gp_max
  ->rcu_seq_end                //获取新的gp_seq,及清除老gp_seq的flag域并加1
  ->rcu_for_each_node_breadth_first //从root node向下逐个初始化node
  ->rcu_seq_end(&rcu_state.gp_seq)  //再更新rcu_state的seq
  ->将rcu状态设置为RCU_GP_IDLE        //表示在正在初始化gp
->将rcu状态设置为RCU_GP_CLEANED

分析完gp线程,你应该发现了此线程大部分时间是在睡眠,等待唤醒。那么谁会唤醒gp线程呢?地方挺多,例如call_rcu()注册回调时,若发现CPU上的callback过载了,就换唤醒gp线程进行一次fqs;比较通用的唤醒位置则是在RCU软中断里,RCU软中断又是被tick中断触发,那么下面依次分析下tick中断和RCU软中断。
Tick中断与RCU软中断详解:
Tick时钟中断:

检查本CPU的QS情况和callback情况,如有需要则唤醒rcu软中断

tick_sched_timer
 ->tick_sched_handle
    ->update_process_times
      /*旧名字叫rcu_check_callbacks*/
      ->rcu_sched_clock_irq    //用来判断是否度过宽限期,若已经度过则触发RCU软中断  
        ->rcu_flavor_sched_clock_irq  //当前cpu的三种qs情况处理,作用类似于schedule函数中的rcu_note_context_switch
          1.仍处于rcu读临界区。 //说明还未渡过QS,参考https://lore.kernel.org/patchwork/patch/979046/
          2.上报deffered qs   //若有deferred QS则立即上报
          3.已经经历了qs       //rcu_data中记录已经渡过qs,稍后触发软中断上报qs
        ->rcu_pending        
          1.检查是否发生了cpu stall
          2.检查当前CPU是否需要上报QS
          3.检查是否有done状态的callback需要执行
        ->invoke_rcu_core  //若存rcu_pending返回1,则唤醒rcu软中断

RCU软中断:

软中断处理程序的主逻辑比较简单,主要是两个功能: 1. 同步rdp和rnp中的内容,并上报QS;2. 处理已经处于DONE状态的callback。

rcu_core_si                //原名rcu_process_callbacks
  ->rcu_core               //原名__rcu_process_callbacks
    ->rcu_check_quiescent_state //同步本cpu的rdp与rnp的内容。然后上报qs
    ->rcu_do_batch         //批量处理callback
      ->f(rhp)             //执行callback

处理callback的部分比较简单,软中断的难点在于同步rdp(即rcu_data)和rnp(即rcu_node)内容的部分rcu_check_quiescent_state()。为什么要同步呢?因为每开启一个新的gp时,初始化tree的操作是从root rcu node开始向leaf rcu node进行同步,并不会将新gp的相关信息更新到各cpu的rcu_data中去,需要各个cpu在自己的软中断或其他调用note_gp_changes()的地方自己查询并更新。也就是说,rdp所持有的gp编号可能仍是老的。下面我们来分析一下这个过程:

预备知识:rcu_data->gp_seq成员代表gp的编号,低两位bit0和bit1用来表示当前当前gp的状态。
rcu_check_quiescent_state
  ->note_gp_changes(rdp)
    ->若rdp->gp_seq==rnp->gp_seq说明未产生新的gp,无需同步,直接return
    ->__note_gp_changes                   
      ->if rcu_seq_completed_gp     //##忽略nodeseq的低两位后,若cpu seq<node seq,即老gp已经完成,且node上已经有新的gpnum了(并不代表新gp开始),但cpu还未意识到
          rcu_advance_cbs           //callback未过载,则推进回调函数,将已结束的宽限期中的callback加入RCU_DONE_TAIL中。并调用rcu_accelerate_cbs。
          rdp->core_needs_qs=false  //cpu仍未更新到最新的gp,暂时不用上报qs
        else                        //##即当前cpu seq和node seq相等,说明本cpu正在经历这个gp
          rcu_accelerate_cbs        //callback未过载,则加速回调函数把最后一个子链RCU_NEXT_TAIL前移。
          更新rdp->core_needs_qs     //若node的mask中已经显示此cpu渡过qs,则将core_needs_qs置为false,否则置为ture
      ->if rcu_seq_new_gp           //判断是否有新的gp了,但是本cpu还不知道。由于rcu_seq_new_gp函数比较抽象,这里用具体数值举例:
                                    //若rdp的seq为0x9800(即低两位是0),那么处理后就是0x9800,若低两位不是0,处理后就是0x9900。
                                    //那么当rnp的seq是9900时,说明没有新的gp,rnp的seq若是9901则说明有一个rdp还未意识到的新gp9901
          初始化本cpu的rdp为新qs做准备  //判断cpu是否已经历过qs,然后给rdp->cpu_no_qs.b.norm和rdp->core_needs_qs
          zero_cpu_stall_ticks       //新gp经历过的tick数归零
      rdp->gp_seq = rnp->gp_seq      //将node seq中的新gpnum更新入本cpu的seq中
      WRITE_ONCE(rdp->gp_seq_needed, rnp->gp_seq_needed) 
      WRITE_ONCE(rdp->gpwrap, false) //清除溢出标志位
    ->rcu_strict_gp_check_qs         
    ->rcu_gp_kthread_wake            //__note_gp_changes若返回true,说明callback过载了,立即唤醒gp线程进行fqs      
  ->rcu_report_qs_rdp
    -> rdp->cpu_no_qs.b.norm==ture或rdp->gp_seq!=rnp->gp_seq //说明已经当前cpu已经经历过qs,直接返回
    ->cpu在rnp->qsmask中对应的bit若为0   //说明上报过qs,无需再上报
    ->rcu_accelerate_cbs
    ->rcu_report_qs_rnp       //逐层向上汇报qs,不仅检查rnp的qsmask是否为0,还会检查rnp上是否有blocked task。
      ->rcu_report_qs_rsp     //到这里说明所有rcu node都经历了qs,设置RCU_GP_FLAG_FQS并唤醒rcu_gp_kthread线程结束当前的gp(此FQS为真正渡过gp的情况)
    ->rcu_gp_kthread_wake     //如果需要则唤醒gp线程处理宽限期事物

关于加速宽限期:
synchronize_rcu_expedited()会将当前宽限期标记为一个加速宽限期,并且加速它。

原理:基本思想是向非idle和非nohz的在线cpu发送IPI核间中断。若被通知的cpu处于临界区,那么设置标志位让它在退出最外层时,通过rcu_read_unlock_special()立即上报静止态(不可抢占的rcu会被设置进程切换标志,来帮助渡过静止态);若被通知的cpu不处于 临界区,则上报立即静止态。

注意:此接口不应出现在loop中,且对rt kernel不友好。整体逻辑和synchronize_rcu是差不多的,只是更加”残忍“。

实现:通过rcu_gp工作队列为所有cpu注册相关的handler,当加速gp时,相关的操作由注册的workqueue来做,其中包括核间中断rcu_exp_handler会标记task的rcu结构体的hint,配合在unlock时识别并处理。

rcu初始化阶段会为加速宽限期expedited gp和srcu创建两个workqueue,分别名为rcu_gp和rcu_par_gp

   rcu_gp_wq = alloc_workqueue("rcu_gp", WQ_MEM_RECLAIM, 0);
   rcu_par_gp_wq = alloc_workqueue("rcu_par_gp", WQ_MEM_RECLAIM, 0);

RCU相关API详解
rcu_read_unlock():

__rcu_read_unlock
  rcu抢占计数-1后且不为0,__rcu_read_unlock即为空操作
  rcu抢占计数-1后等于0,则说明正在退出临界区
     ->判断t->rcu_read_unlock_special.s //为true的话说明临界区发生过抢占,进行特殊处理
       ->rcu_read_unlock_special

rcu_read_unlock_special
  if(preempt_bh_were_disabled||irqs_were_disabled) //若进来之前抢占或软中断或中断是关闭状态的话,QS将被推迟上报,简单处理后直接返回
    ->如果在中断上下文或当前是个加速宽限期,则触发软中断
    ->.....    
    ->return   //关中断软中断抢占都视为读临界区被延长,所以不上报qs
  rcu_preempt_deferred_qs_irqrestore            //上报deferred QS
    ->if (!special.s && !rdp->exp_deferred_qs)  //若读临界区未被抢占过,则直接返回
      ->return
    ->t->rcu_read_unlock_special.s = 0          //被抢占过,清零,后续做处理
    ->若special.b.need_qs为真
      ->rcu_qs 	
    ->if (rdp->exp_deferred_qs) 
      ->rcu_report_exp_rdp
    ->若special.b.blocked为真          //若曾经发生过抢占
      ->rcu_next_node_entry()         //获取当前task在block list上的next的task进程np
      ->list_del_init()               //将本进程从rcu_node上的list中删去
      ->t->rcu_blocked_node = NULL    //清除rcu_blocked_node成员
      ->WRITE_ONCE(rnp->gp_tasks, np) //若当前进程是rcu_node上block当前gp的第一个task,则需要将np重新置位第一个task
      ->若当前node上已经没有blk的进程
        ->rcu_report_unblock_qs_rnp   //上报当前的整个rcu_node已经渡过qs

call_rcu() :

call_rcu
  ->__call_rcu
    ->check_cb_ovld                //更新当前rcu_node的cbovldmask
    /*功能1:将callback加入本cpu的callback链表*/
    ->rcu_segcblist_enqueue 
    /*功能2:若当前cpu处于idle则唤醒rcu softirq,若callback太多或是宽限期时间太久则进行强制宽限期*/
    ->__call_rcu_core
      ->invoke_rcu_core            //如果当前cpu处于EQS,触发软中断重新评估cpu是不是真的处于EQS
        ->raise_softirq            //标准内核的话,触发软中断
        (->invoke_rcu_core_kthread //rt内核的话,唤醒rcu_cpu_kthread_task线程)
      ->若当前cpu积攒了太多callback则执行强制宽限期并唤醒rcu_gp_kthread线程  //unlikely
        ->note_gp_changes          //推进当前cpu上关于宽限期的数据更新,因为宽限期的下发只到rcu_node。rcu_data需要自己从rcu_node中查询
        ->rcu_accelerate_cbs_unlocked  //若当前没有gp,则开启一个新的gp。设置rcu_state.gp_flags为RCU_GP_FLAG_INIT来唤醒gp线程

synchronize_rcu() :
synchronize_rcu的内部实现其实是调用 了call_rcu。大致思路是利用call_rcu为自己注册了一个名为wakeme_after_rcu的callback后就调用wait_for_completion进入睡眠。当结束宽限期后,callback被执行到会去唤醒之前睡眠的线程,唤醒机制是内核的completion同步机制。相关数据结构查看struct rcu_synchronize

synchronize_rcu
  /*判断当前gp是否需要加速,若需要则调用加速接口synchronize_rcu_expedited()*/
  ->synchronize_rcu_expedited  
  /*利用call_rcu接口注册一个名为wakeme_after_rcu的回调*/
  ->wait_rcu_gp(call_rcu)      
    ->call_rcu(rcu_synchronize->rcu_head, wakeme_after_rcu)

总结:
都是文字和代码太抽象,这里像往常一样画了副图,帮助大家对整个宽限期是如何度过的,以及各种情况下CPU是如何上报静止态的有个全局的认识。但是图中没有画读临界区发生抢占的情况,因为太挤了…
在这里插入图片描述
图中的"笑脸"代表CPU在这个时刻度过了QS,接下来简单描述一下图片:

CPU4调用call_rcu()开启了新的GP
CPU0在退出读临界区后发生tick中断,检测到它已经渡过静止态,触发软中断为其上报静止态。
CPU2由于在宽限期开始时未处于读临界区,且随后发生了进程切换,进程切换中标记它度过QS,并在随后的软中断里进行了上报。
CPU3由于处于idle状态,在进入idle进程时操作了rdp->dynticks使成为奇数,意味着CPU处于EQS。并且在随后的一次gp线程的执行中通过force qs为其上报了静止态。
CPU4本身就未发生RCU读,所以一直处于QS状态,在随后的一次RCU软中断中为其上报了QS。
CPU1的读临界区比较长,在软中断中上报QS时,发现自己是最后一个上报QS的,且检查到没有blocked task,故说明当前GP已经结束,随后设置RCU_GP_FLAG_FQS唤醒gp线程结束当前的宽限期,并等待下一个gp的开启。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值