Giriş
Superluminal, CPU profilleme aracı olarak Linux versiyonu üzerinde çalışırken, bir testerımızın sistemi sık sık dondurması sorunu ile karşılaştık. Sorunun çözümü, Linux kernel'inin derinliklerine inmemizi sağladı. Bu süreçte eBPF (Extended Berkeley Packet Filter) ve kernel spinlock mekanizmalarının karmaşıklığını öğrenme fırsatımız oldu.
Sorunun Başlangıcı
Fedora 42 işletim sistemi (kernel 6.17.4-200) kullanan testerımız, Superluminal ile analiz yaparken sistemin periyodik olarak dondurulmasıyla karşılaştı. İlk olarak, sanal makinelerde sorunu tekrar üretmeye çalıştık ancak başarısız olduk. Fiziksel bir makineye geçtiğimizde ise sorunu tekrarlayabildik.
İlk İnceleme
Superluminal ile yapılan analizde, her bir iş parçacığının zaman çizelgesinde şüpheli alanlar tespit edildi. Bu alanlar, iş parçacıklarının aynı süre boyunca meşgul olduğunu gösteriyordu, bu da beklenen iş yüküyle uyuşmuyordu. dmesg çıktısında ise NMI (Non-Maskable Interrupt) yöneticisinin uzun süre çalıştığını gösteren mesajlar yer aldı. Bu durum, kernel içinde bir şeylerin 250 milisaniye kadar sürdüğünü gösteriyor, fakat nedenini anlamak için daha fazla bilgiye ihtiyaç vardı.
Kernel Debugging
Kernel donmalarıyla karşılaştığımızda, normal bir uygulama donmasında olduğu gibi basit bir debugger ile durumu incelemek mümkün olmuyor. Kernel debugging için özel bir makineye ve serial porta ihtiyaç duyuluyor. Eski makinelerde bulunan COM portları modern donanımlarda yer almıyor. Bu nedenle, PCIe kartları alarak bu bağlantıyı sağladık. Ancak, kernel donduğunda debugger'ımız da yanıt vermiyordu. Bu durum, hata ayıklamayı zorlaştırıyordu.
Minimal Reproduksiyon Bulma
Sorunun kaynağını bulmak için, eBPF kodumuzu gözden geçirmeye karar verdik. eBPF kodu 2000 satırdan oluşuyordu ve sorun bu alanda olabileceğini düşündük. Farklı olay türlerini tek tek devre dışı bırakarak denemeler yaptık. Sonuçlar şöyleydi:
Yalnızca örnekleme olayları etkin olduğunda donmalar yaşanmadı.
Yalnızca bağlam değişimi/wake olayları etkin olduğunda donmalar yaşanmadı.
Her iki olay türü etkin olduğunda donmalar tekrar meydana geldi.
Bu durum, eBPF kodunun örnekleme ve bağlam değişimi olayları arasında bir etkileşim yaşadığını gösteriyor. Örnekleme frekansını düşürdüğümüzde donmaların sıklığı azalmıştı. Sonunda, donmaları durdurmayı başardığımız minimal eBPF reproduksiyon kodunu bulduk:
struct {
__uint(type, BPF_MAP_TYPE_RINGBUF);
__uint(max_entries, 512 * 1024 * 1024);
} ringBuffer SEC(".maps");
SEC("tp_btf/sched_switch")
int cswitch(struct bpf_raw_tracepoint_args* inContext) {
struct CSwitchEvent* event = bpf_ringbuf_reserve(&ringBuffer, sizeof(struct CSwitchEvent), 0);
if (event == NULL) return 1;
bpf_ringbuf_discard(event, 0);
return 0;
}
SEC("perf_event")
int sample(struct bpf_perf_event_data* inContext) {
struct SampleEvent* event = bpf_ringbuf_reserve(&ringBuffer, sizeof(struct SampleEvent), 0);
if (event == NULL) return 1;
bpf_ringbuf_discard(event, 0);
return 0;
}
Bu kod, bir bağlam değişimi ve bir örnekleme kesintisi olduğunda çalışan iki eBPF programını tanımlıyor. Bu programlar, bir ring buffer'da alan ayırmaktan başka bir iş yapmıyor. Bu sorunun çözüm süreci, Linux kernel'inin karmaşıklığında derinleşmemizi sağladı.




