İş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:
- 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
- Mod değişimi – çekirdek modundan → kullanıcı moduna geçiş
- 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) klonuexec()→ 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öntem | Hız | Kullanım Alanı |
|---|---|---|
| Pipe | Hızlı | Ebeveyn ↔ çocuk |
| İsimli pipe (FIFO) | Hızlı | İlişkisiz süreçler |
| Mesaj kuyruğu | Orta | Ayrışmış üretici/tüketici |
| Paylaşımlı bellek | En hızlı | Yüksek verimli veri paylaşımı |
| Soket | Yavaş (esnek) | Ağ veya yerel çapraz makine |
| Sinyal | Anlık (sınırlı) | Bildirimler (SIGKILL, SIGTERM) |
Temel Metrikler
| Metrik | Tanı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üresi | bitiş zamanı − varış zamanı |
| Bekleme süresi | Hazı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 | |
|---|---|---|
| Bellek | Ayrı adres alanı | Paylaşımlı adres alanı |
| Oluşturma maliyeti | Yüksek (fork + copy-on-write) | Düşük |
| İletişim | IPC gerekli | Doğrudan bellek erişimi |
| Çökme izolasyonu | Güçlü | Zayıf (bir thread çökerse hepsi çöker) |
| Değişim maliyeti | Yü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:
- MMU bir sayfa hatası istisnası fırlatır
- Çekirdeğin sayfa hatası işleyicisi çalışır
- Çekirdek sayfayı diskten (swap) RAM'e yükler
- Sayfa tablosunu günceller
- 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:
- Karşılıklı dışlama — kaynak yalnızca bir süreç tarafından tutulabilir
- Tut ve bekle — süreç bir kaynağı tutarken diğerini bekler
- Önalsızlık — kaynaklar zorla alınamaz
- Döngüsel bekleme — her biri bir sonrakini bekleyen döngüsel süreç zinciri
Önleme Stratejileri
| Strateji | Nasıl |
|---|---|
| Kilit sıralaması | Kilitleri her zaman aynı global sırada al |
| Zaman aşımlı try-lock | N ms içinde kilit alınamazsa hepsini bırak ve tekrar dene |
| Kaynak tahsis grafiği | Vermeden ö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)Loading comments...