Bölüm 14: İşletim Sistemi Tasarımı
Giriş
Öğrenme Çıktıları
İşletim sistemi tasarımını zorlaştıran etkenleri ve bir işletim sisteminin dört temel hedefini açıklamak.
Basitlik, bütünlük ve verimlilik ilkelerini; yürütme ve veri paradigmalarını sistem çağrısı örnekleriyle tartışmak.
Yapı seçeneklerini yalıtım–performans ödünleşimiyle değerlendirmek; katman sırasını, güvenli genişletmeyi ve çekirdek iş parçacıklarını tasarım kararı olarak tartışmak.
Mekanizma/politika ayrımını, dikliği, iki düzeyli adlandırmayı, bağlama zamanını ve statik/dinamik yapıları uygulamak.
Dolaylılık, yeniden girişlilik, uzay-zaman ödünleşimi, önbellek, ipucu ve yaygın durumu hızlandırma tekniklerini örneklerle kullanmak.
Brooks yasasını yorumlamak; çekirdek test ve hata ayıklama araçlarını seçmek; sanallaştırma, çok çekirdek, Rust, eBPF ve unikernel eğilimlerini değerlendirmek.
Giriş
İşletim Sistemi Tasarlamak Neden Zor?
Linux ≈ 40 milyon satır; kimse bütünü kavrayamaz.
Aygıtlar, kesmeler, iş parçacıkları aynı anda: yarış, kilitlenme.
Bazıları sistemi bozmak, veri çalmak ister; her girdi şüpheli.
Kullanıcılar ayrılmalı, ama seçtikleriyle bilgi paylaşabilmeli.
20-30 yıl yaşar; gelecekteki donanım ve uygulamalar öngörülmeli.
Nasıl kullanılacağı bilinmez: telefondan süper bilgisayara.
Farklı işlemci ve platformlar: x86-64, ARM64, RISC-V…
Eski programlar çalışmalı; yeni fikirler eski kısıtlarla sınırlanır.
Tanenbaum’ın sekiz etkeni. Uzun bakım-onarım ihtiyacı 5. ve 8. maddelerin doğal sonucudur.
Giriş · Adım adım
İşletim Sisteminin Hedefleri
1. Soyutlamalar tanımlamak (abstractions): süreç, adres alanı, dosya, soket gibi kavramlar donanımın karmaşıklığını gizler. Tasarımın en zor kısmı doğru soyutlamayı bulmaktır.
2. İlkel işlemler sağlamak (primitives): bu soyutlamalar üzerinde çalışan işlemler, yani sistem çağrıları. Örn. dosya için open/read/write/close.
3. Yalıtım sağlamak (isolation): kullanıcılar ve süreçler birbirine karışamamalı; ama denetimli paylaşım (dosya izinleri, paylaşımlı bellek) mümkün olmalı.
4. Donanımı yönetmek (hardware): kesmeler, zamanlayıcılar, MMU, aygıt denetleyicileri; düşük seviyeli işler çoğunlukla bu katmanda yapılır.
Arayüz Tasarımı
Arayüz Tasarımı İçin Yol Gösterici İlkeler
“Mükemmelliğe, eklenecek bir şey kalmadığında değil, çıkarılacak bir şey kalmadığında ulaşılır.” (A. de Saint-Exupéry). Az sayıda, iyi düşünülmüş kavram öğrenmesi, gerçekleştirmesi ve doğrulaması kolaydır.
“Her şey olabildiğince basit olmalı, ama daha basit değil.” (Einstein’a atfedilir). Arayüz, kullanıcıların ihtiyaç duyduğu her şeyi yapabilmelidir. Az mekanizma ile çok iş: MINIX 3 çekirdeği iletişim için yalnızca birkaç ilkel (send, receive, sendrec, notify) sunar.
Bir özellik ya da sistem çağrısı verimli gerçekleştirilemiyorsa muhtemelen sahip olmaya değmez. Programcı bir çağrının yaklaşık maliyetini sezebilmelidir: lseek ucuz, read pahalı olabilir.
Arayüz Tasarımı
“Küçük Güzeldir”
- Küçük güzeldir: az sayıda genel çağrı, çok sayıda özel çağrıdan iyidir.
- seL4’ün çekirdeği yaklaşık 10 bin satırdır ve biçimsel olarak doğrulanmıştır; milyonlarca satırlık bir çekirdekte bu mümkün değil.
- Linux’ta çağrı sayısı her yıl birkaç tane artar (ör.
io_uring_*,landlock_*,mseal). - Windows’ta belgelenmiş arayüz Win32 API’dir; çekirdek çağrıları sürümden sürüme değişir.
Arayüz Tasarımı
Kullanıcı Arayüzü Paradigmaları
Kabuk, metin komutları, borular. Otomasyon ve uzaktan yönetim için hâlâ vazgeçilmez.
Pencere, simge, menü, işaretçi (Windows, Icons, Menus, Pointer). Fare tuşlarının anlamı her uygulamada aynı olmalı.
Hareketler (kaydır, yakınlaştır), tam ekran uygulamalar, bildirim merkezi.
Sesli asistanlar ve doğal dil arayüzleri; işletim sistemi hizmetlerine yeni bir giriş noktası.
Arayüz Tasarımı
Yürütme Paradigmaları
(a) Algoritmik kod
int main(void) { int x, y; init(); do_something(); read(fd, &x, sizeof x); do_something_else(); write(fd, &y, sizeof y); keep_going(); exit(0); }
Program akışı program belirler; işletim sistemine hizmet için çağrı yapar. Derleyici, sıralayıcı, toplu işler.
(b) Olay güdümlü kod (event-driven)
int main(void) { msg_t msg; init(); while (get_message(&msg)) { switch (msg.type) { case 1: ... ; break; case 2: ... ; break; case 3: ... ; break; } } }
Akışı dış olaylar (tuş, tıklama, paket, zamanlayıcı) belirler. GUI’ler, sunucular, mikro çekirdek sunucuları.
Yürütme paradigmasının seçimi, sistem çağrılarını da biçimlendirir: algoritmik model bloke eden (blocking) read ister; olay güdümlü model ise “sıradaki olayı ver” türünden bir çağrı (get_message, epoll_wait, io_uring_enter) ister.
Arayüz Tasarımı · Kodu adım adım çalıştır
Olay Döngüsü Nasıl Çalışır?
int main(void) { msg_t m; init(); while (get_message(&m)) { switch (m.type) { case KEY: on_key(&m); break; case CLICK: on_click(&m); break; case TIMER: on_timer(&m); break; } } return 0; }
Program başlar, init() pencereyi ve kaynakları hazırlar. Kuyruk boş.
Kullanıcı tıklar, bir tuşa basar, zamanlayıcı dolar. Olayları işletim sistemi/pencere sistemi toplar ve uygulamanın kuyruğuna ekler.
get_message sıranın başındaki CLICK’i döndürür; switch onu on_click’e yönlendirir.
İşleyici kısa sürede biter ve döngü başa döner. Sıradaki olay KEY: on_key çalışır.
TIMER olayı on_timer’a gider (ör. imleci yakıp söndürmek). Uzun süren bir işleyici tüm arayüzü dondurur; uzun işler ayrı iş parçacığına verilir.
Kuyruk boş: get_message engellenir, süreç uyur ve CPU harcamaz. Yeni bir olay onu uyandırır.
Kullanıcı pencereyi kapatır: kuyruğa QUIT olayı gelir.
QUIT için get_message 0 döndürür, döngü biter, program sonlanır. Aynı kalıp nginx/Node.js (epoll) ve mikro çekirdek sunucularında da kullanılır.
Arayüz Tasarımı
Veri Paradigmaları
- Ortak bir veri paradigması, aynı aracın (
cat,dd, yönlendirme) her kaynakta çalışmasını sağlar:cat /proc/loadavg. - Bedeli: bazı aygıtlar dosya modeline sığmaz; o zaman her şey
ioctlgibi “kaçış kapılarına” yığılır.
Arayüz Tasarımı
Sistem Çağrısı Arayüzü
- Ayrım: çağrı az ve genel, kolaylık fonksiyonları kütüphanede. Yeni bir kolaylık eklemek çekirdeği değiştirmez.
- Bağımsız bileşenler: UNIX süreç oluşturmayı ikiye böler:
fork+execve. Arada yönlendirme,chdirvb. yapılabilir.
- Windows’ta
CreateProcesstek çağrıda 10 parametre alır: güçlü ama karmaşık. - Modern eğilim:
posix_spawn(fork maliyetini önler) ve toplu çağrı arayüzleri (io_uring: tek geçişte çok sayıda istek).
Sistem Yapısı
Sistem Yapısı Seçenekleri
| Tasarım kararı | Kazanılan | Ödenen | Örnek |
|---|---|---|---|
| Her şeyi tek adres alanında doğrudan çağırmak | Hız: tek tuzak, doğrudan çağrılar | Hata yalıtımı yok: bir sürücü hatası sistemi düşürür | Linux, FreeBSD |
| Bağımlılığı tek yöne sınırlamak (katmanlar) | Katman katman test; değişiklik yerel kalır | Katman sırasını seçmek; her geçişin maliyeti | THE, MULTICS |
| Hizmetleri ayrı adres alanlarına koymak | Çöken sunucu yeniden başlatılır; küçük, doğrulanabilir çekirdek | Her istekte IPC, bağlam değiştirme, kopyalama | MINIX 3, QNX, seL4, Zircon |
| Korumayı yönetimden ayırmak | Uygulamaya özel soyutlama (libOS) | Uygulamalar arası paylaşım zor; libOS karmaşıklığı | Aegis/ExOS, unikernel’ler |
| Çalışırken kod yüklemek | Yeniden derlemeden uyarlama | Eklentinin güvenliği: doğrulayıcı ya da güvenli dil gerekir | SPIN, Linux eBPF |
| Saf modelden ödün vermek | Mikro çekirdek düzeni, monolitik hız | Yalıtımın bir kısmı kaybolur | Windows NT, macOS XNU |
Sistem Yapısı · Adım adım
Katmanlı Sistemler: Yedi Katmanlı Bir Tasarım
Tanenbaum’ın önerdiği yedi katmanlı tasarım. 1. katman donanım farklılıklarını gizler; üst katmanlar hep alttakinin sunduğu soyutlamayla çalışır.
Uygulama read çağırır; tuzak 7. katmana gelir. Parametreler (fd geçerli mi, tampon kullanıcı alanında mı?) burada denetlenir.
Dosya sistemi (ext4) dosya ofsetini disk bloğuna çevirir. Blok önbellekte yoksa alt katmanlardan istenir.
Sanal bellek, verinin konacağı sayfa önbelleği çerçevesini ayırır ve kullanıcı tamponunun eşlemesini doğrular.
NVMe sürücüsü okuma komutunu aygıtın kuyruğuna yazar.
Çağıran iş parçacığı G/Ç bitene kadar engellenir; çizelgeleyici başka bir iş parçacığını çalıştırır.
Aygıt bitince kesme üretir; 2. katman kesmeyi işler, uyuyan iş parçacığını hazır yapar. Sonuç katmanlar boyunca yukarı döner.
Katmanların sırası bağımlılıklardan çıkar: sanal bellek sayfaları diske yazdığı için sürücülerin, dosya sistemi sayfa çerçevesi ayırdığı için sanal belleğin üstündedir. Donanımı gizleyen 1. katman taşınabilirliği sağlar; bedeli her katman geçişinin maliyetidir.
Sistem Yapısı
Genişletilebilir Sistemler
- Çekirdek temel işlevleri sağlar; değişen ihtiyaçlar için çalışırken yeni bileşen eklenir.
- Üçüncü taraflar sisteme özellik ekleyebilir; uyumluluk ve güvenlik en büyük sorundur.
- Klasik modül tam yetkilidir: bir hata çekirdeği çökertir. SPIN (1995) eklentileri güvenli dil (Modula-3) ile sınırladı.
- Bugün Linux’ta eBPF: doğrulanmış küçük programlar; izleme, ağ, güvenlik ve 6.12’den beri çizelgeleyici (sched_ext).
Sistem Yapısı
Çekirdek İş Parçacıkları
- Çekirdek tarafından oluşturulur ve yönetilir (
kthread_create); kullanıcı adres alanları yoktur, yalnızca çekirdek belleğinde çalışırlar. - Normal iş parçacığı gibi zamanlanır, engellenebilir, uyuyabilir: kesme işleyicisinde yapılamayan uzun işler buraya ertelenir.
- Bağlam değiştirme ucuz: adres alanı (sayfa tablosu) değişmez.
- Örnekler: ertelenmiş iş (kworker), yumuşak kesmeler (ksoftirqd), sayfa geri kazanma (kswapd), günlük yazma (jbd2).
ps -efçıktısında köşeli parantez içinde görünürler:[kswapd0].
Kullanıcı süreçleri çekirdek iş parçacığı oluşturamaz; sistem çağrıları yalnızca çekirdeğin zaten çalıştırdığı iş parçacıklarına iş verebilir (ör. io_uring’in iş havuzu).
Tasarım İlkeleri · Adım adım
Mekanizma ve Politikanın Ayrılması
Çekirdek yalnızca bir mekanizma sunar: “en yüksek öncelikli hazır iş parçacığını çalıştır”. Şu an T3 (yedekleme, öncelik 8) seçiliyor.
Veritabanı süreci kendi iş parçacıklarını en iyi bilen taraftır: sorguların önemli, yedeklemenin bekleyebileceğine karar verir. Bu bir politikadır ve bir sistem çağrısıyla aktarılır.
Çekirdek kodu hiç değişmeden artık T2’yi seçer. Aynı mekanizma, farklı politikayla farklı davranış üretir.
Başka bir uygulama başka bir politika uygular. Linux 6.12’deki sched_ext bunu bir adım öteye taşır: çizelgeleme politikasının tamamı bir eBPF programı olarak yüklenebilir.
Diğer örnekler: sayfalama (mekanizma: MMU, çerçeve tahsisi; politika: hangi sayfa çıkarılsın), CPU frekansı (mekanizma: cpufreq sürücüsü; politika: schedutil/powersave yönetici), güvenlik (mekanizma: LSM kancaları; politika: SELinux kuralları).
Tasarım İlkeleri
Diklik (Orthogonality)
- Diklik (orthogonality): ayrı kavramları bağımsız olarak birleştirebilme. Birini değiştirmek diğerini etkilemez.
- Linux
clone: neyin paylaşılacağı ayrı ayrı bayraklarla seçilir. Süreç ve iş parçacığı aynı mekanizmanın iki ucudur; konteynerler deCLONE_NEW*bayraklarıyla kurulur. - Windows: süreç bir kaynak kabıdır, iş parçacığı ise çizelgeleme birimi; iki kavram ayrıdır.
- C dilinde de örnek:
static(ömür) ile tür bağımsız seçilir.
Tasarım İlkeleri
Adlandırma: İki Düzeyli Adlar
| Harici ad | Eşleyen | Dahili ad |
|---|---|---|
/usr/ast/mbox | dizin | i-node numarası |
www.giresun.edu.tr | DNS | IP adresi → (ARP) MAC adresi |
dosya tanımlayıcısı 3 | süreç dosya tablosu | çekirdekteki struct file |
| sanal adres | sayfa tablosu | fiziksel çerçeve |
\Device\Harddisk0 | nesne yöneticisi | çekirdek nesnesi (HANDLE) |
Ad, insanların hatırlayabileceği biçimde (ASCII/Unicode dizgisi) seçilir; sistem içinde ise hızlı ve benzersiz bir sayı kullanılır. Aradaki eşleme, adı değiştirmeden nesneyi taşımaya olanak verir.
Tasarım İlkeleri · Adım adım
Ad Çözümleme Adım Adım
Çekirdek open("/usr/ast/mbox") çağrısını alır. Yol bileşenlere ayrılır; hiçbir şey önbellekte yoksa her bileşen için diskten okunmak gerekir.
Kök dizinin i-node’u (i-node 1) okunur, ardından kök dizinin verisi (blok 1). Doğrusal arama: usr → i-node 6. 2 okuma.
/usr için i-node 6 okunur, sonra /usr dizini (blok 132). ast → i-node 26. 4 okuma.
/usr/ast için i-node 26 okunur, sonra dizin bloğu 406. mbox → i-node 60. 6 okuma.
Son olarak dosyanın kendi i-node’u (60) okunur. Sadece dosyayı bulmak için 7 disk erişimi gerekti: bu yüzden önbellek şart.
Tasarım İlkeleri
Bağlama Zamanı
Ad ile nesne arasındaki ilişki önceden sabitlenir. Basit ve hızlı, ama az esnek: program yalnızca belirli adreste çalışır, kütüphane güncellenince yeniden derlenir.
İlişki mümkün olan en son anda kurulur. Daha karmaşık (ek dolaylılık, çalışma zamanı maliyeti) ama daha esnek: sayfa nereye yüklenirse yüklensin, kütüphane güncellenince program değişmez.
Bağlama zamanı, “bir ad hangi anda gerçek nesneye bağlanır?” sorusudur. İşletim sistemleri genel olarak geç bağlamayı seçer; örneğin sanal bellek, bir sayfanın fiziksel yerini her erişimde (TLB yardımıyla) yeniden çözer.
Tasarım İlkeleri · Kodu adım adım çalıştır
Statik ve Dinamik Yapılar
found = 0; for (p = &proc_table[0]; p < &proc_table[PROC_TABLE_SIZE]; p++) { if (p->proc_pid == pid) { found = 1; break; } }
Statik süreç tablosu: altı giriş, bazıları boş (pid 0). Aranan pid 42. found sıfırlanır.
İşaretçi tablonun başına konur. proc_table[0]’ın pid’i 1: eşleşmez.
p++ bir sonraki yapıya ilerler (bayta değil). 314 ≠ 42.
Boş giriş (pid 0) da tek tek taranır: doğrusal aramanın bedeli.
Eşleşme: found = 1, break ile döngüden çıkılır. En kötü durumda tüm tablo taranır: O(n). Küçük ve sabit bir tablo için bu basit kod yeterlidir.
Statik tablo: basit, ayırıcı gerektirmez, ama dolunca yeni süreç oluşturulamaz (eski UNIX’te NPROC). Dinamik yapı (bağlı liste, hash tablosu, Linux’ta xarray): esnek ve aramada O(1)’e yakın, ama bellek ayırıcı, parçalanma ve karmaşık kilitleme getirir. Ara yol: boyutu ayarlanabilir tablolar (/proc/sys/kernel/pid_max).
Tasarım İlkeleri
Yukarıdan Aşağı ve Aşağıdan Yukarı Gerçekleştirme
Önce sistem çağrısı işleyicileri yazılır, alt katmanlar koçan (stub) olarak bırakılır. Arayüz erken netleşir; ama tüm sistem bitmeden gerçekçi test zordur.
Önce donanımı gizleyen katman, kesmeler, zamanlayıcı, sonra iş parçacıkları… Her katman bittiğinde test edilir. Tanenbaum gerçekleştirmede bu yolu önerir; tasarım ise yukarıdan aşağı düşünülmelidir.
Tasarım İlkeleri
Eş Zamanlı ve Eş Zamansız İletişim
- Eş zamanlı: programlaması kolay, akış sıralı okunur; ama her istek için bir iş parçacığı bekler.
- Mikro çekirdeklerde IPC genelde eş zamanlıdır (L4, seL4): tampon gerekmez, çok hızlıdır.
- Eş zamansız: tek iş parçacığı binlerce isteği yürütebilir; ama yarış, hata yönetimi ve tamponlama zorlaşır.
- Linux
io_uring: gönderim (SQ) ve tamamlanma (CQ) halkaları, sistem çağrısı olmadan bile istek gönderilebilir.
Yararlı Teknikler
Donanımı Gizleme
CPU türüne bağlı koşullu derleme
#include "config.h" void init(void) { #if (CPU == X86_64) /* x86-64 başlatma kodu */ #endif #if (CPU == ARM64) /* ARM64 başlatma kodu */ #endif #if (CPU == RISCV64) /* RISC-V başlatma kodu */ #endif }
Sözcük uzunluğuna bağlı derleme
#include "config.h" #include <stdint.h> #if (WORD_LENGTH == 32) typedef int Register; #elif (WORD_LENGTH == 64) typedef long Register; #else /* bugün: taşınabilir */ typedef uintptr_t Register; #endif Register R0, R1, R2, R3;
arch/x86, arch/arm64, arch/riscv dizinleri. Yeni bir işlemciye taşırken yalnız bu kısım yeniden yazılır. Koşullu derleme ölçülü kullanılmalı: her yere dağılan #ifdef kodu okunmaz hâle getirir.Kodlar Tanenbaum’daki Pentium/UltraSPARC örneğinin güncellenmiş hâlidir.
Yararlı Teknikler · Adım adım
Dolaylılık (Indirection)
Kullanıcı U’nun sağındaki tuşa basar. Klavye karakter değil, tuşun konumunu bildirir.
Klavye 0x17 tarama kodunu gönderir (USB klavyelerde HID kullanım kodu; sürücü ortak bir koda çevirir). ABD eşlemesiyle bu i olur.
Yalnızca tablo Türkçe Q ile değiştirilir: aynı tuş artık ı üretir. Donanım ve uygulama değişmedi.
Türkçe F eşlemesinde aynı tuş n olur. Ek bir adım (dolaylılık) esneklik sağlar; bedeli bir tablo okuması.
“Bilgisayar bilimindeki her sorun, bir dolaylılık düzeyi daha ile çözülebilir.” (David Wheeler). İşletim sistemlerinde: sanal bellek (sayfa tablosu), dosya tanımlayıcıları, sembolik bağlar, i-node, aygıt sınıfları.
Yararlı Teknikler
Yeniden Kullanım ve Yeniden Girişlilik
Aynı kodu farklı yerlerde kullanmak: tek bir biteşlem (bitmap) modülü boş disk bloklarını, bellek çerçevelerini ve PID’leri izleyebilir. Linux’ta ortak list.h, kırmızı-siyah ağaç, xarray.
Bir kod parçası aynı anda iki kez çalışabilmelidir: biri yürütülürken kesme gelip aynı fonksiyonu çağırabilir ya da başka bir çekirdek aynı kodu çalıştırır. Kural: paylaşılan statik değişken yok, gerekiyorsa kilit.
char *strtok(char *s, const char *d) { static char *last; /* paylaşılan durum! */ ... } /* yeniden girişli sürüm: durum çağıranda */ char *strtok_r(char *s, const char *d, char **saveptr);
strtok içindeki static değişken, iki iş parçacığı ya da bir kesme işleyicisi aynı anda kullanınca bozulur.
Yararlı Teknikler · Kodu adım adım çalıştır
strtok ve Yeniden Girişlilik: Kod İzleme
char *my_strtok(char *s, char ayr) { static char *kalan; /* herkes paylaşır! */ if (s) kalan = s; if (!kalan || !*kalan) return NULL; char *bas = kalan; while (*kalan && *kalan != ayr) kalan++; if (*kalan) *kalan++ = '\0'; return bas; } /* main: my_strtok(a, ',') a = "ali,veli" */ /* kesme: my_strtok(b, ';') b = "x;y" */ /* main: my_strtok(NULL, ',') */
Ana program virgülle ayrılmış adları ayıklıyor. Fonksiyonun durumu tek bir static işaretçide: kalan, bir sonraki çağrının nereden süreceğini tutar.
İlk çağrı: kalan = a. Virgüle kadar ilerler, virgülün yerine \0 yazar, kalan “veli”nin başını gösterir. Dönüş: ali.
Tam bu anda bir kesme gelir; işleyici de aynı fonksiyonla kendi dizgisini (b = “x;y”) ayıklamaya başlar.
İşleyicinin çağrısı s = b ile static kalan’ın üzerine yazar: artık b’nin içini (“y”) gösteriyor. main’in kaldığı yer kayboldu.
main kaldığı yerden sürdürmek için NULL geçer ama kalan b’yi gösterir: dönüş y, beklenen “veli” değil. Hata nadiren ve zamanlamaya bağlı görünür.
Kök neden: çağrılar arası durum static (paylaşılan) değişkende. Yeniden girişli strtok_r durumu çağıranın verdiği işaretçide tutar; her çağıran kendi kalan’ına sahip olur.
Yararlı Teknikler
Kaba Kuvvet ve Önce Hata Denetimi
Kaba kuvvet (brute force)
- Basit ama “yavaş” görünen çözüm çoğu zaman yeterlidir: 50 girişlik bir tabloda doğrusal arama, hash tablosundan hızlı ve hatasız olabilir.
- Karmaşık algoritma ancak ölçüm gerekli gösterirse: n büyükse veya kod sıcak yoldaysa.
- Sıralama: küçük dizilerde ekleme sıralaması, büyüklerde birleştirme/öbek sıralaması.
Önce hata denetimi
Kötü: kaynak alındıktan sonra denetim
lock(&tbl_lock); buf = kmalloc(n, GFP_KERNEL); if (n > MAX || !access_ok(u, n)) { kfree(buf); unlock(&tbl_lock); return -EINVAL; }
İyi: önce denetim, sonra kaynak
if (n > MAX || !access_ok(u, n)) return -EINVAL; /* önce denetle */ buf = kmalloc(n, GFP_KERNEL); if (!buf) return -ENOMEM; lock(&tbl_lock); ...
Parametreleri kilit ve bellek almadan önce doğrulamak, hata yolundaki geri alma kodunu (ve unutulan unlock hatalarını) ortadan kaldırır.
Performans
Performans: Neden Yavaş, Neyi Hızlandırmalı?
Özellik şişkinliği: her sürüm yeni özellik ekler, eskisini çıkaramaz. Geriye uyumluluk katmanları, güvenlik önlemleri (ör. Spectre/Meltdown yamaları sistem çağrısı maliyetini artırdı), genellik uğruna eklenen dolaylılık.
Yalnızca ölçümle darboğaz olduğu gösterilen kısımlar. “İyi yeterlidir”: %5’lik kazanç için kodu iki kat karmaşıklaştırmak çoğu zaman değmez.
“Erken optimizasyon tüm kötülüklerin anasıdır.” (D. Knuth). Ölçme araçları: perf, eBPF tabanlı bpftrace, alev grafikleri.
Uzay-zaman ödünleşimi · önbellekleme · ipuçları · yerelliği kullanmak · yaygın durumu hızlandırmak.
Performans · Kodu adım adım çalıştır
Uzay-Zaman Ödünleşimi: (a) Döngüyle Bit Sayma
#define BYTE_SIZE 8 int bit_count(int byte) { int i, count = 0; for (i = 0; i < BYTE_SIZE; i++) if ((byte >> i) & 1) count++; return count; }
Örnek bayt 0xB7 (183). Döngüyle her bit tek tek sınanacak; sayaç sıfırdan başlar.
(byte >> 0) & 1 = 1: sayaç artar.
Kaydır, maskele, karşılaştır: her bit için birkaç komut ve bir dal.
Bit 2 de 1.
Bit 3 sıfır: dal alınmaz, sayaç değişmez.
Bit 4 ve 5 de 1.
Bit 6 sıfır.
Son bit 1. Döngü 8 kez döndü.
Sonuç 6. Bellek yok denecek kadar az, ama bayt başına yaklaşık 8 tur × birkaç komut. Daha hızlısı için yer harcayıp zaman kazanmak gerekir.
Performans
Uzay-Zaman Ödünleşimi: (b) Makro ve (c) Tablo
/* (b) makro: döngü yok */ #define bit_count(b) ((b&1) + ((b>>1)&1) + \ ((b>>2)&1) + ... + ((b>>7)&1)) /* (c) tablo: 256 bayt */ char bits[256] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, ...}; #define bit_count(b) (int) bits[b]
| Yöntem | Bellek | Bayt başına iş |
|---|---|---|
| (a) döngü | ~0 | 8 tur: kaydır, maskele, dallan |
| (b) makro | ~0 | ~24 komut, dal yok |
| (c) 256’lık tablo | 256 B | 1 bellek okuması |
| 16’lık tablo (yarım bayt) | 16 B | 2 okuma + toplama |
| POPCNT komutu | 0 | 1 komut (__builtin_popcount) |
Performans · Kodu adım adım çalıştır
Yarım Bayt Tablosuyla Bit Sayma: Kod İzleme
char yarim[16]; /* 16 bayt */ void tablo_kur(void) { yarim[0] = 0; for (int i = 1; i < 16; i++) yarim[i] = (i & 1) + yarim[i >> 1]; } int bit_say(unsigned char b) { return yarim[b & 0x0F] + yarim[b >> 4]; }
256 baytlık tablo yerine 16 baytlık tablo: her 4 bitlik değerin bir sayısı. 0’da hiç bit yok.
i = 1 … 3: i’nin bit sayısı = son biti + (i ≫ 1)’in bit sayısı; i ≫ 1 her zaman daha önce doldurulmuştur. Örn. yarim[3] = 1 + yarim[1] = 2.
i = 4 … 7: i’nin bit sayısı = son biti + (i ≫ 1)’in bit sayısı; i ≫ 1 her zaman daha önce doldurulmuştur. Örn. yarim[7] = 1 + yarim[3] = 3.
i = 8 … 15: i’nin bit sayısı = son biti + (i ≫ 1)’in bit sayısı; i ≫ 1 her zaman daha önce doldurulmuştur. Örn. yarim[15] = 1 + yarim[7] = 4.
bit_say(0xB7): bayt iki yarıya ayrılır: alt 7 = 0111, üst B = 1011. Döngü yok, dal yok.
İki bellek okuması ve bir toplama: 3 + 3 = 6. Döngülü sürüm aynı bayt için 8 tur dönmüştü.
bit_say(0x5C): yarim[C] + yarim[5] = 2 + 2 = 4. 16 bayt bellek, 2 okuma: 256’lık tablo ile döngü arasında bir orta nokta.
Performans
Uzay-Zaman Ödünleşimi: Renk Paleti
| 1024 × 768 görüntü | Boyut (bayt) | Oran |
|---|---|---|
| (a) 24 bit/piksel | 2.359.296 (2,25 MiB) | 1 |
| (b)+(c) 8 bit indis + palet | 786.432 + 768 = 787.200 | ≈ 1/3 |
Yer yaklaşık 3 kat azalır; bedeli her piksel gösterilirken bir tablo bakışı (zaman) ve en fazla 256 renk sınırıdır. GIF ve PNG-8 bu yöntemi kullanır (GIF ayrıca LZW ile sıkıştırır). Boyutlar Python ile hesaplanmıştır.
Performans
Önbelleğe Almak (Caching)
Yol → i-node önbelleği
| Yol | i-node |
|---|---|
/usr | 6 |
/usr/ast | 26 |
/usr/ast/mbox | 60 |
/usr/ast/books | 92 |
/usr/bal | 45 |
/usr/bal/paper.ps | 85 |
- Önbellek yokken
/usr/ast/mbox’ı bulmak: kök i-node’u, kök dizini, /usr i-node’u, /usr dizini, /usr/ast i-node’u, /usr/ast dizini → 6 disk okuması (+ dosyanın i-node’u). - Önbellekte tam yol varsa: bir bellek araması, sıfır disk erişimi.
- Önbellekte yalnızca
/usr/astvarsa, arama oradan devam eder: kısmi isabet de kazançtır. - Linux: dizin girdisi önbelleği (dcache), i-node önbelleği ve sayfa önbelleği. Boş RAM’in “dolu” görünmesinin nedeni budur.
- Önbelleğin bedeli: tutarlılık. Dosya silinir veya yeniden adlandırılırsa kayıt geçersiz kılınmalıdır.
Performans · Kodu adım adım çalıştır
Yol Önbelleği ile Ad Çözümleme: Kod İzleme
/* bil[]: "usr","bal","tez.tex"; n = 3 */ int cozumle(char *bil[], int n) { int k = n, ino = KOK, okuma = 0; while (k > 0 && !onbellekte(bil, k, &ino)) k--; /* en uzun önek */ for (int j = k; j < n; j++) { ino = dizinde_ara(ino, bil[j]); okuma += 2; /* i-node + blok */ onbellege_ekle(bil, j + 1, ino); } return ino; }
Önbellekte beş yol var. /usr/bal/tez.tex aranıyor: tam yol önbellekte yok.
Son bileşen atılır: /usr/bal önbellekte, ino = 45. Kökten başlamaya gerek kalmadı: iki bileşen bedava.
Kalan tek bileşen diskten: 45’in i-node’u ve dizin bloğu okunur, tez.tex → 131. Sonuç önbelleğe eklenir.
Aynı yol yeniden açılınca tam yol önbellekte: döngüler hiç dönmez, 0 disk okuması.
/etc/passwd: ne tam yol ne /etc önbellekte; k 0’a iner, çözüm kök i-node’undan (KOK = 1) başlar.
Kök i-node’u ve kök dizin bloğu okunur: etc → 14; /etc önbelleğe girer.
/etc i-node’u ve dizin bloğu: passwd → 212. Önbelleksiz yolda bileşen başına 2 okuma: toplam 4. Linux’ta bu önbellek dcache’tir.
Performans
İpuçları (Hints)
Önbellekteki değer her zaman doğru olmalıdır. İpucu yanlış olabilir; bu yüzden geçersiz kılma gerekmez, ama kullanmadan önce ucuz bir doğrulama şarttır.
Dağıtık dosya sisteminde “dosya muhtemelen sunucu B’de”; web’de yönlendirme önbelleği; dal tahmini; uygulamadan çekirdeğe madvise(MADV_SEQUENTIAL) ile verilen erişim ipucu.
Performans
Yerelliği Kullanmak (Exploiting Locality)
- Zamansal yerellik: yakın zamanda kullanılan tekrar kullanılır (önbelleğin temeli). Uzamsal yerellik: yakınındaki de kullanılır (önden okuma, büyük sayfalar).
- Birlikte kullanılan verileri yan yana koymak: FFS silindir grupları, ext4 blok grupları; NUMA’da belleği iş parçacığının düğümünden ayırmak.
Performans · Adım adım
Yaygın Durumu Hızlandırmak
Kilitlerin büyük çoğunluğu çekişmesiz alınır. Yaygın durum bu ise, hızlandırılması gereken de budur.
A, atomik karşılaştır-değiştir (CAS) ile 0’ı 1 yapar. Çekirdeğe hiç girilmez: birkaç nanosaniye.
Nadir durum: B de kilidi ister, CAS başarısız olur. Değeri 2 yapar ve futex(FUTEX_WAIT) ile çekirdekte uyur. Yavaş yol yalnızca burada.
A bırakırken eski değer 2 ise futex(FUTEX_WAKE) çağırır; 1 olsaydı hiç çağırmazdı. B uyanır ve kilidi alır.
Genel ilke (Lampson): yaygın durum hızlı, nadir durum doğru olmalı. Diğer örnekler: sistem çağrılarının hızlı yolu, vDSO ile çekirdeğe girmeden clock_gettime, TLB isabeti.
Proje Yönetimi · Adım adım
Efsanevi Adam-Ay (The Mythical Man-Month)
“Adam-ay” (man-month) yanıltıcı bir birimdir: iş, kişiler arasında bölünebilir varsayar. 2 kişi arasında yalnızca 1 iletişim yolu var.
4 kişide 6 yol. Her yeni kişi herkesle eşgüdüm kurmak zorunda; toplantı, belge ve arayüz anlaşmaları artar.
6 kişide 15 yol. Gecikmiş projeye eklenen kişiyi eğitmek, mevcut ekibin zamanını da tüketir.
8 kişide 28 yol. Brooks yasası: “Gecikmiş bir yazılım projesine eleman eklemek, onu daha da geciktirir.”
Brooks’un OS/360 deneyiminden önerdiği zaman dağılımı: 1/3 planlama, 1/6 kodlama, 1/4 modül testi, 1/4 sistem testi. Yani kod yazmak işin yalnızca altıda biridir.
Proje Yönetimi
Takım Yapısı, Deneyim ve “Gümüş Kurşun Yok”
Mills/Brooks’un cerrah ekibi: baş programcı tasarlar ve kodlar, diğerleri destekler. Kavramsal bütünlük birkaç kişide kalır.
İyi programcı ortalamadan kat kat verimlidir. “Birini atmayı planlayın”; ikinci sistem etkisine dikkat.
Brooks (1986): araçlar yalnız arızi karmaşıklığı azaltır, özsel karmaşıklık kalır. Yapay zekâ destekli kodlama da tasarım sorumluluğunu kaldırmaz.
Test ve Hata Ayıklama
İS Kodunu Test Etmek Neden Zor?
Neden zor?
- Ayrıcalıklı mod: hatayı yakalayacak bir üst katman yoktur; çekirdekte tek bir hatalı işaretçi oops/panic ile tüm sistemi, test düzeneğiyle birlikte düşürür.
- Eş zamanlılık: kesmeler, ön alım ve çok çekirdek sayısız iç içe geçme üretir; hata yalnız nadir bir zamanlamada ortaya çıkar.
- Belirlenimsizlik: “heisenbug”lar her çalıştırmada tekrarlanmaz;
printkeklemek bile zamanlamayı değiştirip hatayı saklayabilir. - Çeşitlilik: binlerce sürücü, birçok mimari (x86-64, arm64, RISC-V) ve on binlerce Kconfig seçeneği; tüm bileşimler denenemez.
- Uzun ömürlü durum: sızıntılar ve sayaç taşmaları ancak günlerce çalıştıktan sonra görünür.
Test türleri
| Tür | Ne sınar? (çekirdek örneği) |
|---|---|
| Birim | Tek işlev ya da modül, yalıtılmış hâlde (KUnit) |
| Entegrasyon | Alt sistemlerin birlikte çalışması, kullanıcı alanı arayüzü (kselftest) |
| Sistem | Tüm İS, gerçek ya da sanal makinede (LTP, açılış testleri) |
| Regresyon | Düzeltilen hata geri geldi mi? Her değişiklikte yeniden koşulur (KernelCI) |
| Stres / yük | Aşırı yük, uzun süre, kaynak kıtlığı (stress-ng, rcutorture) |
İS’de başarısız bir test çoğu zaman makinenin kendisini kaybetmek demektir; bu yüzden testler sanal makinelerde, zaman aşımı ve otomatik yeniden başlatmayla koşulur.
Test ve Hata Ayıklama
Çekirdek Test Piramidi
Testler çekirdekle birlikte derlenir, iç işlevleri doğrudan çağırır; UML ya da QEMU’da saniyeler içinde koşar: kunit.py run. Çıktı KTAP biçimindedir.
tools/testing/selftests altında; çalışan çekirdeğin sistem çağrılarını, cgroup, bpf, ağ gibi arayüzlerini dışarıdan sınar: make kselftest.
Linux Test Project: binlerce test; sistem çağrısı anlamı, POSIX uyumu, dosya sistemi ve stres senaryoları. Dağıtımlar sürüm öncesi kullanır.
Test ve Hata Ayıklama
Dinamik Denetleyiciler: KASAN, KCSAN, lockdep
| Araç | Yakaladığı hata | Nasıl çalışır? |
|---|---|---|
KASANCONFIG_KASAN | use-after-free; öbek, yığın ve global dizi taşması; çift serbest bırakma | Derleyici her bellek erişiminin önüne denetim ekler; gölge bellek (her 8 bayta 1 bayt) hangi baytların geçerli olduğunu tutar. Rapor, ayırma ve serbest bırakma yığın izlerini de verir. |
KCSANCONFIG_KCSAN | Veri yarışı: en az biri yazma olan, eş zamanlı ve kilitsiz erişim | Rastgele erişimlerde gözlem noktası kurup kısa süre bekler; bu arada başka bir çekirdek aynı adrese dokunursa raporlar. |
UBSANCONFIG_UBSAN | Tanımsız davranış: işaretli taşma, sıfıra bölme, geçersiz kaydırma, dizi sınırı | Derleyicinin eklediği çalışma zamanı denetimleri. |
lockdepCONFIG_PROVE_LOCKING | Kilit sırası ihlali (AB-BA), kesme bağlamında hatalı kilit kullanımı | Kilit sınıfları arasındaki “önce-sonra” çizgesini tutar; çevrim görünce kilitlenme gerçekleşmeden uyarır. |
kmemleakCONFIG_DEBUG_KMEMLEAK | Bellek sızıntısı | Çöp toplayıcı gibi belleği tarar; hiçbir işaretçinin göstermediği ayrılmış blokları raporlar. |
BUG: KASAN: slab-use-after-free in mydrv_release+0x54/0x90 Read of size 8 at addr ffff888012a4c010 by task cat/1432
Test ve Hata Ayıklama · Kodu adım adım çalıştır
KASAN Gölge Bellek Denetimi: Kod İzleme
/* 8 baytlık bölge başına 1 gölge bayt: 0: hepsi geçerli, 1..7: ilk k bayt, < 0: kırmızı bölge ya da serbest */ void kasan_denetle(void *a, int boy) { int8_t s = *(int8_t *)(((uintptr_t)a >> 3) + GOLGE_OFS); if (s == 0) return; /* hızlı yol */ if (((uintptr_t)a & 7) + boy - 1 >= s) rapor(a, s); } /* p = kmalloc(13); p[3]; p[12]; p[13]; */ /* kfree(p); p[0]; */
kmalloc(13) 16 baytlık bir yuva verir. Gölge: ilk 8 bayt tam geçerli (0), sonraki bölgenin ilk 5 baytı geçerli (13 − 8), arkası kırmızı bölge (0xfc).
p[3]: gölge baytı p+3 ≫ 3 = 0. bölgeden okunur: 0, denetim tek karşılaştırmayla biter.
p[12]: gölge 5; bölge içi konum 12 & 7 = 4. 4 ≥ 5 değil: son geçerli bayt, erişim serbest.
p[13]: 5 ≥ 5: dizinin bir bayt ötesi. Gerçek çekirdekte bu bayt başka bir nesneye ait olabilirdi ve hata sessiz kalırdı; KASAN ilk geçersiz erişimde raporlar.
kfree(p): nesnenin gölgesi 0xfb (serbest) olarak işaretlenir; yuva bir süre karantinada bekletilir ki hemen yeniden kullanılıp hata gizlenmesin.
p[0]: s = −5. Negatif gölge her konumda “≥ s” koşulunu sağlar: serbest bırakma sonrası kullanım raporu, ayırma ve serbest bırakma yığın izleriyle birlikte.
Test ve Hata Ayıklama · Kodu adım adım çalıştır
lockdep Kilit Sırası Çizgesi: Kod İzleme
void lock_acquire(task *t, int L) { for (int i = 0; i < t->n; i++) { int H = t->tutulan[i]; if (yol_var(L, H)) /* L → … → H */ uyar("olası kilitlenme", H, L); kenar_ekle(H, L); /* H, L'den önce */ } t->tutulan[t->n++] = L; } void lock_release(task *t) { t->n--; } /* T1: A,B T2: B,C T3: C,A */
lockdep kilit örneklerini değil sınıflarını izler ve “şu kilit tutulurken bu kilit alındı” ilişkisini bir çizgede biriktirir. Başta çizge boş.
T1 A’yı alır: tutulan başka kilit yok, döngü dönmez; A tutulanlar listesine eklenir.
T1 A’yı tutarken B’yi alır: B’den A’ya yol yok, uyarı yok. Kenar A → B eklenir. Sonra iki kilit bırakılır.
Daha sonra T2 B’yi tutarken C’yi alır: kenar B → C. Çizgede artık A → B → C sırası var.
Başka bir anda T3 önce C’yi alır; tek kilit, sorun yok.
T3 C’yi tutarken A’yı ister: A’dan C’ye yol var (A → B → C). Yeni kenar C → A bir çevrim kapatır: lockdep uyarır.
Üç görev hiç aynı anda çalışmadı, gerçek kilitlenme olmadı. Ama T1, T2 ve T3 bir gün aynı anda koşarsa her biri bir sonrakinin kilidini bekler. lockdep bunu tek bir çalıştırmada, olmadan önce gösterir.
Test ve Hata Ayıklama · Adım adım
Fuzzing: syzkaller Döngüsü
syzkaller sistem çağrılarının imzalarını syzlang açıklamalarından bilir: hangi argüman dosya tanıtıcısı, hangisi bayrak, hangisi işaretçi. Korpus başta boştur ya da tohum programlardan oluşur.
Üretici korpustan bir program seçip mutasyona uğratır ya da yenisini üretir: ör. openat → ioctl → mmap → close. Argümanlar tipe uygun seçildiği için çağrılar hemen EINVAL ile reddedilmez.
Program, KASAN/KCSAN/lockdep açık bir test çekirdeğiyle çalışan QEMU sanal makinelerinde koşar. VM çökse bile ana makine etkilenmez; syz-manager VM’i yeniden başlatır.
KCOV, çekirdekte geçilen temel blokları (PC değerlerini) kaydeder. Bu kapsam geri bildirimi, rastgele denemeyi yönlendirilmiş bir aramaya çevirir.
Yeni kod yolu açan program korpusa eklenir ve sonraki mutasyonların tohumu olur. Döngü, her VM’de saniyede yüzlerce programla durmadan döner.
Çökme ya da KASAN raporu gelirse syzkaller programı yeniden üretir, en küçük hâline indirger ve çoğu zaman bir C yeniden üreticisi çıkarır. syzbot bunu otomatik olarak geliştiricilere bildirir.
Fuzzer tek başına yalnız çökmeleri görür; KASAN gibi denetleyiciler sessiz bellek bozulmalarını ilk geçersiz erişimde görünür kılar. Bu yüzden ikisi birlikte kullanılır.
Test ve Hata Ayıklama
Gözlem ve İz Sürme Araçları
| Araç | Ne sağlar? | Örnek |
|---|---|---|
| printk | Log düzeyleri 0 (EMERG) … 7 (DEBUG); halka tampona yazılır, eşiğin üstündekiler konsola basılır | pr_warn("kuyruk dolu: %d\n", n); |
| dmesg | Halka tamponu okur; kernel.dmesg_restrict=1 ise yalnız CAP_SYSLOG sahibi okur (adres sızıntısını önler) | dmesg -w --level=err,warn |
| dynamic debug | pr_debug satırlarını yeniden derlemeden, çalışırken açar | echo 'module e1000e +p' > …/dynamic_debug/control |
| ftrace | Çekirdek içi izleyici: işlev ve işlev çizgesi izleme, tracepoint olayları (tracefs) | trace-cmd record -p function_graph -g vfs_read |
| perf | Donanım sayaçları ve örneklemeli profil: sıcak noktalar, önbellek ıskaları, çizelgeleyici gecikmesi | perf record -g -- ./uyg, perf report |
| eBPF / bpftrace | Doğrulayıcıdan geçen küçük programlar kprobe ve tracepoint’lere bağlanır; üretimde güvenli gözlem | aşağıdaki tek satırlık örnek |
| kdump / crash | Panikte kexec ile yedek çekirdeğe geçip bellek dökümünü (/proc/vmcore) kaydeder; sonra çevrim dışı incelenir | crash vmlinux vmcore |
# hangi süreç hangi dosyayı açıyor?
bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args.filename)); }'Önce gözlem, sonra tahmin: log ve izler hipotez kurmayı sağlar. Ama ağır printk zamanlamayı değiştirip yarış hatasını gizleyebilir; düşük yüklü izleyiciler (ftrace, eBPF) bu yüzden tercih edilir.
Test ve Hata Ayıklama
QEMU ve gdb ile Çekirdek Hata Ayıklama
# 1. terminal: -s → gdb sunucusu :1234 # -S → CPU başta durur qemu-system-x86_64 -kernel bzImage \ -append "console=ttyS0 nokaslr" \ -nographic -s -S # 2. terminal gdb vmlinux (gdb) target remote :1234 (gdb) hbreak start_kernel (gdb) continue (gdb) bt (gdb) lx-dmesg # çekirdek gdb betiği
vmlinux: sembollü (CONFIG_DEBUG_INFO), sıkıştırılmamış çekirdek;nokaslradresleri sembollerle eşler.- kgdb: aynı gdb oturumu gerçek donanımda seri hat üzerinden (
kgdboc=ttyS0,115200).
BUG: kernel NULL pointer dereference,
address: 0000000000000010
Oops: 0000 [#1] PREEMPT SMP NOPTI
CPU: 2 PID: 1432 Comm: cat
Tainted: G O 6.12.0
RIP: 0010:mydrv_read+0x2a/0x80 [mydrv]
Call Trace:
vfs_read+0xa4/0x2f0
ksys_read+0x63/0xe0
do_syscall_64+0x82/0x170- RIP: işlev + ofset / işlev boyu;
scripts/faddr2linekaynak satırını verir. - Tainted O: ağaç dışı modül yüklü. Çağrı izi aşağıdan yukarı okunur:
read→ VFS → sürücü. - Oops o süreci öldürür, sistem sürebilir;
panic_on_oops=1ise doğrudan panic.
!analyze -v; Driver Verifier (verifier.exe) sürücüleri özel havuz, IRQL ve kaynak sızıntısı denetimleriyle zorlayarak hataları erken yakalar.Test ve Hata Ayıklama · Kodu adım adım çalıştır
Regresyon Avı: git bisect
git bisect start git bisect bad v6.12 git bisect good v6.11 # #500: derle, başlat, sına git bisect bad git bisect good # #250 git bisect good # #375 git bisect bad # #437 git bisect run ./sina.sh git bisect reset
Bilinen iyi (v6.11) ve kötü (v6.12) uçlar verilir. İlk kötü commit bu 1000 commit’in içindedir; git ortadakini, #500’ü çıkarır.
#500 derlenip sınanır: hata var, git bisect bad. Hata #500’de ya da öncesinde başlamıştır; sağ yarı elenir.
#250’de hata yok: good. İlk kötü commit 251–500 arasındadır.
#375 de iyi. Her yanıt aday sayısını yarıya indirir: 1000 → 500 → 250 → 125.
#437 kötü: aralık 376–437. Elle sürdürmek yerine sınamayı bir betiğe bırakabiliriz.
git bisect run kalan adımları kendisi yürütür: 406 iyi, 421 kötü, 413 kötü, 409 iyi, 411 iyi, 412 kötü. Toplam 10 test: 210 = 1024 ≥ 1000.
Sonuç: “#412 is the first bad commit”. reset ile başlangıç dalına dönülür. 1 milyon commit olsaydı bile yalnızca ⌈log₂ 106⌉ = 20 test gerekirdi.
./sina.sh 0 ile biterse commit iyi, 1–127 (125 hariç) ile biterse kötü sayılır; 125 “bu commit sınanamaz, atla” demektir.
Eğilimler
İşletim Sistemlerinde Eğilimler
| Eğilim | İşletim sistemine etkisi |
|---|---|
| Sanallaştırma ve bulut | Misafir çekirdek virtio ile hipervizörle işbirliği yapar; çok kiracılı bulutta yalıtım sınırı hipervizöre taşınır. Konteyner: hafif ama zayıf yalıtım; mikro-VM (Firecracker): VM yalıtımı, milisaniyede açılış |
| Çok çekirdekli yongalar | Ölçeklenebilir kilitler, RCU, NUMA farkındalığı, heterojen çekirdekler |
| Geniş adres alanı | 64 bit, 5 düzeyli sayfa tabloları; tek adres alanlı sistem fikirleri |
| Kesintisiz veri erişimi | Ağ, bulut depolama ve yerel disk arasında şeffaf erişim; NVMe, CXL bellek |
| Pille çalışan sistemler | DVFS, “hızla bitir ve uyu” (race-to-idle), Android Doze, enerji farkında çizelgeleme (EAS) |
| Gömülü ve sensör düğümleri | Gerçek zamanlı küçük çekirdekler: Zephyr, FreeRTOS, Contiki-NG; kilobaytlarca bellek |
| Güvenli diller ve programlanabilir çekirdek | Linux’ta Rust (6.1’den beri), eBPF ve sched_ext, unikernel’ler |
Eğilimler
Çok Çekirdek ve Geniş Adres Alanları
Sunucularda yuva başına 128-192 çekirdek, telefonlarda büyük/küçük (big.LITTLE) karışık çekirdekler. Tek bir büyük çekirdek kilidi ölçeklenmez: ince taneli kilitler, RCU, çekirdek başına veri yapıları gerekir.
Tüm dosyaları belleğe eşlemek (mmap) ya da tek adres alanlı sistemler mümkün olur. Bedeli: daha derin sayfa tablosu yürüyüşü, daha fazla TLB baskısı.
Eğilimler
Rust, eBPF ve Unikernel’ler
Linux 6.1 (Aralık 2022) ile Rust desteği ana dala girdi, ardından ilk Rust sürücüleri geldi. Amaç: kullanım-sonrası-serbest bırakma ve taşma gibi bellek hatalarını derlemede önlemek. Windows çekirdeğinde de Rust bileşenleri var.
Doğrulanmış küçük programlarla çekirdeği yeniden derlemeden genişletmek: izleme, ağ (XDP), güvenlik, çizelgeleme (sched_ext).
Exokernel’in libOS fikrinin bulut sürümü: MirageOS, Unikraft. Tek uygulamalı mikro hizmetler için.
Özet
Özet
| Konu | Ana fikir |
|---|---|
| Zorluklar | Boyut, eş zamanlılık, düşman kullanıcılar, uzun ömür, uyumluluk |
| Hedefler | Soyutlama, ilkel işlemler, yalıtım, donanım yönetimi |
| Arayüz | Basit, bütün, verimli; az ve genel sistem çağrısı |
| Yapı | Yalıtım ile performans ödünleşimi; katman sırası, güvenli genişletme, çekirdek iş parçacıkları |
| İlkeler | Mekanizma/politika, diklik, iki düzeyli ad, geç bağlama |
| Teknikler | Donanımı gizleme, dolaylılık, yeniden girişlilik, önce hata denetimi |
| Performans | Ölçüm; uzay-zaman ödünleşimi, önbellek, ipucu, yerellik, yaygın durum |
| Proje | Brooks yasası, küçük güçlü takımlar, gümüş kurşun yok |
| Test | KUnit, kselftest, LTP; KASAN, KCSAN, lockdep; syzkaller; ftrace, eBPF; QEMU+gdb; git bisect |
| Eğilimler | Bulut, çok çekirdek, enerji, Rust, eBPF, unikernel |
Özet
Alıştırma: Test ve Hata Ayıklama
- Bir sürücüde ara sıra görülen use-after-free hatasını hangi araçlarla, hangi sırayla yakalarsınız? syzkaller neden KASAN açık bir çekirdekle çalıştırılır?
- v6.11 ile v6.12 arasında 12 000 commit var ve bir performans gerilemesi v6.12’de ortaya çıkmış.
git bisecten çok kaç test gerektirir? Sınama betiği hangi çıkış kodlarını kullanmalıdır? - Bir yarış hatası
printkeklenince kayboluyor. Bunun nedeni nedir; hangi araçlarla incelemeye devam edersiniz?
Cevaplar
- KASAN açık test çekirdeğinde hatayı syzkaller ya da stres testiyle tetiklenir; rapordaki ayırma ve serbest bırakma yığın izlerinden nesnenin ömrü çıkarılır; gerekirse QEMU+gdb ile adım adım izlenir. Fuzzer tek başına yalnız çökmeyi görür; KASAN sessiz bozulmayı ilk hatalı erişimde raporlar.
- ⌈log₂ 12 000⌉ = 14, çünkü 213 = 8192 < 12 000 ≤ 16 384 = 214. Betik iyi için 0, kötü için 1–127 (125 hariç), sınanamayan commit için 125 döndürmelidir; ölçüm gürültülüyse birkaç tekrarın ortalaması alınır.
- Ağır
printkzamanlamayı değiştirir (heisenbug). KCSAN ile veri yarışı, lockdep ile kilit sırası denetlenir; olay sırası düşük yüklü ftrace/eBPF izleriyle gözlenir.
Özet
Kontrol Soruları
- Tanenbaum’a göre işletim sistemi tasarımını zorlaştıran sekiz etkenden dördünü açıklayın.
- Basitlik, bütünlük ve verimlilik ilkelerini birer sistem çağrısı örneğiyle tartışın.
- Algoritmik ve olay güdümlü yürütme paradigmalarını karşılaştırın. Olay döngüsünde uzun süren bir işleyici neden sorun olur?
- Yedi katmanlı tasarımda sanal bellek neden aygıt sürücülerinin üstündedir?
- Mikro çekirdekte bir sürücü çöktüğünde ne olur? Monolitik çekirdekte aynı durum neden daha tehlikelidir?
- Mekanizma ile politikayı ayırmayı çizelgeleyici örneğiyle açıklayın. sched_ext bu ilkeyi nasıl uygular?
- /usr/ast/mbox yolunun çözümlenmesi önbelleksiz kaç disk erişimi gerektirir? Önbellek ile ipucu arasındaki fark nedir?
- Bit sayma örneğinde döngü, makro ve tablo yöntemlerini yer ve zaman açısından karşılaştırın.
- futex neden yaygın durumu hızlandırmanın iyi bir örneğidir?
- Brooks yasasını iletişim yolu sayısıyla gerekçelendirin: 10 kişilik ekipte kaç yol vardır?
Cevaplar
- Örn. devasa boyut, eş zamanlılık, düşmanca kullanıcılar, yalıtıma rağmen paylaşım, uzun ömür, genellik, taşınabilirlik, geriye dönük uyumluluk.
- Basitlik: UNIX’in az sayıda çağrısı; bütünlük: exec ailesinin tek execve ile her ihtiyacı karşılaması; verimlilik: lseek ucuz, read pahalı; maliyet sezilebilmeli.
- Algoritmik: akışı program belirler. Olay güdümlü: dış olaylar belirler. Tek iş parçacıklı döngüde uzun işleyici bitene kadar diğer olaylar işlenemez, arayüz donar.
- Sanal bellek sayfa çıkarırken ve getirirken disk sürücüsünü çağırır; her katman yalnız altındakini kullanabildiği için sürücüler daha alttadır.
- Yalnız sürücü süreci ölür; reenkarnasyon sunucusu onu yeniden başlatır. Monolitik çekirdekte sürücü çekirdek adres alanındadır; hatası tüm sistemi çökertir.
- Çekirdek “en yüksek öncelikliyi seç” mekanizmasını sunar; öncelikleri uygulama belirler. sched_ext ile politika eBPF programı olarak yüklenir, çekirdek mekanizması aynı kalır.
- 6 okuma (3 i-node + 3 dizin bloğu), dosyanın i-node’u ile 7. Önbellek her zaman doğrudur; ipucu yanlış olabilir, kullanmadan önce doğrulanır.
- Döngü: yer yok, 8 tur. Makro: yer yok, dal yok ama ~24 komut. Tablo: 256 bayt, tek okuma. Donanımda POPCNT tek komuttur.
- Çekişmesiz kilit kullanıcı alanında tek atomik işlemle alınır; çekirdeğe yalnız nadir çekişme durumunda girilir.
- 10·9/2 = 45 yol. Eklenen her kişi iletişim yükünü karesel artırır ve eğitim için mevcut ekibin zamanını tüketir.
Özet
Kaynaklar
- A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4. baskı (2014) / 5. baskı (2023), Pearson — Bölüm 12: Operating System Design.
- B. W. Lampson, “Hints for Computer System Design”, ACM SOSP, 1983 (ve 2020 güncellemesi “Hints and Principles for Computer System Design”).
- F. P. Brooks Jr., The Mythical Man-Month: Essays on Software Engineering, 20. yıl baskısı, Addison-Wesley, 1995 — “No Silver Bullet” (1986) dahil.
- J. H. Saltzer, M. F. Kaashoek, Principles of Computer System Design: An Introduction, Morgan Kaufmann, 2009.
- D. R. Engler, M. F. Kaashoek, J. O’Toole, “Exokernel: An Operating System Architecture for Application-Level Resource Management”, SOSP 1995.
- G. Klein vd., “seL4: Formal Verification of an OS Kernel”, SOSP 2009; J. N. Herder vd., MINIX 3 güvenilirlik çalışmaları.
- Linux çekirdeği belgeleri: Rust (docs.kernel.org/rust), BPF ve sched_ext; H. Franke vd., “Fuss, Futexes and Furwocks”, 2002.