00 RCU neden var
Geleneksel okuyucu-yazıcı kilitleri, çok çekirdekli sistemlerde okuyucu sayısı arttıkça önbellek çatışması ve boş döngü maliyetiyle yavaşlar. RCU bu maliyeti sıfıra indirir.
rwlock ve seqlock'un sınırları
Klasik rwlock_t ile birden fazla okuyucu eş zamanlı okuyabilir, ancak her read_lock() çağrısı kilidin referans sayacını artırmak için paylaşılan bir atomik değişkene yazar. 64 çekirdekli bir sistemde 64 okuyucunun aynı cache line'a sürekli yazması, önbellek satırı ping-pong (false sharing) sorununa yol açar ve ölçeklenebilirliği dramatik biçimde düşürür.
CPU0 read_lock() → atomic_inc(rwlock.count) → cache line invalid eder CPU1 read_lock() → cache miss → refetch → atomic_inc → invalidate CPU2 read_lock() → cache miss → ... → O(N) maliyet, N = çekirdek sayısı
Lockless okuma motivasyonu
RCU'nun temel fikri şudur: okuyucu tarafında hiçbir paylaşılan değişkene yazmadan okuma yapılabilir. Bunun bedeli yazıcı tarafına yüklenir; yazıcı yeni bir kopya üretir, atomik pointer ataması yapar ve eski kopyanın güvenle silinebileceği noktayı bekler.
Kullanım alanları
Linux kernel'ında RCU en yaygın biçimde aşağıdaki yapılarda kullanılır:
| Alt sistem | RCU kullanımı |
|---|---|
| VFS / dcache | Dizin girişleri (dentry) okuma yolunda RCU ile korunur |
| Ağ yönlendirme | FIB (routing table) aramaları RCU okuma tarafında yapılır |
| Task listesi | for_each_process() — PID araması RCU ile kilitsiz |
| Module listesi | Modül aramaları okuma tarafında RCU korumalı |
| SELinux AVC | Erişim vektör önbelleği RCU ile hızlı aranır |
| netfilter | kural zinciri güncellemeleri RCU pointer swap ile atomik |
RCU yalnızca pointer tabanlı veri yapıları için uygundur. İntegers veya skalarları doğrudan korumak için kullanılamaz. Okuyucu kritik bölgesi uyku içeremez (CONFIG_PREEMPT_RCU olmayan kernelde).
01 RCU temel modeli
Grace period, quiescent state ve publish-subscribe idiom — RCU'nun üç temel kavramı birlikte anlaşıldığında mekanizma netleşir.
Grace period ve quiescent state
RCU, eski pointer'a erişebilecek tüm okuyucuların işini bitirmesini bekler. Bu bekleme süresine grace period denir. Bir CPU'nun RCU okuma kritik bölgesi dışında olduğu her ana quiescent state (sessiz durum) denir. Tüm CPU'lar en az bir quiescent state geçirdiğinde bir grace period tamamlanmış sayılır.
Yazıcı: yeni kopya oluştur → rcu_assign_pointer() → synchronize_rcu() bekle
|
CPU0: [rcu_read_lock ... rcu_read_unlock] ← quiescent state burada
CPU1: [rcu_read_lock ... rcu_read_unlock] ← quiescent state burada
CPU2: context switch (scheduler) ← quiescent state
|
grace period bitti → kfree(eski)
Quiescent state örnekleri
Bir CPU aşağıdaki durumlarda quiescent state'e girer:
Publish-subscribe idiom
RCU, veri yapısı güncellemelerini publish-subscribe modeli ile yapar. Yazıcı yeni veriyi yayınlar (rcu_assign_pointer), okuyucu abone olur (rcu_dereference). İkisi arasında bellek bariyeri garanti edilir.
/* Yazıcı — publish */
struct config *new_cfg = kmalloc(sizeof(*new_cfg), GFP_KERNEL);
memcpy(new_cfg, old_cfg, sizeof(*new_cfg));
new_cfg->value = 42;
rcu_assign_pointer(global_cfg, new_cfg); /* smp_store_release içerir */
/* Okuyucu — subscribe */
rcu_read_lock();
struct config *cfg = rcu_dereference(global_cfg); /* smp_load_acquire içerir */
do_something(cfg->value);
rcu_read_unlock();
Memory ordering garantisi
rcu_assign_pointer() içindeki smp_store_release(), önceki tüm yazmaların okuyucu tarafından görünmesini garanti eder. rcu_dereference() içindeki smp_load_acquire() ise pointer okunduktan sonra yapılan erişimlerin doğru sıralandığını garantiler. Bu çift bariyer olmadan okuyucu yarı-başlatılmış veri görebilir.
02 rcu_read_lock / rcu_read_unlock
Okuyucu tarafı API son derece hafiftir: çoğu konfigürasyonda tek instruction — preemption disable/enable.
API kullanımı
#include <linux/rcupdate.h>
rcu_read_lock();
/* RCU korumalı pointer'lara erişim */
struct my_data *p = rcu_dereference(global_ptr);
if (p)
use(p->field);
rcu_read_unlock();
Preemption modlarına göre davranış
| Kernel konfigürasyonu | rcu_read_lock() ne yapar | Uyku izni |
|---|---|---|
| CONFIG_PREEMPTION=n (server) | Boş makro — maliyet sıfır | Hayır |
| CONFIG_PREEMPTION=y (desktop/RT) | preempt_disable() | Hayır |
| CONFIG_PREEMPT_RCU | preempt_disable() + counter güncelleme | Hayır |
| SRCU (ayrı API) | srcu_read_lock(&srcu_struct) | Evet |
Nested okuma kritik bölgeleri
RCU okuma kritik bölgeleri iç içe geçebilir. Her rcu_read_lock() için bir rcu_read_unlock() gerekir. Preemptible RCU'da sayaç tutulur:
rcu_read_lock(); /* derinlik 1 */
p = rcu_dereference(ptr_a);
rcu_read_lock(); /* derinlik 2 — tamam */
q = rcu_dereference(ptr_b);
use(p, q);
rcu_read_unlock(); /* derinlik 2 */
rcu_read_unlock(); /* derinlik 1 */
Yasak işlemler
Okuma kritik bölgesi içinde aşağıdakiler yasaktır:
rcu_read_lock_bh / rcu_read_lock_sched
Ağ alt sisteminde BH-disabled bağlamda çalışırken rcu_read_lock_bh() kullanılır. Scheduler bağlamında rcu_read_lock_sched() tercih edilir. Bu varyantlar farklı quiescent state tanımları kullanır ve performans açısından rcu_read_lock() ile eşdeğerdir.
/* Ağ RX yolunda softirq bağlamı */
rcu_read_lock_bh();
struct neighbour *n = __ipv4_neigh_lookup_noref(dev, nexthop);
if (n)
neigh_output(n, skb, false);
rcu_read_unlock_bh();
03 rcu_assign_pointer / rcu_dereference
Pointer yayınlama ve okuma API'si — bellek bariyerlerini doğru yerleştirerek yarı-başlatılmış veriye erişimi engeller.
rcu_assign_pointer
Yazıcı tarafında kullanılır. Yeni pointer değerini önceki tüm yazmaları görünür kılarak atomik biçimde yayınlar. Derleyici ve işlemci yeniden sıralamalarını engeller.
/* Tanım (basitleştirilmiş) */
#define rcu_assign_pointer(p, v) \
do { \
smp_store_release(&(p), \
RCU_INITIALIZER(v)); \
} while (0)
/* Kullanım */
struct node *new_node = kzalloc(sizeof(*new_node), GFP_KERNEL);
new_node->data = 100;
rcu_assign_pointer(list_head, new_node);
rcu_dereference
Okuyucu tarafında rcu_read_lock() ile korunan bölgede kullanılır. Alpha mimarisinde smp_read_barrier_depends(), diğer mimarilerde READ_ONCE() eşdeğeridir. Derleyicinin pointer bağımlı yüklemeleri optimize etmesini engeller.
rcu_read_lock();
struct node *p = rcu_dereference(list_head);
/* p artık güvenli — grace period boyunca geçerli */
if (p != NULL) {
int val = p->data; /* Bu erişim korumalı */
pr_info("value: %d\n", val);
}
rcu_read_unlock();
/* p burada artık güvenli DEĞİL — erişim yapılamaz */
rcu_dereference_protected
Yazıcı tarafında, kilit tutulurken yapılan erişimlerde kullanılır. Sparse aracının RCU kontrollerini atlatan bir varyantıdır:
spin_lock(&update_lock);
struct node *p = rcu_dereference_protected(list_head,
lockdep_is_held(&update_lock));
/* Güvenle değişiklik yap */
p->data = new_value;
spin_unlock(&update_lock);
rcu_access_pointer
Pointer'ın NULL olup olmadığını kontrol etmek için rcu_read_lock() dışında kullanılabilir. Yalnızca NULL kontrolü yapılacaksa rcu_dereference() gerekmez:
/* rcu_read_lock gerekmez — sadece NULL kontrolü */
if (rcu_access_pointer(global_ptr) == NULL)
return -ENODEV;
Sparse ile statik analiz
__rcu tip niteleyicisi sparse aracına RCU kullanımlarını kontrol ettirir. Yanlış bağlamda rcu_dereference() çağrılmadığında sparse uyarı verir:
struct config __rcu *global_cfg; /* __rcu etiketi */
/* sparse: rcu_read_lock() olmadan erişim — uyarı */
struct config *p = global_cfg; /* YANLIŞ */
struct config *p = rcu_dereference(global_cfg); /* DOĞRU */
04 synchronize_rcu / call_rcu / kfree_rcu
Yazıcı, yeni pointer'ı yayınladıktan sonra eski veriyi ancak tüm okuyucuların işini bitirdiğinden emin olarak silebilir. Bu üç API farklı bağlamlara hitap eder.
synchronize_rcu — bloklu bekleme
Çağrıyı yapan thread, mevcut grace period tamamlanana kadar uyur. Process bağlamında, yüksek öncelikli kodda kullanılamaz.
struct config *old_cfg;
spin_lock(&cfg_lock);
old_cfg = rcu_dereference_protected(global_cfg,
lockdep_is_held(&cfg_lock));
rcu_assign_pointer(global_cfg, new_cfg);
spin_unlock(&cfg_lock);
synchronize_rcu(); /* tüm okuyucular bitene kadar blokla */
kfree(old_cfg); /* artık güvenli */
call_rcu — asenkron geri çağrım
Grace period beklemek yerine bir callback kaydeder. Softirq bağlamında, interrupt handler'dan çağrılabilir. Grace period dolduğunda callback çalışır:
struct my_obj {
int data;
struct rcu_head rcu; /* callback için head alanı */
};
static void my_obj_reclaim(struct rcu_head *head)
{
struct my_obj *obj = container_of(head, struct my_obj, rcu);
kfree(obj);
}
/* Yazıcı — asenkron silme */
rcu_assign_pointer(global_obj, new_obj);
call_rcu(&old_obj->rcu, my_obj_reclaim);
kfree_rcu — basitleştirilmiş silme
Yalnızca kfree() yapılacaksa callback yazmak gerekmez. kfree_rcu() bunu otomatik halleder. Linux 5.12'den itibaren rcu alanı olmadan da kullanılabilir:
struct my_obj {
int data;
struct rcu_head rcu;
};
/* Eski yol — rcu_head gerektirir */
kfree_rcu(old_obj, rcu);
/* Linux 5.12+ — rcu_head gerektirmez */
kfree_rcu(old_obj);
synchronize_rcu_expedited
Normal synchronize_rcu()'dan daha hızlı tamamlanır — IPI (inter-processor interrupt) göndererek CPU'ları aktif olarak quiescent state'e zorlar. Sistem yükünü artırdığı için sadece test ve hata ayıklama için önerilir:
/* Hızlı ama pahalı — sadece non-production kullanım */
synchronize_rcu_expedited();
Bellek modeli özeti
Yazıcı: [1] new = alloc() + init [2] rcu_assign_pointer(ptr, new) ← smp_store_release [3] synchronize_rcu() ← tüm CPU'lar quiescent [4] kfree(old) ← güvenli Okuyucu (grace period öncesi başlar): rcu_read_lock() p = rcu_dereference(ptr) ← smp_load_acquire use(p) ← [3] bitmeden grace period aktif rcu_read_unlock() ← quiescent state başladı
05 SRCU — Sleepable RCU
Standart RCU okuma kritik bölgesinde uyku yasakken, SRCU (Sleepable RCU) okuyucuların uyumasına izin verir. Bu esnekliğin bedeli daha ağır başlatma ve per-srcu-struct grace period maliyetidir.
SRCU ne zaman gerekir
init_srcu_struct ve DEFINE_SRCU
#include <linux/srcu.h>
/* Statik tanım */
DEFINE_SRCU(my_srcu);
/* Dinamik tanım */
struct srcu_struct my_srcu;
int ret = init_srcu_struct(&my_srcu);
if (ret)
return ret;
/* Temizlik */
cleanup_srcu_struct(&my_srcu);
SRCU okuyucu API
int idx;
idx = srcu_read_lock(&my_srcu); /* integer döner — indeks */
/* Bu bölgede uyku mümkün */
struct data *p = srcu_dereference(global_data, &my_srcu);
if (p) {
mutex_lock(&p->lock); /* uyku — SRCU'da izinli */
process(p);
mutex_unlock(&p->lock);
}
srcu_read_unlock(&my_srcu, idx); /* indeks geri verilir */
synchronize_srcu ve call_srcu
/* Bloklu — bu srcu_struct için tüm okuyucuları bekle */
synchronize_srcu(&my_srcu);
/* Asenkron callback */
call_srcu(&my_srcu, &old_obj->rcu, my_reclaim);
/* Hızlı (expedited) varyant */
synchronize_srcu_expedited(&my_srcu);
SRCU ile standart RCU farkı
| Özellik | RCU | SRCU |
|---|---|---|
| Okuyucu uyku | Yasak | İzinli |
| Okuyucu maliyeti | ~0 (preempt_disable) | Per-CPU sayaç güncelleme |
| Grace period | Global | Per-srcu_struct |
| Birden fazla domain | Yok | Her srcu_struct bağımsız |
| init gerektirme | Statik, init yok | init_srcu_struct() gerekli |
| Kullanım alanı | Genel amaç | Uyuyan okuyucu senaryoları |
SRCU iç mekanizması
SRCU iki adet per-CPU sayaç çifti tutar (A ve B). Okuyucular aktif çifte kaydolur. synchronize_srcu() çağrıldığında geçiş yapılır ve eski çiftteki sayaçların sıfırlanması beklenir. Bu mekanizma preemption disable gerektirmez çünkü CPU'ların quiescent state'ini beklemez; doğrudan okuyucu sayısını takip eder.
06 RCU-protected linked list
Linux kernel, RCU ile korunan bağlı listeler için özel API sağlar. Bu API standart list API'sine paralel bir arayüz sunar.
Temel API
Tam örnek — global cihaz listesi
#include <linux/list.h>
#include <linux/rcupdate.h>
#include <linux/spinlock.h>
struct device_entry {
int id;
char name[32];
struct list_head node;
struct rcu_head rcu;
};
static LIST_HEAD(device_list);
static DEFINE_SPINLOCK(device_lock);
/* Okuyucu — kilit yok, sadece rcu_read_lock */
struct device_entry *find_device(int id)
{
struct device_entry *entry;
rcu_read_lock();
list_for_each_entry_rcu(entry, &device_list, node) {
if (entry->id == id) {
rcu_read_unlock();
return entry; /* Dikkat: caller grace period bitince erişmemeli */
}
}
rcu_read_unlock();
return NULL;
}
/* Yazıcı — spinlock + rcu_assign */
int add_device(int id, const char *name)
{
struct device_entry *entry = kzalloc(sizeof(*entry), GFP_KERNEL);
if (!entry)
return -ENOMEM;
entry->id = id;
strscpy(entry->name, name, sizeof(entry->name));
INIT_LIST_HEAD(&entry->node);
spin_lock(&device_lock);
list_add_rcu(&entry->node, &device_list);
spin_unlock(&device_lock);
return 0;
}
/* Yazıcı — silme */
static void device_free_rcu(struct rcu_head *head)
{
struct device_entry *entry =
container_of(head, struct device_entry, rcu);
kfree(entry);
}
void remove_device(int id)
{
struct device_entry *entry;
spin_lock(&device_lock);
list_for_each_entry(entry, &device_list, node) {
if (entry->id == id) {
list_del_rcu(&entry->node);
spin_unlock(&device_lock);
call_rcu(&entry->rcu, device_free_rcu);
return;
}
}
spin_unlock(&device_lock);
}
hlist_for_each_entry_rcu
Hash table yapıları için hlist varyantları da mevcuttur. Kernel'ın pid hash table'ı bu yapıyı kullanır:
struct hlist_head hash[256];
/* Ekleme */
spin_lock(&hash_lock);
hlist_add_head_rcu(&obj->hnode, &hash[h]);
spin_unlock(&hash_lock);
/* Arama */
rcu_read_lock();
hlist_for_each_entry_rcu(obj, &hash[h], hnode) {
if (obj->key == key)
break;
}
rcu_read_unlock();
list_for_each_entry_rcu ile concurrent güncelleme
Okuyucu iterate ederken yazıcı eleman ekleyebilir/silebilir. list_for_each_entry_rcu() makrosu READ_ONCE() ile her adımda pointer okur, böylece yeni eklenen elemanları görebilir ancak tutarsız pointer görmez.
07 RCU vs diğer mekanizmalar
RCU her senaryoya uygun değildir. Spinlock, seqlock ve hazard pointer ile karşılaştırmalı analiz doğru aracı seçmeye yardımcı olur.
Kapsamlı karşılaştırma tablosu
| Özellik | spinlock | rwlock | seqlock | RCU | Hazard ptr |
|---|---|---|---|---|---|
| Okuyucu maliyeti | Spin (yüksek) | Atomic inc/dec | 2x okuma (retry) | ~Sıfır | Store + fence |
| Yazıcı maliyeti | Spin | Exclusive lock | Seqcount güncelle | Kopya + GP bekleme | Spin + tarama |
| Okuyucu ölçeği | Kötü | Orta | İyi | Mükemmel | İyi |
| Okuyucu uyku | Hayır | Hayır | Hayır | Hayır (SRCU: Evet) | Hayır |
| Bellek overhead | Minimal | Minimal | Minimal | Eski kopya grace period süresince | Per-thread hazard ptr |
| Veri büyüklüğü | Her büyüklük | Her büyüklük | Küçük/skalar | Pointer tabanlı | Pointer tabanlı |
| Yazıcı sayısı | Tek | Tek | Tek/çoklu | Çoklu (kilit ile) | Çoklu |
| Kullanım alanı | Kısa kritik bölge | Çok okuyucu | Küçük veri, çok okuma | Çok okuyucu, nadir yazma | Lock-free yapılar |
seqlock ile RCU karşılaştırması
seqlock, okuyucunun tutarsız veri okumasını tespit edip yeniden denemesine dayanır. Küçük, hızlı okunabilen veri (zaman damgaları, sayaçlar) için idealdir. RCU ise büyük veri yapıları için uygundur çünkü okuyucu retry gerekmez.
/* seqlock okuyucu */
unsigned int seq;
do {
seq = read_seqbegin(&my_seqlock);
memcpy(&local_copy, &shared_data, sizeof(local_copy));
} while (read_seqretry(&my_seqlock, seq));
/* RCU okuyucu — retry yok */
rcu_read_lock();
p = rcu_dereference(global_ptr);
memcpy(&local_copy, p, sizeof(*p));
rcu_read_unlock();
Hangi durumda ne kullanmalı
Overhead ölçümü
Bir 64 çekirdekli sunucuda 100 ns periyotla yapılan ölçümlerde RCU okuyucu maliyeti spinlock'tan ~50x daha düşük çıkar. Yazıcı tarafında synchronize_rcu() birkaç ms sürebilir — bu süre sistemdeki en uzun scheduler tick periyoduna bağlıdır.
08 Hata ayıklama
RCU hataları — grace period ihlali, use-after-free, okuma kritik bölgesinde uyku — sessiz veri bozulmasına yol açar. Kernel, bu hataları yakalamak için çeşitli araçlar sunar.
CONFIG_PROVE_RCU ve lockdep
Lockdep'in RCU uzantısı, yanlış bağlamda rcu_dereference() kullanımını, rcu_read_lock() içinde synchronize_rcu() çağrısını ve __rcu işaretli pointer'lara korumasız erişimi tespit eder:
CONFIG_DEBUG_LOCK_ALLOC=y
CONFIG_PROVE_LOCKING=y
CONFIG_PROVE_RCU=y
CONFIG_RCU_TRACE=y
Bu konfigürasyonlarla kernel, illegal RCU kullanımını stack trace ile birlikte loglar:
[ 1234.567890] WARNING: suspicious RCU usage
[ 1234.567891] kernel/sched/core.c:1234
[ 1234.567892] rcu_dereference() called without rcu_read_lock()
[ 1234.567893] Call Trace:
[ 1234.567894] dump_stack+0x48
[ 1234.567895] lockdep_rcu_suspicious+0x12c
[ 1234.567896] my_driver_read+0x34
rcutorture — stres testi modülü
rcutorture kernel modülü RCU implementasyonunu sistematik biçimde test eder. Writer ve reader thread'leri yoğun şekilde çalıştırarak grace period ve memory ordering'i doğrular:
# rcutorture modülünü yükle
modprobe rcutorture torture_type=rcu nreaders=8 nfakewriters=4 \
stutter=5 irqreader=1 stat_interval=60
# Test sonuçlarını izle
dmesg | grep -i torture
# Temizle
echo 0 > /sys/module/rcutorture/parameters/shutdown_secs
RCU stall — CPU askıda kalması
Bir CPU uzun süre quiescent state'e girmezse (varsayılan 21 saniye) kernel RCU stall mesajı basar. Bu genellikle softirq, NMI veya interrupt handler içinde uzun döngü anlamına gelir:
[ 5678.901234] INFO: rcu_sched detected stalls on CPUs/tasks: { 3 } (detected by 0, 21016 jiffies)
[ 5678.901235] Sending NMI from CPU 0 to CPUs 3:
[ 5678.901236] NMI backtrace for cpu 3
[ 5678.901237] Call Trace:
[ 5678.901238] my_broken_handler+0x8c <-- sonsuz döngü burada
# Stall timeout ayarı (saniye)
echo 60 > /sys/module/rcupdate/parameters/rcu_cpu_stall_timeout
RCU istatistikleri — debugfs
# RCU state bilgisi
cat /sys/kernel/debug/rcu/rcu_sched/rcudata
# Grace period sayacı
cat /sys/kernel/debug/rcu/rcu_sched/rcu_gp
# SRCU istatistikleri
cat /sys/kernel/debug/rcu/rcu_srcu
Yaygın hatalar ve çözümleri
| Hata | Belirti | Çözüm |
|---|---|---|
| rcu_read_unlock() eksik | RCU stall, grace period hiç bitmez | Her lock/unlock çiftini eşle, PROVE_RCU kullan |
| Grace period beklemeden kfree | Use-after-free, KASAN uyarısı | synchronize_rcu() veya kfree_rcu() kullan |
| Kritik bölgede uyku | Kernel panic (might_sleep kontrolü) | SRCU'ya geç veya uyku kodu dışarı taşı |
| rcu_dereference() dışarıda | Alpha'da veri bozulması, PROVE_RCU uyarısı | Her erişim rcu_read_lock() içinde olmalı |
| call_rcu callback'te kilit | Deadlock — softirq bağlamında mutex yasak | Callback'te yalnızca spinlock kullan |
KCSAN ile veri yarışı tespiti
Kernel Concurrency Sanitizer (KCSAN), RCU olmadan yapılan eş zamanlı bellek erişimlerini tespit eder. CONFIG_KCSAN=y ile derlenen kernelde veri yarışları raporlanır:
CONFIG_KCSAN=y
CONFIG_KCSAN_REPORT_ONCE_IN_MS=3000
# Çıktı örneği:
# KCSAN: data-race in my_func / my_func
# write to 0xffff... by task 123:
# read to 0xffff... by task 456:
Geliştirme sırasında CONFIG_PROVE_RCU, CONFIG_DEBUG_LOCK_ALLOC ve CONFIG_KCSAN birlikte etkinleştirin. Bu üç araç birlikte RCU kaynaklı hataların büyük çoğunluğunu derleme veya ilk çalışma anında yakalar.