113 DECLARE_PER_CPU(struct rcu_data, rcu_data); 114 DECLARE_PER_CPU(struct rcu_data, rcu_bh_data); 该结构在SMP定义每个CPU私有数据, #define DECLARE_PER_CPU(type, name) extern __typeof__(type) per_cpu__##name 其中__typeof__(type)是取type的类型。你上面的定义: DECLARE_PER_CPU(struct rcu_data, rcu_data); 实际上相当于: extern struct rcu_data per_cpu__rcu_data; 通过 struct rcu_head { struct rcu_head *next; void (*func)(struct rcu_head *head); }; 该结构构成每个CPU私有数据回调函数链表。具体调用过程通过call_rcu()函数实现的。定义在linux/kernel/rcupdate.c定义:
117void fastcall call_rcu(struct rcu_head *head,
118 void (*func)(struct rcu_head *rcu))
119{
120 unsigned long flags;
121 struct rcu_data *rdp;
122
123 head->func = func;
124 head->next = NULL;
125 local_irq_save(flags);
126 rdp = &__get_cpu_var(rcu_data);
127 *rdp->nxttail = head;
128 rdp->nxttail = &head->next;
129 if (unlikely(++rdp->qlen > qhimark)) {
130 rdp->blimit = INT_MAX;
131 force_quiescent_state(rdp, &rcu_ctrlblk);
132 }
133 local_irq_restore(flags);
134} |
注:首先会把回调函数注册rcu_head结构,head->func = func local_irq_save(flags);屏蔽中断,并保存当前CPU的状态。 而local_irq_restore(flags);重新开中断,并恢复到屏蔽中断前状态。 rdp = &__get_cpu_var(rcu_data);
最终会调用linux/include/asm-i386/percpu.h
70#define __raw_get_cpu_var(var) (*({ \
71 extern int simple_indentifier_##var(void); \
72 RELOC_HIDE(&per_cpu__##var, x86_read_percpu(this_cpu_off));
其中 RELOC_HIDE宏定义如下:
# define RELOC_HIDE(ptr, off) \
({ unsigned long __ptr; \
__ptr = (unsigned long) (ptr); \
(typeof(ptr)) (__ptr + (off)); }) |
作用:访问本CPU的私有数据,相当于取出CPU的私有数据,per_cpu__rcu_data。 并把回调函数注册per_cpu__rcu_data链表中。 likely和unlikely是用来编译优化的,其实都可以没有。我们知道很多cpu里面有告诉缓存,且有预读机制,likely和unlikely就是增加执行判断语句时的命中率如果是if(likely(a)),说明a条件发生的可能性大,那么a为真的语句在编译成二进制的时候就应该紧跟在前面程序的后面,这样就会被cache预读取进去,增加程序执行速度。 unlikely则是正好相反。函数 call_rcu 也由 RCU 写端调用,它不会使写者阻塞,因而可以在中断上下文或 softirq 使用,该函数将把函数 func 挂接到 RCU回调函数链上,然后立即返回。一旦所有的 CPU 都已经完成端临界区操作,该函数将被调用来释放的将绝不在被应用的数据。参数 head 用于记录回调函数 func,一般该结构会作为被 RCU 保护的数据结构的一个字段,以便省去单独为该结构分配内存的操作。使用RCU时,读执行单元必须提供一个信号给写执行单元能够确定数据可以安全地释放或修改的时机。有一个专门的垃圾收集器来探测读执行单元的信号,一旦所有的读执行单元都已经发送信号告知它们都不再使用RCU保护的数据结构,垃圾收集器就调用回调函数完成最后数据释放或修改操作。时钟中断触发垃圾收集器运行,它会检查: 1. 是否在该CPU上有需要处理的回调函数并且已经经过一个grace period(即所有读执行单元已经完成对临界区的访问); 2. 是否没有需要处理的回调函数但有注册的回调函数; 3. 是否该CPU已经完成回调函数的处理; 4. 是否该CPU正在等待一个quiescent state的到来;如果以上四个条件只要有一个满足,它就调用函数rcu_check_callbacks。函数rcu_check_callbacks首先检查该CPU是否经历了一个quiescent state. 520void rcu_check_callbacks(int cpu, int user)
521{
522 if (user ||
523 (idle_cpu(cpu) && !in_softirq() &&
524 hardirq_count() <= (1 << HARDIRQ_SHIFT))) {
525 rcu_qsctr_inc(cpu);
526 rcu_bh_qsctr_inc(cpu);
527 } else if (!in_softirq())
528 rcu_bh_qsctr_inc(cpu);
529 tasklet_schedule(&per_cpu(rcu_tasklet, cpu));
530} |
1. 当前进程运行在用户态;或2. 当前进程为idle且当前不处在运行softirq状态,也不处在运行IRQ处理函数的状态;那么,该CPU已经经历了一个quiescent state,因此通过调用函数rcu_qsctr_inc标记该CPU的数据结构rcu_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将调用tasklet_schedule,它将调度为该CPU设置的tasklet rcu_tasklet,每一个CPU都有一个对应的rcu_tasklet。在时钟中断返回后,rcu_tasklet将在softirq上下文被运行。其中call_rcu_bh()函数的功能几乎与call_rcu()完全相同,唯一的差别就是它把软中断的完成也当作经历一个quiescent state,因此如果写执行单元使用了该函数,在进程上下文的读执行单元必须使用rcu_read_lock_bh()。读者自己分析call_rcu_bh()函数.
阅读(2149) | 评论(0) | 转发(0) |