Linuxのthis_cpu_cmpxchg_double()によるpercpuなデータの交換

Linux kernelでpercpuなデータの入れ替えに使用されるthis_cpu_cmpxchg_double()のメモ。見ているカーネルは3.18。

これはマクロでlinux/include/percpu-defs.hにて下記のように定義されています。

507 #define this_cpu_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2) \
508         __pcpu_double_call_return_bool(this_cpu_cmpxchg_double_, pcp1, pcp2, oval1, oval2, nval1, nval2)

__pcpu_double_call_return_boolマクロも同じファイルにて定義されていて、このような形。

343 #define __pcpu_double_call_return_bool(stem, pcp1, pcp2, ...)           \
344 ({                                                                      \
345         bool pdcrb_ret__;                                               \
346         __verify_pcpu_ptr(&(pcp1));                                     \
347         BUILD_BUG_ON(sizeof(pcp1) != sizeof(pcp2));                     \
348         VM_BUG_ON((unsigned long)(&(pcp1)) % (2 * sizeof(pcp1)));       \
349         VM_BUG_ON((unsigned long)(&(pcp2)) !=                           \
350                   (unsigned long)(&(pcp1)) + sizeof(pcp1));             \
351         switch(sizeof(pcp1)) {                                          \
352         case 1: pdcrb_ret__ = stem##1(pcp1, pcp2, __VA_ARGS__); break;  \
353         case 2: pdcrb_ret__ = stem##2(pcp1, pcp2, __VA_ARGS__); break;  \
354         case 4: pdcrb_ret__ = stem##4(pcp1, pcp2, __VA_ARGS__); break;  \
355         case 8: pdcrb_ret__ = stem##8(pcp1, pcp2, __VA_ARGS__); break;  \
356         default:                                                        \
357                 __bad_size_call_parameter(); break;                     \
358         }                                                               \
359         pdcrb_ret__;                                                    \
360 })
361 

stem##1というような形でサイズごとに呼び出し先が変わる。this_cpu_cmpxchg_doubleマクロの場合はstemにthis_cpu_cmpxchg_double_がつくのでthis_cpu_cmpxchg_double_1とかになります。 これらがどこで定義されているかというとinclude/asm-generic/percpu.hのこの部分ですね。

403 #ifndef this_cpu_cmpxchg_double_1
404 #define this_cpu_cmpxchg_double_1(pcp1, pcp2, oval1, oval2, nval1, nval2) \
405         this_cpu_generic_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2)
406 #endif
407 #ifndef this_cpu_cmpxchg_double_2
408 #define this_cpu_cmpxchg_double_2(pcp1, pcp2, oval1, oval2, nval1, nval2) \
409         this_cpu_generic_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2)
410 #endif
411 #ifndef this_cpu_cmpxchg_double_4
412 #define this_cpu_cmpxchg_double_4(pcp1, pcp2, oval1, oval2, nval1, nval2) \
413         this_cpu_generic_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2)
414 #endif
415 #ifndef this_cpu_cmpxchg_double_8
416 #define this_cpu_cmpxchg_double_8(pcp1, pcp2, oval1, oval2, nval1, nval2) \
417         this_cpu_generic_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2)
418 #endif
419 

相変わらずマクロを辿っていく必要がありますね。this_cpu_generic_cmpxchg_doubleマクロがどこにあるかというと、これも先と同じファイルのinclude/asm-generic/percpu.hでした。コードを見るとraw_cpu_generic_cmpxchg_double()の呼び出し前後で割り込みの禁止・許可が行われているのでそろそろ本題に近づいて来ましたかね。

159 #define this_cpu_generic_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2) \
160 ({                                                                      \
161         int __ret;                                                      \
162         unsigned long __flags;                                          \
163         raw_local_irq_save(__flags);                                    \
164         __ret = raw_cpu_generic_cmpxchg_double(pcp1, pcp2,              \
165                         oval1, oval2, nval1, nval2);                    \
166         raw_local_irq_restore(__flags);                                 \
167         __ret;                                                          \
168 })
169 

raw_cpu_generic_cmpxchg_doubleもマクロで処理はこうなっています。

 96 #define raw_cpu_generic_cmpxchg_double(pcp1, pcp2, oval1, oval2, nval1, nval2) \
 97 ({                                                                      \
 98         int __ret = 0;                                                  \
 99         if (raw_cpu_read(pcp1) == (oval1) &&                            \
100                          raw_cpu_read(pcp2)  == (oval2)) {              \
101                 raw_cpu_write(pcp1, nval1);                             \
102                 raw_cpu_write(pcp2, nval2);                             \
103                 __ret = 1;                                              \
104         }                                                               \
105         (__ret);                                                        \
106 })
107 

最初にpcp1(percpu areaにあるデータ)とovalを比較して同じだった場合に、nvalがpcpのほうに代入されます。この代入はraw_cpu_write()がやっていて、相変わらずマクロを追いかけていく必要があるのですが、最終的には単純にこのような感じのコードになります。

 pcp1 = nval1;

これをmm/slub.cのslab_alloc_node()で見てみます。ここでcはs->cpu_slabです。

2408         tid = c->tid;
2409         preempt_enable();
2410 
2411         object = c->freelist;
〜略〜
2433                 if (unlikely(!this_cpu_cmpxchg_double(
2434                                 s->cpu_slab->freelist, s->cpu_slab->tid,
2435                                 object, tid,
2436                                 next_object, next_tid(tid)))) {

最初にtid、objectが取得されていて、それをしばらくしてからthis_cpu_cmpxchg_double()を使って現在のデータと比較して、他所から変更されていないようなら下記のような処理をするということですね。

s->cpu_slab->freelist = next_object;
s->cpu_slab->tid = next_tid(tid);

マクロを見ていくのは面倒ですねぇ(´・ω・`)