Ana içeriğe geç

Bölüm 16:Fork'lar ve COW'lar Hakkında Konuşalım

14 dakikalık okumaGüncelleme:

Son soru: Buraya nasıl geldik? İlk process’ler nereden çıktı?

Bu kitabın son düzlüğündeyiz. Buraya kadar CPU’nun tek bir instruction pointer’ından başlayıp process’lerin birbirinden nasıl yalıtıldığına kadar geldik. Geriye tek bir soru kaldı ve bu bölüm onun cevabı: madem her process başka bir process tarafından başlatılıyor, bu zincirin ilk halkası nereden çıktı?

Bu bölümde neyi çözüyoruz?

  • fork ile yeni process üretmenin neden “aynı yerden iki kez devam etmek” gibi göründüğünü anlayacağız.
  • Copy-on-Write sayesinde büyük belleklerin neden baştan kopyalanmadığını göreceğiz.
  • Boot sonunda ilk user-space process’in nasıl doğduğunu ve geri kalan programları nasıl başlattığını bağlayacağız.

execve, mevcut process’i değiştirerek yeni bir program başlatıyorsa, tamamen ayrı bir process içinde yeni bir programı nasıl başlatıyorsun? Bilgisayarda birden fazla şey yapmak istiyorsan bu kritik bir yetenek. Bir uygulamaya çift tıkladığında, onu açan program kaybolmaz; yeni uygulama ondan bağımsız şekilde yaşamına devam eder.

Cevap başka bir syscall: fork. Çoklu işlem dünyasının temel taşı budur. fork, mevcut process’i ve belleğini klonlar, kaydedilmiş instruction pointer’ı olduğu yerde bırakır ve ardından iki process’in de normal akışına devam etmesine izin verir. Müdahale edilmezse, iki process birbirinden bağımsız ama aynı koddaki farklı yürütmeler olarak devam eder.

Yeni ortaya çıkan process’e “child”, onu başlatana da “parent” denir. Bir process, fork’ü birden fazla kez çağırabilir ve böylece birden çok child yaratabilir. Her process’in, 1’den başlayarak verilen bir process ID’si (PID) vardır.

Tamamen aynı kodu körlemesine iki kopya hâline getirmek tek başına çok faydalı olmazdı. Bu yüzden fork, parent ve child tarafında farklı değer döndürür. Parent tarafında yeni child process’in PID’sini döndürür, child tarafında ise 0 döner. Böylece her iki taraf da kendi rolüne göre farklı iş yapabilir.

Linux’ta C kütüphanesinin fork() wrapper’ı kernel tarafında clone ailesinin özel bir kullanımına denk gelir; klasik fork davranışı kabaca SIGCHLD ile yeni bir child process üretmektir. clone ve modern clone3 ise hangi kaynakların paylaşılacağını (CLONE_VM, CLONE_FILES gibi) bayraklarla belirlemeye izin verir. Thread oluşturma da bu ailenin başka bir kullanım biçimidir. fork’u temel taşı olarak anlatmak pedagojik olarak doğrudur, ancak kernel/C tarafında resim clone etrafında şekillenir.

main.c
pid_t pid = fork();
// Bu satirdan sonrasi iki process'te birden akar.
// Tek ayirt edici isaret, fork'un dondurdugu deger.

if (pid == 0) {
	// Child process'teyiz.
} else {
	// Parent process'teyiz; pid, child'in numarasi.
}

fork’un garip yanı şu: tek bir çağrı yapıyorsun, o çağrıdan iki kez dönülüyor.

Kafanda oturtmanın en kolay yolu, fork satırını bir çatallanma noktası gibi görmek. O satıra kadar tek bir yürütme akışı vardı; o satırdan sonra aynı koda bakan, aynı değişkenlere sahip, aynı satırdan devam eden iki akış var. İkisi arasındaki tek fark fork’un döndürdüğü sayı: parent’ta child’ın PID’si, child’ta 0. Programın geri kalanı işte bu tek sayıya bakarak hangi tarafta olduğuna karar veriyor.

Aynı fikri bol şemayla görmek istersen şu eski ders notu iyi bir tekrar sunuyor.

Child, Sonucu Parent’a Nasıl Veriyor?

Yukarıdaki koddaki “sonucu parent’a ver” yorumu bir soruyu açıkta bırakıyor. Child ayrı bir adres alanında yaşıyor; bir değişkene yazarak parent’a hiçbir şey iletemez. Peki nasıl iletiyor?

İki klasik yol var.

Çıkış kodu. Child exit(5) ile sonlanır, parent wait() ile bu değeri okur. Basit ama dar bir kanal: yalnızca tek bir bayt taşır. 11. bölümde konuştuğumuz çıkış kodu sözleşmesi tam olarak bu mekanizmadır.

Pipe. Gerçek veri taşımak gerekiyorsa, fork’tan önce bir pipe açılır. Burada devreye kitabın en işlevsel kurallarından biri giriyor: child, parent’ın açık file descriptor’larını devralır. fork anında parent’ta açık olan her fd, child’da da açık olarak belirir. Bu yüzden fork’tan önce açılmış bir pipe, iki tarafın da elinde hazır bekler.

Bu kural sandığından daha geniş bir alanı açıklıyor. 11. bölümdeki > yönlendirmesi ve | pipeline’ı da tam olarak buna dayanıyordu: shell fork ediyor, child devraldığı fd’leri dup2 ile yeniden bağlıyor ve execve çağırıyor. Aynı kuralın bir de güvenlik tarafı var — bölümün sonundaki O_CLOEXEC tartışması oradan çıkıyor.

Her neyse: Unix programları yeni bir program başlatmak istediklerinde tipik olarak önce fork çağırır, ardından child process içinde hemen execve çalıştırır. Buna fork-exec modeli denir. Bir programı başlattığında bilgisayarının yaptığı şey kabaca şuna benzer:

launcher.c
pid_t pid = fork();

if (pid == 0) {
	// Child process'i hemen yeni programla değiştir.
	execve(...);
}

// Buraya geldiysek process değiştirilmedi.
// Parent process'teyiz. İstersek yeni child'ın PID'si de elimizde.

// Parent program burada devam eder...

Child process sonlandığında, parent onu wait() ile temizlemezse bir zombie olarak kalır. Zombie, çıkış kodunu parent’a iletmek için process tablosunda yer tutan, ancak artık çalışmayan bir process’tir. Parent ölürse child genellikle aynı PID namespace içindeki init’e veya varsa subreaper olarak atanmış başka bir process’e evlat edinilir; o process de wait() ile temizleme sorumluluğunu alır.

Moooo!

Fark etmiş olabilirsin: Başka bir program yüklerken bir process’in belleğini yalnızca biraz sonra çöpe atmak üzere tamamen kopyalamak kulağa verimsiz geliyor. Neyse ki elimizde bir MMU var. En pahalı şey fiziksel RAM’deki veriyi çoğaltmaktır; fork() bunun yerine task/process yapıları ve page table eşlemeleri üzerinde çalışır. Page table kopyalamak da bedava değildir, ama devasa veri bloklarını baştan sona RAM’de çoğaltmaktan çok daha ucuzdur. Bu yüzden ilk anda hiç veri kopyalamayız. Yeni process için eski process’in page table eşlemelerinin bir kopyasını çıkarır ve her iki tarafın da aynı fiziksel bellek bloklarına bakmasını sağlarız.

Ama child process’in parent’tan bağımsız ve yalıtılmış olması gerekir. Child’ın parent belleğine yazabilmesi ya da tam tersi kabul edilebilir değil.

İşte burada COW (copy on write) page’leri devreye girer. COW page’lerinde iki process de aynı fiziksel bellekten okuyabilir; ama içlerinden biri yazmaya kalktığı anda o page RAM içinde kopyalanır. Böylece her iki process de, baştan bütün bellek alanını çoğaltmanın maliyetine katlanmadan ayrı birer bellek görüntüsüne kavuşur. Fork-exec modelinin verimli olmasının nedeni tam olarak budur: Yeni binary yüklenmeden önce eski belleğe yazılmadığı için çoğu durumda gerçek kopyalama hiç gerekmez.

COW veriyi kopyalamaz ama page table’ı kopyalar. Adres alanı büyüdükçe bu tablonun kendisi de büyür: 100 GB kullanan bir process’te milyonlarca PTE (page table entry — bir sanal sayfayı bir fiziksel çerçeveye bağlayan tek satır) demektir ve fork’un kendisi gözle görülür biçimde yavaşlar. Bu maliyetten kaçınmanın yollarını bölümün sonundaki Derinleşme panelinde konuşacağız.

COW, pek çok eğlenceli şey gibi, page fault’lar üzerinden uygulanır. fork, parent’ı klonladıktan sonra iki process’in de paylaşılan sayfalarından yazılabilir olanlarını geçici olarak salt okunur işaretler. Zaten salt okunur olan sayfalara (örneğin programın kendi kodu) dokunmasına gerek yoktur; kasıtlı olarak paylaşılan sayfalara ise dokunmaması gerekir — sebebini birazdan kernel kaynağında göreceğiz. Bir program belleğe yazmaya kalktığında, sayfa salt okunur olduğu için yazma başarısız olur. Bu da kernel’ın ele aldığı bir page fault tetikler. Kernel ilgili page’i kopyalar, yazılabilir hâle getirir ve sonra kesmeden çıkıp yazmayı tekrar dener.

Bu “önce paylaş, yazınca kopyala” mantığını adım adım izleyebilirsin:

fork ve copy-on-write
EbeveynkodveristackFiziksel belleksayfa Asayfa Bsayfa CÇocukkodveristacksalt okunuryazma→ page faultsayfa C′ebeveyn hâlâ C'deçocuk artık kopyadasayfa A ve B hâlâ paylaşımlı
Tek process. Ebeveyn process'in sayfaları kendisine ait ve yazılabilir durumda.
  1. Tek process. Ebeveyn process'in sayfaları kendisine ait ve yazılabilir durumda.
  2. fork çağrılır. Kernel yeni bir process yaratır ama sayfaların içeriğini kopyalamaz; iki process de aynı fiziksel sayfalara bakar.
  3. Sayfalar salt okunur işaretlenir. Paylaşılan her sayfa iki tarafta da yazma iznini kaybeder. Okumalar serbest, yazma denemesi kernel'a düşecek.
  4. Çocuk bir sayfaya yazmaya çalışır. Salt okunur sayfaya yazma girişimi page fault üretir ve kontrol kernel'a geçer.
  5. Yalnızca o sayfa kopyalanır. Kernel sayfanın bir kopyasını çıkarır, çocuğa yazılabilir olarak verir ve programı kaldığı yerden sürdürür. Diğer sayfalar hâlâ paylaşımlıdır.

Aynı akışı ders kitabı diyagramlarıyla da görelim. İlk diyagram fork() sonrasını gösteriyor: parent ve child aynı fiziksel bellek sayfalarını paylaşıyor ve yazılabilir sayfalar geçici olarak salt okunur işaretlenmiş durumda. Henüz hiçbir kopyalama yapılmadı.

Copy-on-Write öncesi: parent ve child process aynı fiziksel bellek sayfalarını paylaşıyor.

İkinci diyagram ise bir process sayfaya yazdığında ne olduğunu gösteriyor: kernel o sayfayı kopyalar, yazan process’i yeni kopyaya yönlendirir ve diğer process orijinal sayfayı kullanmaya devam eder.

Copy-on-Write sonrası: yazma gerçekleştiğinde sayfa kopyalanır ve her process kendi sayfasına sahip olur.

Kaynak: Silberschatz, Galvin, Gagne — Operating System Concepts, Ch. 10 — Virtual Memory, Slide 26-27.

A: Tak tak!
B: Kim o?
A: İneğin sözünü kesen.
B: İneğin sözünü kesen ki —
C: MOOOOO!

Başlangıçta (Yaratılış 1:1 Olan Değil)

Normal bir Unix benzeri sistemde bilgisayarındaki process’lerin büyük çoğunluğu, zincirin bir yerinde başka bir program tarafından fork edilip çalıştırılmıştır. Bu ağacın kökünde özel bir process durur: init process. Init process, doğrudan kernel tarafından hazırlanır. Bu, user space’te çalışan ilk programdır ve sistem kapanırken en son ayrılanlardan biridir.

init process’in ne kadar hayati olduğunu anlamak için onu öldürmeye çalışmak gerçek makinelerde iyi bir fikir değildir. Linux kernel’ı PID 1’e gelen bazı ölümcül sinyalleri normal process’lerden farklı ele alır; init/systemd gibi PID 1 programları da SIGTERM gibi sinyalleri sistem kapanışı başlatmak için yorumlayabilir. PID 1 gerçekten çöker veya çıkarsa meşhur “Kernel panic — not syncing: Attempted to kill init!” ailesinden hatalarla karşılaşabilirsin. Kısacası: bunu üretim ya da günlük kullandığın makinede deneme.

Yazar notu: Buradaki init process anlatısı Linux ve macOS gibi Unix benzeri sistemler için geçerli. Windows’un NT kernel’ı aynı işi bambaşka bir mimariyle çözer; bu bölümü okuduktan sonra Windows’un açılışını da anladığını varsayma.

Tıpkı execve bölümünde olduğu gibi sınırı açıkça çiziyorum: NT kernel’ın boot ve process modeli başlı başına ayrı bir kitabın konusu, bu kitabın değil.

Init process, işletim sistemini oluşturan servislerin ve programların büyük bölümünü başlatmaktan sorumludur. Bu programların çoğu da daha sonra kendi çocuklarını üretir.

Kökünde init bulunan bir process ağacı diyagramı.

Init process’in kaybolması, process ağacının kökünü ve servis yönetimini kaybetmek anlamına gelir. Bu yüzden pratik sonuç çoğu sistemde oturumun kapanması, servislerin durması veya kernel panic gibi dramatik bir arıza olur.

Kernel’a Dönüş

Linux kernel kodunu kurcalamak Bölüm 10’da bayağı eğlenceliydi; biraz daha yapalım. Bu kez kernel’ın init process’i nasıl başlattığına bakacağız.

Bilgisayarın açılışı kabaca şu sırayla ilerler:

  1. Anakartın üzerinde, diskten bağımsız duran küçük bir yazılım vardır: firmware (eski adıyla BIOS, modern makinelerde UEFI). Firmware RAM’de durmaz; anakarta lehimli, elektrik kesilince silinmeyen bir flash yongasında yaşar — 2. bölümdeki Derinleşme panelinde bunun neden zorunlu olduğunu konuşmuştuk. Firmware önce donanımı ve RAM’i kullanılabilir hâle getirir, sonra bağlı disklerde bootloader denen küçük programı arar, onun makine kodunu RAM’e kopyalar ve o koda atlar.
  2. Unutma: Henüz tam anlamıyla çalışan bir işletim sistemi dünyasında değiliz. Kernel bir init process başlatana kadar çoklu işlem, syscall ve benzeri üst seviye soyutlamalar aslında ortada yoktur. Boot öncesi bağlamda bir programı “çalıştırmak”, çoğu zaman RAM’deki makine koduna doğrudan atlamak demektir.
  3. Bootloader, kernel’ı bulmak, RAM’e yüklemek ve çalıştırmaktan sorumludur. GRUB gibi bootloader’lar yapılandırılabilir ve hatta birden fazla işletim sistemi arasında seçim yapmana izin verebilir. Windows tarafında Windows Boot Manager bu rolün önemli parçasıdır; modern macOS’ta ise Intel ve Apple Silicon makinelerde boot zinciri farklı ayrıntılara sahiptir, ama temel fikir yine firmware’in işletim sistemi yükleyicisine ve kernel’a yolu açmasıdır.
  4. Kernel artık çalışır durumdadır ve interrupt handler’ları kurmak, driver’ları yüklemek, ilk bellek eşlemelerini oluşturmak gibi geniş bir başlatma rutinine girer. Sonunda init programını başlatır — dikkat et, kernel kendi ayrıcalık seviyesini düşürmez; kernel mode’da kalmaya devam eder ve yeni başlattığı process’i user mode’da çalıştırır.
  5. İşte şimdi gerçekten bir işletim sisteminin kullanıcı alanındayız. Init programı init script’lerini çalıştırır, servisleri başlatır ve shell ile grafik arayüz gibi başka programları devreye sokar.

Linux’un Başlatılması

Yukarıdaki 4. adım, yani kernel’ın kendi kendini kurması, init/main.c içindeki start_kernel fonksiyonunda geçer. Bu fonksiyon yüzlerce satır boyunca sırayla her alt sistemi ayağa kaldırır: bellek yöneticisi, scheduler, interrupt tablosu, zamanlayıcılar, dosya sistemi katmanı.

Sonunda ilginç bir devir teslim olur. Kernel, kendi başlatma kodunun içinden çıkıp PID 1’i doğuracak bir görev yaratır ve kontrolü scheduler’a bırakır. Yani boot’un son adımı “kernel bir programı çalıştırdı” değildir; kernel kendini sıradan bir işletim sistemine dönüştürmüş ve ilk kullanıcı programını sıraya koymuştur.

O ilk program aranırken kernel bir yedek listesi üzerinde yürür:

kernel_init @ init/main.c
/* Sırayla dener; ilk başarılı olan PID 1 olur.
 * Gerçekten bozuk bir makineyi kurtarmak için
 * init yerine Bourne shell de kullanılabilir. */
if (!try_to_run_init_process("/sbin/init") ||
    !try_to_run_init_process("/etc/init")  ||
    !try_to_run_init_process("/bin/init")  ||
    !try_to_run_init_process("/bin/sh"))
	return 0;

panic("No working init found.  Try passing init= option to kernel.");

Bu birkaç satır, kernel’ın kişiliği hakkında beklediğinden fazlasını söylüyor. Listenin sonundaki /bin/sh bir kurtarma kapısıdır: init sistemin bozulduysa makine yine de açılsın, eline çıplak bir shell versin, sen düzelt. Ve hiçbiri bulunamazsa kernel kibarca pes etmez, panic eder. Çünkü çalıştıracak tek bir user-space programı olmayan bir kernel’ın yapabileceği anlamlı hiçbir şey yoktur.

Linux’ta init neredeyse her zaman /sbin/init’tir ya da oraya bir sembolik bağdır; arkasında genelde systemd, OpenRC veya runit durur. macOS’un karşılığı launchd’dir. Kernel olmadığın için onu terminalden elle çalıştırmayı denemeni önermem.

Artık boot sürecinin sonundayız: init process user space’te çalışıyor ve fork-exec modeliyle geri kalan her şeyi başlatıyor.

Çatal Bellek Eşlemesi

fork’ün asıl işi kernel/fork.c içinde döner ve dosyanın açılış yorumu, kernel’ın kendine dürüstlüğünün güzel bir örneğidir: fork’ün kendisi bir kez kavradın mı basittir, asıl belaya bellek yönetiminde girilir.

Gerçekten de öyle. Bellek eşlemesini kopyalayan kod, her bölge için tek bir soru sorar: bu alanın Copy-on-Write olması gerekiyor mu? Cevap kernel kaynağında tek satırlık bir bit kontrolüdür ve okunuşu şu: yazılabilir ama paylaşılmıyorsa COW gerekir. Paylaşılan bellek zaten ortak kullanılmak için oradadır, onu kopyalamak isteneni bozmak olur. Salt okunur bellek ise hiç değişmeyeceği için kopyalanmasına gerek yoktur. Geriye tek bir durum kalır ve tam olarak o durum COW’dur.

Bu fonksiyon copy_page_rangedup_mmapdup_mmcopy_mm zinciriyle en sonunda copy_process’e bağlanır. Yani Unix’te bir programın başlatılması kökeninde hep aynı şeydir: var olan bir process’in kopyalanıp uyarlanması.

Derinleşme`fork()` iyi bir soyutlama mı?

fork yarım asırdır Unix’in temel taşı. Buna karşılık 2019’da bir grup sistem araştırmacısı (Baumann, Appavoo, Krieger, Roscoe) HotOS’ta “A fork() in the road” başlıklı bir bildiri yayımladı. Tezleri şuydu: fork bugün artık kötü bir soyutlamadır ve büyük ölçüde alışkanlıktan yaşamaktadır.

Argümanları kayda değer:

  • Thread’lerle bileşmiyor. fork yalnızca çağıran thread’i kopyalar, diğerlerini bırakır. Ama onların tuttuğu kilitler kopyaya kilitli hâlde geçer ve açacak kimse kalmaz. Bu yüzden POSIX, fork ile exec arasında yalnızca async-signal-safe çağrılara izin verir; pratikte malloc bile yasaktır. Çok thread’li bir programda fork sonrası printf çağırmak tanımsız davranıştır.
  • Ölçeklenmiyor. COW veriyi kopyalamaz ama page table’ları kopyalar. 100 GB’lık bir adres alanı milyonlarca PTE demektir; fork çağrısının kendisi yüz milisaniyelerce sürebilir. Redis’in arka planda anlık görüntü alırken yaşadığı gecikme sıçramalarının kaynağı budur.
  • Güvenli değil. Child, parent’ın her şeyini devralır: adres alanı, açık file descriptor’lar, ASLR düzeni. Ayrıcalık düşürmek “neyi devrettiğini hatırlamak” işine dönüşür ve unutulan her şey bir açıktır.
  • Modüler değil. fork’ün davranışı, programın kullandığı her kütüphaneyi ilgilendirir. Bir kütüphane arka planda thread açtıysa senin fork çağrın onun yüzünden bozulabilir; üstelik bunu önceden bilmenin bir yolu yoktur.

Önerilen alternatif yeni değil: yeni process’i klonlamak yerine tarif ederek yaratmak. posix_spawn tam olarak bunu yapar, Windows’un CreateProcess’i baştan böyle tasarlanmıştır, vfork da aynı derdin eski bir yamasıdır.

Peki fork neden hâlâ her yerde? Çünkü tarife dayalı API’ler ifade gücünden ödün verir. fork sonrası, exec öncesi o küçük pencerede çocuğun içinde istediğin her şeyi yapabilirsin: fd’leri yeniden bağla, namespace değiştir, cgroup’a gir, capability düşür. 18. bölümde göreceğimiz container runtime’larının işi tam olarak o pencerede yaşar ve posix_spawn’ın sabit eylem listesine sığmaz.

Yani tartışma “hangisi doğru” değil; klasik bir tasarım gerilimi: ifade gücü mü, öngörülebilirlik mi? Bu bölümde gördüğün mekanizma gerilimin bir tarafı. Hangi tarafın kazanması gerektiği hâlâ açık bir soru.

İzlemelik: fork ve exec ikilisinin neden bu kadar merkezi olduğunu görsel olarak anlatan bir video için bu kaydı izleyebilirsin.

Özetle…

Peki… programlar nasıl çalışır?

En düşük seviyede cevap şu: işlemciler aptaldır. Ellerinde bellekte bir işaretçi vardır ve onlara başka yere atlamalarını söyleyen bir talimata rastlamadıkları sürece talimatları art arda yürütürler.

Ama bu akış sadece jump talimatlarıyla değişmez; hardware ve software interrupt’lar da yürütmeyi, önceden belirlenmiş başka bir konuma sıçratarak bozabilir. Tek bir işlemci çekirdeği aynı anda birden fazla programı gerçekten çalıştıramaz, ama timer’lar aracılığıyla tekrar tekrar interrupt üretip kernel’ın farklı instruction pointer’lar arasında geçiş yapmasını sağlayarak bunu ikna edici biçimde taklit edebiliriz.

Programlar, aslında olduklarından çok daha düzenli bir ortamda çalıştıklarına inandırılır. User mode, sistem kaynaklarına doğrudan erişimi keser; sayfalama, bellek alanını yalıtır; syscall’lar ise process’lerin altında yatan gerçek yürütme bağlamını bilmeden genel G/Ç yapabilmesini sağlar. Syscall dediğimiz şey, CPU’ya “kernel’ın önceden belirlediği şu kod yoluna git” diyen talimatlardır.

Ama… programlar nasıl çalışır?

Bilgisayar açıldıktan sonra kernel, init process’i başlatır. Bu, makine kodunun çok fazla donanım ayrıntısıyla uğraşmak zorunda olmadığı ilk yüksek seviyeli programdır. Init, bilgisayarının grafik ortamını ve geri kalan servisleri başlatır; yani diğer yazılımların dünyaya gelmesinden sorumlu ana ebeveyndir.

Bir programı başlatmak için init ya da başka bir process önce fork ile kendini klonlar. Bu klonlama verimlidir; çünkü page’ler COW olarak paylaşılır ve fiziksel RAM’i baştan sona kopyalamak gerekmez. Linux tarafında bu hikâyenin büyük kısmı copy_process çevresinde akar.

Ardından child process, gerekirse parent’tan farklı yola sapar. Yeni programı gerçekten başlatmak istediğinde exec ailesinden bir syscall çağırır ve kernel’dan mevcut process’i yeni programla değiştirmesini ister.

Bu yeni program çoğu zaman bir ELF dosyasıdır. Kernel, ELF’yi ayrıştırıp kodun ve verinin yeni sanal bellek düzeninde nereye yükleneceğini belirler. Program dynamic linked ise, gerekirse ELF interpreter’ı da devreye girer.

Son adımda kernel yeni sanal bellek eşlemesini kurar ve user space’e geri döner. Pratikte bu, CPU’nun instruction pointer’ını yeni programın kodunun başlangıcına ayarlamak demektir. Ve işte, program gerçekten çalışmaya başlar.

Şimdi başta sorduğumuz sorulara dönelim:

  • CPU birden fazla process’i takip etmiyorsa nasıl çoklu görev yapıyor? → Timer interrupt’ları ve context switch ile.
  • Programlar neden birbirinin belleğine erişemiyor? → Sanal bellek ve sayfa tabloları ile izole ediliyorlar.
  • Her process kendi dünyasını nasıl görüyor? → MMU her process için farklı sanal→fiziksel eşleme yapıyor.

Hepsinin cevabını şimdi biliyoruz.

Sıradaki bölümde bu dünyanın en çok kullanılan ama en az konuşulan arayüzüne bakacağız: dosyalar.

17. bölüme devam et: Dosya Sistemi ve I/O