Ana içeriğe geç

Bölüm 4: Zamanı Dilimle

Diyelim ki bir işletim sistemi yazıyorsun ve kullanıcıların aynı anda birden fazla program çalıştırabilmesini istiyorsun. Ama elinde çok çekirdekli havalı bir işlemci yok; dolayısıyla CPU aynı anda yalnızca tek bir talimat yürütebiliyor.

Neyse ki akıllı bir işletim sistemi geliştiricisisin. CPU’yu işlemler arasında sırayla paylaştırarak sahte paralellik yaratabileceğini fark ediyorsun. İşlemler arasında hızlı hızlı geçiş yapar ve her birine az miktarda çalışma süresi verirsen, CPU’yu tek bir işlem işgal etmeden sistem duyarlı kalabilir.

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

  • Bir CPU çekirdeğinin aynı anda tek yürütme akışı çalıştırmasına rağmen çoklu görev hissini nasıl verdiğini göreceğiz.
  • Timer interrupt, preemption ve context switch kavramlarını ayıracağız.
  • Linux scheduler anlatısını güncel EEVDF modeliyle uyumlu ama basit bir zihinsel modele indireceğiz.

Peki program kodu çalışırken kontrolü geri nasıl alacaksın? Biraz araştırınca, çoğu bilgisayarda timer chip’ler bulunduğunu keşfediyorsun. Belirli bir süre geçince işletim sisteminin interrupt handler’ına geçişi tetikleyecek şekilde bu timer chip’leri programlayabiliyorsun.

Donanım Interrupt’ları

Bir önceki bölümde, kontrolü user-space programından işletim sistemine aktarmak için mimariye özgü kernel giriş yollarının nasıl kullanıldığını görmüştük. Tarihsel software interrupt modelinde bu geçiş program tarafından gönüllü tetiklenir; yani normal fetch-execute cycle içinde çalışan makine kodu, CPU’ya kontrolü kernel’a bırakmasını söyler.

Donanım interrupt'larının normal yürütmeyi nasıl böldüğünü gösteren bir çizim.

İşletim sistemi scheduler’ları, PIT gibi timer chip‘leri kullanarak multitasking için hardware interrupt üretir:

  1. Program koduna geçmeden önce işletim sistemi, timer chip’i belirli bir süre sonra interrupt tetikleyecek şekilde ayarlar.
  2. İşletim sistemi user mode’a geçer ve programın bir sonraki talimatına atlar.
  3. Süre dolunca timer chip, kernel mode’a geçişi ve işletim sistemi kodunun devreye girmesini sağlayan bir hardware interrupt üretir.
  4. İşletim sistemi artık mevcut programın durumunu kaydedebilir, başka bir programı yükleyebilir ve aynı döngüyü sürdürebilir.

Buna preemptive multitasking denir; bir işlemin zorla kesilmesine de preemption denir. Bu makaleyi tarayıcıda okurken aynı anda müzik de dinliyorsan, bilgisayarın muhtemelen bu döngüyü saniyede binlerce kez yapıyordur.

Timeslice Hesabı

Timeslice, scheduler’ın bir işlemin kesilmeden önce çalışmasına izin verdiği süredir. Timeslice seçmenin en basit yolu, her işleme aynı süreyi vermek ve sırayla hepsi arasında dönmektir. Buna fixed-timeslice round-robin scheduling denir.

Küçük bir jargon notu

Timeslice için bazen “quantum” dendiğini biliyor muydun? Artık biliyorsun ve bunu uygun bir ortamda kullanarak teknik arkadaşlarını etkileyebilirsin. Bu makale boyunca her cümlede kuantum kelimesini kullanmadığım için takdir bekliyorum.

Linux kernel geliştiricileri ayrıca timer tick’lerini saymak için jiffy adlı zaman birimini kullanır. Frekans kernel config’e göre değişebilir; masaüstü/server kurulumlarında 100, 250 veya 1000 Hz gibi değerler görebilirsin.

Günümüzde Linux kurulumlarında tickless (CONFIG_NO_HZ) yaklaşımları yaygındır. CPU boşta (idle) ise veya tek bir ağır işlem çalışıyorsa, timer dinamik olarak ayarlanabilir ve gereksiz interrupt’lar azaltılabilir. Bu, laptop pillerini korumak ve CPU uyku modlarına (C-states) geçişi optimize etmek için kritiktir.

Sabit timeslice yaklaşımının küçük ama önemli bir geliştirmesi, “herkes makul sürede tekrar CPU görebilsin” hedefini koymaktır. Eski CFS anlatılarında bu fikir çoğu zaman target latency üzerinden açıklanırdı: makul sayıda işlem varken bir işlemin preempt edildikten sonra tekrar CPU görmesi için geçmesini istediğin ideal en uzun süre. Bunu kafada canlandırmak biraz zor; birazdan diyagramla daha net olacak.

Timeslice süresi, target latency’nin toplam görev sayısına bölünmesiyle hesaplanabilir. Bu, sabit süre vermekten daha iyidir; çünkü çalışan işlem sayısı az olduğunda gereksiz context switch’leri azaltır. Örneğin target latency 15 ms ve toplam 10 işlem varsa, her işlem yaklaşık 1,5 ms çalışır. Yalnızca 3 işlem varsa her biri 5 ms’lik daha uzun bir timeslice alır ve yine aynı target latency içinde sıraya girmiş olur.

Context switch pahalıdır; çünkü mevcut programın tüm durumunu kaydetmeyi ve başka bir programın durumunu geri yüklemeyi gerektirir. Çok küçük timeslice’lar, işlemler gereğinden sık değiştiği için performansı düşürebilir. Bu yüzden scheduler’larda genelde bir minimum granularity sınırı bulunur. Bu sınır devreye girdiğinde target latency aşılabilir, ama sistem aşırı sık context switch yapmaktan kurtulur.

Bugün bu sayıları “Linux her zaman tam şunu yapar” diye okumamak gerekir. CFS döneminde base_slice_ns gibi ayarlar ve virtual runtime fikri bu adalet hesabını etkilerdi; Linux 6.6 ve sonrası dokümantasyonda CFS’nin yerini EEVDF’ye (Earliest Eligible Virtual Deadline First) bırakmaya başladığını görürsün. EEVDF de hâlâ CPU süresini adil bölüştürmeye çalışır, ama bunu görevlerin geride kalma durumunu (lag) ve sanal deadline’larını dikkate alarak yapar.

Dinamik timeslice round-robin scheduling diyagramı. Üç işlem sırayla çalışıyor, aralara kernel scheduler blokları giriyor ve toplam döngü target latency'yi oluşturuyor.

Bu temel timeslice hesabı, scheduler kavramını anlamak için iyi bir merdivendir ama günümüz makinelerinin yaptığı şeyin tamamı değildir. Çoğu işletim sistemi işlem önceliklerini, deadline’ları, CPU affinity’yi, uyku/uyanma davranışını ve başka sinyalleri de dikkate alan daha karmaşık scheduler’lar kullanır. Linux uzun süre Completely Fair Scheduler ile anlatıldı; current kernel belgeleri artık EEVDF tarafına geçişi vurguluyor. Temel ders değişmiyor: CPU, kısa aralıklarla paylaştırılıyor; sadece “sıradaki kim?” sorusunun hesabı daha akıllı.

Scheduler’ın işlemler arasında geçiş yapmasını anlamak için bir process’in yaşam döngüsüne bakmak faydalıdır. Aşağıdaki diyagram, bir process’in new (oluşturulma) durumundan başlayıp ready (hazır), running (çalışıyor), waiting (bekliyor) ve terminated (sonlanmış) durumları arasında nasıl geçtiğini gösteriyor. Scheduler, tam olarak ready → running ve running → ready geçişlerini yöneten bileşendir; waiting durumu ise process I/O veya başka bir olay beklediğinde devreye girer.

Process state diyagramı: new, ready, running, waiting ve terminated durumları arasındaki geçişler.

Kaynak: Silberschatz, Galvin, Gagne — Operating System Concepts, Ch. 3 — Processes, Slide 9.

Diyagramdaki target latency modeli bu yüzden tarihsel/pedagojik bir sadeleştirme olarak okunmalı. Gerçek Linux scheduler’ı, kernel sürümüne ve config’e göre daha fazla ayrıntı taşır.

İşletim sistemi bir işlemi her preempt ettiğinde, yeni programın kayıtlı yürütme bağlamını da yüklemesi gerekir. Buna bellek bağlamı da dâhildir. Bu, CPU’ya sanal adresleri fiziksel adreslere çevirirken farklı bir page table kullanmasını söyleyerek yapılır. Programların birbirlerinin belleğine erişmesini engelleyen şey de budur; bu tavşan deliğine Bölüm 9 ve Bölüm 11‘da gireceğiz.

Not #1: Kernel’in Preempt Edilebilirliği

Şu ana kadar yalnızca user-space işlemlerin preempt edilmesinden söz ettik. Oysa bir syscall’ın işlenmesi ya da driver kodunun yürütülmesi çok uzun sürerse, kernel kodu da programları yavaşlatabilir.

Linux kernel’ı derleme seçeneğine göre farklı preemption modelleriyle çalışabilir: hiç preempt etmeyen, yalnızca gönüllü noktalarda bırakan, daha agresif preempt eden veya gerçek zamanlı sistemler için PREEMPT_RT kullanan yapılandırmalar vardır. Genel fikir şu: kernel kritik bir işlem yapmadığı ve kritik bir kilit (spinlock vb.) tutmadığı sürece, bazı yapılandırmalarda kendi kodu da kesilebilir ve scheduler tarafından başka bir işe geçilebilir.

Kernel ya da driver yazmıyorsan bunu ayrıntılı bilmen şart değil, ama okuduğum neredeyse her kaynak buna değiniyordu; ben de zinciri bozmamak istedim. Fazladan bağlam nadiren zarardır.

Not #2: Kısa Bir Tarih Dersi

Eski işletim sistemleri, buna klasik Mac OS ve NT öncesi bazı Windows sürümleri de dâhil, preemptive multitasking yerine onun öncülü olan başka bir modeli kullanıyordu. İşletim sisteminin “şimdi seni keseceğim” demesi yerine, programların kendisi gönüllü olarak kontrolü bırakıyordu. Yani bir software interrupt tetikleyip “istersen şimdi başka bir programı çalıştırabilirsin” diyordu. İşletim sisteminin kontrolü geri alıp başka sürece geçmesinin tek yolu buydu.

Buna cooperative multitasking denir. Büyük kusurları vardır: kötü niyetli ya da kötü yazılmış programlar tüm sistemi kolayca dondurabilir ve zaman hassasiyetinin önemli olduğu işlerde kararlı davranış elde etmek neredeyse imkânsızdır. Bu yüzden teknoloji dünyası uzun zaman önce preemptive multitasking’e geçti ve geriye dönüp bakmadı.

Peki CPU aynı anda tek talimat yürütüyorsa, neden bilgisayarımız bu kadar hızlı? Cevap, işlemcinin kendi içinde kullandığı gizli hızlandırma tekniklerinde: pipeline, cache ve paralel yürütme. Bir sonraki bölümde, CPU’nun gerçek dünyadaki performans sırlarını keşfedeceğiz.

5. bölüme devam et: Modern İşlemci Teknikleri