İşletim Sistemleri · Ders Notu
Bölüm 15: Linux
Dersin genel kavramları, yani süreçler, iş parçacıkları, çizelgeleme, sanal bellek, dosya sistemleri ve koruma, gerçek bir sistemde nasıl görünür? Bu not Linux'u bir örnek incelemesi olarak ele alır. Kavramları yeniden tanımlamaz; Linux'a özgü yapılara ve kararlara odaklanır. UNIX'ten Linux'a uzanan tarihle ve kabuğun boru hatlarını nasıl kurduğuyla başlar. Ardından çekirdeğin içine iner: task_struct, clone() bayrakları, sinyal teslimi, CFS'ten EEVDF'ye adil çizelgeleme, dört ve beş düzeyli sayfa tabloları, buddy ve SLUB ayırıcıları, sayfa önbelleği ve OOM. G/Ç ve dosya sistemlerinde aygıt dosyalarını, blk-mq'yu, VFS'i ve ext4 günlüklemesini izler. Güvenlik kesimi izin hesaplarını, yetenekleri ve konteynerlerin temeli olan ad alanları ile cgroups'u anlatır. Not Android ile kapanır.
Öğrenme çıktıları
- Tarih ve aile
- UNIX'ten BSD, MINIX, GNU ve Linux'a uzanan çizgiyi; dağıtımları, sürüm modelini ve Android'i konumlandırmak.
- Arayüzler ve kabuk
- Kütüphane, sistem çağrısı ve çekirdek katmanlarını ayırmak; yönlendirmelerin ve boru hatlarının
pipe,fork,dup2veexecile kurulduğunu izlemek. - Süreçler ve çizelgeleme
forkveexecve'nin çekirdekteki adımlarını, sinyal teslimini,task_struct'ı,clone()bayraklarını ve NPTL'i açıklamak; EEVDF'te uygunluk ve sanal son tarihi hesaplamak.- Bellek
- Adres alanı düzenini ve VMA'ları, dört düzeyli sayfa tablosu yürüyüşünü, bölgeleri, buddy ve SLUB ayırıcılarını, sayfa önbelleğini ve OOM'u yorumlamak.
- G/Ç ve dosyalar
- Aygıt dosyalarını, blk-mq'yu ve soketleri tanımak; VFS nesnelerini, ext4'ün extent ve günlükleme düzenini, bağ sayısını ve
/proc'u açıklamak. - Güvenlik
- rwx, setuid ve umask hesaplamak; ilk eşleşen sınıf kuralını uygulamak; yetenekleri, ad alanlarını, cgroups'u, seccomp ve LSM'i ve Android korumalı alanını tanımak.
15.1Linux'un kökleri
Ders boyunca süreç, iş parçacığı, çizelgeleme, sanal bellek, dosya sistemi ve koruma kavramları genel olarak anlatıldı. Bu son bölüm bir örnek incelemesidir: aynı kavramların yaygın bir gerçek sistemde nasıl gerçeklendiğine bakar. Seçilen sistem Linux'tur. Sunuculardan telefonlara, süper bilgisayarlardan gömülü aygıtlara kadar en çok çalıştırılan çekirdek odur. Kaynak kodu açıktır; her yapının adı ve alanı doğrudan okunabilir. Not bu yüzden kavramları yeniden tanımlamaz. Linux'a özgü ayrıntıya odaklanır: task_struct, clone() bayrakları, EEVDF çizelgeleyicisi, SLUB ayırıcısı, ad alanları ve cgroups.
15.1.1UNIX'ten Linux'a
Linux'un hikâyesi UNIX'le başlar (Şekil 15.1). Ken Thompson ve Dennis Ritchie, Bell Laboratuvarları'nda 1969'da bir PDP-7 üzerinde küçük bir işletim sistemi yazdı. Sistem 1971'de PDP-11'e taşındı. 1973'te çekirdek C ile yeniden yazıldı. Bu adım o güne kadar görülmemiş bir şeydi: işletim sistemleri hep makine diliyle yazılırdı. C ile yazılan UNIX, yeni bir makineye derleyici taşınarak aktarılabildi. Taşınabilirlik UNIX'in yayılmasının asıl nedenidir.
AT&T bir antitröst kararı yüzünden bilgisayar satamıyordu. Kaynak kodu üniversitelere düşük ücretle verdi. Berkeley'deki California Üniversitesi bu kodu geliştirip BSD (Berkeley Software Distribution) adıyla dağıttı. Sanal bellek ve TCP/IP yığını UNIX'e BSD üzerinden girdi. AT&T'nin kendi çizgisi ise System V oldu. Seksenlerde her üretici kendi UNIX'ini çıkardı: SunOS, AIX, HP-UX, Xenix. Bu sürümler birbirinden ayrışınca bir programı hepsinde çalıştırmak zorlaştı. IEEE 1988'de POSIX standardını yayımladı. POSIX bir gerçekleme değildir; sistem çağrılarının ve kütüphane işlevlerinin ortak arayüzünü tanımlar.
Seksenlerin ikinci yarısı özgür bir UNIX arayışıyla geçti. Richard Stallman 1983'te GNU projesini başlattı. Amaç tümüyle özgür, UNIX uyumlu bir sistemdi. Proje derleyiciyi (GCC), kabuğu (bash), C kütüphanesini ve yüzlerce aracı üretti, ama çekirdeği bitiremedi. Andrew Tanenbaum 1987'de öğretim için MINIX'i yazdı. MINIX küçük bir mikro çekirdekti ve kodu bir ders kitabıyla birlikte yayımlandı. Helsinki Üniversitesi öğrencisi Linus Torvalds 1991'de MINIX üzerinde kendi çekirdeğini geliştirmeye başladı. Linux 0.01 aynı yıl yayımlandı. Torvalds çekirdeği 1992'de, 0.12 sürümüyle GPL lisansına geçirdi. GPL, değiştirilmiş kodun da aynı özgürlükle dağıtılmasını şart koşar. GNU'nun araçları ile Linux çekirdeği birleşince kullanılabilir, tümüyle özgür bir sistem ortaya çıktı. Slackware ve Debian gibi ilk dağıtımlar 1993'te doğdu.
Linux UNIX kodu içermez. POSIX arayüzünü baştan yazılmış kodla gerçekler. Tasarımı ise MINIX'in tersidir. Torvalds mikro çekirdek yerine monolitik bir çekirdek seçti. Bu seçim 1992'de Tanenbaum ile Torvalds arasında ünlü bir e-posta tartışmasına yol açtı. Tanenbaum monolitik tasarımı eskimiş buluyordu. Torvalds ise basitlik ve hızı savunuyordu. Bugünkü Linux monolitiktir, ama modüler bir yapıyla esneklik kazanmıştır.
Sonraki on beş yılda çekirdek olgunlaştı. Linux 1.0 1994'te çıktı. 2.0 sürümü (1996) çok işlemcili makineleri, yani simetrik çok işlemeyi (SMP) destekledi. 2.6 serisi (2003) kesilebilir çekirdeği ve yeni çizelgeleyicileri getirdi. 2005'te çekirdek geliştiricilerinin kullandığı ticari sürüm denetim aracı BitKeeper'ın lisansı sorun çıkardı. Torvalds birkaç haftada git'i yazdı; bugün yazılım dünyasının çoğu onu kullanır. İlk Android telefonu, HTC Dream, 2008'de satışa çıktı. 2011'deki 3.0 sürümünden bu yana numaralar teknik bir anlam taşımaz; yalnızca büyür. 6.19'un ardından gelen sürüm, Nisan 2026'da yayımlanan 7.0'dır. Kasım 2017'den bu yana dünyanın en hızlı 500 süper bilgisayarının tamamı Linux çalıştırır. 6.x serisi Rust desteğini (6.1), EEVDF çizelgeleyicisini (6.6) ve genişletilebilir çizelgeleyici sched_ext ile gerçek zaman yamasını (6.12) getirdi.
15.1.2GNU, dağıtımlar ve sürüm modeli
“Linux” sözcüğü, kesin anlamıyla yalnızca çekirdeğin adıdır. Kullanıcının bilgisayarına kurduğu şey bir dağıtımdır (Şekil 15.2). Dağıtım, çekirdeğin üstüne katmanlar ekler: GNU araçları ve C kütüphanesi (çoğunlukla glibc), sistemi başlatan init programı (bugün çoğunlukla systemd), paket yöneticisi ve uygulamalar. Bu yüzden tam sistem için “GNU/Linux” adı da kullanılır. Android aynı çekirdeği kullanır ama GNU araçlarını kullanmaz; C kütüphanesi Bionic'tir.
Dağıtımlar paket biçimine göre ailelere ayrılır (Tablo 15.1). Aynı ailenin üyeleri aynı paket yöneticisini kullanır. Türkiye'de geliştirilen Pardus Debian ailesindendir. Alpine ise glibc yerine küçük musl kütüphanesini ve tek bir ikili dosyada toplanmış BusyBox araçlarını kullanır; bu yüzden konteyner imajlarında sevilir.
| Aile | Örnekler | Paket biçimi / araç |
|---|---|---|
| Debian | Debian, Ubuntu, Mint, Pardus | .deb / apt |
| Red Hat | Fedora, RHEL, Rocky, Alma | .rpm / dnf |
| Arch | Arch, Manjaro | pacman |
| SUSE | openSUSE, SLES | zypper |
| Hafif | Alpine (musl + BusyBox) | apk |
Çekirdeğin kendi sürüm modeli düzenlidir. Bir sürüm yayımlanınca iki haftalık birleştirme penceresi açılır. Bu sürede alt sistem sorumlularının biriktirdiği yeni özellikler ana ağaca alınır. Ardından haftalık sürüm adayları gelir: rc1, rc2 … çoğunlukla rc7. Bu haftalarda yalnız hata düzeltmesi kabul edilir. Böylece yaklaşık 9–10 haftada bir yeni ana sürüm çıkar. Her yılın son ana sürümü uzun süreli destek (long-term support, LTS) alır: 6.6 (2023), 6.12 (2024) ve 6.18 (2025) böyledir. Dağıtımlar çoğunlukla bir LTS çekirdeği seçer ve yıllarca onun güvenlik yamalarını izler.
15.1.3Tasarım ilkeleri
UNIX'i programcılar kendileri için tasarladı. Bu kökenden gelen ilkeler Linux'ta da sürer. Sistem basittir: az sayıda, iyi tanımlı kavram üzerine kuruludur; süreç, dosya ve boru. Tutarlıdır: disk dosyası, terminal, boru ve ağ soketi aynı read ve write çağrılarıyla kullanılır (Şekil 15.3). “Her şey dosyadır” sözü bu tutarlılığı anlatır. Sistem esnektir: araçlar birbirini tanımadan, metin akışları üzerinden birleşir. Programcıya az “sihir” gösterir; başarılı bir komut çoğunlukla hiçbir şey yazmaz.
Boru hatlarını UNIX'e kazandıran Doug McIlroy bu düşünceyi üç kurala bağladı. Her program tek bir işi iyi yapmalıdır. Programlar birlikte çalışacak biçimde yazılmalıdır. Programlar metin akışlarını işlemelidir, çünkü metin evrensel arayüzdür. POSIX bu arayüzü standartlaştırır. Aynı kaynak kod Linux'ta, BSD'de ve macOS'ta derlenip çalışabilir.
15.2Arayüzler ve kabuk
15.2.1Üç arayüz: bir printf'in yolculuğu
Bir Linux sistemi katmanlar hâlinde düşünülebilir (Şekil 15.4). En altta donanım, onun üstünde çekirdek vardır. Çekirdeğin üstünde standart C kütüphanesi, onun üstünde kabuk, düzenleyici, derleyici gibi standart yardımcı programlar durur. En üstte kullanıcılar bulunur. Katmanlar arasında üç arayüz vardır. Sistem çağrısı arayüzü kütüphane ile çekirdek arasındadır. Kütüphane arayüzü programların çağırdığı printf, open, fork gibi işlevlerdir. Kullanıcı arayüzü ise kabuk ve programların kendisidir. POSIX kütüphane arayüzünü tanımlar, sistem çağrılarını tanımlamaz. Hangi işlevin sistem çağrısı olduğu sisteme göre değişebilir.
printf("merhaba\n") çağrısının beş adımlık yolculuğu.Bu katmanları tek bir satırın izini sürerek görmek öğreticidir. Program printf("merhaba\n") çağırsın.
- Uygulama.
printfbir sistem çağrısı değildir. glibc biçimlendirilmiş metni kullanıcı alanındaki bir tampona yazar. Çıktı terminale bağlıysa tampon satır sonunda boşaltılır. - glibc. Tampon boşaltılırken glibc'nin
write(1, buf, 8)sarmalayıcısı çalışır. x86-64'te sistem çağrısı numarasıraxyazmacına konur;write'ın numarası 1'dir. Argümanlarrdi,rsi,rdxyazmaçlarına yerleşir. Sonrasyscallkomutu yürütülür. - Çekirdek girişi. CPU çekirdek moduna geçer ve
entry_SYSCALL_64giriş koduna atlar. Giriş kodu numarayısys_call_tabledizisinde indis olarak kullanır vewrite'ın işleyicisini çağırır. İşleyici VFS katmanına geçer: dosya tanımlayıcısı 1'in arkasındaki nesneyi bulur. - Sürücü. Etkileşimli bir oturumda tanımlayıcı 1 bir sözde terminale (pty) bağlıdır. tty katmanı baytları terminal öykünücüsüne iletir. Çıktı bir diske yönlendirilmiş olsaydı yol sayfa önbelleğine ve blok katmanına giderdi.
- Dönüş. Yazılan bayt sayısı (8)
rax'ta döner.sysretkomutu kullanıcı moduna geri geçer. Hata olsaydı çekirdek negatif bir hata kodu döndürürdü; glibc bunuerrno'ya yazar ve çağırana −1 verir.
Linux x86-64'te 460 dolayında sistem çağrısı vardır. Hepsi aynı kapıdan girer: numara rax'ta, argümanlar yazmaçlarda, giriş syscall ile. Bir programın hangi çağrıları yaptığı strace ile izlenebilir.
Her kütüphane işlevini bir sistem çağrısı sanmak. printf, malloc, opendir ve readdir kütüphane işlevleridir. Kimi hiç çağrı yapmaz, kimi arada bir alttaki çağrıya (write, brk/mmap, getdents64) başvurur. Bir döngüde 1000 kez printf çağırmak, tampon sayesinde yalnızca birkaç write çağrısı üretebilir.
15.2.2Komutlar ve yönlendirme
Linux'ta komutların çoğu küçük, tek işlik programlardır (Tablo 15.2). Filtre adı verilen programlar standart girdiden okur, standart çıktıya yazar. Bu yüzden boru hattında birbirine eklenebilirler. Bazı adlar yanıltıcıdır. touch dosyanın zaman damgasını günceller; dosya yoksa boş bir dosya oluşturur. sync çekirdekteki kirli tamponları diske yazar; iki makine arasında eşitleme yapan araç rsync'tir. /proc bir komut değil, sözde bir dosya sistemidir: cat /proc/meminfo çekirdeğin bellek sayaçlarını okur. Ağ araçları da yenilendi: ip addr, ip route, ss ve nft, eski ifconfig, route, netstat ve iptables'ın yerini aldı.
| Kategori | Komutlar |
|---|---|
| Dosya / dizin | ls cd pwd mkdir rm -r cp mv touch find tar |
| Metin (filtre) | cat less head tail grep sort uniq wc echo |
| Süreç / sistem | ps top kill chmod chown df du sync |
| Ağ | ssh scp ping curl wget traceroute ip ss |
| Bilgi | man uname free lsblk |
Her süreç doğduğunda üç açık dosya tanımlayıcısı bulur: 0 standart girdi, 1 standart çıktı, 2 standart hata. Kabuk bu tanımlayıcıları programı çalıştırmadan önce başka dosyalara yönlendirebilir:
# yönlendirme: 0 = stdin, 1 = stdout, 2 = stderr
sort < girdi.txt > cikti.txt 2> hatalar.log # < okur, > yazar (üzerine)
echo "satır" >> gunluk.txt # >> sona ekler
make 2>&1 | tee derleme.log & # stderr→stdout, boru, arka planİlk satırda kabuk sort'un tanımlayıcı tablosunu kurar (Şekil 15.5). 0'ı girdi.txt'ye, 1'i cikti.txt'ye, 2'yi hatalar.log'a bağlar. Sonra sort'u çalıştırır. sort yönlendirmeden habersizdir; yalnız 0'dan okur, 1'e yazar. > dosyanın üzerine yazar, >> sonuna ekler. 2>&1 “2'yi, 1'in şu anki hedefine kopyala” demektir. Sondaki & komutu arka planda başlatır.
sort < girdi.txt > cikti.txt 2> hatalar.log için kabuğun kurduğu tanımlayıcı tablosu.Yönlendirmeler soldan sağa uygulanır. Kabuk her birini bir dup2 çağrısına çevirir. Sıra bu yüzden sonucu değiştirir.
/yok dizini yoktur. ls /yok /tmp > c.txt 2>&1 ile ls /yok /tmp 2>&1 > c.txt komutlarında hata iletisi nereye gider?
- Birinci komut.
> c.txt: 1 → c.txt.2>&1: 2, 1'in o anki hedefine, yani c.txt'ye kopyalanır. Hem liste hem hata dosyaya gider. - İkinci komut.
2>&1: 1 o anda terminaldir; 2 de terminale bağlanır.> c.txt: yalnız 1 değişir. Liste dosyaya, hata ekrana gider. 2>&1bir “bağ” kurmaz, o anki hedefi kopyalar. 1 sonradan değişse de 2 eski hedefte kalır.
15.2.3Boru hattı
Boru hattı (pipeline), bir programın çıktısını bir sonrakinin girdisine bağlayan komut zinciridir. cat log.txt | grep hata | sort | uniq -c satırını ele alalım (Şekil 15.6). Kabuk satırı ayrıştırır, 3 boru ve 4 süreç oluşturur. Dört süreç aynı anda çalışır. Her biri yalnız kendi standart girdisini ve çıktısını bilir.
cat log.txt | grep hata | sort | uniq -c: yedi satır, beş eşleşme, sıralı beş satır ve iki sayım.cat dosyanın 7 satırını boruya yazar. Bir borunun çekirdekteki tamponu sınırlıdır; Linux'ta varsayılan 64 KiB'tır. Tampon dolarsa yazan süreç bloke olur, okuyan süreç boşalttıkça devam eder. Bu, üretici–tüketici sorununun çekirdekte hazır bir çözümüdür. grep satır satır okur ve eşleşen 5 satırı hemen iletir. Filtre durumsuzdur, akışı bekletmez. sort ise farklıdır. Girdinin sonunu (EOF) görmeden tek satır bile yazamaz, çünkü son satır sıralamada ilk sıraya geçebilir. uniq -c yalnızca ardışık tekrarları sayar. Bu yüzden önüne sort gerekir. Sonuç iki satırdır: 2 × “hata: ağ” ve 3 × “hata: disk”.
uniq'in bütün tekrarları birleştirdiğini sanmak. Sıralanmamış beş satırda (disk, ağ, disk, ağ, disk) hiçbir satır kendinden öncekinin aynısı değildir. uniq -c beş ayrı satır, her birinin önünde 1 yazar. Python benzetimi de aynı sonucu verir.
15.2.4Kod izleme: kabuk bir boru hattını nasıl kurar?
Kabuk ls | wc -l satırını dört sistem çağrısıyla kurar: pipe, fork, dup2 ve exec. Aşağıdaki kod bu işi yapan kabuğun basitleştirilmiş hâlidir:
int fd[2];
pipe(fd); /* 3: okuma, 4: yazma */
if (fork() == 0) { /* çocuk 1 */
dup2(fd[1], 1);
close(fd[0]); close(fd[1]);
execlp("ls", "ls", NULL);
}
if (fork() == 0) { /* çocuk 2 */
dup2(fd[0], 0);
close(fd[0]); close(fd[1]);
execlp("wc", "wc", "-l", NULL);
}
/* kabuk: iki ucu da kapat */
close(fd[0]); close(fd[1]);
wait(NULL); wait(NULL);Kodun ilk satırı iki elemanlı bir dizi ayırır. pipe(fd) çekirdekte bir tampon ve iki uç oluşturur. Çekirdek her zaman en küçük boş tanımlayıcı numarasını verir. 0, 1 ve 2 dolu olduğu için okuma ucu 3, yazma ucu 4 olur. Birinci fork çocuk süreci doğurur. Çocuk, ebeveynin tanımlayıcı tablosunun bir kopyasını alır; iki süreç aynı boruyu gösterir. Çocuk dup2(fd[1], 1) ile 1 numaralı girdiyi borunun yazma ucuna yöneltir. Artık gereksiz olan 3 ve 4'ü kapatır. execlp sürecin görüntüsünü ls ile değiştirir. Tanımlayıcı tablosu exec'ten sonra da korunur. ls hiçbir şeyden habersiz 1'e yazar ve veri boruya akar. İkinci çocuk aynı işi okuma ucu için yapar: dup2(fd[0], 0), iki ucu kapatır ve wc -l olur. Son olarak kabuk iki ucu da kapatır ve iki çocuğu bekler.
| Adım | Kabuk 0 1 2 3 4 | Çocuk 1 | Çocuk 2 |
|---|---|---|---|
pipe(fd) | tty tty tty o y | — | — |
1. fork | tty tty tty o y | tty tty tty o y | — |
dup2(4, 1) + iki close | tty tty tty o y | tty y tty – – | — |
execlp("ls") | aynı | tty y tty – – (artık ls) | — |
2. fork, dup2(3, 0), exec("wc") | tty tty tty o y | aynı | o tty tty – – |
| Kabuk iki ucu kapatır | tty tty tty – – | aynı | aynı |
ls'te, okuma ucu yalnız wc'de açık.Son satırdaki iki close önemsiz görünür ama sistemin doğru çalışması onlara bağlıdır. Bir borunun okuyucusu ancak bütün yazma uçları kapanınca EOF görür. Kabuk 4'ü kapatmasaydı yazma ucu açık kalırdı. ls bitip kendi ucunu kapatsa bile wc daha veri gelebileceğini varsayıp sonsuza dek beklerdi. Kabuk da wc'yi beklediği için ikisi birlikte asılı kalırdı. Ters durumda, okuma ucunun hepsi kapanmışken boruya yazan süreç SIGPIPE sinyali alır ve varsayılan olarak sonlanır.
Çocuklarda fazla uçları kapatmayı unutmak. wc'yi çalıştıran çocuk 4'ü kapatmazsa, kendi elinde tuttuğu yazma ucu yüzünden kendi EOF'unu hiç göremez. Kural kısadır: her süreç kullanmadığı her boru ucunu kapatır.
15.3Çekirdeğin yapısı
15.3.1Monolitik çekirdeğin bileşenleri
Linux çekirdeği üç ana bileşene ayrılır (Şekil 15.8). G/Ç bileşeni terminalleri, ağı ve dosya sistemlerini yönetir. Bellek yönetimi sanal belleği, sayfalamayı ve sayfa önbelleğini yürütür. Süreç yönetimi süreçleri ve iş parçacıklarını oluşturur, sinyalleri işler ve CPU'yu çizelgeler. Hepsinin üstünde sistem çağrısı arayüzü, altında kesmeler ve dağıtıcı (dispatcher) bulunur. Dağıtıcı bağlam değiştirmeyi yapan koddur. Klasik 4.4BSD çekirdeği de aynı üç bileşene ayrılırdı.
G/Ç bileşeni kendi içinde üç sütuna ayrılır. Karakter aygıtları terminaller ve satır disiplininden geçer. Ağ aygıtları soket katmanından ve protokollerden geçer. Blok aygıtları dosya sistemlerinden, genel blok katmanından ve G/Ç çizelgeleyicisinden geçer. Üç sütun tepede sanal dosya sisteminde (VFS) buluşur. Şekildeki kutuların hepsi tek bir adres alanında, çekirdek modunda çalışır. Bir bileşen ötekinin işlevini doğrudan çağırabilir; mesaj geçirmeye ya da bağlam değiştirmeye gerek yoktur. Linux'u monolitik yapan budur. Bedeli de açıktır: herhangi bir bileşendeki hata, örneğin bir sürücüdeki geçersiz işaretçi, bütün çekirdeği çökertebilir.
15.3.2Yüklenebilir çekirdek modülleri
Monolitik bir çekirdek, akla gelebilecek her aygıtın sürücüsünü baştan içerseydi çok büyük olurdu. Linux bu sorunu modüllerle çözer.
Yüklenebilir çekirdek modülü (loadable kernel module, LKM), sistem çalışırken çekirdeğe eklenip çıkarılabilen, .ko uzantılı nesne dosyasıdır. Yüklenen modül çekirdeğin adres alanında, çekirdek ayrıcalığıyla çalışır.
Sürücülerin çoğu, dosya sistemlerinin ve ağ protokollerinin bir kısmı modüldür. Bir modülün yüklenmesi altı adımda izlenebilir (Şekil 15.9). Örnek olarak Intel ağ kartı sürücüsü e1000e yüklensin.
- İstek. Yönetici
modprobe e1000eyazar. Daha sık görülen yol otomatik yüklemedir: aygıt sisteme takılınca çekirdek bir olay üretir, kullanıcı alanındakiudevaygıtın kimliğine uyan modülü bulup yükler. - Bağımlılıklar.
modprobe,/lib/modules/$(uname -r)/modules.depdosyasından modülün hangi modüllere ihtiyaç duyduğunu okur ve önce onları yükler. Daha ilkelinsmodbunu yapmaz; verilen tek dosyayı yüklemeye çalışır. - Kopyalama ve imza.
finit_module()sistem çağrısı modül dosyasını çekirdek belleğine kopyalar. ÇağıranınCAP_SYS_MODULEyeteneği olmalıdır. Secure Boot açıkken modülün imzası doğrulanır; imzasız modül reddedilir. - Bağlama. Çekirdek modülün yer değiştirmelerini yapar. Modülün kullandığı her dış sembolü çekirdeğin sembol tablosunda arar. Yalnız
EXPORT_SYMBOLile dışa açılmış çekirdek işlevleri modüle görünür. - Başlatma. Modülün
module_initile işaretlenmiş işlevi çalışır. Sürücü kendini ilgili alt sisteme kaydeder; bir PCI kartı için bupci_register_driverçağrısıdır. - Eşleşme. PCI alt sistemi sürücünün desteklediği kimliklerle sistemdeki aygıtları karşılaştırır. Eşleşen her aygıt için sürücünün
probe()işlevi çağrılır veeth0benzeri bir ağ arayüzü oluşur.
modprobe, finit_module(), imza denetimi, yer değiştirme ve sembol bağlama, module_init() ve probe().Yüklü modüller lsmod ile listelenir, rmmod ile kaldırılır. Modül sistemi çekirdeği esnek kılar ama koruma getirmez. Modül, çekirdeğin kendisi kadar ayrıcalıklıdır. Bu yüzden yükleme yetkisi sıkı denetlenir.
Çekirdeğin kaynak ağacı 2025 başında 40 milyon satırı geçti. Bu satırların büyük kısmı sürücülerdir. Son yıllarda iki yenilik çekirdeğin güvenliğini artırmaya yöneldi. Birincisi Rust'tır. Rust desteği 6.1'de çekirdeğe girdi; ilk Rust sürücüsü, bir ağ PHY sürücüsü, 6.8'de eklendi. Rust'ın tür sistemi, C sürücülerinde sık görülen bellek hatalarının bir bölümünü derleme anında yakalar. İkincisi eBPF'tir. eBPF, kullanıcının yazdığı küçük programları çekirdekte güvenle çalıştırmaya yarar. Program yüklenmeden önce bir doğrulayıcıdan geçer. Doğrulayıcı programın sonlanacağını ve yalnız izin verilen belleğe eriştiğini kanıtlamadan yüklemeye izin vermez. eBPF ile ağ paketleri süzülür, çekirdek olayları izlenir; bir modül yazmadan çekirdeğin davranışı genişletilir.
15.4Süreçler ve iş parçacıkları
15.4.1fork ve execve çekirdekte
Linux'ta yeni bir program iki adımda başlar (Şekil 15.10). Kabuk (PID 501) önce fork() ile kendini kopyalar; kopya PID 748 alır ve yine kabuk kodunu çalıştırır. Kopya sonra execve() ile kendi görüntüsünü ls ile değiştirir. PID değişmez; değişen, sürecin çalıştırdığı programdır.
fork() yeni bir süreç (aynı program), execve() aynı süreçte yeni bir program üretir.fork() çekirdekte şu işleri yapar:
- Yeni bir
task_structayırır, ebeveyninkinden doldurur ve yeni bir PID atar. - Çocuğa ayrı bir çekirdek yığını ayırır. Ebeveynin kaydedilmiş yazmaçlarını kopyalar; yalnız dönüş değeri yazmacını çocukta 0 yapar.
fork'un “iki kez dönmesi” buradan gelir. - Sayfa tablolarını kopyalar ve yazılabilir sayfaları iki tarafta da salt okunur işaretler. Bu yazınca kopyalamadır (COW): sayfa ancak biri ona yazmaya kalkınca kopyalanır.
- Açık dosyaları paylaştırır. Çocuğun tanımlayıcı tablosu ebeveyninkinin kopyasıdır; her girdi aynı
struct filenesnesini gösterir ve nesnenin referans sayısı artar. - Sinyal işleyicilerini, çalışma dizinini ve kimlik bilgilerini kopyalar.
execve() ise eski görüntüyü atar. Önce dosyanın ELF başlığını okur. Sürecin eski adres alanını tanımlayan mm_struct'ı bırakır ve yenisini kurar. Programın kod ve veri kesimlerini dosyadan mmap ile eşler; hiçbir sayfayı hemen okumaz. Dosyanın setuid ya da setgid biti açıksa sürecin yeni kimlik bilgilerini (cred) hazırlar. Son olarak program sayacını ayarlar. Dinamik bağlanmış bir programda ilk çalışan kod programın kendisi değil, dinamik bağlayıcı ld.so'dur; o, paylaşımlı kütüphaneleri eşleyip programın giriş noktasına atlar. Başarılı bir execve asla geri dönmez, çünkü onu çağıran kod artık yoktur. Başarısız olursa −1 döner. Bu durumda çocuk geleneksel olarak _exit(127) ile çıkar; kabuğun “komut bulunamadı” çıkış kodu 127 buradan gelir.
Eski UNIX'ler fork'ta sürecin veri ve yığın kesimlerini gerçekten kopyalardı. Linux yalnız sayfa tablolarını kopyalar. fork'ların çoğunu hemen bir exec izlediği için kopyalanan sayfa sayısı çok azdır. Bir kabuk binlerce sayfalık adres alanına sahip olsa bile fork'u mikrosaniyeler sürer.
15.4.2Süreç yönetimi çağrıları
Bir sürecin yaşamı birkaç sistem çağrısıyla yönetilir (Tablo 15.4). clone ve clone3 genel yaratma çağrılarıdır; fork onların özel bir hâlidir. exit ile _exit arasındaki fark küçük ama önemlidir: exit bir kütüphane işlevidir, stdio tamponlarını boşaltır ve atexit işlevlerini çağırır; _exit doğrudan çekirdeğe gider.
| Çağrı | İşlev |
|---|---|
fork() | Süreci kopyalayarak yeni süreç oluşturur (COW) |
clone(), clone3() | Neyin paylaşılacağı bayraklarla seçilen yeni görev (iş parçacığı, konteyner) |
execve() | Süreç görüntüsünü yeni programla değiştirir |
waitpid() | Çocuğun bitmesini bekler, çıkış durumunu alır, zombiyi toplar |
exit() / _exit() | Süreci sonlandırır (exit stdio tamponlarını boşaltır) |
kill() | Sürece sinyal gönderir; sinyal sonlandırma olmak zorunda değil |
sigaction(), sigprocmask() | İşleyici atar; engellenen sinyal kümesini okur ve değiştirir |
sigpending(), sigsuspend() | Bekleyen sinyalleri döndürür; maskeyi geçici değiştirip sinyal bekler |
rt_sigreturn() | İşleyiciden dönüşte kaydedilmiş bağlamı geri yükler |
alarm() / pause() | n saniye sonra SIGALRM üretir / bir sinyal gelene kadar uyur |
fork, execve, çalışma, exit, zombi ve waitpid ile toplanma.Biten bir süreç hemen yok olmaz (Şekil 15.11). Belleğini ve dosyalarını bırakır, ama task_struct'ı çıkış durumunu taşıyarak kalır. Bu hâldeki sürece zombi denir; ps onu Z harfiyle gösterir. Ebeveyn waitpid ile durumu okuyunca zombi tamamen silinir. Ebeveyn önce ölürse çocuklar PID 1'e (ya da kendini “alt süreç toplayıcı” ilan etmiş bir sürece) bağlanır; o da onları toplar. Süreç ağacı pstree -p ya da ps -ef --forest ile görülür.
15.4.3Süreç tanımlayıcısı: task_struct
Ders kitaplarındaki süreç kontrol bloğunun (PCB) Linux'taki karşılığı struct task_struct'tır (Şekil 15.12). Yapı büyüktür; x86-64'te birkaç kilobayt tutar ve sürüme göre değişir. Ama içindekilerin çoğu doğrudan veri değil, başka yapılara işaretçidir.
task_struct ve gösterdiği yapılar. Soldakiler iş parçacıkları arasında paylaşılabilir; her görevin kendi çekirdek yığını vardır.Yapının kendi içinde tuttuğu alanlar görevin kimliğini ve çizelgeleme durumunu anlatır. __state görevin hâlidir. pid ve tgid kimliklerdir. prio ve policy önceliği ve çizelgeleme politikasını verir. se alanı gömülü bir sched_entity yapısıdır; adil çizelgeleyicinin kullandığı vruntime ve deadline değerleri buradadır. parent ve children süreç ağacını kurar. Geri kalan alanlar işaretçidir:
mm→mm_struct: adres alanı. Bellek bölgelerinin (VMA) listesi ve en üst sayfa tablosunun adresi (pgd) buradadır. Çekirdek iş parçacıklarınınmm'si boştur.files→files_struct: dosya tanımlayıcısı tablosu. Her girdi birstruct file'ı gösterir.fs→fs_struct: kök dizin, çalışma dizini ve umask.sighandvesignal: sinyal işleyicileri ve süreç düzeyindeki sinyal durumu.cred: uid, gid ve yetenekler.nsproxy: görevin içinde yaşadığı ad alanları (PID, ağ, bağlama …).cgroups→css_set: görevin bağlı olduğu kontrol grupları ve sınırları.
Bu düzenin sonucu, Linux'un en şaşırtıcı tasarım kararıdır: süreç ile iş parçacığı arasında yapısal bir fark yoktur. İkisi de bir görevdir, yani bir task_struct'tır. Fark, alt yapıların paylaşılıp paylaşılmadığıdır. İki ayrı sürecin mm işaretçileri farklı mm_struct'ları gösterir. Aynı sürecin iki iş parçacığının mm işaretçileri ise aynı nesneyi gösterir.
Linux'ta her görevin çekirdek içi benzersiz kimliği pid'dir. Aynı süreçteki iş parçacıklarının ortak kimliği iş parçacığı grubu kimliğidir (thread group ID, tgid). Kullanıcının “PID” diye gördüğü değer tgid'dir: getpid() tgid'i, gettid() görevin kendi pid'ini döndürür.
PID'i 900 olan tek iş parçacıklı bir süreç iki kez pthread_create çağırıyor. Çekirdek yeni görevlere sırayla 901 ve 902 kimliklerini veriyor. Her görevde getpid() ve gettid() ne döndürür?
- Ana iş parçacığı:
pid= 900,tgid= 900.getpid()= 900,gettid()= 900. Grubun lideri, kimliği grup kimliğine eşit olan görevdir. - Yeni iş parçacıkları
CLONE_THREADile doğduğu için grubuntgid'ini miras alır:getpid()ikisinde de 900'dür.gettid()901 ve 902'dir. pstek bir 900 süreci gösterir.ps -Lya da/proc/900/task/dizini üç görevi ayrı ayrı listeler.
__state alanının değerleri ps'in STAT sütununda harflerle görünür. R çalışıyor ya da çalışmaya hazırdır; Linux bu iki durumu ayırmaz. S kesilebilir uykudur: görev bir olayı bekler ve bir sinyalle uyandırılabilir. D kesilemez uykudur: görev çoğunlukla bir disk işleminin bitmesini bekler ve sinyaller onu uyandırmaz. T durdurulmuş, Z zombi demektir. D durumundaki bir görev SIGKILL ile bile hemen ölmez. Yanıt vermeyen bir ağ dosya sistemi bu yüzden kill -9'a direnen süreçler üretebilirdi. Çekirdek bu sorunu azaltmak için bekleyişlerin çoğunda artık TASK_KILLABLE kullanır: görev yalnız ölümcül bir sinyalle uyandırılabilir.
15.4.4clone() bayrakları ve NPTL
Süreç ile iş parçacığı aynı yapıyla temsil edildiğine göre, ikisini aynı çağrı yaratabilir. O çağrı clone()'dur.
clone(bayraklar, …) yeni bir görev oluşturan Linux sistem çağrısıdır. Bayrakların her biri, ebeveynin bir alt yapısının çocukla paylaşılacağını ya da çocuğa yeni bir kopyasının verileceğini seçer. Bayrak verilmeyen yapı kopyalanır.
Paylaşım bayrakları task_struct'ın işaretçileriyle birebir eşleşir (Tablo 15.5). CLONE_VM verilirse çocuğun mm'si ebeveyninkiyle aynı nesneyi gösterir; verilmezse adres alanı COW ile kopyalanır. CLONE_FILES tanımlayıcı tablosunu, CLONE_FS kök ve çalışma dizinini, CLONE_SIGHAND sinyal işleyicilerini paylaştırır. CLONE_THREAD çocuğu ebeveynin iş parçacığı grubuna sokar: çocuk aynı tgid'i alır ve dışarıdan aynı süreç görünür.
| Bayrak | Verilirse | Verilmezse |
|---|---|---|
CLONE_VM | adres alanı (mm) paylaşılır | COW ile kopyalanır |
CLONE_FILES | tanımlayıcı tablosu paylaşılır | tablo kopyalanır (aynı struct file'lar) |
CLONE_FS | kök, çalışma dizini, umask paylaşılır | kopyalanır |
CLONE_SIGHAND | sinyal işleyicileri paylaşılır (CLONE_VM gerektirir) | kopyalanır |
CLONE_THREAD | aynı iş parçacığı grubu, aynı tgid (CLONE_SIGHAND gerektirir) | yeni süreç, yeni tgid |
CLONE_NEWPID, CLONE_NEWNS, CLONE_NEWNET, CLONE_NEWUTS, CLONE_NEWIPC, CLONE_NEWUSER, CLONE_NEWCGROUP | çocuk yeni bir ad alanında başlar | ebeveynin ad alanında kalır |
Üç tipik çağrı bayrakların gücünü gösterir (Şekil 15.13).
fork(). glibc'defork, yalnızSIGCHLDbayrağıyla çağrılan birclone'dur. Hiçbir paylaşım bayrağı yoktur; çocuk her yapının kopyasını alır, bellek COW ile paylaşılır. Ayrı PID, ayrı TGID: yeni bir süreç.SIGCHLDburada bir paylaşım bayrağı değil, çocuk bitince ebeveyne gönderilecek sinyalin numarasıdır.pthread_create(). Linux'un iş parçacığı kütüphanesi NPTL (Native POSIX Thread Library) şu bayrakları verir:CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID. Her şey paylaşılır ve yeni görev aynı TGID'e girer. Son üç bayrak iş parçacığının yerel deposunu kurar ve bitişinipthread_join'e bildirir.- Konteyner. Ad alanı bayrakları çocuğa dünyanın yeni bir görünümünü verir.
CLONE_NEWPIDile doğan çocuk kendi PID ad alanının ilk sürecidir: içeride kendini PID 1 sanır, dışarıda sıradan bir PID taşır. Konteynerlerin temeli budur.
clone: çocuk yapıların kopyasını ve yeni ad alanlarını alır; içeride PID 1'dir.NPTL'in kurduğu model 1:1'dir: her kullanıcı iş parçacığı bir çekirdek görevine karşılık gelir. Çekirdek iş parçacıklarını doğrudan çizelgeler; biri bloke olunca ötekiler çalışmayı sürdürür, çok çekirdekli makinede gerçekten paralel çalışırlar. NPTL 2003'te, 2.6 serisiyle geldi. Ondan önceki LinuxThreads da 1:1'di ama her iş parçacığı ayrı bir PID taşıyordu ve sinyal işleme POSIX'e uymuyordu. CLONE_THREAD ve TGID kavramı bu sorunları çözmek için eklendi.
Bayraklar bağımsız olarak birleştirilebildiği için “yarım süreç, yarım iş parçacığı” görevler de mümkündür. CLONE_VM | CLONE_FILES ile, ama CLONE_THREAD olmadan doğan bir görev ayrı bir süreç olarak görünür; kendi TGID'i vardır. Yine de ebeveyniyle aynı belleği ve aynı tanımlayıcı tablosunu kullanır. Birinin açtığı dosya ötekinde de açıktır. Klasik UNIX'te böyle bir ara biçim yoktur. clone3() (5.3) aynı işi daha geniş bir argüman yapısıyla yapar; yeni bayraklar artık ona eklenir.
ps'teki PID'in bir iş parçacığını gösterdiğini sanmak. Çok iş parçacıklı bir sunucu ps'te tek satırdır, çünkü ps TGID'leri listeler. Bir iş parçacığına sinyal göndermek için tgkill ve görev kimliği gerekir; kill sinyali bütün gruba gönderir ve grup içinden uygun bir iş parçacığı onu alır.
15.5Sinyaller
15.5.1POSIX sinyalleri
Sinyal (signal), bir sürece bir olayı bildiren yazılım kesmesidir. Her sinyalin bir numarası ve varsayılan bir eylemi vardır. Süreç her sinyal için varsayılan eylemi, yok saymayı ya da kendi işleyicisini seçebilir.
Sinyaller iki türlü doğar. Senkron sinyaller sürecin kendi komutunun sonucudur: geçersiz bellek erişimi SIGSEGV, tamsayıyı sıfıra bölme SIGFPE üretir. Eş zamansız sinyaller dışarıdan gelir: klavyeden Ctrl-C SIGINT, kill komutu varsayılan olarak SIGTERM üretir. Bir işleyicinin olağan işleri kaynakları temizlemek, günlük yazmak ya da yapılandırmayı yeniden okumaktır.
| Sinyal | No | Ne zaman üretilir? | Varsayılan |
|---|---|---|---|
SIGHUP | 1 | Terminal bağlantısı koptu; cinlerde “yapılandırmayı yeniden oku” | sonlandır |
SIGINT | 2 | Ctrl-C (ön plan süreç grubuna) | sonlandır |
SIGQUIT | 3 | Ctrl-\ | çekirdek dökümü |
SIGABRT | 6 | abort() | çekirdek dökümü |
SIGFPE | 8 | Aritmetik hata | çekirdek dökümü |
SIGKILL | 9 | kill -9; yakalanamaz, yok sayılamaz | sonlandır |
SIGUSR1 / SIGUSR2 | 10 / 12 | Kullanıcı tanımlı | sonlandır |
SIGSEGV | 11 | Geçersiz bellek erişimi | çekirdek dökümü |
SIGPIPE | 13 | Okuyanı kalmamış boruya yazma | sonlandır |
SIGALRM | 14 | alarm() süresi doldu | sonlandır |
SIGTERM | 15 | kill'in varsayılanı; nazik sonlandırma isteği | sonlandır |
SIGCHLD | 17 | Çocuk bitti ya da durdu | yok say |
SIGCONT / SIGSTOP | 18 / 19 | Sürdür / durdur (yakalanamaz); Ctrl-Z SIGTSTP (20) üretir | sürdür / durdur |
SIGKILL ile SIGTERM'in ikisi de kill komutuyla gönderilir. Fark yakalanabilirliktedir. SIGTERM'i süreç yakalayabilir; açık dosyalarını kapatır, geçici dosyalarını siler ve nazikçe çıkar. SIGKILL ve SIGSTOP ise yakalanamaz, engellenemez ve yok sayılamaz. Yönetici, kontrolden çıkmış bir süreci her durumda durdurabilsin diye böyle tasarlanmışlardır. Doğru sıra önce SIGTERM, gerekirse SIGKILL'dir. SIGRTMIN ile SIGRTMAX arasındaki numaralar (glibc'de 34–64) gerçek zamanlı sinyallere ayrılmıştır. Klasik sinyallerden farklı olarak bunlar kuyruğa girer: aynı sinyal üç kez gelirse üç kez teslim edilir.
15.5.2Sinyalin teslim yolu
Bir sinyalin üretilmesi ile işlenmesi ayrı anlardır (Şekil 15.14). Arada sinyal bekler. Ctrl-C örneği bu yolu baştan sona gösterir.
- Üretim. Kullanıcı Ctrl-C'ye basar. tty sürücüsünün satır disiplini bu karakteri tanır ve ön plan süreç grubundaki her sürece
SIGINTüretir. Çekirdeğin yaptığı tek iş, hedef görevin bekleyen sinyal kümesinde 2 numaralı sinyalin bitini açmaktır. - Denetim. Sinyal hemen işlenmez. Süreç bir sistem çağrısından ya da bir kesmeden kullanıcı moduna dönerken çekirdek “bekleyen ve engellenmemiş bir sinyal var mı?” diye bakar. Uyuyan bir süreç ise sinyalle uyandırılır ve o da dönüş yolunda bu denetimden geçer.
- Çerçeve. Süreç
sigactionile bir işleyici kurmuşsa çekirdek sürecin kullanıcı yığınına bir sinyal çerçevesi yazar. Çerçevede kesintiye uğrayan anın yazmaçları ve bir dönüş trampolini bulunur. Çekirdek program sayacını işleyicinin adresine ayarlar. - İşleyici. İşleyici kullanıcı modunda, sürecin kendi koduyla çalışır.
- Dönüş. İşleyici dönünce trampolin
rt_sigreturnsistem çağrısını yapar. Çekirdek çerçevedeki yazmaçları geri yükler. - Devam. Süreç kesildiği komuttan (şekilde PC = 0x401a2c) devam eder. İşleyici kurulmamış olsaydı varsayılan eylem uygulanırdı;
SIGINTiçin bu, sürecin sonlanmasıdır.
rt_sigreturn çağrısıyla olur.İşleyici, kesilen kodun ortasında, onunla aynı iş parçacığında çalışır. Süreç tam o anda malloc'un içinde, öbeğin kilidini tutarken kesilmiş olabilir. İşleyici de malloc ya da printf çağırırsa aynı kilidi ikinci kez almaya çalışır ve süreç kilitlenir. Bu yüzden işleyicide yalnız eş zamansız sinyal güvenli (async-signal-safe) işlevler kullanılabilir: write, _exit gibi. En sağlam kalıp, işleyicide yalnız volatile sig_atomic_t türünden bir bayrağı 1 yapmak ve asıl işi ana döngüde bayrağa bakarak yürütmektir.
sigreturn'ü işleyiciye giriş çağrısı sanmak. İşleyiciye giriş bir sistem çağrısı değildir; çekirdek program sayacını değiştirerek süreci işleyiciye “yollar”. rt_sigreturn işleyiciden dönüşü yapar ve onu programcı değil, trampolin çağırır.
15.5.3Kod izleme: bekleyen sinyalleri seçme
Çekirdek her görev için iki bit kümesi tutar: bekleyen sinyaller ve engellenen sinyaller. n numaralı sinyal (n − 1). bittedir. Aşağıdaki kod, kullanıcı moduna dönüşteki denetimin sadeleştirilmiş hâlidir:
/* kullanıcı moduna dönerken (çekirdek) */
void sinyal_bak(task *t) {
unsigned long h = t->bekl & ~t->engl;
while (h) {
int sig = __builtin_ctzl(h) + 1;
t->bekl &= ~(1UL << (sig - 1));
teslim_et(t, sig); /* işleyici/varsayılan */
h = t->bekl & ~t->engl;
}
}
/* önce: sigprocmask(SIG_BLOCK, {SIGINT}) */
/* sonra: sigprocmask(SIG_UNBLOCK, {SIGINT}) */Üçüncü satır teslime hazır kümeyi hesaplar: hazır = bekleyen & ~engelli. Döngü bu küme boşalana kadar döner. __builtin_ctzl(h) sayının sağdaki sıfır bitlerini sayar, yani en düşük açık bitin konumunu verir; +1 bunu sinyal numarasına çevirir. Böylece en küçük numaralı hazır sinyal önce seçilir. Seçilen sinyalin biti bekleyen kümesinden silinir ve sinyal teslim edilir. Teslimden sonra küme yeniden hesaplanır, çünkü işleyicinin çalışması kümeleri değiştirebilir.
Senaryo şöyledir. Süreç kritik bir bölüme girmeden önce sigprocmask(SIG_BLOCK, {SIGINT}) ile SIGINT'i engeller. Uyurken üç sinyal gelir: Ctrl-C (2), kill -USR1 (10) ve bir çocuğun bitişi (17). SIGUSR1 için bir işleyici kuruludur.
| Olay | bekleyen | engelli | hazır | Sonuç |
|---|---|---|---|---|
SIG_BLOCK {SIGINT} | {} | {2} | {} | — |
| 2, 10, 17 gelir | {2, 10, 17} | {2} | — | yalnız bitler açılır |
| Kullanıcı moduna dönüş | {2, 10, 17} | {2} | {10, 17} | en küçük: 10 |
| 10 teslim | {2, 17} | {2} | {17} | SIGUSR1 işleyicisi çalışır |
| 17 teslim | {2} | {2} | {} | SIGCHLD: varsayılan, yok say |
SIG_UNBLOCK {SIGINT} | {2} | {} | {2} | dönüşte aynı denetim |
| 2 teslim | {} | {} | {} | işleyici yok: süreç sonlanır |
Bit gösterimiyle, üç sinyal geldikten sonra bekleyen kümesi 0x10202'dir: 2. sinyal 1. bit (0x2), 10. sinyal 9. bit (0x200), 17. sinyal 16. bit (0x10000). Engelli küme 0x2'dir. Hazır küme 0x10202 & ~0x2 = 0x10200 olur. En düşük açık bit 9'dur, yani sinyal 10.
SIGINT varsayılan eylemle süreci sonlandırır.İzlemeden iki ders çıkar. Birincisi, engellemek ertelemektir, yok saymak değildir. SIGINT kritik bölüm boyunca kaybolmadı; bekleyen kümesinde durdu ve engel kalkar kalkmaz teslim edildi. Yok saymak (SIG_IGN) ise sinyali geldiği anda atar. İkincisi, klasik sinyaller sayılmaz. Bekleyen kümesi bir bit kümesidir. Ctrl-C kritik bölüm sırasında üç kez basılsaydı bit yine tek kez açık olurdu ve sinyal tek kez teslim edilirdi.
15.6Çizelgeleme
15.6.1Çizelgeleme sınıfları
Linux çizelgeleyicisi tek bir algoritma değil, öncelik sırasına dizilmiş çizelgeleme sınıflarıdır. Her görev bir sınıfa aittir. Bir CPU boşalınca çekirdek sınıflara sırayla sorar ve çalıştırılacak görevi veren ilk sınıf kazanır. Sıra şöyledir:
- stop: çekirdeğin bir CPU'yu durdurmak ya da görev taşımak için kullandığı iç sınıf.
- deadline:
SCHED_DEADLINEpolitikası. Görev bir çalışma süresi, bir son tarih ve bir dönem bildirir; çizelgeleyici en erken son tarihi önce çalıştırır. - rt:
SCHED_FIFOveSCHED_RR, 1–99 arası sabit öncelikler. FIFO görevi kendi isteğiyle bırakana kadar çalışır; RR aynı öncelikteki görevler arasında zaman dilimiyle döner. - fair:
SCHED_NORMAL(adı eskidenSCHED_OTHER),SCHED_BATCHveSCHED_IDLE. Sıradan süreçlerin hepsi buradadır. 6.6'dan beri bu sınıfı EEVDF yönetir. - idle: CPU'da başka iş yokken çalışan boşta görevi.
6.12 ile gelen sched_ext, çizelgeleme politikasının eBPF programı olarak yüklenmesine izin verir. Bir şirket kendi iş yüküne uygun çizelgeleyiciyi çekirdeği yeniden derlemeden deneyebilir. Program hata yaparsa çekirdek onu kaldırıp görevleri adil sınıfa geri verir. Mekanizma çekirdekte, politika kullanıcıdadır.
Her CPU'nun kendi çalışma kuyruğu (struct rq) vardır. Her sınıf bu kuyrukta kendi veri yapısını tutar. Görevler CPU'lar arasında arada bir dengelenir. Yük dengeleyici CPU'ları çizelgeleme alanları (scheduling domains) hiyerarşisinde görür: aynı çekirdeğin donanım iş parçacıkları, aynı yongadaki çekirdekler, aynı NUMA düğümündeki yongalar. Görev önce yakın düzeyde taşınır, çünkü önbelleği ve belleği orada daha sıcaktır.
15.6.2O(n)'den CFS'e
Adil sınıfın seçim kuralı yirmi yılda dört kez değişti (Tablo 15.8). 2.4 çekirdeğindeki çizelgeleyici her seçimde bütün hazır görevleri tarardı. Görev sayısıyla doğrusal büyüyen bu maliyet, yüzlerce görevli sunucularda sorun oldu. 2.6.0 ile gelen O(1) çizelgeleyicisi her öncelik düzeyi için ayrı bir liste tuttu: 140 listelik bir dizi ve hangi listenin boş olmadığını gösteren bir biteşlem. En yüksek öncelikli görev, biteşlemde ilk açık biti bulan tek bir komutla seçiliyordu. Zaman dilimi biten görev ikinci bir “süresi dolmuş” diziye geçer, etkin dizi boşalınca iki dizinin rolü değişirdi. Hız sorunu çözüldü, ama etkileşimli görevleri tanımak için kullanılan sezgisel kurallar karmaşıklaştı ve öngörülemez davranışlar üretti.
| Sürüm | Seçim kuralı | Veri yapısı |
|---|---|---|
| 2.4 (2001) | O(n): her seçimde bütün hazır görevler taranır | tek liste |
| 2.6.0–2.6.22 (2003) | O(1): öncelik başına liste, etkin ve süresi dolmuş iki dizi | 140 listelik dizi + biteşlem |
| 2.6.23–6.5 (2007) | CFS: en küçük vruntime | kırmızı-siyah ağaç |
| 6.6+ (2023) | EEVDF: uygun görevler (v ≤ V) içinde en erken sanal son tarih | aynı ağaç, alt ağaç bilgisiyle zenginleştirilmiş |
2.6.23'te Ingo Molnár'ın tamamen adil çizelgeleyicisi (Completely Fair Scheduler, CFS) geldi. CFS'in fikri ideal bir çok görevli işlemciye öykünmektir. İdeal işlemcide n görev aynı anda, her biri 1/n hızla çalışırdı. Gerçek işlemci bunu yapamaz, ama hangi görevin kendi payının en çok gerisinde kaldığını izleyebilir. Bu izleme her görevin sanal çalışma süresiyle yapılır.
15.6.3Ağırlıklar ve sanal çalışma süresi
Her görevin nice değerinden türeyen bir ağırlığı w vardır; nice 0'ın ağırlığı 1024'tür. Görev gerçek zamanda Δt süre çalışınca sanal çalışma süresi (vruntime) şu kadar artar:
nice değeri −20 ile +19 arasındadır; küçük değer yüksek öncelik demektir. Ağırlıklar çekirdekteki sched_prio_to_weight tablosundan okunur. Tablo her nice adımında ağırlığı yaklaşık 1,25 kat değiştirir: nice 1 → 820, nice 0 → 1024, nice −1 → 1277. Sonuç, nice değeri bir artan görevin CPU payının kabaca %10 azalmasıdır. Uçlar nice −20 için 88 761, nice 19 için 15'tir.
Formül iki uçta sezgiyle örtüşür. Ağırlığı büyük görevin sanal saati yavaş işler; gerçek zamanda çok çalışsa da sanal süresi az artar. Ağırlığı küçük görevin sanal saati hızlı işler. CFS'in kuralı, en küçük vruntime'lı görevi çalıştırmaktır. Görevler kırmızı-siyah ağaçta vruntime'a göre sıralanır; en soldaki düğüm seçilir. Eklemek ve silmek O(log n)'dir. En soldaki düğüm önbellekte tutulduğu için seçim O(1)'dir.
nice 0 (w = 1024), nice 5 (w = 335) ve nice −5 (w = 3121) olan üç görev 6 ms çalışıyor. Her birinin vruntime'ı ne kadar ilerler? Sürekli hazır bir nice 0 ile bir nice 5 görevi tek CPU'yu hangi oranda paylaşır?
- nice 0: 6 × 1024 / 1024 = 6 ms.
- nice 5: 6 × 1024 / 335 ≈ 18,34 ms. Sanal saati üç kat hızlı işler.
- nice −5: 6 × 1024 / 3121 ≈ 1,97 ms.
- Uzun vadede iki görevin
vruntime'ı birbirine yakın kalır. Bunun için gerçek çalışma süreleri ağırlıklarla orantılı olmalıdır: nice 0 için 1024 / (1024 + 335) ≈ %75,3, nice 5 için 335 / 1359 ≈ %24,7.
Yeni uyanan bir görev ne olacak? Saatlerce uyumuş bir görevin vruntime'ı çok geride kalmıştır. Olduğu gibi kuyruğa girerse uzun süre CPU'yu tekeline alır. CFS bu yüzden uyanan görevin vruntime'ını kuyruğun en küçük değerinin biraz gerisine çekerdi. Bu tür ayarlar zamanla çoğaldı: gecikme hedefi (sched_latency), en küçük dilim, uyanma ayrıntı düzeyi. Her biri bir sorunu çözerken başka bir iş yükünde yeni bir sorun doğurdu. CFS'in asıl eksiği ise kuramsaldı. CFS bir görevin ne kadar CPU alacağını iyi belirliyordu, ama ne zaman alacağı konusunda bir güvence vermiyordu. Ses ya da oyun gibi gecikmeye duyarlı bir görev, payı küçük olsa bile kısa aralıklarla CPU'ya ihtiyaç duyar.
15.6.4EEVDF: uygunluk ve sanal son tarih
Linux 6.6 (2023) adil sınıfın seçim kuralını EEVDF (Earliest Eligible Virtual Deadline First) ile değiştirdi. Algoritma Ion Stoica ve Hussein Abdel-Wahab'ın 1995 tarihli çalışmasına dayanır; çekirdeğe Peter Zijlstra uyarladı. EEVDF iki kavram ekler: uygunluk ve sanal son tarih.
Kuyruğun ortalama sanal zamanı, görevlerin vruntime'larının ağırlıklı ortalamasıdır: V = Σ wᵢvᵢ / Σ wᵢ. Bir görevin alacağı (lag) lagᵢ = wᵢ (V − vᵢ)'dir. Alacağı sıfır ya da pozitif olan, yani vᵢ ≤ V koşulunu sağlayan görev uygundur (eligible). Her görevin bir sanal son tarihi vardır: dᵢ = vᵢ + dilim × 1024 / wᵢ. EEVDF, uygun görevler arasından sanal son tarihi en erken olanı seçer.
Alacak, görevin ideal işlemcide alacağı hizmet ile gerçekte aldığı hizmet arasındaki farktır. Pozitif alacak, görevin payının gerisinde kaldığını gösterir; çizelgeleyici ona borçludur. Negatif alacak, görevin payından fazlasını almış olduğunu gösterir; görev uygun değildir ve sırası gelene kadar bekler. Bütün görevlerin alacakları toplamı her an sıfırdır. Uygunluk koşulu, bir görevin payını aşarak CPU'yu tekeline almasını engeller. Sanal son tarih ise ne zaman sorusunu yanıtlar. Dilimi kısa olan görevin son tarihi yakındır ve uygun olduğu anda öne geçer.
CFS'ten EEVDF'ye geçişte değişmeyenler de az değildir. sched_entity içindeki vruntime, nice → ağırlık tablosu ve çalışma kuyruğundaki kırmızı-siyah ağaç aynen kaldı. Ağaç hâlâ vruntime'a göre sıralıdır. Her düğüm ayrıca kendi alt ağacındaki en erken son tarihi tutar. Çizelgeleyici bu ek bilgiyle “uygun ve en erken son tarihli” görevi ağaçta tek bir inişle, O(log n) adımda bulur. Değişen, CFS'in ayar düğmeleridir. Gecikme hedefi ve uyanma ayrıntı düzeyi kalktı; yerine tek bir temel dilim geldi. Değeri /sys/kernel/debug/sched/base_slice_ns dosyasında görülür. Varsayılan, milisaniyenin kesri olan bir taban değerin CPU sayısıyla ölçeklenmiş hâlidir; sekiz ve daha çok CPU'lu makinelerde birkaç milisaniyedir. Aşağıdaki örneklerde dilim 3 ms alınmıştır.
Tek CPU'da iki sürekli hazır görev var: A nice 0 (w = 1024), B nice 5 (w = 335). İkisi de v = 0'dan başlıyor, dilim 3 ms. Her görev seçilince dilimini bitirene kadar çalışıyor. İlk 24 ms'de seçimler nasıl olur?
- t = 0. V = 0; ikisi de uygun. Son tarihler: dA = 0 + 3 = 3, dB = 0 + 3 × 1024 / 335 ≈ 9,17. A seçilir.
- t = 3. vA = 3, dA = 6. V = (3 × 1024 + 0 × 335) / 1359 ≈ 2,26. A uygun değil (3 > 2,26). Tek uygun görev B'dir; seçilir.
- t = 6. vB ≈ 9,17, dB ≈ 18,34. V ≈ 4,52. A uygun (3 ≤ 4,52), B değil. A seçilir.
- t = 9 ve t = 12. V sırasıyla 6,78 ve 9,04; vA 6 ve 9. A iki kez daha uygundur ve seçilir. B'nin v'si 9,17 olduğundan V onu henüz geçmemiştir.
- t = 15. vA = 12, V ≈ 11,30. A uygun değil, B uygun (9,17 ≤ 11,30). B seçilir. Ardından desen tekrarlanır.
24 ms'de A altı dilim (18 ms), B iki dilim (6 ms) çalışır: %75 ile %25. Bu, ağırlıkların oranıdır (1024 : 335 ≈ 3 : 1).
| t (ms) | vA / dA | vB / dB | V | Uygun | Seçilen |
|---|---|---|---|---|---|
| 0 | 0 / 3 | 0 / 9,17 | 0 | A, B | A |
| 3 | 3 / 6 | 0 / 9,17 | 2,26 | B | B |
| 6 | 3 / 6 | 9,17 / 18,34 | 4,52 | A | A |
| 9 | 6 / 9 | 9,17 / 18,34 | 6,78 | A | A |
| 12 | 9 / 12 | 9,17 / 18,34 | 9,04 | A | A |
| 15 | 12 / 15 | 9,17 / 18,34 | 11,30 | B | B |
| 18 | 12 / 15 | 18,34 / 27,51 | 13,56 | A | A |
| 21 | 15 / 18 | 18,34 / 27,51 | 15,82 | A | A |
Pay, CFS'te olduğu gibi ağırlıklarla belirlenir. EEVDF'in farkı, iki görevin dilimleri farklı olduğunda ortaya çıkar. Gecikmeye duyarlı bir görev sched_setattr() çağrısıyla daha kısa bir dilim isteyebilir; çağrının sched_runtime alanı adil sınıfta dilim isteği olarak yorumlanır. Bu olanak 6.12'de tamamlandı. Kısa dilim daha erken son tarih, dolayısıyla daha sık seçilme demektir. Pay ise değişmez.
A ve B ikisi de nice 0. A'nın dilimi 3 ms, B 1 ms istemiş. İlk 12 ms'de seçim sırası nedir?
- t = 0: ikisi de uygun; dA = 3, dB = 1. B seçilir ve 1 ms çalışır.
- t = 1: vB = 1, V = 0,5. Yalnız A uygundur; A 3 ms çalışır.
- t = 4: vA = 3, V = 2. B uygun, dB = 2. B seçilir. t = 5'te (V = 2,5, dB = 3) ve t = 6'da (V = 3, ikisi de uygun; dB = 4 < dA = 6) yine B seçilir.
- t = 7: vB = 4 > V = 3,5. A çalışır (7–10). Sonra B iki dilim daha alır.
Sıra B AAA BBB AAA BB'dir. 12 ms'de iki görev de 6 ms çalıştı; paylar eşittir. Ama B ilk dilimini hemen aldı ve hiçbir zaman 3 ms'den uzun beklemedi.
EEVDF'i “en küçük vruntime'ı seçen CFS”le karıştırmak. En küçük vruntime'lı görev her zaman uygundur, ama seçilen o olmayabilir. Örneğin V = 12 iken A (v = 10, d = 14) ve B (v = 12, d = 13) uygundur. EEVDF son tarihi erken olan B'yi seçer; CFS A'yı seçerdi.
15.7Önyükleme
Güç düğmesine basıldığı andan oturum açma ekranına kadar bir dizi program birbirine devreder (Şekil 15.16).
- UEFI. Ana karttaki ürün yazılımı donanımı sınar. Diskteki EFI sistem bölümünü (ESP) bulur ve oradaki EFI uygulamasını yükler. Secure Boot açıksa her aşama bir sonrakinin imzasını doğrular.
- Önyükleyici. GRUB2 ya da systemd-boot çekirdek görüntüsünü (
vmlinuz) veinitramfs'i belleğe yükler, çekirdeğe komut satırı parametrelerini verir. Çekirdek bir EFI uygulaması olarak doğrudan da başlatılabilir. Çekirdek, initramfs ve komut satırı tek bir imzalı dosyada birleşirse buna birleşik çekirdek görüntüsü (UKI) denir. - Çekirdek. Sıkıştırılmış görüntü kendini açar.
start_kernel()bellek yönetimini, kesmeleri ve çizelgeleyiciyi kurar. Bu kodu çalıştıran ilk görev PID 0 olur ve sonunda boşta görevine dönüşür. Çekirdekkernel_init'i (PID 1) ve çekirdek iş parçacıklarının atasıkthreadd'yi (PID 2) oluşturur. - initramfs. Kök dosya sistemini bağlamak için gereken sürücüler çekirdekte olmayabilir: NVMe, RAID, şifreli disk. initramfs RAM'de geçici bir kök sağlar. İçindeki betikler gerekli modülleri yükler, gerçek kökü bağlar ve
switch_rootile ona geçer. - systemd. PID 1 artık systemd'dir. Birim (unit) dosyalarındaki bağımlılıklara göre hizmetleri paralel başlatır ve bir hedefe ulaşır:
multi-user.targetya dagraphical.target. - Oturum. Konsolda
agetty→login→ kabuk, masaüstünde gdm, uzaktan sshd kullanıcıyı karşılar. Kullanıcı oturumu için ayrıca birsystemd --userörneği başlar.
Klasik UNIX'teki init → getty → login → sh zinciri bugün de tanınabilir. Fark, PID 1'in artık hizmetleri bağımlılık çizgesine göre paralel başlatan, onları cgroup'larda izleyen ve çöken hizmeti yeniden başlatan büyük bir program olmasıdır. Açılışın hangi aşamada ne kadar sürdüğü systemd-analyze ile ölçülür.
PID 1'i sıradan bir süreç sanmak. PID 1 sonlanırsa çekirdek panikler. Öksüz kalan süreçler ona bağlanır ve zombilerini o toplar. Konteynerde de içerideki PID 1 bu görevleri üstlenir; zombileri toplamayan bir uygulama PID 1 olarak çalıştırılırsa konteynerde zombiler birikir.
15.8Bellek yönetimi
15.8.1Sürecin adres alanı ve VMA'lar
x86-64'te dört düzeyli sayfalamayla sanal adresler 48 bittir. Linux bu alanı ikiye böler (Şekil 15.17). Alt yarı, 247 B = 128 TiB, kullanıcıya aittir. Üst yarı, yine 128 TiB, çekirdeğindir. Çekirdek yarısı bütün süreçlerde aynıdır ve kullanıcı modunda erişilemez. İki yarının arasında kullanılamayan dev bir delik vardır. Beş düzeyli sayfalamada (LA57) kullanıcı alanı 256 B = 64 PiB'a çıkar.
Kullanıcı yarısının düzeni alttan üste şöyledir. En alttaki sayfa hiç eşlenmez; NULL işaretçiye erişen program SIGSEGV alır. Sonra program kodu (text) gelir: salt okunur ve çalıştırılabilir, aynı programı çalıştıran bütün süreçler arasında paylaşılır. Ardından ilk değerli küresel değişkenler (data) ve sıfırla başlatılan küreseller (bss) durur. Öbek bss'in hemen üstünden başlar ve brk ile yukarı büyür. Adres alanının üst ucunda yığın vardır ve aşağı büyür. Yığının altında, yine aşağı doğru büyüyen mmap bölgesi bulunur: paylaşımlı kütüphaneler (libc.so, ld.so), eşlenmiş dosyalar ve iş parçacıklarının yığınları buraya yerleşir. ASLR (adres alanı düzeninin rastgeleleştirilmesi) yığının, mmap bölgesinin ve öbeğin başlangıcını her çalıştırmada değiştirir; saldırganın adresleri tahmin etmesi zorlaşır. Öbek ile mmap bölgesi arasındaki boşluk yalnız sanaldır, fiziksel bellek harcamaz.
Sanal bellek alanı (VMA, struct vm_area_struct), adres alanının aynı izinleri ve aynı kaynağı taşıyan bitişik bir bölgesidir. Her VMA başlangıç ve bitiş adresini, izinlerini (r, w, x, paylaşımlı ya da özel) ve varsa eşlendiği dosyayı tutar.
Bir sürecin VMA'ları mm_struct içinde tutulur. 6.1'den beri bunlar bir maple ağacında durur. Bu ağaç, aralık aramasına uygun, önbellek dostu bir B-ağacı türüdür; daha önceki kırmızı-siyah ağaç ile bağlı listenin yerini aldı. Sayfa hatası olunca çekirdeğin ilk sorusu “bu adres hangi VMA'da?” olur. Adres hiçbir VMA'da değilse ya da VMA erişime izin vermiyorsa sürece SIGSEGV gönderilir. Adres geçerli bir VMA'daysa sayfa ya dosyadan okunur ya da sıfırlanmış yeni bir çerçeve verilir. Bir sürecin VMA'ları cat /proc/PID/maps ile satır satır görülür.
15.8.2Paylaşım, eşlenmiş dosyalar ve bellek çağrıları
İki süreç aynı programı çalıştırınca text sayfalarını fiziksel bellekte bir kez tutar (Şekil 15.18). İki sürecin sayfa tabloları aynı çerçeveleri gösterir. Aynı dosyayı paylaşımlı eşleyen iki süreç de aynı sayfa önbelleği çerçevelerini görür. Her sürecin veri ve yığın sayfaları ise kendine aittir.
veri.db dosyasını aynı fiziksel çerçevelerde paylaşır.| Çağrı | İşlev |
|---|---|
brk / sbrk | Öbeğin sonunu taşıyarak büyütür ya da küçültür |
mmap | Dosyayı, aygıtı ya da anonim belleği adres alanına eşler |
munmap | Eşlemeyi kaldırır, belleği sisteme geri verir |
mprotect | Bölgenin izinlerini (r/w/x) değiştirir |
madvise | Çekirdeğe kullanım ipucu verir (ör. MADV_DONTNEED) |
msync | Paylaşımlı eşlemenin kirli sayfalarını dosyaya yazar |
mmap'in iki eşleme türü vardır. MAP_SHARED ile yazılanlar dosyaya ve aynı dosyayı eşleyen öteki süreçlere görünür. MAP_PRIVATE ile ilk yazmada sayfa kopyalanır ve dosya değişmez; programın veri kesimi böyle eşlenir. Dosyası olmayan eşlemeye anonim eşleme denir; sayfaları ilk dokunuşta sıfırlanmış çerçevelerden gelir. glibc'nin malloc'u küçük istekleri öbekten karşılar ve öbeği gerektikçe brk ile büyütür. 128 KiB ve üstündeki istekleri ise doğrudan anonim mmap ile karşılar. Bu eşik varsayılandır ve glibc onu çalışma sırasında uyarlayabilir. Büyük bloklar böylece free anında munmap ile hemen sisteme döner.
15.8.3Sayfa tablosu yürüyüşü: dört ve beş düzey
x86-64'te 4 KiB'lık sayfa 12 bitlik ofset ister. 48 bitlik adresin kalan 36 biti dört tabloya bölünür. Her tablo 4 KiB'lık bir sayfadır ve 8 baytlık 512 girdi taşır; bir tablo indisi 9 bittir. Linux'un tablo adları şöyledir: PGD (bit 47–39), PUD (38–30), PMD (29–21) ve PTE (20–12). 63–48. bitler 47. bitin kopyası olmalıdır. Bu koşulu sağlayan adrese kanonik adres denir. Kullanıcı adreslerinde bu bitler 0, çekirdek adreslerinde 1'dir.
Sanal adres 0x00007f3a1c2b5e88. Dört indis ve ofset nedir? PTE'deki çerçeve numarası 0x3c5a1 ise fiziksel adres nedir?
- Ofset en düşük 12 bittir, yani son üç onaltılık hane: 0xe88.
- PGD = adres >> 39 = 0xfe = 254. 63–48. bitler 0, 47. bit de 0'dır; adres kanoniktir.
- PUD = (adres >> 30) & 0x1ff = 232. PMD = (adres >> 21) & 0x1ff = 225. PTE = (adres >> 12) & 0x1ff = 181.
- Fiziksel adres = çerçeve × 4096 + ofset = 0x3c5a1 × 0x1000 + 0xe88 = 0x3c5a1e88. Çarpma, onaltılıkta çerçeve numarasının sonuna üç sıfır eklemektir.
Yürüyüşün kendisi şöyledir (Şekil 15.19). CR3 yazmacı sürecin PGD tablosunun fiziksel adresini tutar; bağlam değiştirmede çekirdek CR3'ü yeni sürecin mm->pgd'siyle yükler. PGD[254] bir PUD tablosunu gösterir. PUD[232] bir PMD tablosunu gösterir; 1 GiB'lık dev sayfa bu düzeyde de bitebilir. PMD[225] bir PTE tablosunu gösterir; 2 MiB'lık dev sayfa ve saydam dev sayfalar (THP) burada biter. PTE[181] asıl girdidir: geçerli (present) biti, çerçeve numarası, okuma/yazma (R/W), kullanıcı/yönetici (U/S), çalıştırma yasağı (NX), kirli (D) ve erişildi (A) bitleri. Geçerli biti 0 ise donanım sayfa hatası üretir.
Her bellek erişiminde dört ek okuma yapmak pahalıdır. TLB çeviriyi önbelleğe alır; bir sonraki erişimde dört okuma atlanır. TLB ıskasında ise yürüyüşü donanım yapar; çekirdek yalnız tabloları kurar. Dev sayfalar yürüyüşü bir ya da iki düzey kısaltır ve tek bir TLB girdisiyle çok daha büyük bir alanı kapsar.
Beş düzeyli sayfalama (LA57) Linux 4.14 ile desteklenmeye başladı. PGD ile PUD arasına P4D adlı bir düzey ekler ve sanal adresi 57 bite çıkarır. Linux'un sayfa tablosu kodu genel olarak beş düzeylidir. Donanım daha azını kullanıyorsa aradaki düzeyler “katlanır”: dört düzeyli bir makinede P4D, tek girdili ve PGD'ye gömülü sayılır. Aynı kod böylece iki düzeyli 32 bit ARM'dan beş düzeyli x86-64'e kadar çalışır.
İndisleri adresin onaltılık hanelerinden doğrudan okumaya çalışmak. 9 bitlik alanlar 4 bitlik hanelerle hizalı değildir. Doğru yol kaydırıp maskelemektir: (adres >> k) & 0x1ff, k = 39, 30, 21, 12. Yalnız 12 bitlik ofset son üç haneye denk gelir.
15.8.4Fiziksel belleğin gösterimi
Linux fiziksel belleği üç düzeyli bir hiyerarşiyle tanımlar: düğüm → bölge → sayfa (Şekil 15.20). Çok soketli bir sunucuda her işlemci soketinin kendine yakın belleği vardır. Bu düzene NUMA (tekdüze olmayan bellek erişimi) denir. Her NUMA düğümü bir pglist_data yapısıyla tanımlanır. Bir CPU belleği öncelikle kendi düğümünden ayırır, çünkü uzak düğüme erişim daha yavaştır.
struct page dizisi ve buddy boş listeleri.Her düğüm bölgelere (zone) ayrılır. ZONE_DMA ilk 16 MiB'tır; yalnız 24 bit adresleyebilen eski ISA aygıtları içindir. ZONE_DMA32 ilk 4 GiB'tır; 32 bit DMA yapabilen aygıtlar içindir. ZONE_NORMAL geri kalan RAM'dir. ZONE_MOVABLE yalnız taşınabilir sayfaları tutar; bellek modülü sökülmeden önce boşaltılabilir ve dev sayfa oluşturmayı kolaylaştırır. 32 bit çekirdeklerde çekirdeğin doğrudan eşleyemediği yüksek bellek için ZONE_HIGHMEM de vardı. 64 bit sistemlerde bu bölge yoktur, çünkü çekirdek adres alanı bütün RAM'i doğrudan eşleyecek kadar büyüktür.
Her fiziksel sayfa çerçevesi için çekirdekte bir struct page bulunur. Yapının boyu 64 bayttır. Bunlar mem_map adlı büyük bir dizide durur. 4 KiB'lık her sayfa için 64 B, RAM'in 64 / 4096 ≈ %1,56'sı demektir. 16 GiB'lık bir makinede bu dizi 256 MiB tutar. Ardışık sayfaları tek bir birim olarak yönetmek için 5.16 ile folio kavramı geldi. Bir folio birden çok sayfayı kapsayabilir; sayfa önbelleği ve geri kazanım kodu sayfa sayfa yerine folio folio çalışır.
Her bölgenin free_area dizisi buddy ayırıcının boş listelerini tutar. Liste k, 2k sayfalık boş blokları içerir. k 0'dan 10'a kadardır: 4 KiB'tan 4 MiB'a. İstek en küçük uygun bloktan karşılanır; büyük blok ikiye bölünür, serbest kalan blok “kardeşi” de boşsa birleşir. Bölgelerdeki boş blok sayıları /proc/buddyinfo dosyasından okunur. Her bölgenin ayrıca üç su seviyesi (min, düşük, yüksek) vardır; geri kazanım bunlara göre çalışır.
15.8.5SLUB nesne ayırıcısı
Çekirdek çok sayıda küçük ve aynı boyda nesne ayırır: her süreç için bir task_struct, her açık dosya için bir struct file, her ad için bir dentry (192 B), her dosya için bir i-node. Bunların her biri için bir sayfa ayırmak hem yavaş hem israftır.
SLUB, Linux'un nesne ayırıcısıdır. Her nesne türü için bir nesne önbelleği (kmem_cache) tutar. Önbellek, buddy ayırıcıdan aldığı bir ya da birkaç ardışık sayfayı (bir slab) aynı boyda nesnelere böler. Boş nesneler bir serbest listeye bağlanır.
Önbelleklerin bir kısmı belirli türlere aittir: task_struct, dentry, inode_cache (Şekil 15.21). kmalloc ise genel amaçlıdır ve boy sınıflarına ayrılmış önbellekler kullanır: kmalloc-8, kmalloc-16 … kmalloc-8k. 100 baytlık bir istek kmalloc-128'den karşılanır. Bundan büyük istekler doğrudan buddy ayırıcıya gider. Fiziksel olarak bitişik olması gerekmeyen büyük alanlar için vmalloc kullanılır; o, dağınık çerçeveleri sanal olarak bitişik bir bölgeye eşler.
Bir önbellekte üç tür slab bulunur. Her CPU'nun bir etkin slab'ı vardır; ayırma ve bırakmanın çoğu kilit almadan burada olur. Kısmi (partial) slab'larda hem dolu hem boş nesne vardır. Dolu slab'ların bütün nesneleri kullanımdadır. Etkin slab boşalınca CPU kısmi listeden yeni bir slab alır; o da yoksa buddy'den sayfa ister. Önbelleklerin durumu /proc/slabinfo dosyasından ya da slabtop komutuyla izlenir.
SLUB'ın adı tarihten gelir. Linux'ta uzun yıllar üç ayırıcı birlikte yaşadı. SLAB, Bonwick'in Solaris için önerdiği tasarımın ilk uyarlamasıydı ve karmaşık kuyruklar tutuyordu. SLOB küçük gömülü sistemler için yazılmıştı. SLUB, 2.6.22'de SLAB'ın sadeleştirilmiş hâli olarak geldi ve zamanla varsayılan oldu. SLOB 6.4'te, SLAB 6.8'de kaldırıldı. Bugün tek ayırıcı SLUB'dır.
dentry nesnesi 192 bayttır. Tek sayfalık (4 KiB) bir slab'a kaç dentry sığar ve kaç bayt boş kalır?
- 4096 / 192 = 21,33. Tam sayı kısmı 21 nesnedir.
- 21 × 192 = 4032. Kalan 4096 − 4032 = 64 bayt.
- Kayıp sayfanın %1,6'sıdır. Her dentry için ayrı bir 4 KiB'lık sayfa ayrılsaydı kayıp %95 olurdu.
15.8.6Kod izleme: SLUB serbest listesi
SLUB'ın hızının sırrı, serbest listeyi tutmak için hiç ek bellek kullanmamasıdır. Boş bir nesnenin içi zaten kullanılmıyordur. SLUB “sonraki boş nesne” işaretçisini nesnenin kendi içine yazar. Aşağıdaki kod hızlı yolun sadeleştirilmiş hâlidir:
void *slab_ayir(kmem_cache *c) {
void *nesne = c->freelist; /* listenin başı */
if (!nesne)
nesne = yeni_slab(c); /* yavaş yol */
c->freelist = *(void **)nesne; /* sonraki */
return nesne;
}
void slab_birak(kmem_cache *c, void *nesne) {
*(void **)nesne = c->freelist; /* nesnenin içine */
c->freelist = nesne;
}
/* a,b = ayir; birak(a); c,d,e,f = ayir */slab_ayir listenin başını alır. Liste boşsa yavaş yola girer: yeni_slab buddy'den bir sayfa ister, onu nesnelere böler ve ilk nesneyi verir. Sonra c->freelist'i, alınan nesnenin ilk 8 baytında yazan işaretçiye ilerletir. *(void **)nesne ifadesi “nesnenin başındaki 8 baytı bir işaretçi olarak oku” demektir. slab_birak bunun tersini yapar: listenin eski başını bırakılan nesnenin içine yazar ve nesneyi listenin başı yapar. Liste böylece bir yığın gibi çalışır: son bırakılan ilk verilir.
İzleme kmalloc-1k önbelleğinde yapılır. Bir slab 4 KiB'lık tek sayfadır ve 1 KiB'lık dört nesne taşır: +0, +1024, +2048, +3072. Başta liste +0 → +1024 → +2048 → +3072 → NULL'dır. Program önce a ve b'yi ayırır, a'yı bırakır, sonra c, d, e ve f'yi ayırır.
| İşlem | Verilen nesne | freelist sonra | Açıklama |
|---|---|---|---|
| a = ayir | +0 | +1024 | listenin başı; içindeki işaretçi +1024 |
| b = ayir | +1024 | +2048 | hızlı yol: kilit yok, sayfa ayırma yok |
| birak(a) | — | +0 | +0'ın içine eski baş (+2048) yazılır |
| c = ayir | +0 | +2048 | a'nın az önce bıraktığı “sıcak” nesne |
| d = ayir | +2048 | +3072 | |
| e = ayir | +3072 | NULL | slab doldu |
| f = ayir | 2:+0 | 2:+1024 | yavaş yol: yeni sayfa, dört nesne |
İzlemedeki üç gözlem SLUB'ın tasarımını özetler. c'nin a'nın yerine düşmesi rastlantı değildir. Son bırakılan nesnenin baytları büyük olasılıkla hâlâ CPU önbelleğindedir; onu hemen yeniden vermek önbellek ıskasını azaltır. İkincisi, yaygın durum birkaç komuttur: bir yükleme, bir yazma, bir dönüş. Gerçek SLUB bu adımı CPU başına yapılarla ve kilit yerine atomik bir karşılaştır-değiştir komutuyla yapar. Üçüncüsü, nadir durum, yani yeni sayfa gereken f, yavaştır ama doğrudur. Bütün nesneler aynı boyda olduğu için slab içinde dış parçalanma oluşmaz; bırakılan her boşluk bir sonraki istek için tam uygundur.
Bırakılmış bir nesneyi kullanmaya devam etmek. Bırakılan nesnenin ilk baytları artık serbest listenin işaretçisidir. Nesneye yazan hatalı kod listeyi bozar; bir sonraki ayırma rastgele bir adres döndürür. Bu tür hatalar çekirdek açıklarının sık kaynağıdır. Çekirdek bu yüzden işaretçiyi gizli bir değerle karıştırarak saklayabilir ve hata ayıklama seçenekleriyle bırakılan nesneleri bir desenle doldurup denetleyebilir.
15.8.7Sayfa önbelleği, geri kazanım ve OOM
Linux'un bellek felsefesi kısa bir cümleyle özetlenir: boş bellek israftır. Kullanılmayan RAM sayfa önbelleği olarak dosya verisi için kullanılır. Bir dosya bir kez okununca sayfaları önbellekte kalır; ikinci okuma diske gitmez. free -h çıktısındaki “available” sütunu bu yüzden “free”den büyüktür: önbelleğin geri alınabilir kısmını da sayar. 16 GiB'lık bir makinede başta 5 GiB uygulama belleği, 8 GiB sayfa önbelleği ve 2 GiB boş bellek bulunsun (Tablo 15.12). Çekirdeğin kendisi ve tamponları kalan 1 GiB'ı tutar.
| Aşama | Anonim | Önbellek | Boş | Takas | Çekirdeğin tepkisi |
|---|---|---|---|---|---|
| Başlangıç | 5 | 8 | 2 | 0 | — |
| Uygulama büyüdü | 8,3 | 6 | 0,7 | 0 | düşük seviye aşıldı: kswapd uyanır |
| Soğuk sayfalar gider | 9 | 4,7 | 1,3 | 1 | MGLRU, zswap → takas |
| Baskı sürüyor | 11,5 | 3,2 | 0,3 | 2 | min altı: doğrudan geri kazanım |
| OOM | 4,6 | 2 | 8,4 | 2 | bellek_yiyen (PID 7311) öldürüldü |
Geri kazanım su seviyelerine göre kademeli çalışır (Şekil 15.23).
- Arka plan. Boş bellek düşük su seviyesinin altına inince her düğümün
kswapdçekirdek iş parçacığı uyanır. Temiz önbellek sayfalarını hemen bırakır; kirli olanları önce diske yazar. Boş bellek yüksek seviyeye çıkınca yeniden uyur. Uygulama bu sırada fark edilir biçimde yavaşlamaz. - Kurban seçimi. Hangi sayfaların gideceğini 6.1 ile gelen çok nesilli LRU (MGLRU) belirler. Sayfalar erişim nesillerine ayrılır; en soğuk nesil önce gider. Önbellek sayfası atılabilir, çünkü verisi diskteki dosyada vardır. Soğuk anonim sayfaların ise dosyası yoktur. zswap onları önce RAM'de sıkıştırır, gerekirse takas alanına yazar.
- Doğrudan geri kazanım. Ayırma hızı geri kazanımı geçer ve boş bellek min seviyesinin altına inerse, bellek isteyen süreç sayfa bekleyemez. Geri kazanımı kendisi yapar ve bu sırada takılır. Uygulamanın gecikmesi belirgin biçimde artar.
- OOM. Hiçbir şey yetmezse OOM killer (bellek tükenince süreç öldüren mekanizma) devreye girer. En yüksek
oom_score'lu süreciSIGKILLile öldürür. Puan çoğunlukla sürecin bellek kullanımıyla orantılıdır. Yönetici/proc/PID/oom_score_adjdosyasına −1000 ile +1000 arası bir değer yazarak bir süreci korur ya da öne çıkarır. Bir cgroup'un bellek sınırı aşılırsa kurban yalnız o gruptan seçilir.
bellek_yiyen'i öldürdü, boş bellek 8,4 GiB'a çıktı.Takas alanının ne kadar istekli kullanılacağı vm.swappiness ayarıyla belirlenir; varsayılan 60'tır. Değer düştükçe çekirdek anonim sayfaları takasa göndermek yerine önbellek sayfalarını bırakmayı yeğler.
free çıktısındaki küçük “free” değerini bellek sıkıntısı sanmak. Uzun süre çalışmış bir Linux makinesinde boş bellek hep azdır, çünkü RAM önbellekle doludur. Asıl gösterge “available” değeri, takas etkinliği ve /proc/pressure/memory dosyasındaki bellek baskısı (PSI) sayaçlarıdır.
15.9Giriş/çıkış
15.9.1Aygıtlar dosyadır: major ve minor numaralar
Linux'ta disklere ve terminallere /dev dizinindeki aygıt dosyaları üzerinden erişilir. Uygulama bir diske de bir terminale de aynı open, read ve write çağrılarıyla ulaşır. ls -l bu dosyalar için boyut yerine iki sayı gösterir:
$ ls -l /dev # seçilmiş satırlar
brw-rw---- 1 root disk 8, 0 sda
brw-rw---- 1 root disk 8, 1 sda1
brw-rw---- 1 root disk 259, 0 nvme0n1
crw-rw-rw- 1 root root 1, 3 null
crw-rw-rw- 1 root tty 5, 0 ttySatırın ilk harfi aygıt türüdür. c karakter aygıtıdır: bayt akışı sunar, çoğunlukla önbelleksizdir; terminal, /dev/null ve fare böyledir. b blok aygıtıdır: sabit boylu bloklara rastgele erişim sunar, sayfa önbelleğini ve G/Ç çizelgeleyicisini kullanır. Boyut sütunundaki iki sayı major ve minor numaralardır (Şekil 15.24). Major numara sürücüyü seçer: 8, SCSI disk sürücüsü sd'dir; SATA ve USB diskler de ona bağlanır. Minor numara o sürücünün yönettiği birimi seçer. sd sürücüsü her diske 16 minor ayırır: 0 tüm disk, 1–15 bölümler. Bu yüzden sda1 8,1'dir; ikinci disk sdb 8,16'dan başlar, sdb3 8,19'dur. NVMe diskler 259 numaralı genişletilmiş blok majoruna (blkext) bağlanır.
open("/dev/sda1"): i-node'daki tür ve major numara sürücüyü, minor numara bölümü seçer.Aygıt dosyası diskte veri tutmaz. i-node'u yalnız türü ve iki numarayı saklar. open bu numaralarla çekirdeğin aygıt tablosuna bakar ve sürücüyü bulur. Bugün /dev'deki düğümleri elle oluşturmak gerekmez. Çekirdek bir aygıt bulunca devtmpfs sözde dosya sisteminde düğümü kendisi açar; udev izinleri ayarlar ve kalıcı adlar ekler (/dev/disk/by-uuid/…). Ağ arayüzleri (eth0, wlan0) ise /dev'de yoktur. Onlara soketlerle erişilir. “Her şey dosyadır” ilkesinin en bilinen istisnası budur.
15.9.2G/Ç sisteminin yapısı
Üç aygıt türü çekirdekte üç ayrı yoldan iner (Şekil 15.25). Karakter aygıtları VFS'ten sonra tty katmanına ve satır disiplinine, oradan karakter sürücüsüne gider. Blok aygıtlarına giden yol dosya sisteminden (ext4, XFS, Btrfs), sayfa önbelleğinden, blok katmanından ve G/Ç çizelgeleyicisinden geçer. Ağ yolu soket katmanından başlar: TCP ya da UDP, IP ve paket süzgeci netfilter, kuyruk disiplini (qdisc) ve ağ sürücüsü. Ağ sürücüleri NAPI arayüzünü kullanır: yoğun trafikte her paket için kesme üretmek yerine kesmeleri kapatıp paketleri toplu yoklar.
Ağ yolunda veri sk_buff yapılarında taşınır. Bir sk_buff paketin verisini ve başlıklar için önden ayrılmış boşluğu tutar; her katman başlığını eklerken veriyi kopyalamaz, yalnız işaretçiyi kaydırır. XDP ve eBPF ile paketler daha sürücü düzeyinde, yığına girmeden süzülebilir ya da yönlendirilebilir. Klasik BSD'de de benzer bir üç yol ayrımı vardı. System V'in STREAMS adlı modül yığını ise Linux ana çekirdeğine hiç girmedi.
15.9.3Sürücü arayüzü: file_operations ve terminal
Eski UNIX ders kitapları her sürücü için ayrı giriş noktalarını tablo hâlinde listeler: disk için k_open, k_read, terminal için tty_open, tty_read ve benzerleri. Linux bu giriş noktalarını tek bir işlev işaretçisi yapısında toplar:
static const struct file_operations my_fops = {
.owner = THIS_MODULE,
.open = my_open,
.release = my_release, /* close */
.read = my_read,
.write = my_write,
.unlocked_ioctl = my_ioctl,
.poll = my_poll,
.mmap = my_mmap,
};
cdev_init(&cdev, &my_fops); /* major:minor → fops */Bir karakter sürücüsü bu yapıyı doldurur ve cdev_init ile bir major:minor aralığına bağlar. Uygulama aygıt dosyasında read() çağırınca VFS çağrıyı ilgili struct file'ın f_op->read işaretçisine yönlendirir. close'un karşılığı .release'tir ve yalnız son tanımlayıcı kapanınca çağrılır. Aygıta özgü komutlar .unlocked_ioctl ile gelir. .poll, aygıtın select, poll ve epoll ile beklenebilmesini sağlar. .mmap aygıt belleğini doğrudan sürecin adres alanına eşler. Bu, C dilinde nesne yönelimli tasarımdır: işlev işaretçisi tablosu bir sanal yöntem tablosu gibi çalışır. Aynı desen VFS'in her yerinde tekrarlanır.
Terminal G/Ç'si termios arayüzüyle ayarlanır. tcgetattr ve tcsetattr terminal parametrelerini okur ve yazar. Kanonik modda (ICANON) satır disiplini satır düzenlemesini kendisi yapar: geri silme işlenir ve program satırı ancak Enter'dan sonra alır. Ham modda her tuş hemen iletilir; metin düzenleyiciler ve parola soran programlar bu modu kullanır. ECHO bayrağı yazılanın ekrana yansıtılıp yansıtılmayacağını belirler. cfsetispeed ve cfsetospeed giriş ve çıkış baud hızını ayarlar. Bu ayar bugün çoğunlukla gerçek seri portlar için anlamlıdır; terminal pencereleri sözde terminaldir (pty).
15.9.4Blok katmanı: blk-mq
Tek kuyruklu eski blok katmanı, saniyede yüzlerce işlem yapan döner diskler için tasarlanmıştı. Her istek tek bir kuyruğa ve tek bir kilide uğruyordu. Saniyede milyonlarca işlem yapabilen NVMe SSD'lerde bu kilit darboğaz oldu. Bugünkü blok katmanı blk-mq çok kuyrukludur. Her CPU isteklerini kendi yazılım kuyruğuna yazar. İstekler isteğe bağlı bir G/Ç çizelgeleyiciden geçip aygıtın donanım kuyruklarına dağıtılır. NVMe denetleyicileri çok sayıda donanım kuyruğu sunar; çoğunlukla her CPU'ya bir kuyruk düşer ve CPU'lar arasında kilit paylaşımı kalmaz.
Dört çizelgeleyici seçeneği vardır. none hiç sıralama yapmaz; hızlı NVMe aygıtlarında çoğunlukla en iyisidir. mq-deadline okumaları ve yazmaları son tarihlerle sıralar, hiçbir isteğin aç kalmamasını sağlar. bfq (bütçe adil kuyruklama) süreçler arasında adil bant genişliği paylaştırır; masaüstündeki yavaş disklerde etkileşimi korur. kyber hedef gecikmelere göre kuyruk derinliğini ayarlar. Etkin çizelgeleyici aygıt başına /sys/block/<aygıt>/queue/scheduler dosyasından okunur; köşeli parantez içindeki seçilidir. Dosyaya bir ad yazmak çizelgeleyiciyi değiştirir. Asenkron G/Ç'nin yeni arayüzü io_uring de bu katmanın üstüne kurulur: uygulama ile çekirdek paylaşılan iki halka tampon üzerinden istek ve sonuç alışverişi yapar, sistem çağrısı sayısı azalır.
15.9.5Ağ: soketler ve bağlantı kurulumu
Soket, ağ iletişiminin uç noktasıdır ve bir dosya tanımlayıcısıyla temsil edilir. Bir TCP bağlantısının kuruluşu iki taraftaki çağrılarla izlenebilir (Şekil 15.26).
- Sunucu hazırlanır.
socket()bir soket oluşturur; en küçük boş tanımlayıcı olan 3'ü alır.bindsoketi 80 numaralı porta bağlar.listen(3, 128)soketi dinleme durumuna geçirir ve bekleyen bağlantılar için bir kuyruk açar. - Bekleme. Sunucu
accept'te bloke olur. İstemci kendi soketini oluşturur. - El sıkışma. İstemci
connectçağırır. İstemci çekirdeği SYN gönderir, sunucu çekirdeği SYN-ACK ile yanıtlar, istemci ACK ile tamamlar. Bu üçlü el sıkışmayı iki çekirdek yürütür; uygulamalar paketleri hiç görmez. - Yeni soket. Bağlantı kurulunca
acceptyeni bir soket döndürür (fd 4). fd 3 dinlemeyi sürdürür. Böylece tek bir dinleyen soket binlerce bağlantı soketi doğurabilir. - Veri ve kapanış. Veri
writevereadile akar. Çekirdek veriyisk_buff'lara koyar, TCP/IP başlıklarını ekler; sürücü paketi kartın halka tamponuna yazar.closekarşı tarafa FIN gönderir.
socket, bind, listen, accept, connect, üçlü el sıkışma, veri ve FIN.Çok istemcili bir sunucu her bağlantı için bir iş parçacığı açabilir. Binlerce bağlantıda bu pahalılaşır. Linux'un yanıtı epoll'dur. Sunucu ilgilendiği soketleri bir kez kaydeder. Sonra tek bir epoll_wait çağrısıyla yalnız hazır olan soketlerin listesini alır. Maliyet toplam bağlantı sayısıyla değil, hazır soket sayısıyla orantılıdır.
15.10Dosya sistemleri
15.10.1Dizin ağacı ve bağlama
Windows'taki sürücü harflerinin aksine Linux'ta tek bir dizin ağacı vardır (Şekil 15.27). Başka bir dosya sistemi ağacın herhangi bir dizinine bağlanır (mount). mount /dev/sdb1 /mnt/usb komutundan sonra USB belleğin kök dizini /mnt/usb'de görünür. Bağlama noktasının eski içeriği bağlama süresince gizlenir.
/mnt/usb noktasına bağlanmış bir vfat dosya sistemi.Dizinlerin rolleri Dosya Sistemi Hiyerarşisi Standardı (FHS) ile belirlenir. /usr salt okunur, paylaşılabilir yazılımı tutar: komutlar /usr/bin'de, paylaşımlı kütüphaneler /usr/lib'de, çekirdek modülleri /usr/lib/modules'te. /etc metin biçimindeki yapılandırma dosyalarıdır. /var değişken veriyi (günlükler, kuyruklar, önbellekler), /home kullanıcı dizinlerini, /root yöneticinin dizinini tutar. /boot'ta çekirdek ve initramfs durur. /tmp geçici dosyalar içindir ve çoğu dağıtımda RAM'deki tmpfs'tir. /dev, /proc ve /sys diskte değil, çekirdeğin bellekte ürettiği sözde dosya sistemleridir. Çoğu dağıtım “usrmerge” ile /bin, /sbin ve /lib'i /usr altına taşıdı. Eski yollar, uyumluluk için sembolik bağ olarak kaldı.
15.10.2VFS nesneleri
Linux onlarca dosya sistemini aynı çağrılarla kullanır: ext4, XFS, Btrfs, FAT, NFS, procfs. Bunu sanal dosya sistemi (VFS) sağlar. VFS dört nesne türü tanımlar (Şekil 15.28):
- superblock: bağlanmış bir dosya sistemi. Blok boyu, kök i-node ve dosya sistemi işlemleri buradadır.
- inode: dosyanın kendisi. Boyut, izinler, zamanlar ve verinin yeri. Bellekteki
struct inode, diskteki i-node'un bir kopyasıdır. - dentry: bir ad ile bir i-node arasındaki bağ.
"x.txt" → inode 1234. Yol çözümlemesini hızlandırmak için önbellekte tutulur. - file: açık bir dosya oturumu. Dosya konumu (
f_pos), açılış bayrakları ve referans sayısı.
struct file, dentry ve inode ilişkisi. A ile çocuğu A′ aynı struct file'ı paylaşır; C aynı dosyayı ayrıca açmıştır.Şekildeki durum üç açık dosya tablosu düzeyini somutlaştırır. A süreci x.txt'yi açtı ve 150 bayt okudu. Sonra fork etti. Çocuk A′'nün 3 numaralı tanımlayıcısı aynı struct file #1'i gösterir; referans sayısı 2'dir ve konum ortaktır. A′ 10 bayt daha okursa A'nın konumu da 160 olur. C süreci aynı dosyayı kendisi açtı. Onun struct file #2'si ayrıdır ve konumu 0'dır. İki struct file da aynı dentry'yi, dentry de aynı inode'u gösterir. Sürecin tanımlayıcı tablosu task_struct → files_struct içindedir.
Her nesnenin bir işlem tablosu vardır: super_operations, inode_operations, file_operations, dentry_operations. VFS bir işi yaparken bu tablolardaki işlev işaretçisini çağırır. ext4'te read ext4'ün okuma işlevine, NFS'te ağ üzerinden bir RPC'ye, procfs'te çekirdek verisini o anda metne çeviren bir işleve gider. Çağıran program farkı görmez.
15.10.3Dosya ve dizin çağrıları, stat
Dosya sisteminin sistem çağrısı arayüzü küçüktür (Tablo 15.13). Birkaç ayrıntı sık karıştırılır. rmdir yalnız boş dizini siler. rename atomiktir: hedef ad varsa tek adımda yer değiştirir; bir dosyayı güvenle güncellemenin yolu, yeni içeriği geçici dosyaya yazıp rename etmektir. opendir, readdir ve closedir sistem çağrısı değildir; getdents64 üzerine kurulu kütüphane işlevleridir.
| Çağrı | İşlev |
|---|---|
open, creat, close | Aç / oluştur, fd döndür; tanımlayıcıyı kapat |
read, write, lseek | Konumdan oku / yaz; konumu taşı |
stat, fstat, lstat | Bilgi al (lstat: bağın kendisi) |
pipe, dup2, fcntl | Boru oluştur; fd kopyala; kilit ve bayraklar |
mkdir, rmdir | Dizin oluştur / sil (boşsa) |
link, unlink, symlink | Ad ekle / sil; sembolik bağ oluştur |
rename, chdir | Taşı (atomik); çalışma dizinini değiştir |
getdents64 | Dizin girdilerini oku |
stat bir yolu çözer, i-node'u okur ve alanlarını struct stat'a kopyalar (Şekil 15.29). st_ino i-node numarası, st_mode tür ve izin bitleri, st_nlink katı bağ sayısı, st_size bayt cinsinden boyuttur. st_blocks diskte ayrılan alanı 512 baytlık birimlerle verir; seyrek bir dosyada st_size'tan çok küçük olabilir. Üç zaman damgası vardır: st_atime son erişim, st_mtime son içerik değişikliği, st_ctime son i-node değişikliğidir. st_ctime oluşturma zamanı değildir; chmod ya da link da onu günceller. Oluşturma zamanı yalnız daha yeni statx() çağrısıyla, stx_btime alanından okunabilir.
stat: yol çözümlenir, i-node okunur, alanlar struct stat'a kopyalanır.15.10.4Dosya kilitleme
Aynı dosyayı paylaşan süreçler fcntl ile bayt aralıklarını kilitleyebilir. Kilit bir struct flock ile tanımlanır: tür (F_RDLCK paylaşımlı, F_WRLCK özel), başlangıç (l_start) ve uzunluk (l_len). Paylaşımlı kilitler birbiriyle çakışabilir; özel kilit hiçbir kilitle çakışamaz. Bu, okuyucu–yazar kilidinin bayt düzeyindeki karşılığıdır.
16 baytlık bir dosyada sırayla A 4–7'ye, B 6–9'a, C 2–11'e paylaşımlı kilit koyuyor. Sonra D yalnız 9. bayta özel kilit istiyor. Hangi istekler kabul edilir?
- A:
F_RDLCK,l_start= 4,l_len= 4. Dosyada kilit yok; kabul. - B: 6–9 aralığı A ile 6–7'de örtüşür. İkisi de paylaşımlı; kabul.
- C: 2–11, A ve B'nin tamamını kapsar. Yine paylaşımlı; kabul. 6 ve 7. baytlar artık üç kilitle kaplıdır.
- D: 9. bayt B ve C'nin aralığındadır. Özel kilit paylaşımlı kilitlerle çakışır.
F_SETLKile istenirse çağrı beklemeden hata döndürür (EAGAINya daEACCES).F_SETLKWile istenirse D, B ve C kilitlerini kaldırana dek bekler.
F_SETLKW ile bekleyen iki süreç birbirinin kilidini bekleyebilir. Çekirdek bekleme ilişkilerini izler; bir döngü fark ederse bekleyen çağrıya EDEADLK döndürür. Bu, kilitlenme tespitinin gerçek bir örneğidir. Bu kilitler tavsiye niteliğindedir (advisory): kilidi sormayan bir süreç dosyaya yine yazabilir. Bütün dosya için daha basit flock() vardır. fcntl kilitlerinin önemli bir kusuru, sürece bağlı olmalarıdır: süreç dosyanın herhangi bir tanımlayıcısını kapatınca bütün kilitleri düşer ve aynı süreçteki iş parçacıkları kilitleri ayırt edemez. Açık dosya tanımına bağlı OFD kilitleri (F_OFD_SETLK) bu kusuru giderir.
15.10.5ext2 ve ext4'ün disk düzeni
Klasik UNIX dosya sisteminin düzeni basitti: önyükleme bloğu, süperblok, i-node tablosu ve veri blokları. Bu düzende bir dosyanın i-node'u diskin başında, verisi ise herhangi bir yerde olabilirdi; disk kolu ikisi arasında sürekli gidip gelirdi. ext2 diski blok gruplarına böldü (Şekil 15.31). Her grubun kendi blok biteşlemi, i-node biteşlemi, i-node tablosu ve veri blokları vardır. Çekirdek bir dosyanın i-node'u ile verisini aynı gruba, bir dizinin dosyalarını da dizinin grubuna yakın koymaya çalışır. Süperblok ve grup tanımlayıcıları bazı gruplarda yedeklenir; birinci kopya bozulursa yedekten kurtarılır.
ext2'nin i-node'u 12 doğrudan blok işaretçisi ile tekli, ikili ve üçlü dolaylı işaretçi taşır. 4 KiB'lık bloklarla bir dolaylı blok 1024 işaretçi tutar. Erişilebilen alan katlanarak büyür: 12 × 4 KiB = 48 KiB, + 1024 × 4 KiB = 4 MiB, + 1024² × 4 KiB = 4 GiB, + 1024³ × 4 KiB = 4 TiB. Dolaylı bloklarla teorik sınır böylece yaklaşık 4 TiB'tır. ext2'de gerçek sınır ise 2 TiB'tır: i-node'daki 32 bitlik i_blocks alanı ayrılan alanı 512 baytlık birimlerle sayar ve 232 × 512 B = 2 TiB'ta tıkanır. Kök dizinin i-node numarası her zaman 2'dir; 1 eskiden bozuk blokların listesine ayrılmıştı.
ext4 büyük dosyalar için dolaylı bloklar yerine extent kullanır. Bir extent ardışık bir blok aralığını üç sayıyla tanımlar: mantıksal blok, uzunluk ve fiziksel blok. (0, 2048, 34816) extent'i dosyanın ilk 2048 bloğunun, yani 8 MiB'ının diskte 34816. bloktan başlayıp ardışık durduğunu söyler. Dolaylı bloklarla aynı bilgi 2048 işaretçi, iki dolaylı blok demekti. i-node'a dört extent sığar; daha çoğu gerekirse extent'ler i-node'dan başlayan bir ağaca yerleşir. Tek bir extent en çok 32 768 blok, yani 4 KiB'lık bloklarla 128 MiB kapsar. 4 KiB blokla bir dosya en çok 16 TiB olabilir. ext4 ayrıca gecikmeli ayırma kullanır: veri bloklarının yerine, veri diske yazılacağı ana kadar karar vermez; böylece büyük ardışık extent'ler seçebilir.
15.10.6ext4'te günlükleme
Bir write() ve ardından fsync() birkaç bloğu değiştirir. Verinin kendisi (D) yazılır; yanında üst veriler değişir: i-node (M1), blok biteşlemi (M2) ve extent (M3). Bu blokların bir kısmı yazılıp öbürleri yazılmadan elektrik kesilirse dosya sistemi tutarsız kalır. Örneğin biteşlem bloğu kullanılıyor gösterir ama hiçbir i-node onu göstermez. ext4 bu soruna günlükleme ile çözüm bulur. Günlüğü JBD2 katmanı yönetir (Şekil 15.32).
- Veri önce. Varsayılan
data=orderedmodunda D önce asıl yerine yazılır. Böylece üst veri hiçbir zaman çöp içeren bir bloğu göstermez. - Günlük işlemi. M1, M2 ve M3 günlüğe tek bir işlem (T42) olarak yazılır: bir tanım bloğu, üst veri bloklarının kopyaları ve en sonda bir commit kaydı. Commit kaydı diske ulaştığı an işlem kalıcıdır.
- Checkpoint. Üst veri blokları sonra, acele etmeden asıl yerlerine yazılır. Günlükteki alan yeniden kullanılır; günlük dairesel bir tampondur.
- Kurtarma. Çökmeden sonra bağlama sırasında tam bir fsck taraması yapılmaz. Günlük okunur. Commit kaydı olan işlemler yeniden oynatılır. Commit'i olmayan yarım işlemler atılır ve dosya sistemi eski tutarlı hâlinde kalır. Bu iş saniyeler sürer.
Öteki iki mod farklı ödünleşimler sunar. data=journal veriyi de günlüğe yazar: en güvenlisidir ama her veri bloğu iki kez yazıldığı için en yavaşıdır. data=writeback veri ile üst veri arasında sıra güvencesi vermez: hızlıdır ama çökmeden sonra bir dosya eski ya da çöp içerikli bloklar gösterebilir. ext4 varsayılan olarak yaklaşık beş saniyede bir commit yapar. fsync çağrılmayan bir ekleme, commit'ten önce elektrik kesilirse tümüyle kaybolabilir; ama data=ordered sayesinde dosya hiçbir zaman çöp göstermez. XFS de üst veri günlüğü kullanır. Btrfs ise günlük yerine yazınca kopyalamayla tutarlı kalır.
Günlüklemenin veriyi koruduğunu sanmak. Varsayılan modda günlük yalnız üst veriyi korur ve yalnız tutarlılığı garanti eder. Uygulamanın son yazdıkları fsync olmadan kaybolabilir. Verinin diske ulaştığından emin olmak isteyen program fsync çağırmalıdır.
15.10.7Dizinler ve yol çözümleme
ext2/ext4'te bir dizin, girdilerden oluşan sıradan bir dosyadır. Her girdi i-node numarasını, kayıt uzunluğunu, ad uzunluğunu, dosya türünü ve adın kendisini tutar. Silinen bir girdi yok edilmez; alanı bir önceki girdinin kayıt uzunluğuna eklenir. Binlerce girdili dizinlerde doğrusal arama yavaşlar. ext4 bu dizinlerde adların karma değerine göre sıralanmış bir ağaç (htree) kullanır.
open("/home/ali/notlar.txt") nasıl çözülür? (Şekil 15.33)
- Yol mutlaktır; arama kökten başlar. Kök dizinin i-node'u (2) okunur. Veri bloklarındaki girdilerde
homearanır: 131073. Kök için x (arama) izni denetlenir. homedizininin (131073) verisindealiaranır: 131074.alidizininde (131074)notlar.txtaranır: 132010. Göreli bir yol olsaydı arama sürecin çalışma dizininden başlardı.- i-node 132010 okunur. İzinler denetlenir; okuma izni varsa bir
struct fileoluşturulur ve en küçük boş tanımlayıcı, örneğin 3, döner.
/home/ali/notlar.txt yolunun çözümlenmesi: i-node 2 → 131073 → 131074 → 132010.i-node numaraları rastgele değildir. 131073 ile 132010'un yakınlığı, ext4'ün bir dizinin dosyalarını aynı blok grubuna koyma çabasını yansıtır. Her adım bir disk okuması gerektirseydi yol çözümleme çok yavaş olurdu. dentry önbelleği (dcache) ad → i-node eşlemelerini bellekte tutar. Sık kullanılan yollar diske hiç gitmeden çözülür. dcache olumsuz sonuçları da saklar: “bu dizinde böyle bir ad yok” bilgisi de önbelleğe girer. Var olmayan bir dosyayı her açılışta arayan bir program böylece diski yormaz.
15.10.8Kod izleme: bağ sayısı ve unlink
Linux'ta bir dosyanın adı ile kendisi ayrı şeylerdir. Ad bir dizin girdisidir; dosya ise i-node'dur. İki tür bağ bu ayrımı kullanır (Şekil 15.34):
$ ln a.txt yedek/b.txt # katı
$ ln -s a.txt k # sembolik
$ ls -li
500 -rw-r--r-- 2 ali ali 9 a.txt
777 lrwxrwxrwx 1 ali ali 5 k -> a.txtKatı bağ aynı i-node'a verilmiş ikinci bir addır. a.txt ile yedek/b.txt aynı i-node'u (500) gösterir ve bağ sayısı 2'dir. Sembolik bağ ise ayrı bir i-node'dur (777); içeriği yalnız hedefin yoludur. ls -l onu l türüyle ve k -> a.txt biçiminde gösterir. Boyutu 5'tir, çünkü "a.txt" beş karakterdir. Katı bağ dosya sistemi sınırını aşamaz, çünkü i-node numarası yalnız kendi dosya sisteminde anlamlıdır. Dizinlere de konamaz, çünkü ağaçta döngü oluşturabilir. Sembolik bağ başka bir dosya sistemine ve dizinlere işaret edebilir. Bedeli, hedef silinince bağın sarkmasıdır.
Bir i-node iki sayaç taşır. Bağ sayısı diskteki dizin girdilerini, açık dosya sayısı bellekteki referansları sayar. Veri ancak ikisi de sıfıra inince serbest kalır. Aşağıdaki program bu iki sayacı adım adım değiştirir:
int fd = open("a.txt", O_CREAT|O_RDWR, 0644);
write(fd, "veri", 4);
link("a.txt", "b.txt"); /* katı bağ */
symlink("a.txt", "s.txt"); /* sembolik */
unlink("a.txt");
int r1 = open("b.txt", O_RDONLY);
int r2 = open("s.txt", O_RDONLY);
unlink("b.txt");
close(r1);
close(fd);| Satır | Dizin girdileri | 812 bağ | 812 açık | Sonuç |
|---|---|---|---|---|
1–2 open, write | a.txt → 812 | 1 | 1 | fd = 3 |
3 link | a.txt, b.txt → 812 | 2 | 1 | veri kopyalanmaz |
4 symlink | + s.txt → 813 ("a.txt") | 2 | 1 | 812 değişmez |
5 unlink("a.txt") | b.txt, s.txt | 1 | 1 | veri b.txt'den erişilebilir |
6 open("b.txt") | b.txt, s.txt | 1 | 2 | r1 = 4 |
7 open("s.txt") | b.txt, s.txt | 1 | 2 | r2 = −1, ENOENT |
8 unlink("b.txt") | s.txt | 0 | 2 | ad yok ama veri duruyor |
9 close(r1) | s.txt | 0 | 1 | fd 3 hâlâ çalışır |
10 close(fd) | s.txt | 0 | 0 | i-node ve bloklar serbest |
Satırların anlamı şöyledir. open yeni bir i-node (812) ayırır ve dizine a.txt → 812 girdisini ekler. link yalnız yeni bir dizin girdisi yazar; a.txt ile b.txt eşittir ve hangisinin “asıl” olduğu sorusunun yanıtı yoktur. symlink ayrı bir i-node (813) oluşturur ve 812'ye dokunmaz. unlink("a.txt") bir adı siler ve bağ sayısını 1'e indirir; adı “silmek” dosyayı silmek değildir. b.txt sorunsuz açılır ve en küçük boş tanımlayıcı 4 döner. s.txt açılınca çekirdek içindeki yolu, "a.txt"'yi izler. O ad artık yoktur: sembolik bağ sarkmıştır ve open −1 döndürür. unlink("b.txt") son adı da siler. Bağ sayısı 0'dır, ama iki tanımlayıcı açık olduğu için i-node ve veri henüz serbest bırakılmaz. ls dosyayı artık göstermez, yine de fd 3 üzerinden okuma ve yazma çalışır. Son tanımlayıcı kapanınca iki sayaç da sıfıra iner ve bloklar serbest kalır.
Geçici dosyaların bilinen kalıbı bu davranışa dayanır. Program dosyayı açar ve hemen unlink eder. Dosya hiçbir dizinde görünmez, başka bir süreç onu adıyla bulamaz. Program çöktüğünde bile çekirdek tanımlayıcıları kapatır ve alan kendiliğinden geri döner. Linux bu kalıp için O_TMPFILE bayrağını da sunar: dosya baştan adsız oluşur.
Diskte yer açmak için büyük bir günlük dosyasını silip alanın hemen geri gelmesini beklemek. Dosyaya yazan süreç tanımlayıcıyı açık tuttuğu sürece bloklar serbest kalmaz; df değişmez. Böyle silinmiş ama açık dosyalar lsof +L1 ile bulunur. Doğru yol, süreci yeniden başlatmak ya da dosyayı silmek yerine boşaltmaktır.
15.10.9ext4'ün ötesi
Linux'un desteklediği dosya sistemleri farklı ihtiyaçlara yanıt verir (Tablo 15.15). ext4 olgun, hızlı ve onarım aracı güvenilir olduğu için çoğu dağıtımda varsayılandır. XFS paralel G/Ç'de ve çok büyük dosyalarda güçlüdür; RHEL'in varsayılanıdır. F2FS flash belleğe uygun, günlük yapılı (log-structured) bir tasarımdır ve Android telefonlarda yaygındır.
| Dosya sistemi | Öne çıkan özellik | Tipik kullanım |
|---|---|---|
| ext4 | Günlükleme, extent, olgun ve hızlı fsck | Çoğu dağıtımda varsayılan |
| XFS | Paralel G/Ç, büyük dosyalar, gecikmeli ayırma | RHEL varsayılanı, büyük sunucular |
| Btrfs | Yazınca kopyalama, anlık görüntü, sağlama toplamı, sıkıştırma, alt birimler | Fedora ve openSUSE varsayılanı |
| F2FS | Flash'a uygun günlük yapılı tasarım | Android telefonlar |
| tmpfs | Tamamen RAM'de (sayfa önbelleği + takas) | /tmp, /run, /dev/shm |
| NFS, SMB | Ağ dosya sistemleri | Paylaşılan ev dizinleri |
Btrfs'in tutarlılık yöntemi günlükten farklıdır (Şekil 15.36). Hiçbir bloğun üzerine yazılmaz. B4 bloğu değişince yeni içerik boş bir yere, B4′'ye yazılır. B4'ü gösteren düğüm de değişmek zorundadır; onun da yeni bir kopyası (düğüm′) yazılır. Bu zincir köke kadar sürer ve yeni bir kök (v2) oluşur. Son adım, süperbloğun yeni kökü göstermesidir; bu tek bir atomik yazmadır. Çökme bu yazmadan önce olursa eski kök (v1) bütünüyle geçerlidir. Eski kökü saklamak bedavaya bir anlık görüntü (snapshot) verir: v1 ile ulaşılan ağaç, değişiklikten önceki dosya sistemidir.
VFS sayesinde bütün bu dosya sistemleri aynı sistem çağrılarıyla kullanılır. Hangi dizinde hangi dosya sisteminin bağlı olduğunu findmnt ya da df -T gösterir.
15.10.10/proc ve /sys
Linux çekirdeğin iç durumunu da dosya olarak sunar. Bu dosyalar diskte yoktur; okunduğu anda çekirdek tarafında üretilir.
$ cat /proc/loadavg
0.42 0.37 0.30 2/1234 98765
$ grep -E "MemTotal|MemAvailable" /proc/meminfo
MemTotal: 16303452 kB
MemAvailable: 9876544 kB
$ ls /proc/self
cmdline cwd environ exe fd maps status …
$ cat /sys/block/nvme0n1/queue/scheduler
[none] mq-deadline kyber bfq
$ sysctl vm.swappiness # = /proc/sys/vm/swappiness
vm.swappiness = 60/proc (procfs) iki tür bilgi taşır. Her süreç için bir dizin vardır: /proc/PID/status durumu ve bellek kullanımını, maps VMA'ları, fd/ açık tanımlayıcıları gösterir. /proc/self, okuyan sürecin kendi dizinidir. Çekirdek istatistikleri de buradadır. /proc/loadavg çıktısındaki ilk üç sayı son 1, 5 ve 15 dakikanın yük ortalamalarıdır; 2/1234 çalışan ve toplam görev sayısını, son sayı en son atanan PID'i verir. Örnekteki /proc/meminfo satırları, 15,55 GiB'lık toplam belleğin 9,42 GiB'ının (%60,6) kullanılabilir olduğunu söyler. ps, top ve free verilerini buradan okur. /proc/sys altındaki dosyalar ayarlanabilir çekirdek parametreleridir. sysctl vm.swappiness aslında /proc/sys/vm/swappiness dosyasını okur.
/sys (sysfs) çekirdeğin aygıt modelini gösterir. Her aygıt, sürücü ve veri yolu bir dizindir. Her dosya tek bir değer tutar. /sys/block/nvme0n1/queue/scheduler dosyası blk-mq çizelgeleyicisini gösterir: köşeli parantez içindeki none seçilidir. /sys/fs/cgroup ise cgroup v2'nin arayüzüdür; kaynak sınırları bu dizindeki dosyalara yazılarak konur. Süreç ağacını, tanımlayıcıları ve bellek haritalarını etkileşimli olarak incelemek için Linux Süreç Gezgini kullanılabilir.
15.11Güvenlik
15.11.1chmod ve umask hesapları
Linux'ta her dosyanın sahip, grup ve diğerleri için üçer izin biti vardır: r okuma, w yazma, x çalıştırma. Bunların önünde üç özel bit durur: setuid, setgid ve yapışkan bit. On iki bit dört sekizli rakamla yazılır. Her sekizli rakam üç bittir: 7 = 111 = rwx, 5 = 101 = r-x, 4 = 100 = r--. chmod 754 betik.sh böylece rwxr-xr-- verir.
chmod'un sembolik biçimi mevcut bitleri değiştirir. g+w bit düzeyinde VEYA (OR 020), o-r bit düzeyinde maskelemedir (AND ~004). 0754'e chmod g+w,o-r uygulanınca grup yazma biti açılır, diğerlerinin okuma biti kapanır: sonuç 0770, rwxrwx---'dur (Şekil 15.37).
chmod g+w,o-r: 0754 OR 020 AND ~004 = 0770.umask, sürecin yeni oluşturduğu dosyalardan silinecek izin bitlerinin maskesidir. Yeni dosya 666, yeni dizin 777 izin ister. Gerçek izin, istek & ~umask'tir.
umask 022 ve 027 iken touch a ile mkdir d hangi izinleri üretir?
- umask 022: dosya 666 & ~022 = 0644 (
rw-r--r--), dizin 777 & ~022 = 0755 (rwxr-xr-x). Çoğu dağıtımın varsayılanı budur. - umask 027: dosya 666 & ~027 = 0640 (
rw-r-----), dizin 777 & ~027 = 0750 (rwxr-x---). Diğerleri hiçbir şey göremez. - Sekizli çıkarma ilk durumda doğru sonucu tesadüfen verir; ikincide 666 − 027 = 637 (
rw--wxrwx) gibi yanlış bitleri açan bir değer üretir. umask bit temizler, çıkarma yapmaz.
Üç özel bitin her biri farklı bir sorunu çözer (Şekil 15.38). setuid (4xxx) çalıştırılabilir dosyada, çalışan sürecin etkin kullanıcı kimliğini dosya sahibininki yapar. passwd komutu 4755 (rwsr-xr-x) izinlidir: sıradan bir kullanıcı onu çalıştırınca süreç root yetkisiyle /etc/shadow'a yazabilir. setgid (2xxx) aynısını grup için yapar; dizinde ise yeni dosyaların dizinin grubunu miras almasını sağlar. Yapışkan bit (1xxx) dizinde anlamlıdır. /tmp 1777 (rwxrwxrwt) izinlidir: herkes yazabilir, ama herkes yalnız kendi dosyasını silebilir. Sembolik gösterimde x'in yerine geçen s ya da t, alttaki x bitinin de açık olduğunu gösterir. x kapalıysa büyük harf (S, T) yazılır.
rwsr-xr-x) ve yapışkan + 777 = 1777 (rwxrwxrwt).Sonuçlar stat -c "%a %A" dosya ile denetlenebilir; komut sekizli ve sembolik izni birlikte yazar. umask -S maskeyi sembolik gösterir. İzin hesapları İşletim Sistemi Koruma ve İzinler Laboratuvarı simülatöründe etkileşimli olarak denenebilir.
15.11.2Erişim denetimi, root ve yetenekler
Bir süreç bir dosyaya eriştiğinde çekirdek sürecin etkin kimliklerine bakar ve tek bir izin sınıfı seçer:
- Etkin uid 0 (root) ise izin verilir. Yalnız çalıştırma isteğinde dosyada en az bir x biti aranır.
- Etkin uid dosyanın sahibiyse yalnız sahip bitlerine bakılır.
- Etkin gid ya da ek gruplardan biri dosyanın grubuysa yalnız grup bitlerine bakılır.
- Aksi hâlde diğerleri bitlerine bakılır.
İlk eşleşen sınıf kazanır; sonraki sınıflara hiç bakılmaz. Bu yüzden ----rw-rw- izinli bir dosyayı sahibi okuyamaz, ama grup üyeleri ve başkaları okuyabilir. Sahip, isterse chmod ile izni kendine geri verebilir.
Bir sürecin iki tür kimliği vardır. Gerçek uid süreci başlatan kullanıcıdır. Etkin uid izin denetiminde kullanılan kimliktir; setuid programlarda ikisi ayrışır. access() çağrısı erişimi bilerek gerçek uid ile sınar. setuid bir programın “bu kullanıcı bu dosyayı gerçekten okuyabilir mi?” sorusunu yanıtlamasına yarar. setuid ve setresuid ise kimlik değiştirir; iyi yazılmış bir hizmet ayrıcalıklı işini bitirince bu çağrılarla yetkisini bırakır. Kullanıcı ve grup kimlikleri /etc/passwd ve /etc/group dosyalarında tutulur. Parola özetleri yalnız root'un okuyabildiği /etc/shadow'dadır. sudo, yetkilendirilmiş bir kullanıcıya komut başına root yetkisi verir.
Klasik modelde root her şeyi yapabilir, ötekiler hiçbir ayrıcalıklı işi yapamaz. Bu “ya hep ya hiç” yaklaşımı tehlikelidir: ping yalnız ham soket açabilmek için setuid root olmak zorundaydı ve içindeki bir hata saldırgana bütün sisteme erişim veriyordu.
Yetenek (capability), root'un ayrıcalıklarından bağımsız olarak verilip alınabilen bir parçadır. Linux root yetkisini 40'tan fazla yeteneğe böler: CAP_NET_RAW ham soket açmak, CAP_NET_BIND_SERVICE 1024'ün altındaki portlara bağlanmak, CAP_SYS_MODULE modül yüklemek, CAP_DAC_OVERRIDE dosya izinlerini aşmak içindir.
setcap cap_net_raw+ep /usr/bin/ping komutundan sonra ping setuid root olmadan ham soket açabilir. Bir açık bulunsa bile saldırgan yalnız ham soket açabilir; dosyaları okuyamaz, modül yükleyemez. Yetenekler süreç başına kümeler hâlinde tutulur ve task_struct'ın cred yapısında saklanır.
15.11.3Kod izleme: dosya izin denetimi
Çekirdeğin izin denetimi aşağıdaki işlevin sadeleştirilmiş hâlidir. istek bir bit maskesidir: okuma 4, yazma 2, çalıştırma 1.
int izin_var(cred *c, inode *f, int istek) {
int bit, m = f->mode; /* r4 w2 x1 */
if (c->euid == 0) /* root */
return !(istek & 1) || (m & 0111);
if (c->euid == f->uid)
bit = (m >> 6) & 7; /* sahip */
else if (grupta(c, f->gid))
bit = (m >> 3) & 7; /* grup */
else
bit = m & 7; /* diğer */
return (bit & istek) == istek;
}Root dalında dönüş ifadesi biraz şaşırtıcıdır. !(istek & 1), istekte çalıştırma yoksa doğrudur; okuma ve yazma için izin bitlerine hiç bakılmaz. Çalıştırma isteniyorsa m & 0111 sahip, grup ya da diğerlerinden en az birinde x biti arar. Root bile hiçbir x biti olmayan bir dosyayı çalıştıramaz, çünkü öyle bir dosya büyük olasılıkla bir program değildir. Sonraki if zinciri tek bir sınıf seçer. Seçilen üç bit kaydırma ve maskeyle alınır: sahip için (m >> 6) & 7, grup için (m >> 3) & 7, diğerleri için m & 7. Son satır, istenen bütün bitlerin açık olup olmadığını sınar.
Dosya rapor.txt'nin sahibi ali (uid 1000), grubu ekip, modu 0640 (rw-r-----) olsun. veli (1001) ekip grubundadır, can (1002) değildir.
| Kim, istek | Mod | Sınıf | bit | Sınama | Sonuç |
|---|---|---|---|---|---|
| ali, w (2) | 0640 | sahip | 6 (rw-) | 6 & 2 == 2 | izin |
| veli, w (2) | 0640 | grup | 4 (r--) | 4 & 2 = 0 ≠ 2 | ret |
| veli, r (4) | 0640 | grup | 4 (r--) | 4 & 4 == 4 | izin |
| can, r (4) | 0640 | diğer | 0 (---) | 0 & 4 = 0 ≠ 4 | ret |
| root, r (4) | 0640 | root | — | istekte x yok | izin |
| ali, r (4) | 0066 | sahip | 0 (---) | 0 & 4 = 0 ≠ 4 | ret |
EACCES).Son satır izlemenin en öğretici durumudur. Mod 0066'da (----rw-rw-) can ve veli dosyayı okuyabilir. ali ise sahip olduğu için sahip sınıfına düşer; grup üyeliğine hiç bakılmaz. Sahip bitleri boş olduğundan istek reddedilir. Gerçek çekirdekte bu denetimin önünde ve arkasında başka katmanlar da vardır: erişim denetim listeleri (ACL), yetenekler ve güvenlik modülleri.
15.11.4Konteynerler: ad alanları ve cgroups
Konteyner, Linux çekirdeğinde ayrı bir nesne değildir. Sıradan süreçlerin, iki çekirdek özelliğiyle yalıtılmış hâlidir. Bu özellikler ad alanları ve cgroups'tur.
Ad alanı (namespace), bir süreç grubuna sistem kaynaklarının ayrı bir görünümünü veren çekirdek özelliğidir. Ad alanındaki süreçler yalnız o görünümdeki PID'leri, bağlama noktalarını, ağ arayüzlerini ya da kullanıcı kimliklerini görür. Kontrol grubu (control group, cgroup), bir süreç grubunun CPU, bellek, G/Ç ve süreç sayısı gibi kaynak kullanımını sınırlayan ve ölçen çekirdek özelliğidir.
Kısa bir kural ikisini ayırır: ad alanı ne görüleceğini, cgroup ne kadar kullanılacağını belirler. Linux'ta sekiz ad alanı türü vardır (Tablo 15.17). Her biri clone, unshare ya da setns çağrısında bir bayrakla seçilir. Bir sürecin hangi ad alanlarında olduğu /proc/PID/ns/ dizinindeki bağlardan okunur; iki süreç aynı ad alanındaysa bağlar aynı numarayı gösterir.
| Ad alanı | Bayrak | Yalıttığı kaynak |
|---|---|---|
| PID | CLONE_NEWPID | Süreç kimlikleri; içerideki ilk süreç PID 1 |
| Bağlama | CLONE_NEWNS | Bağlama noktaları, dosya sistemi ağacı |
| Ağ | CLONE_NEWNET | Ağ arayüzleri, yönlendirme tablosu, portlar, güvenlik duvarı |
| UTS | CLONE_NEWUTS | Ana makine adı ve alan adı |
| IPC | CLONE_NEWIPC | System V IPC, POSIX ileti kuyrukları |
| Kullanıcı | CLONE_NEWUSER | uid/gid eşlemesi: içerideki root dışarıda sıradan bir uid |
| cgroup | CLONE_NEWCGROUP | cgroup ağacının görünen kökü |
| Zaman | CLONE_NEWTIME | Açılıştan beri geçen ve monoton saatler (5.6+) |
Bağlama ad alanı tarihte ilk gelendir. O zaman başka türlerinin olacağı düşünülmediği için bayrağın adı yalnız CLONE_NEWNS'dir. Kullanıcı ad alanı özeldir: ayrıcalıksız bir süreç bile onu oluşturabilir ve içinde root olabilir. İçerideki uid 0, dışarıda örneğin uid 100000'e eşlenir. İçerideki root yalnız kendi ad alanındaki kaynaklar üzerinde yetkilidir. “Köksüz” (rootless) konteynerler bu özelliğe dayanır.
Bir konteyner çalışma zamanının (runc, crun) yaptığı iş beş adımda izlenebilir (Şekil 15.40).
- Ad alanları.
clone()ya daunshare()ile yeni ad alanları oluşturulur: kendi PID numaraları, bağlama ağacı, ağ yığını, ana makine adı, IPC, kullanıcı eşlemesi, cgroup ve saat görünümü. - Kök dosya sistemi. Bağlama ad alanında
pivot_rootile imajın kök dosya sistemine geçilir. Kök çoğunlukla overlayfs'tir: imajın salt okunur katmanları üst üste konur, en üste konteynere özel yazılabilir bir katman eklenir. Aynı imajdan çalışan yüz konteyner alt katmanları paylaşır. - Kaynak sınırları. cgroup v2 dizinine sınırlar yazılır:
cpu.max="50000 100000",memory.max= 512M,pids.max= 100. - Saldırı yüzeyi. Gereksiz yetenekler düşürülür.
no_new_privsbayrağı açılır; böylece setuid programlar bile yetki kazandıramaz. Onlarca riskli sistem çağrısını engelleyen bir seccomp süzgeci ve bir AppArmor ya da SELinux profili uygulanır. - Başlatma. Son olarak
execveuygulamayı çalıştırır. Uygulama içeride PID 1'dir, konakta PID 48213'tür.
cpu.max dosyasındaki "50000 100000" ne anlama gelir? Konteynere tam iki CPU vermek için ne yazılır?
- Biçim
"kota dönem"'dir, ikisi de mikrosaniye. Grup her 100 000 µs'lik (100 ms) dönemde toplam 50 000 µs CPU zamanı kullanabilir. - 50 000 / 100 000 = 0,5: grup yarım CPU'luk zaman alır. Kotayı doldurursa dönemin geri kalanında, birden çok CPU'su boş olsa bile çalıştırılmaz.
- İki CPU için kota dönemin iki katı olmalıdır:
"200000 100000". Kota birden çok CPU'ya yayılabilir; grubun iki iş parçacığı iki çekirdekte aynı anda çalışabilir.
cgroup v2'de bütün denetleyiciler (cpu, memory, io, pids) tek bir hiyerarşiyi paylaşır. Eski v1'de her denetleyicinin ayrı ağacı vardı ve bir süreç farklı ağaçlarda tutarsız gruplara düşebiliyordu. Bellek sınırı aşılırsa OOM killer yalnız o gruptan bir kurban seçer. systemd de her hizmeti kendi cgroup'una koyar. systemd-cgls ağacı, systemd-cgtop grupların anlık kullanımını gösterir.
Konteynerlerin hepsi aynı çekirdeği paylaşır. Bu, onları sanal makineden çok daha hafif kılar: konteyner milisaniyeler içinde başlar ve kendi çekirdeği için bellek harcamaz. Bedeli yalıtımın zayıflığıdır. Çekirdekteki bir açık bütün konteynerleri birden etkileyebilir. Sanal makinede ise saldırganın önce konuk çekirdeği, sonra hiper yöneticiyi aşması gerekir.
Ad alanını güvenlik sınırı, cgroup'u görünürlük aracı sanmak. Bir PID ad alanı süreçleri gizler ama bellek kullanımını sınırlamaz; sınırsız bir konteyner konağın belleğini tüketebilir. Tersine, cgroup belleği sınırlar ama konaktaki süreçleri gizlemez. Konteyner için ikisi birlikte gerekir.
15.11.5seccomp ve Linux güvenlik modülleri
Bir sistem çağrısı nesnesine ulaşmadan önce birkaç denetimden geçer (Şekil 15.41). Örnek olarak bir uygulama open("/etc/shadow") çağırsın.
EPERM ya da EACCES ile döner.- seccomp. Süreç kendine bir seccomp-BPF süzgeci kurmuşsa çağrı önce ondan geçer. Süzgeç çağrının numarasına ve argümanlarına bakan küçük bir BPF programıdır. İzin verebilir, bir hata kodu döndürebilir ya da süreci öldürebilir. Chrome'un oluşturucu süreçleri, systemd hizmetleri, Docker ve Android uygulamaları bu süzgeçleri kullanır.
- DAC. İsteğe bağlı erişim denetimi (discretionary access control) rwx bitlerine, uid ve gid'e bakar. Buna isteğe bağlı denir, çünkü izinleri dosyanın sahibi kendi isteğiyle değiştirebilir.
- Yetenekler. DAC reddederse çekirdek sürecin bu reddi aşan bir yeteneği olup olmadığına bakar; örneğin
CAP_DAC_OVERRIDE. - LSM. Son olarak Linux güvenlik modüllerinin kancaları çalışır. Bunlar zorunlu erişim denetimi uygular: kuralları yönetici koyar ve root bile onları aşamaz.
Başlıca güvenlik modülleri dört tanedir. SELinux etiket tabanlıdır: her sürecin ve dosyanın bir tipi vardır; politika hangi tipin hangi tipe ne yapabileceğini söyler. RHEL, Fedora ve Android'de kullanılır. AppArmor yol tabanlı profiller kullanır: “bu program yalnız şu dosyalara erişebilir”. Yazması daha kolaydır; Ubuntu, SUSE ve Debian'da yaygındır. Landlock (5.13) ayrıcalıksız bir sürecin kendini korumalı alana almasını sağlar: “bundan sonra yalnız ./veri altına yazabilirim”. Bir süreç bu kısıtı bir kez koyunca kaldıramaz; çocukları da miras alır.
15.12Android
Android, Linux çekirdeği üzerinde çalışan ama GNU kullanıcı alanını kullanmayan bir sistemdir (Şekil 15.42). C kütüphanesi Google'ın yazdığı Bionic'tir. Uygulamalar Java ya da Kotlin ile yazılır ve Android Çalışma Zamanında (ART) çalışır. Donanım soyutlama katmanı (HAL) üreticilerin sürücülerini çatıdan ayırır. Android çekirdeğe üç önemli ek getirdi: Binder IPC mekanizması, enerji yönetimi için uyanıklık kilitleri (wakelock) ve zorunlu SELinux. Binder zamanla ana çekirdeğe de girdi.
Bir uygulamanın başlaması ilginç bir fork kullanımıdır. Önyüklemede init, zygote adlı süreci başlatır. zygote ART'ı ve çatının binlerce sınıfını ile kaynaklarını bir kez yükler ve bekler. Bir uygulama açılırken zygote fork eder. Yüklü sayfalar COW ile paylaşılır; her uygulama sınıfları yeniden yüklemek zorunda kalmaz ve başlatma hızlanır. Bellek de tasarruf edilir, çünkü çatının sayfaları bütün uygulamalarda tek kopyadır. Çocuk süreç sonra uygulamanın benzersiz uid'ine, SELinux bağlamına (untrusted_app) ve seccomp süzgecine geçer. Burada fork sonrası exec yapılmaz; zaten hazır olan görüntü kullanılır.
Uygulama konum ya da kamera gibi bir sistem hizmetini Binder ile çağırır. Binder, /dev/binder aygıtı üzerinden çalışan bir uzak yordam çağrısı mekanizmasıdır. Çekirdek veriyi alıcının önceden eşlenmiş tamponuna tek bir kopyayla taşır. Ayrıca arayanın uid'ini de iletir. Bu uid'i uygulama uyduramaz, çünkü onu çekirdek ekler. Hizmet izni buna göre denetler.
Android'in korumalı alanı Linux'un en eski koruma mekanizmasına dayanır: her uygulama ayrı bir Linux kullanıcısıdır. Şekildeki uygulama uid 10123 ile çalışır ve /data/data/com.ornek.app dizini yalnız bu uid'e açıktır. Bir uygulama başka bir uygulamanın dosyalarını okumaya kalkarsa sıradan dosya izinleri onu durdurur. Bellek azalınca çekirdeğin OOM killer'ı yerine kullanıcı alanındaki lmkd devreye girer. lmkd bellek baskısını PSI sayaçlarıyla izler ve önemsiz arka plan uygulamalarını öldürür. Kullanıcı o uygulamaya döndüğünde Android onu kaydedilmiş durumundan yeniden başlatır.
15.13Dersin kavramları Linux'ta
Bu örnek inceleme, dersin genel kavramlarının her birine Linux'ta somut bir karşılık buldu (Tablo 15.18). Tablonun son satırı, Linux'un bugünkü yönünü özetler: mekanizma çekirdekte kalırken politika giderek kullanıcıya bırakılıyor.
| Konu | Kavram | Linux'taki karşılığı |
|---|---|---|
| Yapılar | Sistem çağrısı, monolitik çekirdek | syscall, LKM, eBPF |
| Süreçler | PCB, oluşturma, IPC | task_struct, fork/exec/wait, sinyal, boru |
| İş parçacıkları | Çekirdek iş parçacığı modeli | clone(), NPTL 1:1 |
| Çizelgeleme | Adil paylaşım, gerçek zaman | EEVDF, FIFO/RR/DEADLINE, sched_ext |
| Senkronizasyon | Kilit, koşul değişkeni | futex, döner kilit, RCU |
| Kilitlenme | Tespit ve önleme | lockdep, fcntl EDEADLK |
| Bellek | Ayırma, parçalanma | bölgeler, buddy listeleri, SLUB |
| Sanal bellek | Sayfalama, yer değiştirme | 4/5 düzey tablo, COW, MGLRU, zswap, OOM |
| Dosya sistemleri | i-node, dizin, günlük | VFS, ext4, XFS, Btrfs |
| G/Ç | Sürücü, çizelgeleme | aygıt dosyaları, blk-mq, io_uring |
| Çok işlemcili | Yük dengeleme, NUMA | CPU başına kuyruklar, çizelgeleme alanları |
| Güvenlik | Erişim denetimi, yalıtım | rwx, yetenekler, ad alanları, SELinux, seccomp |
| Tasarım | Mekanizma ve politika ayrımı | cgroup, sched_ext: politika kullanıcıda |
Tabloda adı geçen ama bu notta ayrıntılanmayan iki mekanizma kısa bir açıklamayı hak eder. futex (hızlı kullanıcı alanı muteksi), pthread kilitlerinin temelidir. Kilit boştayken alma ve bırakma tümüyle kullanıcı alanında, tek bir atomik komutla olur. Çekirdeğe yalnız çekişme varken, yani bir iş parçacığının uyuması gerektiğinde gidilir. RCU (oku–kopyala–güncelle), okuyucuların hiç kilit almadan veri yapısını okuduğu bir senkronizasyon yöntemidir. Yazar yapının yeni bir kopyasını hazırlar ve işaretçiyi atomik olarak değiştirir. Eski kopya, onu okuyor olabilecek bütün okuyucular bitene kadar silinmez. lockdep ise çekirdeğin kendi kilitlenme denetleyicisidir: kilitlerin hangi sırayla alındığını izler ve bir sıra döngüsü gördüğü anda, kilitlenme henüz yaşanmadan uyarı verir.
15.14Özet
- Linux, UNIX'in POSIX arayüzünü baştan yazılmış kodla gerçekleyen monolitik bir çekirdektir. GNU araçlarıyla birleşerek dağıtımları oluşturur. Yaklaşık 9–10 haftada bir sürüm çıkar; her yılın son sürümü LTS olur.
- Katmanlar arasında üç arayüz vardır: kullanıcı, kütüphane ve sistem çağrısı.
printfbir tampona yazar; asıl sistem çağrısıwrite'tır vesyscallkomutuyla çekirdeğe girer. - Kabuk yönlendirmeleri soldan sağa
dup2ile, boru hatlarınıpipe,fork,dup2veexecile kurar. Kullanılmayan yazma ucu kapatılmazsa okuyucu EOF göremez. - Yüklenebilir modüller monolitik çekirdeğe esneklik verir ama çekirdek ayrıcalığıyla çalışır. eBPF doğrulanmış küçük programları çekirdekte güvenle çalıştırır.
- Linux'ta süreç ve iş parçacığı aynı yapıdır:
task_struct. Farkmm,files,fs,sighandgibi alt yapıların paylaşılıp paylaşılmadığıdır.clone()bayrakları bunu seçer; NPTL 1:1 modelini kurar. Kullanıcının gördüğü PID,tgid'dir. - Sinyaller bekleyen kümesinde bir bit olarak bekler ve kullanıcı moduna dönüşte, hazır = bekleyen & ~engelli kümesinin en küçük numarasından başlayarak teslim edilir. Engellemek ertelemektir; klasik sinyaller sayılmaz.
- Adil sınıf 6.6'dan beri EEVDF ile çalışır: uygun görevler (v ≤ V) arasından sanal son tarihi en erken olan seçilir. Pay ağırlıklara (nice 0 → 1024) bağlıdır; kısa dilim daha sık seçilme sağlar.
- Adres alanı VMA'lardan oluşur ve maple ağacında tutulur. x86-64'te 48 bitlik adres 9+9+9+9+12 bitlik dört düzeyli yürüyüşle çevrilir; LA57 beşinci düzeyi ekler.
- Fiziksel bellek düğüm → bölge → sayfa hiyerarşisindedir; buddy ayırıcı 20–210 sayfalık blokları yönetir. SLUB küçük nesneleri slab'larda tutar; serbest liste işaretçisi boş nesnenin içindedir ve hızlı yol kilitsizdir.
- Boş RAM sayfa önbelleğine gider. Baskı altında kswapd, MGLRU, zswap/takas, doğrudan geri kazanım ve son çare olarak OOM killer devreye girer.
- Aygıt dosyalarında major numara sürücüyü, minor numara birimi seçer. Blok katmanı blk-mq ile çok kuyrukludur. Sürücüler
file_operationstablosuyla VFS'e bağlanır. - VFS'in dört nesnesi superblock, inode, dentry ve file'dır. ext4 blok grupları, extent'ler ve
data=orderedgünlükleme kullanır. Veri, bağ sayısı ve açık dosya sayısı ikisi de sıfır olunca serbest kalır. - İzin denetiminde ilk eşleşen sınıf kazanır; umask bit temizler (istek & ~umask). Yetenekler root'u parçalara böler. Konteyner = ad alanları (ne görülür) + cgroups (ne kadar kullanılır) + seccomp ve LSM.
- Android aynı çekirdeği Bionic, ART, Binder, zygote ve uygulama başına uid ile kullanır.
15.15Kavram listesi
Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.
- monolitik çekirdek monolithic kernel
- yüklenebilir çekirdek modülü loadable kernel module
- sistem çağrısı system call
- kabuk shell
- boru, boru hattı pipe, pipeline
- dosya tanımlayıcısı file descriptor
- süreç kontrol bloğu (
task_struct) process control block - iş parçacığı grubu kimliği thread group ID
- sinyal signal
- yazınca kopyalama copy-on-write
- sanal çalışma süresi virtual runtime
- uygunluk, sanal son tarih eligibility, virtual deadline
- önyükleyici bootloader
- sanal bellek alanı (VMA) virtual memory area
- buddy (kardeş) sistemi buddy system
- slab ayırıcı slab allocator
- sayfa önbelleği page cache
- sanal dosya sistemi (VFS) virtual file system
- dosya kilitleme file locking
- günlükleme journaling
- katı bağ / sembolik bağ hard link / symbolic link
- yetenek capability
- ad alanı namespace
- kontrol grubu (cgroup) control group
- konteyner container
15.16Kendini Dene
x86-64'te 4 KiB sayfa ve dört düzeyli tablo kullanılıyor. 0x00007ffd4a3b2010 adresinin dört indisi ve ofseti nedir? PTE'deki çerçeve numarası 0x2f00a ise fiziksel adres nedir?
Cevap
PGD = adres >> 39 = 255, PUD = 501, PMD = 81, PTE = 434, ofset = 0x010. Fiziksel adres 0x2f00a × 4096 + 0x010 = 0x2f00a010. Bu adres bir yığın adresidir: PGD 255, kullanıcı yarısının son girdisidir.
nice 0 (w = 1024) ve nice 1 (w = 820) iki sürekli hazır görev tek CPU'yu paylaşıyor. Uzun vadede CPU nasıl paylaşılır? nice 1 görevi 4 ms çalışınca
vruntime'ı ne kadar artar?Cevap
Pay ağırlıkla orantılıdır: 1024 / 1844 ≈ %55,5 ve 820 / 1844 ≈ %44,5. Bir nice adımı payı yaklaşık 10 puan kaydırır. Δv = 4 × 1024 / 820 ≈ 4,995 ms.
Bir kuyrukta V = 12 ms. A (v = 10, d = 14), B (v = 12, d = 13) ve C (v = 13, d = 12) görevleri var. EEVDF hangisini seçer? CFS hangisini seçerdi?
Cevap
Uygun görevler v ≤ V olanlardır: A ve B. C'nin son tarihi en erken olsa da uygun değildir, çünkü payından fazlasını almıştır. A ile B arasında son tarihi erken olan B seçilir. CFS en küçük
vruntime'ı, yani A'yı seçerdi.Bir süreç
SIGUSR1'i engellemiş veSIGINTiçin bir işleyici kurmuş. Uyurken sırayla 17, 10 ve 2 numaralı sinyaller geliyor. Kullanıcı moduna dönüşte hangi sinyaller hangi sırayla teslim edilir? Hangisi bekler?Cevap
Bekleyen {2, 10, 17}, engelli {10}; hazır {2, 17}. En küçük numara önce: 2 (işleyici çalışır), sonra 17 (
SIGCHLD, varsayılan: yok say). 10 bekleyen kümesinde kalır ve engel kalkınca teslim edilir. Geliş sırası seçimde rol oynamaz.Yeni bir
kmalloc-512slab'ı (4 KiB, sekiz nesne: +0, +512, …) ile şu işlemler yapılıyor: a, b, c = ayir; birak(b); birak(a); d, e, f = ayir. d, e ve f hangi adresleri alır?Cevap
a = +0, b = +512, c = +1024; freelist +1536. birak(b): +512 → +1536. birak(a): +0 → +512 → +1536. Son bırakılan ilk verilir: d = +0, e = +512, f = +1536.
clone(CLONE_VM | CLONE_FILES | SIGCHLD, …)ile,CLONE_THREADverilmeden oluşturulan görevps'te nasıl görünür? Çocuk yeni bir dosya açarsa ebeveyn onu kullanabilir mi?Cevap
CLONE_THREADolmadığı için çocuğun kendi TGID'i vardır;ps'te ayrı bir süreç olarak görünür ve bitince ebeveyneSIGCHLDgönderir. Yine de adres alanını ve tanımlayıcı tablosunu ebeveynle paylaşır. Çocuğun açtığı dosyanın numarası ebeveynde de geçerlidir; ebeveyn aynı numarayla okuyabilir.umask 077 ve 002 iken
touch avemkdir dhangi izinleri üretir?Cevap
umask 077: dosya 0600 (
rw-------), dizin 0700 (rwx------). umask 002: dosya 0664 (rw-rw-r--), dizin 0775 (rwxrwxr-x). 002, grup çalışması için ortak dizinlerde kullanılır.progdosyasının sahibi ali, grubu ekip, modu 0750. veli ekip grubunda, can değil. veliprog'u çalıştırabilir mi? can okuyabilir mi? root, modu 0644 olan bir betiği çalıştırabilir mi?Cevap
veli grup sınıfına düşer: bit 5 (r-x), 5 & 1 = 1, izin. can diğerleri sınıfına düşer: bit 0, ret. root için okuma ve yazmada bitlere bakılmaz, ama çalıştırmada en az bir x biti gerekir; 0644'te hiç yoktur, ret (
EACCES).echo x > f,ln f g,ln -s f hçalıştırılıyor. Bir süreçg'yi açıp tanımlayıcıyı tutuyor. Sonrarm fverm gyapılıyor. Veri blokları ne zaman serbest kalır?cat hne yazar?Cevap
f ve g aynı i-node'dur; bağ sayısı 2'dir. İki
rm'den sonra bağ sayısı 0 olur, ama açık bir tanımlayıcı bulunduğu için bloklar ancak süreç tanımlayıcıyı kapatınca serbest kalır. h,"f"yolunu tutar; o ad artık yoktur.cat h“No such file or directory” (ENOENT) hatası verir.Bir konteynerin
cpu.maxdosyasında"25000 100000"yazıyor. Konteyner ne kadar CPU kullanabilir? Bu sınır içeridekipsçıktısını etkiler mi?Cevap
Her 100 ms'lik dönemde 25 ms, yani çeyrek CPU. Sınır bir cgroup ayarıdır; yalnız ne kadar kullanılacağını belirler.
ps'in neyi gösterdiğini PID ad alanı belirler, cgroup değil.
15.17İleri okuma
- R. Love, Linux Kernel Development, 3. baskı, Addison-Wesley, 2010: süreç yönetimi, çizelgeleme, bellek ve VFS bölümleri
task_struct'tan slab ayırıcısına kadar bu nottaki yapıların çekirdek kodundaki karşılığını sade bir dille gösterir. CFS dönemine aittir. - M. Kerrisk, The Linux Programming Interface, No Starch Press, 2010: sinyaller, süreç oluşturma, borular, dosya kilitleme ve yetenekler için en kapsamlı programcı kaynağı; yazarın man7.org'daki kılavuz sayfaları güncel tutulur.
- Linux çekirdek belgeleri, docs.kernel.org, “EEVDF Scheduler” ve “Control Group v2” sayfaları: EEVDF'in kavramlarını ve cgroup v2 denetleyicilerinin bütün dosyalarını birinci elden anlatır.
- I. Stoica, H. Abdel-Wahab, “Earliest Eligible Virtual Deadline First: A Flexible and Accurate Mechanism for Proportional Share Resource Allocation”, Old Dominion Üniversitesi teknik raporu, 1995: alacak, uygunluk ve sanal son tarih kavramlarının kuramsal temeli.
15.18Kaynaklar
- A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4. baskı (2014) / 5. baskı (2023), Pearson — Bölüm 10: Case Study 1: Unix, Linux and Android.
- A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10. baskı, Wiley, 2018 — Bölüm 20: The Linux System.
- R. Love, Linux Kernel Development, 3. baskı, Addison-Wesley, 2010.
- M. Kerrisk, The Linux Programming Interface, No Starch Press, 2010; man7.org kılavuz sayfaları (namespaces(7), capabilities(7), signal(7), fcntl(2), clone(2)).
- The Linux Kernel Documentation, docs.kernel.org — scheduler (EEVDF, sched-ext), admin-guide/mm (MGLRU, zswap), block (blk-mq), filesystems (vfs, ext4), userspace-api (seccomp, landlock), admin-guide/cgroup-v2.
- Android Open Source Project, source.android.com — mimari, Binder, uygulama korumalı alanı.
- D. M. Ritchie, K. Thompson, “The UNIX Time-Sharing System”, Communications of the ACM, 17(7), 1974.
- I. Stoica, H. Abdel-Wahab, “Earliest Eligible Virtual Deadline First: A Flexible and Accurate Mechanism for Proportional Share Resource Allocation”, teknik rapor, Old Dominion University, 1995.
İlgili sayfalar: Bölüm 15 sunumu · Çalışma soruları · Linux Süreç Gezgini · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.