İşletim Sistemi 101 – #6 (İşletim Sistemlerinin Yapısı)
Önceki yazılarımdan da göreceğiniz üzere, işletim sistemi hayli önemli görevleri olan, kompleks bir yapı. Böyle bir yazılımın geliştirme sürecinin de – gerek yönetsel gerekse teknik faaliyetler bakımından – ciddi süreçlerden geçeceğini düşünmek de yersiz olmaz. Bu yazımda, işletim sistemlerinin yapılarını anlamaya / anlatmaya çalışacağım.
Monolithic Sistemler
Karşımıza en sık çıkan sistemlerdir. Bu sistemlerin temel olayı, tüm işletim sisteminin tek bir program olarak kernel mode‘da çalışmasıdır.
İşletim sistemleri, birtakım prosedürlerin bir araya getirilmesiyle oluşturan yazılım grubu demiştik. Hatta önceki yazılardan birinde “sistem çağrıları“ndan bahsetmiştik. Monolithic çekirdek mimarisinde, tüm bu prosedürler tek bir çalıştırılabilir dosya içerisinde toplanır. Bu sayede sistemdeki prosedürler birbirleri tarafından da rahatlıkla çağrılabilir.
Oldukça etkili bir yöntem gibi görünse de, konunun bambaşka yerlere çekilebilmesi de mümkün. Tüm bu prosedürler birbirlerini rahatlıkla çağırabiliyor ama bunu yapmaları bir sıkıntı yaratır mı? Ben bu yüzlerce hatta binlerce prosedürden hangilerini kullanacağım? Kullanmayacağım prosedürler neden hem disk hem de RAM kaynağımı kullanıyor? Bu prosedür grubunun içinde bir ya da birkaç tanesi sorun yaşarsa ne yapacağız? Bütün sistem çökmeyecek mi? (Evet, çökecek.)
Peki bu mimarinin iyi tarafları yok mudur? Tabii ki var. Tek program, tek process. Kernel’ın RAM üzerindeki yeri yurdu belli. Herhangi bir işletim sistemi çağrısında context switching yapmak zorunda da değiliz. Bütün işi halledecek bir çekirdek var ve o da çalışıyor. İşlerimizin muhatabı tek bir nokta. Bu da performansı olumlu yönde etkiliyor.
Her ne kadar basit bir yapı gibi görünse de, monolithic yapının da kendi içinde bir mimarisi olabilir. Örneğin, sistem çağrılarını alıp işleyen ve çalıştıran servis prosedürleri olabilir. Başka bir prosedür grubu, servis prosedürlerinin işlerine koşmakla sorumlu olabilir. Servis prosedürlerinden gelen talepler konusunda yetkili bir alan olarak farklı bir prosedür grubu atanabilir. Yani prosedürler arası görev dağılımı yapılması gayet mümkündür.
Drivers
Konu burada da bitmiyor. Tüm bileşenler tek bir programda toplandıysa, örneğin “sürücüler (drivers)” konusunu nasıl çözüyoruz? Örneklemeye çalışalım. Dünya üzerinde on binlerce fare modeli var. Çip, üretici, controller tarafına gelince bunları gruplayabiliriz tabii ki. On binler olmaz ama yüzlerce vardır. Bunun klavyesi var, yazıcısı var, USB belleği var… Fakat bu cihazların çoğunu bilgisayara takıyoruz, çalışıyor. Yani sistem, bu sürücülere sahip ki bu cihazları çalıştırabiliyor. Peki bütün dünyanın donanım sürücülerinin benim bilgisayarımda ne işi var? Neden yüklendi? Neden çalıştırıldı? Neden sistem kaynaklarımı tüketiyor?
Tabii ki konu böyle değil. Az önce belirttiğim gibi, on binlerce ürün için on binlerce driver yok. Ek olarak, çoğu ürünün driver’ı deli dehşet boyutlarda da değil. Dolayısıyla herhangi bir ürünü bilgisayarınıza taktığınızda sürücü kurmadan kullanabiliyorsanız, bu sürücü zaten sizde var demektir. Peki, boyutları küçük olduğu için diskten çok yer almıyorlar dedik. RAM’i ne yapacağız? Asla kullanmayacağım bir sürücü, neden benim sistemime yüklensin ki?
Burada izlenen yaklaşım ise şu. Evet, sürücüler sende var. Fakat çalıştırmadık. Kenarda bekliyorlar. Hani bilgisayarınıza bir cihaz taktığınız anda “Donanım değişiklikleri taranıyor, sürücüler yükleniyor, cihazınız hazır.” gibi bildirimler görürsünüz ya. İşte o cihazı bağladığınız anda bilgisayarınız bir talep alır. Talebin geldiği aygıtı tanırsa, kendi sürücü havuzuna bakar. İlgili sürücü modülünü RAM’e alır ve çalıştırır. Bu sayede siz de taktığınız aygıtı kullanabilir hâle gelirsiniz. UNIX tarafında bu “yüklenebilir eklentilere” shared libraries^1 diyoruz. Windows’ta ise “Dynamic–Link Libraries (DLL)“^2 diyoruz. “Bıdı bıdı.dll bulunamadığından uygulama çalıştırılmadı.” hatası şimdi daha anlamlı geliyordur belki de. 🙂
Yazının sonlarına doğru bu tasarıma ayrıca değineceğiz.
Yani..
Özetlemek gerekirse; tüm sürücüler, bellek yönetimi, process yönetimi, dosya sistemi gibi bileşenlerin tamamı tek bir pakette toplanıyor, tek bir noktada çalışıyor. Bu mimariyi kullanan bazı sistemler: BSD, MS-DOS, UNIX, Linux^3
Layered Sistemler
Hadi biraz tarih, biraz eğlenceli bilgi. Hollandalı matematikçi, bilgisayar bilimci – ve daha birçok şey olan – ünlü bir kişi var. Teknolojiye yaptığı katkılar kendisine “Turing Ödülü” de kazandırmış. “Bir graph üzerinde yer alan iki nokta arasındaki en kısa yolun bulunması” ile ilgili de sağlam çalışmaları mevcut. Baya elzem bir konu değil mi? Mesela Türkiye’den Rusya’daki bir sunucuya erişmek istiyorsunuz. Çeşitli santraller, çeşitli sunucular üzerinde gidiyor network paketleriniz. Aynı ya da farklı bir yoldan size tekrar geliyor. Ciddi kısa zamanda üstelik. İki nokta arasındaki en kısa yolu bulan tatlı bir algoritmamız var işte. İster “Gezgin Satıcı Problemi (Traveling Salesman Problem)” deyin, isterseniz farklı bir şey. Fakat günün sonunda konu “en kısa yol algoritmasına” geliyor. Burada da karşımıza çıkan isim Edsger Wybe Dijkstra.^4
Dijkstra’nın bilgisayar bilimi için önemini tartışacak değilim. Kitaplar doldurur bu konular. İşletim sistemi tarafında da önemli katkıları var. Oraya odaklanmak istiyorum. Eindhoven Teknoloji Üniversitesi’nde (Technische Hogeschool Eindhoven, Eindhoven University of Technology), Dijkstra önderliğindeki ekibin geliştirdiği bir sistem oldu. 1968 yılında yayınlanan bu yapı, ismi kasten böyle verilmese de, üniversitenin adının baş harflerinden dolayı “THE” adını aldı.^5 Sistemin kendisi başlı başına bir olay. Gerek semaphore gerekse virtual memory implementasyonları açısından oldukça yüksek öneme sahip.
Konumuzla bağlantılı olan noktası ise, bu sistemin çok katmanlı yapıda olması.
Katman | İşlev | Açıklama |
5 | Kullanıcı | Sistemi kullanan kişi |
4 | Kullanıcı programları | Kullanıcının işlerini yerine getirmesini sağlayan programlar. |
3 | I/O yönetimi | Bilgisayara bağlı cihazlar arasında girdi/çıktı yönetimi. |
2 | Kullanıcı – process iletişimi | İşletim sistemi konsolu ile process’ler arasındaki iletişim. |
1 | Bellek yönetimi | Process’lere atanmış / atanacak bellek alanlarının yönetimi. |
0 | CPU yönetimi ve multiprogramming | Interrupt, context switching gibi işler. Günümüzün CPU scheduler’ı. |
Takdir edersiniz ki, katmanlı bir yapıda yapılacak değişiklikler nispeten daha kolay olacaktır. Benzer şekilde, hataların fark edilmesi süreci de daha rahat olacaktır. Sorunu tüm çekirdekte aramak yerine çekirdeğin ilgili katmanında arayabiliriz. Bu da sistemin tasarım ve implementasyonunu kolaylaştırır. Ayrıca, her katman kendinden önceki katmanlara ait fonksiyonları kullanabilir. Tüm yapının birbiri ile iletişimde olmasına gerek yoktur.
Katmanlar işi kendi içinde çözerek bir üst katmana gönderebilir. Bu durumda – örneğin – layer 4’te yer alan kullanıcı programlarının layer 1’de yer alan bellek yönetimi ile ilgili veri yapılarını bilmesine gerek yoktur. Layer 4’te çalışan bir uygulama talebini iletir ve sonucunu bekler. Alt katmanlardaki verilere erişimi kısıtlanarak sistemin güvenliğine de katkı sağlanabilir.
Bu yapıdaki zorluklardan biri, katmanların doğru belirlenmesidir. Her katman, yalnızca kendinden öncekilerle iletişime geçebildiği için, doğru yapılandırılmamış katmanlar zinciri iletişim sırasında sorunlar yaşayabilir. Yapılacak bir iş, birden fazla katmandan dönüş bekleyebilir. Bu durumda bu katmanların yerleri, kullanım önceliklerine göre belirlenmelidir. Çalışacak bir servisin, hangi katmanda yer alacağına net bir şekilde karar verilmelidir ve her senaryoda bu servisin bu katmanda olması herhangi hiçbir aksaklığa sebep olmamalıdır.
Yukarıdaki tabloya bakarak başka bir senaryo düşünelim. Kullanıcı olarak programa bir talepte bulundum. Diske veri yazmak istiyorum. Layer 4’te çalışan programımda “Kaydet” butonuna tıkladım. Programım layer 3’e (I/O) bir çağrı yaptı. Oradan bellek yönetimine yeni bir çağrı tetiklendi, sonra cpu scheduling’e ayrı bir çağrı gitti. Neticede donanıma talebi iletmiş olduk. Kulaktan kulağa oynar gibi aslında. Fakat en başta “a” harfini kaydetmek istiyorduk. Süreç ilerledikçe “Ben senden şunu istiyorum.” şeklinde başlayan konuşma “Şu benden böyle bir şey istedi.”, “Biri şöyle bir şey istedi diye bu da benden böyle bir şey istedi.” şeklinde devam eden çağrılar zincirine dönüşebilir.
Microkernel Sistemler
Katmanlı yaklaşım sonrasında, geliştiriciler için yeni bir yol ayrımı ortaya çıktı. Kernel-kullanıcı bağımlılığını nereye kadar sürdüreceğiz? Kernel mode’da çalışan şeylerin sayısını mümkün mertebe az tutmalıyız. Çünkü burada çalışan süreçlerde oluşacak bir bug, bütün sistemi alt üst eder. Kernel çökerse, sistem çöker.
“Her 1000 satırda karşılaşılan bug miktarı”^6 konusunda yapılan bazı araştırmalar, bug oluşma olasılığını bazı konulara indirgemeyi başardı. Bug derinliği konusunda yapılan bu araştırmaları referans alırsak, 5 milyon satırlık monolithic bir çekirdek, yüksek ihtimalle 10000 – 50000 arasında bug içerecek. Tabii ki bunların her biri kritik değil. Burada olay, hata yapılma ihtimali. Yani “İşlem başarısız” mesajı yerine “İlşem başarısız” yazdırmak da bu sayıların içine dâhil. Fakat durumun tehlikesini şuradan da anlayabiliriz. Birçok cihazın üzerinde “reset” düğmesi var. Yani her şeyin sarpa sarıp sıfırdan başlamasının gerekeceği senaryoların yaşanması hayli mümkün.
Hâl böyle olunca, bu verilerin üzerine bir de kocaman monolithic kernel’lar eklenince, farklı bir mimari ihtiyacı ortaya çıktı. 1980’li yıllarda, Carnegie Mellon University araştırmacıları “Mach“^7 projesine yöneldi ve çekirdeğini de microkernel yaklaşımıyla geliştirdi. Microkernel mimarisinin ilk örneklerinden bu çekirdek, aslında epey tanıdığımız bir yapı. Bu çekirdek, GNU Hurd projesinin de temelini oluşturuyor.^8 Ek olarak, Apple’ın MacOS, iOS, iPadOS, tvOS, watchOS gibi projelerinde kullanılan çekirdeğin de temellerini oluşturuyor.
Bu tasarımda, elzem olmayan tüm parçalar kernel’dan çıkarılır ve sistem ya da kullanıcı programı olarak implemente edilir. Bunun sonucunda daha küçük bir kernel elde edilir. Tabii daha yolun başında şu soru karşımıza çıkıyor: Neyi kernel’dan çıkaralım, neyi bırakalım? İşletim sistemini mümkün mertebe fazla parçaya bölüp, her parçanın kendi işini en iyi şekilde yapmasını sağlamak ve bu ayrımları net bir şekilde yapmak; bu tasarımın temelini oluşturur. Parçalardan yalnızca biri, yani microkernel, kernel mode’da çalışır. Kalan bütün servisler daha az yetkili kullanıcı programları ve sistem programları olarak çalışır. Bu sayede, modüllerde yaşanacak bir problem, yalnızca ilgili modülün çökmesine sebep olur. Örneğin, network’ü yöneten modül çöktüğünde sisteminiz tamamen çökmez. Evet, network’e çıkamaz hâle gelirsiniz. Fakat sonrasında çalışmalarınızı kaydedebilir, offline işlerinizi tamamlayabilir ve bilgisayarınızı sağlıklı bir şekilde kapatabilirsiniz. Ya da! “Reincarnation Server (Reenkarnasyon Sunucusu)” devreye girer ve sorunlu modülü yeniden başlatır.
Her şeyin çok güzel göründüğü paragrafların sonunda kaşı gözü dağıtıyoruz hep, biliyorsunuz. Gerek implementasyon kolaylığı gerekse performans konusunda microkernel isteneni veremiyor. Servisler arası ciddi bir iletişim gerekli. Servislerin net ayrımı yapılmalı. Gerekli iletişim sayısı ve dolayısıyla iletişim süresi ve mesaj boyutları arttığında da performans düşüşü kaçınılmaz oluyor.
Modüler Sistemler
Yine geldik “hepsi çok güzel ama hepsi sorunlu, hadi karıştıralım” dediğimiz anlardan birine. Object-Oriented Programming mantığıyla ilerlenen modüler bir yapı oluşturuluyor bu tasarımda.
Çekirdeğin birtakım temel bileşenleri var. Bu bileşenlere ek olarak bazı modüller gerek boot sırasında gerekse sonradan kernel’a bağlanıyor. Bu tasarım sayesinde kernel temel servisleri zaten kendisi verebiliyorken bazı özelliklerin de dinamik olarak sisteme katılabilmesi sağlanıyor. Monolithic kernel kısmında bahsettiğim gibi driver örneğini düşünebilirsiniz. Ne monolithic yapıdaki gibi tek parça bir kernel var ne de microkernel yapıdaki gibi servisler arası “message passing” gerekiyor. İşlerin sorumlu modülleri var. İhtiyaç hâlinde bu modüller kernel’a linkleniyor ve sistem çalışmaya devam ediyor.
Bu tarz sistemlere “Hibrit Sistemler” denildiğini de görebilirsiniz. Aslında ilk paragrafta belirttiğim gibi, farklı mimariler arasından optimum seçimlerin yapılmasıyla oluşturulmuş bir yapı bu. Benzer durumu CPU scheduling konusunda da yaşamıştık hatırlarsanız.

Sonuç Olarak
İşletim sistemi dünyasında gerek kullanılan araçlar gerekse çekirdek tarafında birçok bilim insanı ve geliştiricinin katkısı bulunmaktadır. Araştırma amaçlı denenmiş farklı mimarilerin de var olması mümkündür. Bu yazımda en sık karşılaşılan mimarilerden bahsetmeye çalıştım.
Tek beklentim, “aaa süper cihaz” diyerek elinize aldığınız o saçma fiyat etiketli ürünlerin her birinin arkasında, anılmayı çok daha fazla hak eden insanlar olduğunun farkına varmanız. Umarım faydalı olmuştur.
Bağlantılar
1- Library (computing) #Shared Libraries, https://en.wikipedia.org/wiki/Library_(computing)#Shared_libraries
2- Dynamic-link library, https://en.wikipedia.org/wiki/Dynamic-link_library
3- Monolithic kernel, https://en.wikipedia.org/wiki/Monolithic_kernel
4- Edsger W. Dijkstra, https://en.wikipedia.org/wiki/Edsger_W._Dijkstra
5- THE multiprogramming system, https://en.wikipedia.org/wiki/THE_multiprogramming_system
6- Basis for claim that the number of bugs per line of code is constant regardless of the language used, https://stackoverflow.com/questions/2898571/basis-for-claim-that-the-number-of-bugs-per-line-of-code-is-constant-regardless#2899659
7- Mach (kernel), https://en.wikipedia.org/wiki/Mach_%28kernel%29
8- What is the GNU Hurd?, https://www.gnu.org/software/hurd/hurd/what_is_the_gnu_hurd.html
Referans
1 – Operating System Concepts Essentials, Abraham SILBERSCHATZ (Yale University), Peter Baer GALVIN (Corporate Technologies, Inc.), Greg GAGNE (Westminster College)
2 – Modern Operating Systems, Andrew S. TANENBAUM, Herbert BOS (Vrije Universiteit)