İş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, dup2 ve exec ile kurulduğunu izlemek.
Süreçler ve çizelgeleme
fork ve execve'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.

1969–1983UNIX doğuyor1969UNIX, PDP-7 (Bell Labs)1971PDP-11’e taşındı1973C ile yeniden yazıldı1977BSD (Berkeley)1983System V, 4.2BSD1983GNU projesi1984–1993Özgür UNIX arayışı1987MINIX (Tanenbaum)1988POSIX standardı1989GPL lisansı1991Linux 0.01 (Torvalds)19920.12: GPL’ye geçiş1993Slackware, Debian1994–2010Linux olgunlaşıyor1994Linux 1.019962.0: SMP desteği1999Red Hat halka arz20032.6 serisi2005git2008Android: HTC Dream2011–2026Her yerde Linux20113.0 numarası2017Top500: %100 Linux20226.0; 6.1’de Rust20236.6: EEVDF20246.12: sched_ext, RT20266.x → 7.0 serisi
Şekil 15.1: UNIX'ten Linux'a dört dönem: UNIX'in doğuşu, özgür UNIX arayışı, Linux'un olgunlaşması ve her yerde Linux.

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.

Uygulamalar: masaüstü, sunucu yazılımlarıPaket yöneticisi + depolar (apt, dnf, pacman)init sistemi: systemdGNU araçları + C kütüphanesi (glibc, musl)Linux çekirdeğidağıtım(distro)Android: Linux çekirdeği + Bionic libc,GNU araçları yok
Şekil 15.2: Bir dağıtımın katmanları: çekirdek, GNU araçları ve C kütüphanesi, init sistemi, paket yöneticisi ve uygulamalar.

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.

Tablo 15.1: Başlıca dağıtım aileleri.
AileÖrneklerPaket biçimi / araç
DebianDebian, Ubuntu, Mint, Pardus.deb / apt
Red HatFedora, RHEL, Rocky, Alma.rpm / dnf
ArchArch, Manjaropacman
SUSEopenSUSE, SLESzypper
HafifAlpine (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.

/home/ali/not.txt/dev/tty1boru (pipe)soketfd → read() / write()
Şekil 15.3: Her şey dosyadır: disk dosyası, terminal, boru ve soket aynı dosya tanımlayıcısı arayüzüyle kullanılır.

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.

KullanıcılarStandart yardımcı programlarkabuk, editör, derleyiciStandart kütüphane (glibc)printf, open, fork sarmalayıcılarıLinux çekirdeğisüreç, bellek, dosya, G/ÇDonanımCPU, bellek, diskler, terminalkullanıcı arayüzükütüphane arayüzüsistem çağrısı arayüzükullanıcı moduçekirdek modu1 · Uygulamaprintf("merhaba\n");glibc tamponuna yazar2 · glibcwrite(1, buf, 8)rax ← 1 (write), syscall3 · Çekirdek girişientry_SYSCALL_64sys_call_table[1] → vfs_write4 · Sürücüpty/tty sürücüsüterminal ekrana basar5 · Dönüşrax ← 8 (yazılan bayt)sysret → kullanıcı modu
Şekil 15.4: UNIX/Linux katmanları ve 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.

  1. Uygulama. printf bir 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.
  2. 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ı rax yazmacına konur; write'ın numarası 1'dir. Argümanlar rdi, rsi, rdx yazmaçlarına yerleşir. Sonra syscall komutu yürütülür.
  3. Çekirdek girişi. CPU çekirdek moduna geçer ve entry_SYSCALL_64 giriş koduna atlar. Giriş kodu numarayı sys_call_table dizisinde indis olarak kullanır ve write'ın işleyicisini çağırır. İşleyici VFS katmanına geçer: dosya tanımlayıcısı 1'in arkasındaki nesneyi bulur.
  4. 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.
  5. Dönüş. Yazılan bayt sayısı (8) rax'ta döner. sysret komutu kullanıcı moduna geri geçer. Hata olsaydı çekirdek negatif bir hata kodu döndürürdü; glibc bunu errno'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.

Sık yapılan hata

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ı.

Tablo 15.2: Sık kullanılan komutlar.
KategoriKomutlar
Dosya / dizinls cd pwd mkdir rm -r cp mv touch find tar
Metin (filtre)cat less head tail grep sort uniq wc echo
Süreç / sistemps top kill chmod chown df du sync
Ağssh scp ping curl wget traceroute ip ss
Bilgiman 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’un tanımlayıcı tablosu (kabuk exec’ten önce kurar):0 stdingirdi.txt1 stdoutcikti.txt2 stderrhatalar.log
Şekil 15.5: 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.

Örnek 15.1

/yok dizini yoktur. ls /yok /tmp > c.txt 2>&1 ile ls /yok /tmp 2>&1 > c.txt komutlarında hata iletisi nereye gider?

  1. 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.
  2. İ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.
  3. 2>&1 bir “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.txtPID 2101|çıktı (stdout)hata: diskbilgi: açılışhata: ağhata: diskuyarı: ısıhata: ağhata: diskgrep hataPID 2102|çıktı (stdout)hata: diskhata: ağhata: diskhata: ağhata: disksortPID 2103|çıktı (stdout)hata: ağhata: ağhata: diskhata: diskhata: diskuniq -cPID 2104çıktı (stdout) 2 hata: ağ 3 hata: diskterminal
Şekil 15.6: 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”.

Sık yapılan hata

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.

Tablo 15.3: ls | wc -l kurulurken tanımlayıcı tabloları (tty: terminal; y: borunun yazma ucu; o: okuma ucu).
AdımKabuk 0 1 2 3 4Çocuk 1Çocuk 2
pipe(fd)tty tty tty o y——
1. forktty tty tty o ytty tty tty o y—
dup2(4, 1) + iki closetty tty tty o ytty y tty – –—
execlp("ls")aynıtty y tty – – (artık ls)—
2. fork, dup2(3, 0), exec("wc")tty tty tty o yaynıo tty tty – –
Kabuk iki ucu kapatırtty tty tty – –aynıaynı
kabuk0tty1tty2tty34ls0tty1boru-y2tty34wc0boru-o1tty2tty34boru tamponuy →→ o
Şekil 15.7: Son durum: borunun yazma ucu yalnız 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.

Sık yapılan hata

Ç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ı.

Sistem çağrılarıG/Ç bileşeniSanal dosya sistemi (VFS)TerminallerSatır disipliniKarakter aygıt sürücüleriSoketlerAğ protokolleriAğ aygıt sürücüleriDosya sistemleriGenel blok katmanıG/Ç çizelgeleyiciBlok aygıt sürücüleriBellek yönetimiSanal bellekSayfalama,sayfa değiştirmeSayfa önbelleğiSüreç yönetimiSinyal işlemeSüreç/iş parçacığıoluşturma, sonlanmaCPU çizelgelemeKesmelerDağıtıcı (dispatcher)
Şekil 15.8: Linux çekirdeğinin yapısı: G/Ç, bellek ve süreç yönetimi; üstte sistem çağrıları, altta kesmeler ve dağıtıcı.

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.

Tanım 15.1

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.

  1. İstek. Yönetici modprobe e1000e yazar. Daha sık görülen yol otomatik yüklemedir: aygıt sisteme takılınca çekirdek bir olay üretir, kullanıcı alanındaki udev aygıtın kimliğine uyan modülü bulup yükler.
  2. Bağımlılıklar. modprobe, /lib/modules/$(uname -r)/modules.dep dosyasından modülün hangi modüllere ihtiyaç duyduğunu okur ve önce onları yükler. Daha ilkel insmod bunu yapmaz; verilen tek dosyayı yüklemeye çalışır.
  3. Kopyalama ve imza. finit_module() sistem çağrısı modül dosyasını çekirdek belleğine kopyalar. Çağıranın CAP_SYS_MODULE yeteneği olmalıdır. Secure Boot açıkken modülün imzası doğrulanır; imzasız modül reddedilir.
  4. 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_SYMBOL ile dışa açılmış çekirdek işlevleri modüle görünür.
  5. Başlatma. Modülün module_init ile işaretlenmiş işlevi çalışır. Sürücü kendini ilgili alt sisteme kaydeder; bir PCI kartı için bu pci_register_driver çağrısıdır.
  6. 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 ve eth0 benzeri bir ağ arayüzü oluşur.
kullanıcı alanı# modprobe e1000emodules.dep: bağımlılıklarçekirdekfinit_module()imza denetimiyer değiştirmesembol bağlamamodule_init()sürücü kaydıPCI aygıtı eşleşir → probe()çekirdek sembol tablosuEXPORT_SYMBOL ile dışa açılanlareth0 arayüzü hazır
Şekil 15.9: Bir modülün yüklenmesi: 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.

shPID 501shPID 748lsPID 7481. fork() → yeni süreç2. execve() → aynı süreçkopya: aynı programgörüntü ls ile değişir
Şekil 15.10: fork() yeni bir süreç (aynı program), execve() aynı süreçte yeni bir program üretir.

fork() çekirdekte şu işleri yapar:

  1. Yeni bir task_struct ayırır, ebeveyninkinden doldurur ve yeni bir PID atar.
  2. Ç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.
  3. 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.
  4. Açık dosyaları paylaştırır. Çocuğun tanımlayıcı tablosu ebeveyninkinin kopyasıdır; her girdi aynı struct file nesnesini gösterir ve nesnenin referans sayısı artar.
  5. 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.

Tablo 15.4: Süreç yönetimi sistem çağrıları.
Ç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()kopya, COWexecve()yeni programçalışırsinyaller, G/Çexit()kaynaklar bırakılırzombi (Z)durum bekliyorwaitpid()ebeveyn toplarkill()/sigaction() ile sinyal, alarm()/pause() ile zamanlama her aşamada araya girebilir
Şekil 15.11: Bir sürecin yaşamı: 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.

struct task_struct__state (R, S, D…)pid, tgidprio, policyse: vruntime, deadlinemmfilesfssighand, signalcrednsproxycgroupsparent, childrenmm_structVMA’lar, pgd (sayfa tablosu)files_structfd dizisi → struct filefs_structkök dizin, cwd, umasksighand_structsinyal işleyicilericreduid, gid, yeteneklernsproxypid, net, mnt… ad alanlarıcss_setcgroup sınırlarısched_entityEEVDF ağacındaki düğümiş parçacıkları bunları paylaşabilirher görev: ayrı çekirdek yığını
Şekil 15.12: 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ın mm'si boştur.
  • files → files_struct: dosya tanımlayıcısı tablosu. Her girdi bir struct file'ı gösterir.
  • fs → fs_struct: kök dizin, çalışma dizini ve umask.
  • sighand ve signal: 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.

Tanım 15.2

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.

Örnek 15.2

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?

  1. Ana iş parçacığı: pid = 900, tgid = 900. getpid() = 900, gettid() = 900. Grubun lideri, kimliği grup kimliğine eşit olan görevdir.
  2. Yeni iş parçacıkları CLONE_THREAD ile doğduğu için grubun tgid'ini miras alır: getpid() ikisinde de 900'dür. gettid() 901 ve 902'dir.
  3. ps tek bir 900 süreci gösterir. ps -L ya 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.

Tanım 15.3

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.

Tablo 15.5: Başlıca clone bayrakları ve etkilediği yapılar.
BayrakVerilirseVerilmezse
CLONE_VMadres alanı (mm) paylaşılırCOW ile kopyalanır
CLONE_FILEStanımlayıcı tablosu paylaşılırtablo kopyalanır (aynı struct file'lar)
CLONE_FSkök, çalışma dizini, umask paylaşılırkopyalanır
CLONE_SIGHANDsinyal işleyicileri paylaşılır (CLONE_VM gerektirir)kopyalanır
CLONE_THREADaynı 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şlarebeveynin ad alanında kalır

Üç tipik çağrı bayrakların gücünü gösterir (Şekil 15.13).

  1. fork(). glibc'de fork, yalnız SIGCHLD bayrağıyla çağrılan bir clone'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ç. SIGCHLD burada bir paylaşım bayrağı değil, çocuk bitince ebeveyne gönderilecek sinyalin numarasıdır.
  2. 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şini pthread_join'e bildirir.
  3. Konteyner. Ad alanı bayrakları çocuğa dünyanın yeni bir görünümünü verir. CLONE_NEWPID ile 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.
konteyner: clone(CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET …)ebeveynPID 900, TGID 900çocukPID 1 (içeride)mm (adres alanı)mm (adres alanı)files (açık dosyalar)files (açık dosyalar)fs (kök, cwd)fs (kök, cwd)sighand (işleyiciler)sighand (işleyiciler)nsproxy (ad alanları)yeni ad alanlarıkopya + yeni ad alanları
Şekil 15.13: Ad alanı bayraklarıyla 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.

Sık yapılan hata

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

Tanım 15.4

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.

Tablo 15.6: Sık karşılaşılan sinyaller (numaralar x86 ve ARM Linux içindir).
SinyalNoNe zaman üretilir?Varsayılan
SIGHUP1Terminal bağlantısı koptu; cinlerde “yapılandırmayı yeniden oku”sonlandır
SIGINT2Ctrl-C (ön plan süreç grubuna)sonlandır
SIGQUIT3Ctrl-\çekirdek dökümü
SIGABRT6abort()çekirdek dökümü
SIGFPE8Aritmetik hataçekirdek dökümü
SIGKILL9kill -9; yakalanamaz, yok sayılamazsonlandır
SIGUSR1 / SIGUSR210 / 12Kullanıcı tanımlısonlandır
SIGSEGV11Geçersiz bellek erişimiçekirdek dökümü
SIGPIPE13Okuyanı kalmamış boruya yazmasonlandır
SIGALRM14alarm() süresi doldusonlandır
SIGTERM15kill'in varsayılanı; nazik sonlandırma isteğisonlandır
SIGCHLD17Çocuk bitti ya da durduyok say
SIGCONT / SIGSTOP18 / 19Sürdür / durdur (yakalanamaz); Ctrl-Z SIGTSTP (20) üretirsü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.

  1. Ü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.
  2. 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.
  3. Çerçeve. Süreç sigaction ile 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.
  4. İşleyici. İşleyici kullanıcı modunda, sürecin kendi koduyla çalışır.
  5. Dönüş. İşleyici dönünce trampolin rt_sigreturn sistem çağrısını yapar. Çekirdek çerçevedeki yazmaçları geri yükler.
  6. Devam. Süreç kesildiği komuttan (şekilde PC = 0x401a2c) devam eder. İşleyici kurulmamış olsaydı varsayılan eylem uygulanırdı; SIGINT için bu, sürecin sonlanmasıdır.
kullanıcı moduçekirdek modumain() döngüsükesildiği yer: PC = 0x401a2chandler(SIGINT)bayrak = 1; return;rt_sigreturntrampolin: sistem çağrısıtty sürücüsüCtrl-C → SIGINTtask_structbekleyen: 2 · maske: boşsinyal çerçevesiyazmaçlar kullanıcı yığınınabağlam geri yüklendi, kaldığı yerden devam
Şekil 15.14: Bir sinyalin üretilmesi, beklemesi ve işleyiciye teslimi. Dönüş 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.

Sık yapılan hata

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.

Tablo 15.7: sinyal_bak izlemesi (bit işlemleri Python ile yeniden çalıştırıldı).
OlaybekleyenengellihazırSonuç
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.

1234567891011121314151617bekleyenengellihazır2 SIGINT · 10 SIGUSR1 · 17 SIGCHLDSIGINT → varsayılan: sonlandır
Şekil 15.15: İzlemenin sonu: engel kalkınca hazır kümesine giren 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:

  1. stop: çekirdeğin bir CPU'yu durdurmak ya da görev taşımak için kullandığı iç sınıf.
  2. deadline: SCHED_DEADLINE politikası. 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.
  3. rt: SCHED_FIFO ve SCHED_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.
  4. fair: SCHED_NORMAL (adı eskiden SCHED_OTHER), SCHED_BATCH ve SCHED_IDLE. Sıradan süreçlerin hepsi buradadır. 6.6'dan beri bu sınıfı EEVDF yönetir.
  5. 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.

Tablo 15.8: Adil sınıfın seçim kuralının evrimi.
SürümSeçim kuralıVeri yapısı
2.4 (2001)O(n): her seçimde bütün hazır görevler taranırtek liste
2.6.0–2.6.22 (2003)O(1): öncelik başına liste, etkin ve süresi dolmuş iki dizi140 listelik dizi + biteşlem
2.6.23–6.5 (2007)CFS: en küçük vruntimekırmızı-siyah ağaç
6.6+ (2023)EEVDF: uygun görevler (v ≤ V) içinde en erken sanal son tarihaynı 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

Tanım 15.5

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:

Δv = Δt × 1024 / w

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.

Örnek 15.3

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?

  1. nice 0: 6 × 1024 / 1024 = 6 ms.
  2. nice 5: 6 × 1024 / 335 ≈ 18,34 ms. Sanal saati üç kat hızlı işler.
  3. nice −5: 6 × 1024 / 3121 ≈ 1,97 ms.
  4. 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.

Tanım 15.6

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.

Örnek 15.4

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?

  1. t = 0. V = 0; ikisi de uygun. Son tarihler: dA = 0 + 3 = 3, dB = 0 + 3 × 1024 / 335 ≈ 9,17. A seçilir.
  2. 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.
  3. t = 6. vB ≈ 9,17, dB ≈ 18,34. V ≈ 4,52. A uygun (3 ≤ 4,52), B değil. A seçilir.
  4. 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.
  5. 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).

Tablo 15.9: Karar anlarında v, d ve V (A nice 0, B nice 5, dilim 3 ms; Python benzetimiyle üretildi).
t (ms)vA / dAvB / dBVUygunSeçilen
00 / 30 / 9,170A, BA
33 / 60 / 9,172,26BB
63 / 69,17 / 18,344,52AA
96 / 99,17 / 18,346,78AA
129 / 129,17 / 18,349,04AA
1512 / 159,17 / 18,3411,30BB
1812 / 1518,34 / 27,5113,56AA
2115 / 1818,34 / 27,5115,82AA

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.

Örnek 15.5

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?

  1. t = 0: ikisi de uygun; dA = 3, dB = 1. B seçilir ve 1 ms çalışır.
  2. t = 1: vB = 1, V = 0,5. Yalnız A uygundur; A 3 ms çalışır.
  3. 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.
  4. 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.

Sık yapılan hata

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).

  1. 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.
  2. Önyükleyici. GRUB2 ya da systemd-boot çekirdek görüntüsünü (vmlinuz) ve initramfs'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.
  3. Ç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. Çekirdek kernel_init'i (PID 1) ve çekirdek iş parçacıklarının atası kthreadd'yi (PID 2) oluşturur.
  4. 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_root ile ona geçer.
  5. 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.target ya da graphical.target.
  6. Oturum. Konsolda agetty → login → kabuk, masaüstünde gdm, uzaktan sshd kullanıcıyı karşılar. Kullanıcı oturumu için ayrıca bir systemd --user örneği başlar.
UEFIürün yazılımıGRUB2 /systemd-bootÇekirdekvmlinuzinitramfsgeçici köksystemdPID 1Oturumgetty, gdm, sshd• agetty → login → bash• veya gdm grafik oturum• sshd uzaktan giriş• systemd --user• systemd-analyze: süreLinux başladı
Şekil 15.16: Linux önyükleme aşamaları: UEFI, önyükleyici, çekirdek, initramfs, systemd ve oturum.

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.

Sık yapılan hata

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.

Çekirdek alanıtüm süreçlerde aynı; kullanıcı modunda erişilemezkanonik olmayan delikbu adresler kullanılamazYığın (stack) ↓main, yerel değişkenler; ASLR ile rastgele başlangıçmmap bölgesi ↓libc.so, ld.so, eşlenmiş dosyalar, iş parçacığı yığınlarıÖbek (heap) ↑ brkmalloc’un küçük istekleri; brk ile büyürbsssıfırla başlatılan küresellerdatailk değerli küresellertext (kod)salt okunur + çalıştırılabilir; süreçler arasında paylaşılırNULL sayfası eşlenmez → SIGSEGVx86-64, 4 düzey (48 bit):kullanıcı 2⁴⁷ B = 128 TiBçekirdek 128 TiB5 düzey, LA57 (57 bit):kullanıcı 2⁵⁶ B = 64 PiBcat /proc/PID/maps
Şekil 15.17: x86-64 Linux'ta bir sürecin sanal adres alanı: kod, veri, bss, öbek, mmap bölgesi, yığın ve çekirdek alanı.

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.

Tanım 15.7

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.

Süreç ASüreç BFiziksel bellektextsayfa önbelleğiveri Ayığın Averi Byığın Byığın Averi.db (eşl.)veri Atextyığın Bveri.db (eşl.)veri Btextyeşil oklar: paylaşılan çerçeveler
Şekil 15.18: A ve B süreçleri program metnini ve eşlenmiş veri.db dosyasını aynı fiziksel çerçevelerde paylaşır.
Tablo 15.10: Bellek yönetimi sistem çağrıları.
Çağrıİşlev
brk / sbrkÖbeğin sonunu taşıyarak büyütür ya da küçültür
mmapDosyayı, aygıtı ya da anonim belleği adres alanına eşler
munmapEşlemeyi kaldırır, belleği sisteme geri verir
mprotectBölgenin izinlerini (r/w/x) değiştirir
madviseÇekirdeğe kullanım ipucu verir (ör. MADV_DONTNEED)
msyncPaylaşı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.

Örnek 15.6

Sanal adres 0x00007f3a1c2b5e88. Dört indis ve ofset nedir? PTE'deki çerçeve numarası 0x3c5a1 ise fiziksel adres nedir?

  1. Ofset en düşük 12 bittir, yani son üç onaltılık hane: 0xe88.
  2. PGD = adres >> 39 = 0xfe = 254. 63–48. bitler 0, 47. bit de 0'dır; adres kanoniktir.
  3. PUD = (adres >> 30) & 0x1ff = 232. PMD = (adres >> 21) & 0x1ff = 225. PTE = (adres >> 12) & 0x1ff = 181.
  4. 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.

sanal adres 0x00007f3a1c2b5e88işaret = 0bit 63–48PGD = 254bit 47–39PUD = 232bit 38–30PMD = 225bit 29–21PTE = 181bit 20–12ofset = 0xe88bit 11–0CR3PGD tablosu512 girdi × 8 B[254]PUD tablosu512 girdi × 8 B[232]PMD tablosu512 girdi × 8 B[225]PTE tablosu512 girdi × 8 B[181]4 KiB çerçeveçerçeve0x3c5a1+ 0xe88fiziksel adres = 0x3c5a1e88TLB: sonraki erişimde 4 bellek okuması atlanır
Şekil 15.19: 0x00007f3a1c2b5e88 için dört düzeyli yürüyüş: PGD 254, PUD 232, PMD 225, PTE 181 ve ofset 0xe88.

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.

Sık yapılan hata

İ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.

NUMA düğümü 0 (pglist_data)ZONE_DMA0–16 MiB (eski ISA)ZONE_DMA320–4 GiB (32 bit DMA)ZONE_NORMALgeri kalan RAMZONE_MOVABLEtaşınabilir sayfalarher bölgede:free_area[0…10] (buddy listeleri)LRU listeleri, su seviyelerimem_map: struct page dizisi70: boş80: boş150: eşlenmiş200: boşher 4 KiB sayfa için 64 B→ RAM’in %1,56’sıaddress_space (dosya)folio: ardışık sayfalarıtek birim olarak yönetir (5.16+)boş liste
Şekil 15.20: Fiziksel bellek: NUMA düğümü, bölgeler, 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.

Tanım 15.8

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.

Buddy ayırıcı: sayfa çerçeveleri (alloc_pages)kmem_cache: task_structkmem_cache: dentry (192 B)kmem_cache: kmalloc-64kmem_cache: inode_cachedentry önbelleğinin içi:CPU 0 etkin slabkilitsiz hızlı yolkısmi (partial) slabboş nesnesi olanlardolu slabtamamen kullanılıyorslab = 1 veya birkaç ardışık sayfa
Şekil 15.21: SLUB: nesne önbellekleri buddy ayırıcıdan aldığı sayfaları sabit boyda nesnelere böler. Her CPU'nun etkin bir slab'ı, önbelleğin kısmi ve dolu slab'ları vardır.

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.

Örnek 15.7

dentry nesnesi 192 bayttır. Tek sayfalık (4 KiB) bir slab'a kaç dentry sığar ve kaç bayt boş kalır?

  1. 4096 / 192 = 21,33. Tam sayı kısmı 21 nesnedir.
  2. 21 × 192 = 4032. Kalan 4096 − 4032 = 64 bayt.
  3. 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.

Tablo 15.11: Serbest liste izlemesi (Python ile birebir benzetildi; “2:” ikinci slab sayfasıdır).
İşlemVerilen nesnefreelist sonraAçıklama
a = ayir+0+1024listenin başı; içindeki işaretçi +1024
b = ayir+1024+2048hızlı yol: kilit yok, sayfa ayırma yok
birak(a)—+0+0'ın içine eski baş (+2048) yazılır
c = ayir+0+2048a'nın az önce bıraktığı “sıcak” nesne
d = ayir+2048+3072
e = ayir+3072NULLslab doldu
f = ayir2:+02:+1024yavaş yol: yeni sayfa, dört nesne
slab sayfası (4 KiB)+0c+1024b+2048d+3072e2:+0f2:+1024boş2:+2048boş2:+3072boşfreelist → 2:+1024 → 2:+2048 → 2:+3072 → NULLf = ayir()
Şekil 15.22: İzlemenin sonu: birinci slab dolu (c, b, d, e), f ikinci slab'ın ilk nesnesi.

İ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.

Sık yapılan hata

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.

Tablo 15.12: Bellek baskısı altında 16 GiB'lık bir makine (GiB).
AşamaAnonimÖnbellekBoşTakasÇekirdeğin tepkisi
Başlangıç5820—
Uygulama büyüdü8,360,70düşük seviye aşıldı: kswapd uyanır
Soğuk sayfalar gider94,71,31MGLRU, zswap → takas
Baskı sürüyor11,53,20,32min altı: doğrudan geri kazanım
OOM4,628,42bellek_yiyen (PID 7311) öldürüldü

Geri kazanım su seviyelerine göre kademeli çalışır (Şekil 15.23).

  1. 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.
  2. 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.
  3. 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.
  4. OOM. Hiçbir şey yetmezse OOM killer (bellek tükenince süreç öldüren mekanizma) devreye girer. En yüksek oom_score'lu süreci SIGKILL ile öldürür. Puan çoğunlukla sürecin bellek kullanımıyla orantılıdır. Yönetici /proc/PID/oom_score_adj dosyası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.
4,6 GiBanonim (uygulama)2 GiBboş 8,4 GiBRAM 16 GiBboş bellek8,4 GiByüksekdüşükmin(büyütülmüş)kswapd0arka planda geri kazanımMGLRU nesillerigen 0 (soğuk) … gen 3 (sıcak)zswap → takassoğuk anonim sayfalardoğrudan geri kazanımayıran süreç beklerOOM killeren yüksek oom_score → SIGKILLtakas: 2 GiBzswap + swap alanıbellek_yiyen (PID 7311)öldürüldü
Şekil 15.23: Bellek baskısının son aşaması: OOM killer 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.

Sık yapılan hata

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 tty

Satı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: tür bmajor 8, minor 1blok aygıt tablosu7 → loop8 → sd (SCSI disk)259 → blkext (NVMe)minor: 0 = tüm disk, 1 = bölüm 1disk başına 16 minor: sdb = 16
Şekil 15.24: 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.

Sistem çağrıları: open, read, write, ioctl, mmap, send, recvVFS (sanal dosya sistemi)Soket katmanıKarakter aygıtlarıtty + satır disiplinikarakter sürücüsüBlok aygıtlarıext4 / XFS / Btrfssayfa önbelleğiblok katmanı: blk-mqG/Ç çizelgeleyiciblok sürücüsü (NVMe)Ağ aygıtlarıTCP / UDPIP + netfilterkuyruk disiplini (qdisc)ağ sürücüsü (NAPI)Donanım: terminal, NVMe SSD, ağ kartı
Şekil 15.25: Linux G/Ç sistemi: karakter, blok ve ağ aygıtları için ayrı katmanlar.

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).

  1. Sunucu hazırlanır. socket() bir soket oluşturur; en küçük boş tanımlayıcı olan 3'ü alır. bind soketi 80 numaralı porta bağlar. listen(3, 128) soketi dinleme durumuna geçirir ve bekleyen bağlantılar için bir kuyruk açar.
  2. Bekleme. Sunucu accept'te bloke olur. İstemci kendi soketini oluşturur.
  3. 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.
  4. Yeni soket. Bağlantı kurulunca accept yeni 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.
  5. Veri ve kapanış. Veri write ve read ile akar. Çekirdek veriyi sk_buff'lara koyar, TCP/IP başlıklarını ekler; sürücü paketi kartın halka tamponuna yazar. close karşı tarafa FIN gönderir.
Sunucuİstemcisocket() → fd 3bind(:80)listen(3, 128)accept() → fd 4read(4) / write(4)socket() → fd 3connect(sunucu:80)write(3) / read(3)close(3) → FINSYNSYN-ACKACKçekirdek el sıkışmayı tamamladı,accept yeni bir soket döndürürGET / HTTP/1.1200 OK …
Şekil 15.26: TCP soketleriyle istemci–sunucu bağlantısı: 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.

/binbootdevetchomelibprocsystmpusrvarmnt→ usr/bin→ usr/libsanalsanaldevtmpfsalibinlibshareusbvfat (USB)foto/ belge/mount /dev/sdb1 /mnt/usb
Şekil 15.27: Linux dizin ağacı ve /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ı.
Süreç A (fd)0tty1tty2tty3→ dosya#14çocuk A′ (fork)0tty1tty2tty3→ dosya#14Süreç C0tty1tty2tty3→ dosya#24struct file #1f_pos = 150 O_RDONLYreferans = 2struct file #2f_pos = 0referans = 1dentry"x.txt"inode 1234boyut, izin, extentext4 → diskVFS nesneleri:superblock: bağlı dosya sistemiinode: dosyanın kendisidentry: ad → inode (önbellek)file: açık dosya oturumu
Şekil 15.28: Dosya tanımlayıcı tablosu, 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.

Tablo 15.13: Dosya ve dizin sistem çağrıları.
Çağrıİşlev
open, creat, closeAç / oluştur, fd döndür; tanımlayıcıyı kapat
read, write, lseekKonumdan oku / yaz; konumu taşı
stat, fstat, lstatBilgi al (lstat: bağın kendisi)
pipe, dup2, fcntlBoru oluştur; fd kopyala; kilit ve bayraklar
mkdir, rmdirDizin oluştur / sil (boşsa)
link, unlink, symlinkAd ekle / sil; sembolik bağ oluştur
rename, chdirTaşı (atomik); çalışma dizinini değiştir
getdents64Dizin 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("a.txt", &st)dizin: a.txt → 812i-node 812st_ino, st_mode, st_nlink, st_size, zamanlar …
Şekil 15.29: 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.

Örnek 15.8

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?

  1. A: F_RDLCK, l_start = 4, l_len = 4. Dosyada kilit yok; kabul.
  2. B: 6–9 aralığı A ile 6–7'de örtüşür. İkisi de paylaşımlı; kabul.
  3. C: 2–11, A ve B'nin tamamını kapsar. Yine paylaşımlı; kabul. 6 ve 7. baytlar artık üç kilitle kaplıdır.
  4. D: 9. bayt B ve C'nin aralığındadır. Özel kilit paylaşımlı kilitlerle çakışır. F_SETLK ile istenirse çağrı beklemeden hata döndürür (EAGAIN ya da EACCES). F_SETLKW ile istenirse D, B ve C kilitlerini kaldırana dek bekler.
0123456789101112131415A: paylaşımlı kilit 4–7B: paylaşımlı kilit 6–9C: paylaşımlı kilit 2–11baytD: özel (yazma) kilidi bayt 9 → F_SETLKW ile blokepaylaşımlı (okuma) kilitler çakışabilir; özel (yazma) kilidi hiçbir kilitle çakışamaz
Şekil 15.30: fcntl bayt aralığı kilitleri: üç paylaşımlı kilit ve bayt 9 için bekleyen özel kilit isteği.

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.

Disk bölümü:önyüklemeblok grubu 0blok grubu 1blok grubu 2…blok grubu nsüperblokgrup tanımlayıcılarıblok biteşlemii-node biteşlemii-node tablosuveri bloklarıi-node (256 B)mod (tür + izin)uid, gid, boyutatime, mtime, ctimebağ sayısı12 doğrudan bloktekli dolaylıikili dolaylıüçlü dolaylı12 × 4 KiB = 48 KiB+ 1024 × 4 KiB = 4 MiB+ 1024² × 4 KiB = 4 GiB+ 1024³ × 4 KiB = 4 TiBext4: extent ağacıi-node’da 4 extent sığar:(mantıksal blok, uzunluk, fiziksel blok)0, 2048, 34816 → 8 MiB tek girditek extent en çok 32768 blok= 128 MiB; dosya ≤ 16 TiBdolaylı blok yerine az sayıdabüyük ardışık parça
Şekil 15.31: ext2/ext4 disk düzeni, blok grupları, i-node'un blok işaretçileri ve ext4 extent'leri.

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).

  1. Veri önce. Varsayılan data=ordered modunda D önce asıl yerine yazılır. Böylece üst veri hiçbir zaman çöp içeren bir bloğu göstermez.
  2. 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.
  3. 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.
  4. 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.
Bellek (sayfa önbelleği)Günlük (journal)Diskteki asıl yerlerveri Di-node M1bitmap M2extent M3kirli sayfalarveri DT42 tanımM1M2M3T42 commitM1M2M3commit’ten sonra çökme → bağlarken T42 yeniden oynatılırcommit’ten önce çökme → T42 yok sayılır; eski tutarlı hâl
Şekil 15.32: ext4 sıralı günlükleme: veri, günlük işlemi T42, commit ve checkpoint; çökme commit'ten önce mi sonra mı olduğuna göre iki sonuç.

Ö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.

Sık yapılan hata

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.

Örnek 15.9

open("/home/ali/notlar.txt") nasıl çözülür? (Şekil 15.33)

  1. Yol mutlaktır; arama kökten başlar. Kök dizinin i-node'u (2) okunur. Veri bloklarındaki girdilerde home aranır: 131073. Kök için x (arama) izni denetlenir.
  2. home dizininin (131073) verisinde ali aranır: 131074.
  3. ali dizininde (131074) notlar.txt aranır: 132010. Göreli bir yol olsaydı arama sürecin çalışma dizininden başlardı.
  4. i-node 132010 okunur. İzinler denetlenir; okuma izni varsa bir struct file oluşturulur ve en küçük boş tanımlayıcı, örneğin 3, döner.
open("/home/ali/notlar.txt")/ (i-node 2).2..2etc524289home131073usr786433home (131073).131073..2ali131074ayse131090ali (131074).131074..131073foto132011notlar.txt132010i-node 132010-rw-r--r-- aliboyut 2 KiBextent: 1 blok→ struct file, fd 3dizin girdisi (ext2/4): i-node no · kayıt uzunluğu · ad uzunluğu · tür · adsilinen girdi bir öncekinin kayıt uzunluğuna eklenir; büyük dizinlerde htree (hash ağaç) kullanılır
Şekil 15.33: /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.txt

Katı 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.

/home/alia.txt500k777/home/ali/yedekb.txt500i-node 500bağ sayısı = 2katı baği-node 777 (bağ)hedef: "a.txt"yol ileveri blokları
Şekil 15.34: Katı bağ aynı i-node'u gösteren ikinci bir addır; sembolik bağ hedefin yolunu içeren ayrı bir i-node'dur.

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ı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.

Sık yapılan hata

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.

Tablo 15.15: Linux'ta sık kullanılan dosya sistemleri.
Dosya sistemiÖne çıkan özellikTipik kullanım
ext4Günlükleme, extent, olgun ve hızlı fsckÇoğu dağıtımda varsayılan
XFSParalel G/Ç, büyük dosyalar, gecikmeli ayırmaRHEL varsayılanı, büyük sunucular
BtrfsYazınca kopyalama, anlık görüntü, sağlama toplamı, sıkıştırma, alt birimlerFedora ve openSUSE varsayılanı
F2FSFlash'a uygun günlük yapılı tasarımAndroid telefonlar
tmpfsTamamen RAM'de (sayfa önbelleği + takas)/tmp, /run, /dev/shm
NFS, SMBAğ dosya sistemleriPaylaşı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.

kök v1kök v2düğümdüğümdüğüm′B1B2B3B4B4′B4 değişince yalnız yol kopyalanır;kök v1 = anlık görüntü (snapshot)
Şekil 15.36: Btrfs'te yazınca kopyalama: B4 değişince yalnız kökten B4'e giden yol kopyalanır; eski kök anlık görüntü olur.

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 betik.shsgtrwxrwxrwxönceki0754000111101100rwxr-xr--g+w: OR 0200020000000010000o-r: AND ~0040004000000000100sonuç0770000111111000rwxrwx---
Şekil 15.37: chmod g+w,o-r: 0754 OR 020 AND ~004 = 0770.
Tanım 15.9

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.

Örnek 15.10

umask 022 ve 027 iken touch a ile mkdir d hangi izinleri üretir?

  1. 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.
  2. umask 027: dosya 666 & ~027 = 0640 (rw-r-----), dizin 777 & ~027 = 0750 (rwxr-x---). Diğerleri hiçbir şey göremez.
  3. 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.

$ chmod 4755 passwd ; chmod 1777 /tmpsgtrwxrwxrwxsetuid + 7554755100111101101rwsr-xr-xyapışkan + 7771777001111111111rwxrwxrwt
Şekil 15.38: setuid + 755 = 4755 (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:

  1. Etkin uid 0 (root) ise izin verilir. Yalnız çalıştırma isteğinde dosyada en az bir x biti aranır.
  2. Etkin uid dosyanın sahibiyse yalnız sahip bitlerine bakılır.
  3. Etkin gid ya da ek gruplardan biri dosyanın grubuysa yalnız grup bitlerine bakılır.
  4. 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.

Tanım 15.10

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.

Tablo 15.16: izin_var izlemesi (işlev Python'a birebir çevrilip çalıştırıldı).
Kim, istekModSınıfbitSınamaSonuç
ali, w (2)0640sahip6 (rw-)6 & 2 == 2izin
veli, w (2)0640grup4 (r--)4 & 2 = 0 ≠ 2ret
veli, r (4)0640grup4 (r--)4 & 4 == 4izin
can, r (4)0640diğer0 (---)0 & 4 = 0 ≠ 4ret
root, r (4)0640root—istekte x yokizin
ali, r (4)0066sahip0 (---)0 & 4 = 0 ≠ 4ret
rapor.txt: sahip ali, grup ekip, mod 0066sahip---gruprw-diğerrw-istek: ali (uid 1000) · rret (EACCES)
Şekil 15.39: Mod 0066'da ali sahip sınıfına düşer ve orada hiç bit yoktur: 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.

Tanım 15.11

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.

Tablo 15.17: Linux ad alanları.
Ad alanıBayrakYalıttığı kaynak
PIDCLONE_NEWPIDSüreç kimlikleri; içerideki ilk süreç PID 1
BağlamaCLONE_NEWNSBağlama noktaları, dosya sistemi ağacı
AğCLONE_NEWNETAğ arayüzleri, yönlendirme tablosu, portlar, güvenlik duvarı
UTSCLONE_NEWUTSAna makine adı ve alan adı
IPCCLONE_NEWIPCSystem V IPC, POSIX ileti kuyrukları
KullanıcıCLONE_NEWUSERuid/gid eşlemesi: içerideki root dışarıda sıradan bir uid
cgroupCLONE_NEWCGROUPcgroup ağacının görünen kökü
ZamanCLONE_NEWTIMEAçı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).

  1. Ad alanları. clone() ya da unshare() 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ü.
  2. Kök dosya sistemi. Bağlama ad alanında pivot_root ile 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.
  3. Kaynak sınırları. cgroup v2 dizinine sınırlar yazılır: cpu.max = "50000 100000", memory.max = 512M, pids.max = 100.
  4. Saldırı yüzeyi. Gereksiz yetenekler düşürülür. no_new_privs bayrağı 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.
  5. Başlatma. Son olarak execve uygulamayı çalıştırır. Uygulama içeride PID 1'dir, konakta PID 48213'tür.
Tek Linux çekirdeği (konak)Konak süreçlerisystemd (PID 1)sshd (PID 820)containerdapp → PID 48213Konteynerpid nsmnt nsnet nsuts nsipc nsuser nscgroup nstime nskök: overlayfsimaj katmanları (ro) + üst (rw)cgroup v2cpu.max · memory.max · pids.maxgüvenlikyetenek düşür, seccomp, LSMexecve(app)içeride PID 1
Şekil 15.40: Bir konteynerin kuruluşu: sekiz ad alanı, overlayfs kök, cgroup v2 sınırları ve güvenlik ayarları; hepsi tek bir konak çekirdeği üzerinde.
Örnek 15.11

cpu.max dosyasındaki "50000 100000" ne anlama gelir? Konteynere tam iki CPU vermek için ne yazılır?

  1. 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.
  2. 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.
  3. İ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.

Sık yapılan hata

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.

uygulamaseccompDACyeteneklerLSM kancalarınesneopen("/etc/shadow")izin / errno / öldürrwx, uid, gidCAP_DAC_OVERRIDE?SELinux, AppArmorerişim verildiherhangi bir aşama “hayır” derse: EPERM / EACCES (seccomp: süreç öldürülebilir)
Şekil 15.41: Bir sistem çağrısının seccomp, DAC, yetenek ve LSM denetimlerinden geçmesi. Herhangi bir aşama “hayır” derse çağrı EPERM ya da EACCES ile döner.
  1. 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.
  2. 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.
  3. Yetenekler. DAC reddederse çekirdek sürecin bu reddi aşan bir yeteneği olup olmadığına bakar; örneğin CAP_DAC_OVERRIDE.
  4. 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.

Uygulamalar (APK)Android çatısı (Java/Kotlin API)ART çalışma zamanı + yerel kütüphaneler (Bionic)HAL (donanım soyutlama)Linux çekirdeği + Binder, SELinuxzygoteçatı önceden yüklüsystem_serveruid 1000 (system)com.ornek.appuid 10123 · untrusted_appfork() + kimlik/dev/bindertek kopya +arayan uid/data/data/com.ornek.appyalnız uid 10123lmkd: arka planı öldür
Şekil 15.42: Android mimarisi, zygote ile uygulama başlatma, Binder IPC ve uygulama korumalı alanı.

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.

Tablo 15.18: Dersin kavramları ve Linux'taki karşılıkları.
KonuKavramLinux'taki karşılığı
YapılarSistem çağrısı, monolitik çekirdeksyscall, LKM, eBPF
SüreçlerPCB, oluşturma, IPCtask_struct, fork/exec/wait, sinyal, boru
İş parçacıklarıÇekirdek iş parçacığı modeliclone(), NPTL 1:1
ÇizelgelemeAdil paylaşım, gerçek zamanEEVDF, FIFO/RR/DEADLINE, sched_ext
SenkronizasyonKilit, koşul değişkenifutex, döner kilit, RCU
KilitlenmeTespit ve önlemelockdep, fcntl EDEADLK
BellekAyırma, parçalanmabölgeler, buddy listeleri, SLUB
Sanal bellekSayfalama, yer değiştirme4/5 düzey tablo, COW, MGLRU, zswap, OOM
Dosya sistemlerii-node, dizin, günlükVFS, ext4, XFS, Btrfs
G/ÇSürücü, çizelgelemeaygıt dosyaları, blk-mq, io_uring
Çok işlemciliYük dengeleme, NUMACPU başına kuyruklar, çizelgeleme alanları
GüvenlikErişim denetimi, yalıtımrwx, yetenekler, ad alanları, SELinux, seccomp
TasarımMekanizma 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ı. printf bir tampona yazar; asıl sistem çağrısı write'tır ve syscall komutuyla çekirdeğe girer.
  • Kabuk yönlendirmeleri soldan sağa dup2 ile, boru hatlarını pipe, fork, dup2 ve exec ile 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. Fark mm, files, fs, sighand gibi 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_operations tablosuyla VFS'e bağlanır.
  • VFS'in dört nesnesi superblock, inode, dentry ve file'dır. ext4 blok grupları, extent'ler ve data=ordered gü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.

15.16Kendini Dene

  1. 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.

  2. 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.

  3. 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.

  4. Bir süreç SIGUSR1'i engellemiş ve SIGINT iç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.

  5. Yeni bir kmalloc-512 slab'ı (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.

  6. clone(CLONE_VM | CLONE_FILES | SIGCHLD, …) ile, CLONE_THREAD verilmeden oluşturulan görev ps'te nasıl görünür? Çocuk yeni bir dosya açarsa ebeveyn onu kullanabilir mi?

    Cevap

    CLONE_THREAD olmadığı için çocuğun kendi TGID'i vardır; ps'te ayrı bir süreç olarak görünür ve bitince ebeveyne SIGCHLD gö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.

  7. umask 077 ve 002 iken touch a ve mkdir d hangi 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.

  8. prog dosyasının sahibi ali, grubu ekip, modu 0750. veli ekip grubunda, can değil. veli prog'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).

  9. 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. Sonra rm f ve rm g yapılıyor. Veri blokları ne zaman serbest kalır? cat h ne 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.

  10. Bir konteynerin cpu.max dosyasında "25000 100000" yazıyor. Konteyner ne kadar CPU kullanabilir? Bu sınır içerideki ps çı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

  1. 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.
  2. A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10. baskı, Wiley, 2018 — Bölüm 20: The Linux System.
  3. R. Love, Linux Kernel Development, 3. baskı, Addison-Wesley, 2010.
  4. 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)).
  5. 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.
  6. Android Open Source Project, source.android.com — mimari, Binder, uygulama korumalı alanı.
  7. D. M. Ritchie, K. Thompson, “The UNIX Time-Sharing System”, Communications of the ACM, 17(7), 1974.
  8. 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.