Sıfırdan bir işletim sistemi inşa etmek, C yazan kernel hacker'ların işi gibi görünüyor. Ancak bir öğleden sonranızı ayırarak Python ile basitleştirilmiş bir simülasyon oluşturabilirsiniz ve süreç yönetiminin mantığının yüksek seviyeli bir dilde de bir o kadar acımasız olduğunu hızla keşfedeceksiniz. Bunu acı bir tecrübeyle öğrendim. Küçük bir işletim sistemi simülatörü yazmak için oturdum. Hedef mütevazıydı: birkaç süreç oluşturmak, bunları zamanlamak ve işleri bittiğinde onları tamamlandı olarak işaretlemek. Kod kısaydı. Mantık kusursuz görünüyordu. Sonra çalıştırdım ve hiçbir süreç sonlanmıyordu.

Neden Python ile Mini Bir İşletim Sistemi İnşa Edilmeli?

Gerçek bir işletim sistemi; bellek sayfalama, dosya sistemleri, donanım kesmeleri ve aygıt sürücülerini idare eder. Bir simülasyon tüm bunları bir kenara bırakır ve odaklanmanızı temel fikre, yani duruma (state) izin verir. Bir süreç tanımlarsınız. Bir PID'si, bir burst süresi (burst time) ve bir yaşam döngüsü durumu vardır. Hazır (Ready). Çalışıyor (Running). Tamamlandı (Finished). Bir zamanlayıcı döngüsü (scheduler loop) bir sonraki adayı seçer, durumunu ilerletir, bir zaman dilimini (time slice) simüle eder ve onu tamamlandı durumuna geçirir.

Python bu tür deneyler için mükemmel bir araçtır çünkü pointer aritmetiğini ve bellek hizalamasını görmezden gelmenize olanak tanır. Sözlüklerden oluşan bir liste, süreç tablonuz (process table) olur. Bir while döngüsü ise kernel zamanlayıcınız (kernel scheduler) haline gelir. Standart kütüphane araçlarından başka bir şeye ihtiyaç duymadan round-robin zamanlamayı veya öncelik kuyruklarını uygulayabilirsiniz. Yaklaşılabilir hissettiriyor, sonrasında gelen hatanın bu kadar sinir bozucu olmasının nedeni de tam olarak buydu.

Kurulum

Simülasyonum process_table adlı bir liste kullanıyordu. Her bir girdi şu şekilde yapılandırılmış bir sözlüktü:

{
    "pid": 1,
    "burst_time": 3,
    "status": "ready"
}

Zamanlayıcı basit bir while döngüsü çalıştırıyordu. Tabloyu, durumu "finished" olmayan ilk süreci bulmak için tarıyordu. Bir tane bulduğunda, o süreci bir simüle edilmiş döngü boyunca çalıştırmak için execute_tick(p) adlı yardımcı bir fonksiyon çağırıyordu. execute_tick içinde, sürecin durumunu "running" olarak ayarlıyor, burst süresini azaltıyor ve kalan işin sıfıra ulaşıp ulaşmadığını kontrol ediyordum. Eğer sıfıra ulaşmışsa, durumu "finished" olarak güncelliyordum. Dış döngünün, her süreç tamamlandı durumuna ulaştığında sona ermesi gerekiyordu.

Kağıt üzerinde akış temizdi. Hazır bir süreç bul. Çalıştır. Tamamlanıp tamamlanmadığını kontrol et. Bitene kadar tekrarla. Zamanlayıcının işini yapışını izlemek için print ifadeleri bile eklemiştim. Süreçlerin seçildiğini görebiliyordum. Döngü dönmeye devam ediyordu. Yine de süreçler, sonsuza dek çalışan ve asla ilerlemeyen, ebedi bir şimdiki zamana girmiş gibi görünüyordu.

Belirti

Bu, başarısızlığın en kötü türüdür: sessiz olanı. Terminale hiçbir stack trace (yığın izi) püskürmedi. Hiçbir IndexError veya KeyError takip edebileceğim bir ipucu vermedi. Yorumlayıcı (interpreter) gayet mutluydu. Program sadece düzgün davranmıyordu. Süreçler başlıyor ama asla bitmiyordu. Akışı yeniden izlemek için saatlerimi harcadım.

Döngü koşulu mu yanlıştı? Belki de burst süresi hesaplamasında bir "off-by-one" (bir farkla hata) hatası yapmıştım. Süreç tablosu yerinde güncellenmek yerine gölgeleniyor (shadowed) veya kopyalanıyor muydu? Sonlandırma koşulum yanlış anahtarı mı kontrol ediyordu? Daha fazla print ekledim. Her bir boolean ifadesini denetledim. Asıl önemli olan o tek satır hariç her şeyi sorguladım.

Suçlu

Sonra onu gördüm. execute_tick içinde şunu yazmıştım:

p["status"] == "running"

İki eşittir işareti. Bir atama değil, bir karşılaştırma. Çözüm tek bir karakter uzaklıktaydı:

p["status"] = "running"

Python'da p["status"] == "running" tamamen geçerli bir ifadedir. True veya False olarak değerlendirilir ve ardından ben onu hiçbir şeye atamadığım için yorumlayıcı sonucu atar. Bu satır kesinlikle yararlı bir şey yapmaz. Sözlük girdisi dokunulmadan kalır, önceki durumunu korur ve süreç yaşam döngüsünde asla ilerleyemez.

Onu tek eşittir işaretine çevirdim. Betiği tekrar çalıştırdım. Simülasyon canlandı. Süreçler, tam planlandığı gibi hazır, çalışıyor ve tamamlandı aşamalarından geçti. Fazladan bir tuş vuruşu bana saatlerimi mal etmişti.

Bu Hatalar Neden Gizlenir?

Bunun bu kadar can yakmasının nedeni, Python'ın bir ifade ifadesini (expression statement) açıkça geçersiz bir sözdizimi (syntax) olmadığı sürece hata olarak işaretlememesidir. Hata, anlamsal bir yazım yanlışıydı (semantic typo). Program durumu karşılaştırdı, bir boolean üretti ve onu çöpe attı. Karşılaştırmanın kendisi False dönebileceği için süreç önceki durumunda takılı kaldı ve dış döngünün durması için hiçbir neden kalmadı.

Bunu onaylama yanlılığı ile pekiştiriyorsunuz. Bir atama yapmayı amaçladığınız için bir atama yazdığınızı biliyorsunuz. Kodu beşinci kez okuduğunuzda, beyniniz sembolü otomatik olarak düzeltir. Rubber ducking'in işe yaramasının sebebi budur. Sizi her satırı, yazılan ile kastettiğiniz arasındaki boşluğun görünür hale geleceği kadar yavaş bir şekilde ifade etmeye zorlar.

Bu tür küçük hataları bulmak, çarpıcı çökmeleri bulmaktan daha zordur. Bir segfault veya sözdizimi hatası kendini hemen belli eder. Sessiz bir no-op ise sadece durumu bozar ve programın aksayarak devam etmesine izin verir. Hata daha ileride ortaya çıkar ve içgüdünüz, nedeni değil belirtiyi hata ayıklamaya (debug) yönelir.

Daha İyi Bir Savunma

Sadece gözlerinize güvenemezsiniz. Bu olaydan sonra, hatayı daha erken yakalayabilecek birkaç alışkanlığımı değiştirdim.

İlk olarak, bir sözlükte (dictionary) durum tutuyorsanız, süreç durumları için bir dataclass veya enum.Enum kullanmayı düşünün. Durumlarınızı sabitler veya enum üyeleri olarak tanımlayın:

from enum import Enum

class ProcessState(Enum):
    READY = "ready"
    RUNNING = "running"
    FINISHED = "finished"

Açık tiplerle, mypy gibi araçlar statik analiz sırasında şüpheli karşılaştırmaları işaretleyebilir. Bir atama yapılması gereken yerde yanlışlıkla yapılan bir karşılaştırma, tipler beklentilerle uyuşmadığında fark edilmesi çok daha kolay hale gelir.

İkinci olarak, zamanlayıcı (scheduler) mantığını yazmadan önce durum geçişleri için birim testleri (unit tests) yazın. Tek bir iş adımı (tick) olan bir süreç oluşturan, zamanlayıcıyı çalıştıran ve nihai durumun FINISHED olduğunu doğrulayan basit bir test anında başarısız olurdu. Bu başarısızlık, tüm döngü içinde kaybolmama izin vermek yerine, aramayı durum güncelleme mantığına daraltırdı.