İşletim Sistemleri Süreç Yönetimi: Dispatching ve Zamanlama

İşletim Sistemleri Süreç Yönetimi: Dispatching ve Zamanlama

Süreç (Process) Nedir?

Süreç, çalışmakta olan bir programdır. Diskteki statik bir binary dosyanın aksine, bir sürecin şunları vardır:

  • PID – benzersiz süreç kimliği
  • Program sayacı – bir sonraki çalıştırılacak komut
  • Stack – yerel değişkenler, fonksiyon çağrı çerçeveleri
  • Heap – dinamik olarak ayrılan bellek
  • Dosya tanımlayıcıları – açık dosyalar, soketler
  • Süreç Kontrol Bloğu (PCB) – çekirdeğin tüm bunları takip ettiği veri yapısı

Süreç Durum Diyagramı

Her süreç sonlu bir durum kümesi içinde geçiş yapar:

          fork()
            │
            ▼
        ┌────────┐
        │  YENİ  │
        └────┬───┘
             │ kabul edildi
             ▼
        ┌────────┐   zamanlayıcı dispatch   ┌─────────┐
        │ HAZIR  │ ─────────────────────────►│ ÇALIŞIYOR│
        │ kuyruk │ ◄──────────────────────── │  (CPU)  │
        └────────┘   önce al / zaman doldu  └────┬────┘
             ▲                                   │
             │         G/Ç veya olay bekleniyor  │
             │   ┌────────────────────────────── ┘
             │   ▼
        ┌────────────┐
        │  BEKLİYOR  │  (G/Ç, mutex, sleep bekliyor…)
        └─────┬──────┘
              │ G/Ç tamamlandı / olay tetiklendi
              └──────────────────► HAZIR
                                        │
                                        │ exit()
                                        ▼
                                   ┌─────────┐
                                   │ ZOMBİ   │ ◄── ebeveyn wait() çağırmadı
                                   └─────────┘
                                        │ wait()
                                        ▼
                                   ┌──────────┐
                                   │ SONLANDI │
                                   └──────────┘

Dispatcher (Dağıtıcı)

Dispatcher, zamanlayıcı tarafından seçilen bir sürece CPU kontrolünü devreden düşük seviyeli çekirdek bileşenidir. Üç şey yapar:

  1. Bağlam değişimi (Context switch) – mevcut sürecin CPU yazmaçlarını PCB'ye kaydeder, bir sonraki sürecin yazmaçlarını PCB'den yükler
  2. Mod değişimi – çekirdek modundan → kullanıcı moduna geçiş
  3. Atlama (Jump) – yeni süreci kaldığı yerden devam ettirmek için program sayacını ayarlar

Dispatch gecikmesi = bir süreci durdurup diğerini başlatma süresi. Bunu en aza indirmek, etkileşimli yanıt verme açısından kritiktir.


Zamanlama Algoritmaları

1. İlk Gelen İlk Çalışır (FCFS)

Basit kuyruk. Önalma yok. Uzun işler kısaları engeller ("konvoy etkisi").

2. En Kısa İş Önce (SJF)

Optimal ortalama bekleme süresi — ancak burst süresinin önceden bilinmesini gerektirir. Uzun süreçler için açlık (starvation) riski vardır.

3. Round Robin (RR)

Her süreç sabit bir zaman dilimi alır (genellikle 10–100 ms). Zaman dilimi dolunca → önalınır → hazır kuyruğunun sonuna eklenir.

Zaman:  0    10   20   30   40
        [P1] [P2] [P3] [P1] [P2] ...   (zaman dilimi = 10ms)

Zaman paylaşımlı sistemler için en uygundur (Linux masaüstü, web sunucuları).

4. Çok Seviyeli Geri Bildirimli Kuyruk (MLFQ)

Gerçek işletim sistemlerinin çoğunun kullandığı algoritma (Linux CFS, Windows, macOS).

  • Farklı önceliğe sahip birden fazla kuyruk
  • Yeni süreçler en yüksek öncelikten başlar
  • Bir süreç zaman dilimini tamamen kullanırsa → bir seviye düşürülür
  • G/Ç ağırlıklı süreçler (az CPU kullananlar) yüksek öncelikte kalır
Öncelik 0 (en yüksek): ──[P_etkileşimli]──
Öncelik 1:             ──[P_karma]──
Öncelik 2 (en düşük):  ──[P_toplu]──[P_toplu]──

5. Tamamen Adil Zamanlayıcı (CFS) — Linux

Sabit zaman dilimleri yerine CFS, her süreç için vruntime (sanal çalışma süresi) takip eder. En düşük vruntime'a sahip süreç her zaman sıradakidir. O(log n) zamanlama için kırmızı-siyah ağaçta saklanır.


Bağlam Değişimi (Context Switch) Derinlemesine

A kullanıcı süreci çalışıyor
        │
   zamanlayıcı kesmesi (veya syscall)
        │
        ▼
  çekirdek A'nın yazmaçlarını → PCB_A'ya kaydeder
  çekirdek zamanlayıcıyı çalıştırır → B'yi seçer
  çekirdek B'nin yazmaçlarını ← PCB_B'den yükler
        │
        ▼
B kullanıcı süreci çalışıyor

Maliyet: ~1–10 µs. Şunları içerir:

  • Yazmaç kaydetme/geri yükleme
  • TLB temizleme (ASID etiketlemesi olmayan mimarilerde)
  • Yeni süreç için önbellek ısınması

Fork ve Exec

pid_t pid = fork();   // süreci kopyalar
if (pid == 0) {
    execve("/bin/ls", args, env);  // bellek görüntüsünü değiştirir
} else {
    wait(NULL);  // ebeveyn çocuğu bekler
}
  • fork() → ebeveynin yazma-sırasında-kopyala (copy-on-write) klonu
  • exec() → yeni binary'i yükler, adres alanını değiştirir
  • İkisi birlikte "yeni bir program oluştur" işlevini gerçekleştirir

Süreçlerarası İletişim (IPC)

YöntemHızKullanım Alanı
PipeHızlıEbeveyn ↔ çocuk
İsimli pipe (FIFO)Hızlıİlişkisiz süreçler
Mesaj kuyruğuOrtaAyrışmış üretici/tüketici
Paylaşımlı bellekEn hızlıYüksek verimli veri paylaşımı
SoketYavaş (esnek)Ağ veya yerel çapraz makine
SinyalAnlık (sınırlı)Bildirimler (SIGKILL, SIGTERM)

Temel Metrikler

MetrikTanım
CPU kullanımıCPU'nun meşgul olduğu süre yüzdesi (hedef: %40–90)
Verim (Throughput)Saniyede tamamlanan süreç sayısı
Dönüş süresibitiş zamanı − varış zamanı
Bekleme süresiHazır kuyruğunda geçirilen süre
Yanıt süresiİstekten ilk yanıta kadar geçen süre

Özet

Diskteki program
      │  fork/exec
      ▼
  Süreç (PCB oluşturuldu)
      │
      ▼
  Hazır Kuyruk  ◄──── önalma
      │  dispatcher
      ▼
  CPU çalıştırması
      │
   ┌──┴──┐
   │     │
  G/Ç  exit()
   │
  Bekle → Hazır → CPU …

İşletim sistemi, yalnızca birkaç CPU çekirdeğinde yüzlerce programın aynı anda çalışıyor yanılsamasını yaratmak için süreçleri bu döngüde sürekli çevirir.


Thread ile Süreç Karşılaştırması

Thread, bir süreç içindeki hafif bir çalıştırma birimidir. Bir süreçteki tüm thread'ler aynı heap'i ve dosya tanımlayıcılarını paylaşır, ancak her birinin kendi stack'i ve yazmaçları vardır.

Süreç
├── PCB (PID, dosya tanımlayıcıları, heap, kod segmenti)
├── Thread 1 → stack, yazmaçlar, program sayacı
├── Thread 2 → stack, yazmaçlar, program sayacı
└── Thread 3 → stack, yazmaçlar, program sayacı
SüreçThread
BellekAyrı adres alanıPaylaşımlı adres alanı
Oluşturma maliyetiYüksek (fork + copy-on-write)Düşük
İletişimIPC gerekliDoğrudan bellek erişimi
Çökme izolasyonuGüçlüZayıf (bir thread çökerse hepsi çöker)
Değişim maliyetiYüksek (TLB temizleme)Düşük

Kullanıcı Alanı ve Çekirdek Alanı Thread'leri

  • 1:1 model (Linux pthreads, Windows thread'leri) — her kullanıcı thread'i bir çekirdek thread'ine eşlenir. Çok çekirdekte gerçek paralellik, ancak oluşturma maliyeti yüksek.
  • M:N model (Go goroutine'leri, Erlang süreçleri) — M kullanıcı thread'i N çekirdek thread'i üzerinde çoğullanır. Ucuz oluşturma, zamanlayıcı kullanıcı alanında yaşar.
  • Green thread'ler — tamamen kullanıcı alanında, çekirdek müdahalesi yok. Birden fazla çekirdeği doğal olarak kullanamaz.

CPU Ayrıcalık Seviyeleri (Ring'ler)

Modern CPU'lar, ayrıcalık ring'leri aracılığıyla donanım düzeyinde koruma uygular:

Ring 0 — Çekirdek modu
  ├── Doğrudan donanım erişimi
  ├── Ayrıcalıklı komutları çalıştırabilir (HLT, IN, OUT)
  └── Bellek eşlemelerini yönetir (sayfa tabloları)

Ring 3 — Kullanıcı modu
  ├── Doğrudan donanım erişimi yok
  ├── Bellek diğer süreçlerden izole
  └── Çekirdek hizmetleri için syscall kullanmak zorunda

Bir süreç, sistem çağrısı (x86-64'te syscall komutu) aracılığıyla Ring 3'ten Ring 0'a geçer. CPU kullanıcı durumunu kaydeder, stack'i çekirdek stack'ine geçirir ve çekirdeğin syscall işleyicisine atlar.

Kullanıcı süreci
    │  read(fd, buf, n)   ← RAX'ta syscall numarası
    ▼
  SYSCALL komutu
    │
    ▼
  Çekirdek syscall işleyicisi
    │  argümanları doğrula, G/Ç gerçekleştir
    ▼
  SYSRET komutu
    │
    ▼
Kullanıcı süreci devam eder

Sanal Bellek ve Sayfalama (Paging)

Her süreç, tüm adres alanına sahip olduğunu zanneder. Çekirdek, sanal adresleri → fiziksel adreslere eşleyen bir sayfa tablosu tutar.

Sanal Adres Alanı (süreç başına)        Fiziksel RAM
┌──────────────────┐
│   Stack (↓ büyür)│ SA: 0x7fff...  ──► FA: 0x3a00...
├──────────────────┤
│   (kullanılmayan)│
├──────────────────┤
│   Heap (↑ büyür) │ SA: 0x5600...  ──► FA: 0x1200...
├──────────────────┤
│   BSS / Data     │ SA: 0x4040...  ──► FA: 0x8800...
├──────────────────┤
│   Kod (.text)    │ SA: 0x4000...  ──► FA: 0x0200...
└──────────────────┘

Sayfalar genellikle 4 KB büyüklüğündedir. CPU'nun Bellek Yönetim Birimi (MMU), her bellek erişiminde TLB (Çeviri Ara Belleği) önbelleğini kullanarak çeviriyi yapar.

Sayfa Hatası (Page Fault)

Bir süreç şu anda RAM'de olmayan bir sayfaya erişirse:

  1. MMU bir sayfa hatası istisnası fırlatır
  2. Çekirdeğin sayfa hatası işleyicisi çalışır
  3. Çekirdek sayfayı diskten (swap) RAM'e yükler
  4. Sayfa tablosunu günceller
  5. Süreci şeffaf bir şekilde devam ettirir

Kilitlenme (Deadlock)

Kilitlenme, her biri kümedeki başka bir sürecin tuttuğu kaynağı bekleyen süreçler kümesinin oluşmasıyla gerçekleşir — çıkışı olmayan bir döngü oluşur.

Süreç A, Kilit 1'i tutuyor, Kilit 2'yi bekliyor
Süreç B, Kilit 2'yi tutuyor, Kilit 1'i bekliyor
        → hiçbiri devam edemiyor

Dört Gerekli Koşul (Coffman, 1971)

Kilitlenmenin oluşması için dördü aynı anda gerçekleşmelidir:

  1. Karşılıklı dışlama — kaynak yalnızca bir süreç tarafından tutulabilir
  2. Tut ve bekle — süreç bir kaynağı tutarken diğerini bekler
  3. Önalsızlık — kaynaklar zorla alınamaz
  4. Döngüsel bekleme — her biri bir sonrakini bekleyen döngüsel süreç zinciri

Önleme Stratejileri

StratejiNasıl
Kilit sıralamasıKilitleri her zaman aynı global sırada al
Zaman aşımlı try-lockN ms içinde kilit alınamazsa hepsini bırak ve tekrar dene
Kaynak tahsis grafiğiVermeden önce döngüleri tespit et
Bankacı algoritmasıYalnızca güvenli bir durum mevcutsa ver

Senkronizasyon Primitifleri

Mutex (Karşılıklı Dışlama Kilidi)

pthread_mutex_lock(&m);
// kritik bölge — aynı anda yalnızca bir thread
pthread_mutex_unlock(&m);

Sahiplik tabanlı: yalnızca kilitleyen thread açabilir.

Semafor

sem_wait(&s);   // azalt; değer == 0 ise engelle
// kritik bölge
sem_post(&s);   // artır; bekleyeni uyandır

Sahiplik tabanlı değil. Thread'ler arası sinyal vermek için kullanılır (üretici/tüketici).

Spinlock

while (atomic_test_and_set(&lock));  // meşgul bekle
// kritik bölge
atomic_clear(&lock);

Uyumak yerine CPU döngüleri harcar. Yalnızca kritik bölge çok kısa olduğunda ve çok çekirdekli sistemde kullanışlıdır (uyumak döndürmekten daha pahalı olur).

Koşul Değişkeni

pthread_mutex_lock(&m);
while (!kosul_saglandı)
    pthread_cond_wait(&cond, &m);   // atomik olarak kilidi bırakır + uyur
// devam et
pthread_mutex_unlock(&m);

Bir koşul doğru olana kadar thread'i engellemek için kullanılır.


Çok Çekirdekli Zamanlama (SMP)

N çekirdekli bir makinede çekirdek, N çalıştırma kuyruğu (çekirdek başına bir) ve global bir yük dengeleyici tutar.

Çekirdek 0 kuyruğu: [P1] [P4] [P7]
Çekirdek 1 kuyruğu: [P2] [P5]
Çekirdek 2 kuyruğu: [P3] [P6] [P8] [P9]
                               ↑
                  yük dengeleyici P9'u → Çekirdek 1'e taşır

CPU Benzeşimi (Affinity)

Önbellek çöküşünü önlemek için bir süreç belirli çekirdeklere sabitlenebilir:

taskset -c 0,1 ./programim   # yalnızca 0 ve 1. çekirdeklerde çalıştır

NUMA (Tekdüze Olmayan Bellek Erişimi)

Büyük sunucularda CPU'lar node'lara gruplandırılır. Kendi node'unuzdaki RAM'e erişim hızlıdır; uzak node RAM'ine erişim 2–4× daha yavaştır.

Node 0              Node 1
┌──────────┐        ┌──────────┐
│ Çekirdek │◄─QPI──►│ Çekirdek │
│ 0–7      │        │  8–15    │
│ RAM 0    │        │  RAM 1   │
└──────────┘        └──────────┘

Linux'un NUMA'ya duyarlı ayırıcısı, çalışan süreçle aynı node'da bellek tahsis etmeye çalışır.


Gerçek Zamanlı Zamanlama

Standart zamanlayıcılar verim ve adalet için optimize eder. Gerçek zamanlı sistemlerin bir son tarihe kadar garantili yanıt vermesi gerekir.

SCHED_FIFO ve SCHED_RR (POSIX)

  • SCHED_FIFO — en yüksek öncelikli RT süreci engellenene veya vazgeçene kadar çalışır. Zaman dilimi yok.
  • SCHED_RR — aynısı ama zaman dilimiyle. Eşit öncelikli RT görevler arasında Round Robin.
  • RT görevler her zaman SCHED_OTHER (normal) görevlerin önüne geçer.

Oran Monoton Zamanlama (RMS)

Statik öncelik ataması: daha kısa periyot → daha yüksek öncelik. Sabit öncelikli önalan zamanlaması için kanıtlanmış biçimde optimaldir.

Görev A: periyot 20ms, çalışma 3ms  → öncelik 1 (en yüksek)
Görev B: periyot 50ms, çalışma 10ms → öncelik 2
Görev C: periyot 100ms, çalışma 20ms→ öncelik 3

Zamanlanabilirlik için CPU kullanım sınırı: U ≤ n(2^(1/n) − 1) → büyük n için ~%69.


Süreç Oluşturma İç İşleyişi (Linux)

clone() sistem çağrısı
    │
    ▼
copy_process()
    ├── dup_task_struct()    → yeni PCB + çekirdek stack'i ayır
    ├── copy_mm()            → adres alanını klonla veya paylaş
    ├── copy_files()         → dosya tanımlayıcı tablosunu klonla veya paylaş
    ├── copy_signal()        → sinyal işleyicilerini klonla
    └── sched_fork()         → zamanlayıcı alanlarını başlat, vruntime = ebeveynin
    │
    ▼
wake_up_new_task()           → çalıştırma kuyruğuna ekle

fork() sadece tam kopya bayraklarıyla clone()'dur. pthread_create() ise paylaşımlı bellek + stack bayraklarıyla clone()'dur.


Hepsini Bir Araya Getirince

Donanım zamanlayıcısı her 1ms'de bir tetiklenir (HZ=1000)
        │
        ▼
  IRQ işleyicisi → scheduler_tick()
        │
        ├── mevcut görevin vruntime'ını güncelle
        ├── daha yüksek öncelikli bir görev çalışmaya hazır mı kontrol et
        │       └── evet ise → TIF_NEED_RESCHED bayrağını ayarla
        │
        ▼
  Kesmeden geri dön
        │
        ├── TIF_NEED_RESCHED ayarlı mı?
        │       └── evet → schedule() çağır
        │
        ▼
  schedule()
        ├── pick_next_task()  → kırmızı-siyah ağacı gez, en düşük vruntime'ı seç
        ├── context_switch()  → yazmaçları kaydet/geri yükle, sayfa tablolarını değiştir
        └── yeni süreçte kullanıcı moduna dön

Donanım kesmesinden yeni bir sürecin çalışmasına kadar tüm bu pipeline, modern bir Linux çekirdeğinde yaklaşık 3–10 µs sürer.

Comments

(0)
Top commentsNewest first

0/3000 • Press Ctrl + Enter to submit

Loading comments...