İşletim Sistemleri · Ders Notu
Bölüm 2: İşletim Sistemi Yapıları
Bir işletim sistemi tek parça bir program gibi görünür. Oysa içinde süreç yönetimi, bellek yönetimi, dosya sistemi, aygıt sürücüleri ve ağ yığını gibi onlarca bileşen vardır. Bu bileşenlerin hangi modda çalışacağı ve birbirleriyle nasıl konuşacağı, sistemin hızını, güvenilirliğini ve bakımını belirler. Not, işletim sisteminin kullanıcılara ve programlara sunduğu hizmetlerle başlar. Ardından süreç, adres alanı, dosya, bağlama, boru ve kilitlenme gibi temel kavramları tanıtır. Donanımın işletim sistemini nasıl koruduğu, programların sistem çağrılarıyla çekirdekten nasıl hizmet istediği ve temel POSIX çağrıları kod izlemeleriyle adım adım anlatılır. Son kesimler monolitik, katmanlı, mikro çekirdek, modüler, hibrit ve exokernel yapıları karşılaştırır; sanal makinelere, bağlayıcı ile yükleyiciye ve bilgisayarın açılış sürecine değinir.
Öğrenme çıktıları
- Hizmetler ve kavramlar
- İşletim sisteminin sunduğu hizmetleri; süreç, adres alanı, dosya, bağlama ve boru hattı kavramlarını açıklamak.
- Sistem çağrıları
- Kullanıcı modundan çekirdek moduna geçişi, parametre aktarımını ve temel POSIX çağrılarını adım adım izlemek.
- Çekirdek yapıları
- Monolitik, katmanlı, mikro çekirdek, modüler, hibrit ve exokernel yapılarını güçlü ve zayıf yönleriyle karşılaştırmak.
- Sanallaştırma ve açılış
- Sanal makine ve konteyner kavramlarını, bağlayıcı ile yükleyicinin işini ve açılış adımlarını tanımak.
2.1Yapı ve hizmetler
Bir işletim sistemini iki açıdan incelemek mümkündür. Dışarıdan bakan biri, sistemin ne iş gördüğünü sorar: hangi hizmetleri sunar, programlar bu hizmetlere nasıl ulaşır? İçeriden bakan biri ise sistemin nasıl kurulduğunu sorar: bileşenler nerede çalışır, birbirlerini nasıl çağırır? Bu kesim iki soruyu da kısaca ele alır. Önce “yapı” sözcüğüyle ne kastedildiğini, sonra işletim sisteminin dışarıya sunduğu hizmetleri tanıtır.
2.1.1Yapı neden önemlidir?
Günümüzün bir işletim sistemi tek başına küçük bir program değildir. Süreçleri oluşturan ve sonlandıran süreç yöneticisi, belleği paylaştıran bellek yöneticisi ve diskteki verileri dosya olarak sunan dosya sistemi bunlardan yalnız üçüdür. Her aygıtın ayrı bir sürücüsü, ağ paketlerinin de kendi yığını vardır. Linux çekirdeğinin kaynak kodu 2025 yılında 40 milyon satırı geçmiştir. Bu büyüklükte bir yazılımı tek bir düzensiz yığın olarak yazmak, anlamak ve bakımını yapmak mümkün değildir.
Tasarımcı her bileşen için iki karar verir. Birincisi, bileşenin nerede çalışacağıdır: donanımın bütün yetkilerini kullanan çekirdek modunda mı, yoksa yetkileri kısıtlı kullanıcı modunda mı? İkincisi, bileşenlerin nasıl haberleşeceğidir: biri ötekini sıradan bir fonksiyon gibi mi çağıracak, yoksa iki bileşen birbirine ileti mi gönderecek?
İşletim sistemi bileşenlerinin nerede çalışacağına (çekirdek modu ya da kullanıcı modu) ve nasıl haberleşeceğine (fonksiyon çağrısı ya da ileti) dair genel stratejiye işletim sistemi yapısı denir.
Yapı seçimi, sistemin günlük hayatını doğrudan etkiler. Karmaşıklığın ne kadar iyi yönetildiği ve bir değişikliğin kaç dosyaya dokunduğu bu seçime bağlıdır. Bir hatanın kaç satır kod içinde aranacağı ve sistemin başka bir işlemciye ne kadar kolay taşınacağı da öyle. Ne var ki hiçbir yapı her ölçütte en iyisi değildir. Şekil 2.1 üç ana hedefi bir üçgenin köşelerine yerleştirir: performans, güvenilirlik ve yalıtım, esneklik ve bakım. Bir köşeye yaklaşan tasarım, genellikle öteki köşelerden uzaklaşır.
Bir örnek, bu ödünleşimi somutlaştırır. Bir dosya sistemi kodu, çekirdeğin içinde öteki bileşenlerle birlikte çalışıyorsa bir dosyayı okumak birkaç fonksiyon çağrısı kadar ucuzdur. Aynı kod ayrı bir süreç olarak kullanıcı modunda çalışıyorsa her istek bir ileti ve birkaç mod geçişi gerektirir; okuma pahalılaşır. Buna karşılık dosya sistemi kodundaki bir hata, ikinci durumda yalnız o süreci çökertir, birinci durumda ise bütün çekirdeği düşürebilir. Masaüstü bir bilgisayar, bir veri merkezi sunucusu, bir gömülü cihaz ve bir uçağın uçuş denetim bilgisayarı bu dengeyi farklı noktalarda kurar. Tek doğru yapının olmaması bundandır.
Yapıyı, kullanıcının gördüğü arayüzle karıştırmak. İki işletim sistemi aynı sistem çağrılarını, aynı komutları ve aynı disk biçimini sunup yine de çok farklı yapıda olabilir. Yapı, sistemin içindeki iş bölümüyle ilgilidir; dışarıdan bakan kullanıcı çoğu zaman bu farkı görmez.
2.1.2İşletim sistemi hizmetleri
İşletim sistemi, üzerinde çalışan programlara bir çalışma ortamı hazırlar ve bu ortamın parçası olan bir dizi hizmet (operating system services) sunar. Hizmetlerin ayrıntısı sistemden sisteme değişir, ama ana gruplar hemen her sistemde aynıdır. Şekil 2.2 bu grupları katmanlar hâlinde gösterir.
Şekildeki en önemli ayrıntı, katmanlar arasındaki geçiş noktasıdır. Kullanıcı ister bir pencereye tıklasın ister komut satırına komut yazsın, yaptığı iş sonunda bir ya da birkaç sistem çağrısına dönüşür. Arayüz, kullanıcının isteğini sistem çağrılarına çeviren bir aracıdır. Hizmetlerin kendisi sistem çağrısı katmanının altında, işletim sisteminin içindedir ve donanımı doğrudan yönetir.
2.1.3Hizmetler kime hizmet eder?
Hizmetleri yararlandıkları tarafa göre iki gruba ayırmak alışılmış bir yoldur. Birinci grup, kullanıcıya ve programlara doğrudan yardım eder:
- Arayüz. Kullanıcı sistemle komut satırı, grafik kullanıcı arayüzü (graphical user interface, GUI), dokunmatik ekran ya da sesli komutla konuşur.
- Program yürütme. Sistem bir programı belleğe yükler, çalıştırır ve sonlandırır. Programın normal biçimde mi yoksa bir hatayla mı bittiğini de kaydeder.
- Giriş/çıkış (G/Ç) işlemleri. Program bir aygıta doğrudan erişmez; isteğini işletim sistemine iletir. Böylece hem verimlilik hem koruma sağlanır.
- Dosya sistemi. Dosya oluşturma, silme, okuma, yazma, arama ve izinleri yönetme işleri bu hizmetin kapsamındadır.
- İletişim. Süreçler aynı makinede ya da ağ üzerinden bilgi alışverişi yapar. Bunun iki temel yolu paylaşımlı bellek ve ileti geçirmedir.
- Hata algılama. Bellek hatası, aygıt arızası, sıfıra bölme ya da yetkisiz bellek erişimi gibi durumlar algılanır ve uygun biçimde ele alınır.
İkinci grup, kullanıcıya doğrudan görünmez; sistemin bir bütün olarak verimli ve güvenli çalışmasını sağlar:
- Kaynak ayırma. İşlemci (CPU), bellek ve depolama birçok süreç arasında paylaştırılır.
- Kaynak izleme (accounting). Hangi kullanıcının ya da sürecin ne kadar kaynak kullandığı kaydedilir; günlük kayıtları tutulur.
- Koruma. Süreçler birbirine ve çekirdeğe zarar veremez.
- Güvenlik. Kimlik doğrulama, yetkilendirme ve dış saldırılara karşı savunma bu hizmetin işidir.
Kullanıcıların çoğu işletim sistemini sistem çağrıları üzerinden değil, sistem programları üzerinden tanır. Dizin içeriğini listeleyen ls, dosya kopyalayan cp, çalışan süreçleri gösteren ps, paket yöneticisi, derleyici ve dosya yöneticisi bu tür programlardır. Bunlar sıradan kullanıcı programlarıdır, ama işlerini yapmak için yoğun biçimde sistem çağrısı kullanırlar. Kullanıcının gözünde “işletim sistemi”, çoğu zaman bu programların toplamıdır.
Bir elektronik tablo programında bir formülün yeniden hesaplanması bir işletim sistemi hizmeti midir?
Çözüm. Değildir. Formülü hesaplamak programın kendi mantığıdır; işlemci bu hesabı programın kendi kodunu yürüterek yapar. İşletim sistemi bu sırada programa yalnız işlemci zamanı ve bellek sağlar. Tablo diske kaydedilirken ise durum değişir: programın diske doğrudan erişme yetkisi yoktur, dosya sistemi hizmetini sistem çağrılarıyla kullanır. Aynı programın bir işi kendi kodudur, öteki işi işletim sistemi hizmetidir.
2.2Temel kavramlar
İşletim sistemleri, donanımın karmaşık ayrıntılarını birkaç temel soyutlamanın arkasına gizler. Bu soyutlamalar o kadar yaygındır ki işletim sistemini anlatan her metin aynı kavramlara dayanır: süreç, adres alanı, dosya, dizin, bağlama, boru ve kilitlenme. Kesimin amacı bu kavramlara ilk bir tanım vermektir. Her birinin ayrıntısı, ilgili kavramın kendi bölümünde derinleştirilir; burada önemli olan, kavramların birbiriyle nasıl ilişkilendiğini görmektir.
2.2.1Süreç
Diskteki bir program dosyası, kendi başına hiçbir şey yapmaz. Makine komutlarından ve başlangıç verilerinden oluşan pasif bir dosyadır. Program çalıştırıldığında işletim sistemi onu belleğe yükler, ona bir yığın ayırır, işlemciye ilk komutunu gösterir ve programı izlemeye başlar. Çalışmakta olan bu canlı örneğe süreç adı verilir.
Süreç (process), yürütülmekte olan bir programdır. Program diskteki pasif dosyadır; süreç onun etkin örneğidir. Her süreçle birlikte bir adres alanı (kod, veri, yığın), yazmaçların değerleri (program sayacı ve yığın göstericisi dahil), açık dosyalar, alarmlar, ilişkili süreçlerin listesi ve öteki kaynaklar tutulur.
Aynı programdan aynı anda birden çok süreç oluşturulabilir. İki ayrı terminalde bash çalıştıran bir kullanıcının makinesinde, diskteki tek bash dosyasından iki süreç vardır. İkisi aynı kodu yürütür, ama her birinin değişkenleri, açık dosyaları ve o anda bulunduğu komut farklıdır.
İşletim sistemi, bir süreci askıya alıp başka bir süreci çalıştırmak zorunda kalır. Askıya alınan sürecin daha sonra kaldığı yerden sürmesi için o anki bütün durumunun bir yerde saklanması gerekir. Bu bilgiler süreç tablosunda tutulur. Tablonun her girdisi bir sürece aittir ve süreç kontrol bloğu (process control block, PCB) adını alır. Şekil 2.3 küçük bir süreç tablosunu ve çalışan sürecin PCB içeriğini gösterir.
systemd, uyuyor), 812 (bash, bekliyor), 1045 (ls, çalışıyor) ve 1046 (grep, hazır). Çalışan sürecin PCB'sinde PID, durum ve öncelik; program sayacı, yığın göstericisi ve öteki yazmaçlar; bellek haritası; açık dosyalar tablosu; sinyaller, alarmlar, kullanıcı kimliği ve ebeveyn bilgisi bulunur.Şekildeki her süreç bir süreç kimliğiyle (process identifier, PID) tanınır. Durum sütunu, sürecin o anda ne yaptığını gösterir. Tek işlemcili bir makinede aynı anda yalnız bir süreç çalışır. Öteki süreçler ya işlemci sırası bekler (hazır) ya da klavyeden bir tuşa basılması gibi bir olayı bekler. PCB'deki yazmaç değerleri, süreç işlemciden alınırken kaydedilir ve süreç yeniden çalışacağı zaman işlemciye geri yüklenir. Süreç, aradaki kesintiyi fark etmez.
Süreçler çoğu zaman tek başına çalışmaz; birbirleriyle veri alışverişi yapar. Bunun için işletim sisteminin sunduğu mekanizmaların ortak adı süreçler arası iletişimdir (interprocess communication, IPC). Boru, paylaşımlı bellek, ileti kuyruğu ve soket bu mekanizmalardan bazılarıdır.
2.2.2Süreç ağacı
UNIX benzeri sistemlerde yeni bir süreç, var olan bir sürecin fork() sistem çağrısıyla oluşur. Çağrıyı yapan sürece ebeveyn, oluşan sürece çocuk denir (parent / child process). Çocuk da kendi çocuklarını oluşturabildiği için süreçler kendiliğinden bir ağaç biçiminde dizilir. Şekil 2.4 bu ağacın iki aşamasını gösterir.
fork() çağırmıştır: B ve C, A'nın çocuklarıdır. Altta B süreci de D, E ve F olmak üzere üç çocuk oluşturmuştur.Başlangıçta yalnız A süreci vardır. A iki kez fork() çağırınca B ve C doğar; A ikisinin de ebeveynidir. Ardından B üç kez fork() çağırır ve D, E, F süreçleri oluşur. Bu ağaçta D'nin ebeveyni B, B'nin ebeveyni A'dır. Linux'ta bütün ağacın kökü, açılışta çekirdeğin başlattığı ve PID'si 1 olan systemd (eski sistemlerde init) sürecidir. Çalışan bir sistemde pstree -p komutu bu ağacı süreç kimlikleriyle birlikte yazdırır.
fork()'un yeni ve boş bir program başlattığını sanmak. fork() ebeveynin bir kopyasını oluşturur; çocuk, ebeveynle aynı kodu aynı noktadan yürütmeye devam eder. Çocuğun başka bir program çalıştırması için ayrıca execve çağrısı gerekir. Bu ikili, kabuk örneğiyle 2.5.2'de ayrıntılı olarak izlenir.
2.2.3Adres alanı ve bellek kesimleri
Her süreç belleği kendi başına kullanıyormuş gibi görür. Sürecin kullanabildiği adreslerin kümesine onun adres alanı (address space) denir. Adres alanı 0'dan başlayıp bir üst sınıra kadar uzanır. x86-64 mimarisindeki Linux'ta bir sürecin kullanıcı alanı 128 TiB'tır; bu, çoğu makinenin fiziksel belleğinden çok daha büyüktür. Fiziksel bellekten büyük adres alanlarını mümkün kılan teknik sanal bellektir.
Bellekte aynı anda çok sayıda süreç bulunur. İşletim sistemi bu süreçleri hem birbirinden hem de kendisinden korumak zorundadır: bir sürecin hatalı bir göstericisi başka bir sürecin verisini ya da çekirdeğin kodunu bozmamalıdır. Bu koruma, donanımdaki bellek yönetim birimi ve işletim sisteminin birlikte çalışmasıyla sağlanır.
Bir sürecin adres alanı birkaç kesime ayrılır. Şekil 2.5 tipik düzeni aşağıdan yukarıya doğru gösterir.
- Metin. Programın makine kodudur. Salt okunur tutulur; aynı programı çalıştıran süreçler ve bir sürecin bütün iş parçacıkları bu kesimi paylaşır.
- Veri ve BSS. Global ve
staticdeğişkenler burada durur. İlk değeri kaynak kodda verilmiş olanlar veri kesimine, verilmemiş olanlar BSS kesimine yerleşir. - Öbek (heap). Çalışma anında
mallocya danewile ayrılan dinamik bellektir. Veri kesimi gibi öbek de sürecin bütün iş parçacıklarınca paylaşılır. - Yığın (stack). Fonksiyon parametreleri, yerel değişkenler ve dönüş adresleri burada tutulur. Her iş parçacığının kendi yığını vardır.
Bir C programında int sayac = 5; ve int tablo[1000]; global olarak, main içinde ise int i; ve char *p = malloc(64); tanımlanmıştır. Her nesne hangi kesimdedir?
Çözüm. sayac ilk değeri olan bir global değişkendir: veri kesimindedir. tablo ilk değer almamış bir global dizidir: BSS kesimindedir ve yükleme sırasında sıfırlarla doldurulur. i ve p göstericisinin kendisi main'in yerel değişkenleridir: yığındadır. p'nin gösterdiği 64 baytlık alan ise malloc ile ayrıldığı için öbektedir. Fonksiyonların makine kodu da metin kesimindedir.
2.2.4Dosyalar ve dizin hiyerarşisi
Disk, kendi başına yalnız numaralı bloklardan oluşan bir depolama alanıdır. Bir program “47 291 numaralı bloğu oku” gibi isteklerle çalışmak zorunda kalsaydı her programcı diskin iç yapısını öğrenmek zorunda kalırdı. İşletim sistemi bu ayrıntıyı gizler ve programlara daha temiz bir soyutlama sunar: dosya.
Dosya, verileri kalıcı olarak saklayan adlandırılmış bir birimdir. Metin, resim, video ya da çalıştırılabilir bir program bir dosyada saklanabilir. Dosyalar dizinlerde (directory) toplanır; dizinler de başka dizinleri içerebildiği için bir ağaç oluşur.
Ağaçtaki bir dosyaya yol adıyla (path name) ulaşılır. Mutlak yol, kök dizinden (/) başlar: /home/ali/notlar.txt. Göreli yol ise sürecin o anki çalışma dizininden başlar. Çalışma dizini /home olan bir süreç aynı dosyaya ali/notlar.txt diye ulaşır. Şekil 2.6 örnek bir dizin ağacını gösterir.
bin, home, usr ve dev dizinleri vardır. /home/ali dizininde notlar.txt ve odev.c dosyaları, /dev dizininde ise sda ve tty aygıt dosyaları bulunur.Bir süreç bir dosyayı açtığında işletim sistemi ona küçük bir tamsayı verir. Bu sayıya dosya tanımlayıcısı (file descriptor, fd) denir. Süreç daha sonra dosyayı adıyla değil bu sayıyla anar: “3 numaralı dosyadan 100 bayt oku” gibi. Her süreç üç açık tanımlayıcıyla başlar: 0 standart girdi (stdin), 1 standart çıktı (stdout), 2 standart hata (stderr). Süreç yeni bir dosya açtığında ona en küçük boş numara verilir; ilk açılan dosya bu yüzden genellikle 3 numarayı alır.
UNIX'in dikkat çekici bir fikri, aygıtların da dosya gibi görünmesidir. Bunlara özel dosya denir ve iki türü vardır. Blok aygıtları veriyi sabit büyüklükteki bloklar hâlinde saklar ve bloklara istenen sırayla erişime izin verir: /dev/sda ve /dev/nvme0n1 diskleri gibi. Karakter aygıtları ise veriyi bir karakter akışı olarak üretir ya da tüketir: terminal (/dev/tty) ve seri port gibi. Bir program terminale yazarken de bir dosyaya yazarken de aynı write çağrısını kullanır.
2.2.5Dosya sistemini bağlama
Bir bilgisayara USB bellek takıldığında, bellekteki dosya sistemi başlangıçta ayrı bir ağaçtır. Sistemin ana ağacıyla hiçbir bağı yoktur ve hiçbir yol adıyla ona ulaşılamaz. UNIX'te tek bir dizin ağacı vardır; yeni bir dosya sisteminin bu ağaca eklenmesine bağlama (mount) denir.
Bağlama, bir dosya sisteminin kök dizinini mevcut ağaçtaki bir dizine iliştirme işlemidir. İliştirmenin yapıldığı dizine bağlama noktası denir. Bağlamadan sonra yeni dosya sistemindeki dosyalara, bağlama noktasıyla başlayan yol adlarıyla ulaşılır.
/dev/nvme0n1p2) ve USB bellekteki ayrı ağaç görülür; USB'deki a ve b dizinlerine hiçbir yoldan ulaşılamaz. Altta USB'nin kökü /mnt dizinine bağlanmıştır; artık /mnt/a/x ve /mnt/b/z yolları geçerlidir.Şekil 2.7 işlemi iki anda gösterir. Bağlama noktası olarak genellikle boş bir dizin kullanılır. Dizin boş değilse eski içeriği silinmez; yalnız bağlantı kaldırılana kadar görünmez olur. Windows bu soruyu farklı çözer: her disk ya da bölüm genellikle ayrı bir sürücü harfi (C:, D:) alır ve ayrı bir ağaç olarak kalır.
Linux'ta bağlama işi mount komutuyla yapılır. Aşağıdaki komut dizisi sık kullanılan biçimleri gösterir:
$ lsblk # diskleri ve bölümleri listele
$ sudo mount /dev/sdb1 /mnt # sdb1'i /mnt dizinine bağla
$ sudo mount -o ro /dev/sdb1 /mnt # salt okunur (read-only) bağla
$ sudo mount -o remount,rw /mnt # okuma-yazma olarak yeniden bağla
$ findmnt /mnt # bağlama bilgisini göster
$ sudo umount /mnt # bağlantıyı kaldırlsblk sistemdeki diskleri ve bölümleri listeler; buradan USB belleğin /dev/sdb1 adını aldığı görülür. İkinci satır onu /mnt'ye okuma-yazma kipinde bağlar. -o ro seçeneği salt okunur bağlama yapar; kurtarma ve adli inceleme işlerinde diskteki veriyi yanlışlıkla değiştirmemek için bu kip tercih edilir. remount,rw zaten bağlı bir dosya sistemini okuma-yazma kipine geçirir. findmnt bağlama bilgisini, umount ise bağlantının kaldırılmasını sağlar. Bu komutların çoğu yönetici yetkisi gerektirdiği için sudo ile çalıştırılır.
Açılışta kendiliğinden bağlanacak dosya sistemleri /etc/fstab dosyasında ya da systemd bağlama birimlerinde tanımlanır. Masaüstü sistemlerde takılan bir USB bellek ise udisks2 hizmeti aracılığıyla /run/media/… altına kendiliğinden bağlanır. Şekil 2.8 bağlamadan sonra USB'deki belge.pdf dosyasının nasıl /mnt/belge.pdf yoluyla erişilebilir hâle geldiğini özetler.
mount /dev/sdb1 /mnt komutundan sonra ağaç. /mnt dizini USB'deki dosya sistemine bağlanmıştır; USB'nin kökündeki belge.pdf artık /mnt/belge.pdf yoluyla açılır.2.2.6Boru
Kabukta ls -l | grep "notlar" gibi bir komut yazıldığında iki program aynı anda çalışır: ls dizin listesini üretir, grep bu listeden içinde “notlar” geçen satırları seçer. İki program arasında geçici bir dosya yoktur. Veriyi birinden ötekine taşıyan yapı bir borudur.
Boru (pipe), iki süreci bağlayan, çekirdekte tamponlanan tek yönlü bir bayt kanalıdır. Bir süreç borunun yazma ucuna yazar, öteki süreç okuma ucundan okur. Süreçler boruyu sıradan bir dosya gibi read ve write çağrılarıyla kullanır.
İki tür boru vardır. Anonim boru pipe() çağrısıyla oluşur ve adı yoktur; bu yüzden yalnız ortak ataya sahip süreçler, tipik olarak ebeveyn ve çocuk süreçler arasında kullanılır. Adlandırılmış boru (FIFO) ise mkfifo ile oluşturulur ve dosya sisteminde bir adı vardır. Birbiriyle akrabalığı olmayan süreçler bile bu adı açarak aynı boruyu kullanabilir.
Kabuğun yukarıdaki komutu nasıl kurduğu dört adımda izlenebilir:
- Boru oluşturulur. Kabuk önce
pipe(fd)çağırır. Çekirdek bir tampon ayırır ve iki tanımlayıcı döndürür:fd[0]okuma ucu,fd[1]yazma ucudur. - Süreçler oluşturulur ve uçlar yönlendirilir. Kabuk iki kez
forkeder. Birinci çocuktadup2çağrısıyla standart çıktı (1) borunun yazma ucuna, ikinci çocukta standart girdi (0) okuma ucuna yönlendirilir. Ardından çocuklar kendilerinilsvegrepprogramlarıyla değiştirir. - Veri akar.
lslisteyi standart çıktısına, yani boruya yazar.grepstandart girdisinden, yani borudan okur ve yalnız “notlar” içeren satırları terminale yazar. İki program da bir boruya bağlı olduklarını bilmez. - Akış denetimi ve bitiş. Tampon dolunca yazan süreç, boşalınca okuyan süreç bloke olur.
lsbitip borunun bütün yazma uçları kapanıncagrep'inreadçağrısı 0 döndürür. Bu değer dosya sonu (EOF) demektir vegrepde biter.
ls -l | grep "notlar" boru hattı. Üstte uçların yönlendirilmesi: ls'in standart çıktısı fd[1]'e, grep'in standart girdisi fd[0]'a bağlanmıştır. Altta akış denetimi: boru tamponu (64 KiB) çekirdektedir; tampon dolunca yazan, boşken okuyan bekler, yazma ucu kapanınca okuyan dosya sonunu görür.Kabuğun kendi elindeki boru uçlarını kapatmayı unutması. Okuyan süreç, dosya sonunu ancak borunun bütün yazma uçları kapandığında görür. Ebeveyn kabuk, fork sonrasında kendi kopyasındaki yazma ucunu açık tutarsa ls bitse bile grep sonsuza dek yeni veri bekler.
2.2.7Kilitlenme
Süreçler yalnız işlemciyi değil, yazıcı, dosya, bellek tamponu ya da kilit gibi başka kaynakları da kullanır. Bazı kaynakları aynı anda yalnız bir süreç tutabilir. İki ya da daha fazla süreç birbirinin elindeki kaynağı beklemeye başlarsa hiçbiri ilerleyemez.
Bir süreç kümesindeki her süreç, kümedeki başka bir sürecin tuttuğu kaynağı bekliyorsa ve bu yüzden hiçbiri ilerleyemiyorsa kümede kilitlenme (deadlock) vardır.
Günlük hayattaki karşılığı, dört yönden aynı anda bir kavşağa giren ve her biri sağındaki aracın geçmesini bekleyen dört araçtır. Kurala uyan sürücülerin hiçbiri yerinden kıpırdamaz. Durumu çizmenin standart yolu, kaynak tahsis çizgesidir (resource allocation graph). Şekil 2.10 iki süreç ve iki kaynaklık bir örneği iki aşamada gösterir.
(a) durumunda henüz sorun yoktur: P1 R1'i, P2 R2'yi tutmaktadır ve ileride ötekinin kaynağını isteyeceklerdir. Bu isteklerin ikisi de yapıldığında (b) durumuna gelinir. P1, P2'nin tuttuğu R2'yi; P2 de P1'in tuttuğu R1'i bekler. Çizgede kapalı bir döngü oluşmuştur ve iki süreç de sonsuza dek bekler. Kaynakların her birinin tek örneği varsa çizgedeki bir döngü kilitlenmenin kesin işaretidir. Kaynaklar birden çok örnekliyse döngü, kilitlenme olasılığını gösterir ama tek başına kanıt değildir.
2.3Koruma
Bellekte aynı anda çekirdek ve onlarca kullanıcı süreci bulunur. Süreçlerden biri hatalı ya da kötü niyetli olabilir. Böyle bir süreç diske doğrudan yazarsa dosya sistemini bozabilir; kesmeleri kapatırsa işletim sistemini devre dışı bırakabilir; sonsuz bir döngüye girerse işlemciyi hiç bırakmayabilir. İşletim sistemi bu tehlikelere karşı yalnız yazılımla korunamaz, çünkü sorunlu kod da aynı işlemcide çalışmaktadır. Korumanın temeli donanımdadır.
2.3.1Çift modlu çalışma
Günümüz işlemcileri en az iki çalışma modu tanır. Hangi modda olunduğu, işlemcideki bir mod bitinde tutulur. Silberschatz'ın kullandığı gösterimde bu bit çekirdek modunda 0, kullanıcı modunda 1'dir.
Çift modlu çalışma (dual-mode operation), işlemcinin çekirdek modu (kernel mode) ile kullanıcı modu (user mode) arasında ayrım yapmasıdır. Ayrıcalıklı komutlar yalnız çekirdek modunda yürütülebilir. Kullanıcı modunda böyle bir komut denenirse donanım bir tuzak üretir ve kontrol işletim sistemine geçer.
Ayrıcalıklı komutların ortak yanı, yanlış kullanıldıklarında başka süreçlere ya da sistemin bütününe zarar verebilmeleridir. G/Ç aygıtlarına doğrudan erişmek, zamanlayıcıyı ayarlamak, kesmeleri kapatmak ve sayfa tablolarını değiştirmek bu gruptadır. Sistem saatini okumak gibi kimseye zarar vermeyen işler ise kullanıcı modunda serbestçe yapılabilir.
Kullanıcı modunda ayrıcalıklı bir komut denendiğinde donanım komutu yürütmez; bunun yerine bir tuzak (trap) üretir. İşlemci çekirdek moduna geçer ve işletim sisteminin önceden belirlediği bir işleyiciye atlar. İşletim sistemi de çoğu zaman hatalı süreci sonlandırır. Çekirdeğe girmenin yalnız üç meşru yolu vardır: sistem çağrısı, donanım kesmesi ve istisna (sıfıra bölme, geçersiz bellek erişimi gibi). Üçünde de işlemci, sürecin seçtiği bir adrese değil, çekirdeğin belirlediği sabit bir giriş noktasına atlar.
Gerçek işlemcilerde mod sayısı ikiden fazla olabilir. x86 mimarisi 0'dan 3'e kadar dört koruma halkası tanımlar, ama Linux ve Windows yalnız halka 0'ı (çekirdek) ve halka 3'ü (kullanıcı) kullanır. ARM mimarisinde EL0'dan EL3'e kadar dört ayrıcalık düzeyi vardır; sanallaştırma için ayrıca bir düzey ayrılmıştır.
Çift modun tek başına yeterli olduğunu düşünmek. Mod ayrımı, sürecin ayrıcalıklı komut çalıştırmasını engeller; ama iki sorunu çözmez. Süreç ayrıcalıklı hiçbir komut kullanmadan sonsuz döngüye girip işlemciyi tutabilir; bunun çaresi zamanlayıcıdır. Süreç başka bir sürecin belleğine sıradan bir yükleme komutuyla erişmeye çalışabilir; bunun çaresi de bellek yönetim birimidir (MMU).
2.3.2Zamanlayıcı
Bir süreç while (1) ; gibi bir döngüye girerse hiçbir ayrıcalıklı komut çalıştırmadan işlemciyi sonsuza dek elinde tutabilir. İşletim sistemi de çalışmak için işlemciye ihtiyaç duyduğundan bu durumu kendi başına fark edemez. Çözüm, belirli bir süre sonunda işlemciyi kesen bir donanım parçasıdır.
Zamanlayıcı (timer), belirli bir süre sonunda kesme üreten donanım sayacıdır. İşletim sistemi sayaca bir değer yükler; fiziksel saatin her darbesinde sayaç bir azalır; sayaç sıfıra indiğinde bir kesme üretilir ve kontrol işletim sistemine geçer.
Sayaca değer yüklemek ayrıcalıklı bir komuttur. Böylece kullanıcı programı zamanlayıcıyı durduramaz ya da süresini uzatamaz. Zamanlayıcı kesmesi geldiğinde işletim sistemi şunu sorar: o anda çalışan süreç kendisine ayrılan zaman dilimini doldurdu mu? Doldurduysa süreç işlemciden alınır ve işlemci başka bir sürece verilir. Bu düzene kesmeli çizelgeleme (preemptive scheduling) denir. Sürecin kendisi sonlandırılmaz; yalnız sırası başkasına geçer. Süreç ancak kendisine tanınan toplam işlemci süresi sınırını (Linux'ta RLIMIT_CPU) aşarsa sonlandırılır.
A ve B adlı iki süreç vardır. İşletim sistemi A'yı çalıştırmadan önce sayaca 3 yüklüyor. Saatin her darbesinde ne olur?
- Yükleme. İşletim sistemi çekirdek modunda sayaca 3 yazar, ardından kullanıcı moduna geçip A'yı başlatır.
- 1. darbe. Sayaç 2 olur. A çalışmayı sürdürür; olup bitenden haberi yoktur.
- 2. darbe. Sayaç 1 olur. A hâlâ işlemcidedir.
- 3. darbe. Sayaç 0'a iner ve zamanlayıcı kesmesi üretilir. İşlemci çekirdek moduna geçer ve kesme işleyicisi çalışır.
- Çizelgeleme. Çizelgeleyici A'nın diliminin dolduğunu görür, A'nın durumunu PCB'sine kaydeder ve B'yi seçer. Sayaca yeniden değer yükler ve B'yi kullanıcı modunda başlatır.
A, üç darbe süresince işlemciyi kullanmıştır. A sonsuz bir döngüde olsa bile bu süre sonunda işlemciyi bırakmak zorunda kalır.
Linux'ta zamanlayıcı kesmesi düzenli aralıklarla, saniyede 100 ile 1000 arasında üretilir; her kesmeye bir tik denir. Sık tik, daha ince zaman dilimleri ve daha duyarlı çizelgeleme sağlar, ama her tikin işlenmesi işlemciden zaman alır. Üstelik sistem boştayken bile işlemci her tikte uyanmak zorunda kalır. Bu yüzden güncel çekirdekler, boşta tik üretmeyen bir kip (tickless, NO_HZ) de sunar; özellikle dizüstü bilgisayarlarda bu kip enerji kazandırır.
2.4Sistem çağrıları
Çift modlu çalışma, kullanıcı programlarının donanıma doğrudan erişmesini yasaklar. Oysa programların dosya okuması, bellek istemesi, ekrana yazması ve yeni süreç oluşturması gerekir. Bu işleri yapabilen tek taraf çekirdektir. Programın çekirdekten hizmet istemesinin denetimli yolu sistem çağrısıdır.
2.4.1Sistem çağrısı nedir?
Sistem çağrısı (system call), programların dosya, bellek, süreç, zaman ve ağ gibi işletim sistemi hizmetlerine erişmek için kullandığı tanımlı arayüzdür. Çağrıyı uygulama başlatır; istenen işi çekirdek, çekirdek modunda yürütür.
Programcı sistem çağrılarını çoğu zaman doğrudan kullanmaz. Arada bir uygulama programlama arayüzü (application programming interface, API) bulunur. UNIX benzeri sistemlerde bu arayüz POSIX standardıdır ve C kütüphanesi (glibc ya da musl) içinde gerçeklenir. Windows'ta Windows API, Java'da ise Java API aynı görevi görür. API iki yarar sağlar. Birincisi taşınabilirliktir: POSIX'e uygun yazılmış bir program, sistem çağrılarının numaraları ve ayrıntıları farklı olan başka bir UNIX'te de derlenip çalışır. İkincisi kolaylıktır: API fonksiyonları, ham sistem çağrılarının zahmetli ayrıntılarını gizler.
Sistem çağrılarının kümesi sistemden sisteme değişir. x86-64 üzerindeki Linux'ta yaklaşık 370 sistem çağrısı vardır. Temel kavramlar ise ortaktır: her sistemde süreç oluşturan, dosya açan, okuyan ve yazan çağrılar bulunur. Bir programın hangi sistem çağrılarını yaptığını görmek için Linux'ta strace, macOS'ta dtruss, Windows'ta Process Monitor aracı kullanılır.
2.4.2API'den çekirdeğe
Bir C programının open("notlar.txt", O_RDONLY) çağrısını izleyelim. Şekil 2.13 çağrının yolculuğunu baştan sona gösterir.
- Sarmalayıcı. Program, C kütüphanesindeki
open()fonksiyonunu sıradan bir fonksiyon gibi çağırır. Bu fonksiyon bir sarmalayıcıdır (wrapper): çağrı numarasını ve parametreleri yazmaçlara koyar, ardından tuzak komutunu çalıştırır. İşlemci çekirdek moduna geçer. - Sistem çağrısı arayüzü. Çekirdekteki giriş kodu, çağrı numarasını sistem çağrısı tablosunda indis olarak kullanır. x86-64 Linux'ta
read0,write1,open2,close3 numaralıdır. - Gerçekleme. Tablodaki gösterici, çekirdekteki
openkodunu gösterir. Bu kod yol adını çözer, dosyayı bulur, izinleri denetler ve bir açık dosya kaydı oluşturur. - Dönüş. Sonuç kullanıcıya döner. 0, 1 ve 2 dolu olduğundan ilk boş tanımlayıcı 3'tür;
open3 döndürür ve işlemci yeniden kullanıcı moduna geçer.
open çağrısının kullanıcı modundan çekirdek moduna yolu. Sistem çağrısı arayüzü 2 numaralı girdiyi bulur, tablodaki gösterici çekirdekteki open() gerçeklemesine götürür ve sonuç (fd = 3) kullanıcı moduna döner.Bir ayrıntı, kütüphane ile çekirdek arasındaki ilişkiyi iyi gösterir. Güncel glibc, programın çağırdığı open()'u aslında openat sistem çağrısıyla (x86-64'te 257 numara) gerçekler. Program yine open yazar ve aynı sonucu alır; aradaki değişikliği API gizler. strace ile bakıldığında ise çekirdeğe giden çağrının adı openat olarak görünür.
2.4.3Sistem çağrısı tablosu
Sistem çağrısı tablosunun nasıl çalıştığını küçük bir C modeliyle izlemek mümkündür. Aşağıdaki kodda tablo yalnız üç girdiden oluşur. Gerçek Linux çekirdeğinde tablo birkaç yüz girdi içerir, ama mantık aynıdır.
typedef long (*cagri_t)(long, long, long);
cagri_t tablo[3] = {sys_read, sys_write,
sys_open};
long syscall_giris(long no, long a,
long b, long c) {
if (no < 0 || no >= 3)
return -ENOSYS; /* geçersiz numara */
return tablo[no](a, b, c);
}İlk satır, üç long parametre alıp long döndüren bir fonksiyonu gösteren bir gösterici türü tanımlar ve buna cagri_t adını verir. İkinci satırdaki tablo, bu türden üç göstericilik bir dizidir: 0. girdi sys_read'i, 1. girdi sys_write'ı, 2. girdi sys_open'ı gösterir. syscall_giris fonksiyonu çekirdeğin tek giriş noktasını temsil eder. Önce çağrı numarasının tablonun sınırları içinde olup olmadığını denetler. Numara geçersizse -ENOSYS döndürür; geçerliyse tablodaki göstericiyi kullanarak doğru fonksiyonu çağırır ve onun sonucunu döndürür.
Programın art arda üç çağrı yaptığını varsayalım: üç bayt yazan bir write, open("notlar.txt", 0) ve tabloda karşılığı olmayan syscall(99). Her çağrıda syscall_giris nasıl çalışır?
| Çağrı | no | Sınır denetimi | Çalışan kod | Sonuç |
|---|---|---|---|---|
write | 1 | 0 ≤ 1 < 3: geçerli | tablo[1] → sys_write | 3 (yazılan bayt sayısı) |
open | 2 | 0 ≤ 2 < 3: geçerli | tablo[2] → sys_open | 3 (yeni tanımlayıcı) |
syscall(99) | 99 | 99 ≥ 3: geçersiz | yok | −ENOSYS (−38); programa −1, errno = ENOSYS |
İlk iki çağrıda libc sarmalayıcısı numarayı rax yazmacına koyar ve syscall komutunu çalıştırır. Donanım çekirdek moduna geçip tek giriş noktasına atlar. Sınır denetimi geçer ve tablodaki işlev göstericisi doğru işleyiciyi çağırır. Üçüncü çağrıda numara tablonun dışındadır. Denetim olmasaydı tablo[99], dizinin sonundan çok ötedeki bir bellek hücresini işlev göstericisi gibi kullanırdı; çekirdek rastgele bir adrese atlardı. Denetim sayesinde çağrı −38 değeriyle reddedilir. libc bu negatif değeri errno = ENOSYS biçimine çevirir ve programa −1 döndürür.
no = 1: tablo[1] seçilmiş ve sys_write 3 döndürmüştür. İkinci karede no = 99: numara tablonun dışında kaldığı için çağrı −ENOSYS ile reddedilmiştir.Bu modelden çıkan ders geneldir: çekirdek, kullanıcıdan gelen hiçbir değere doğrulamadan güvenmez. Çağrı numarası, dosya tanımlayıcısı, tampon adresi ve uzunluğu, kullanıcı programının yazdığı değerlerdir ve hepsi denetlenir. read çağrısına verilen tampon adresinin gerçekten o sürecin belleğinde olup olmadığı da bu denetimlerden biridir.
2.4.4Örnek: bir dosyayı kopyalamak
Sıradan görünen bir program bile çok sayıda sistem çağrısı yapar. Bir dosyayı başka bir dosyaya kopyalayan ve dosya adlarını kullanıcıdan isteyen bir program düşünelim. Programın adımları ve her adımın kullandığı sistem çağrıları şöyledir:
| Adım | Sistem çağrıları |
|---|---|
| Girdi dosyasının adını iste ve oku | write, read |
| Çıktı dosyasının adını iste ve oku | write, read |
| Girdi dosyasını aç (yoksa hata ver ve çık) | open |
| Çıktı dosyasını oluştur (varsa hata ver ve çık) | open + O_CREAT |
| Döngü: girdiden oku, çıktıya yaz | read, write |
| Dosyaları kapat | close |
| Ekrana “tamamlandı” yaz | write |
| Normal biçimde sonlan | exit |
Kullanıcıdan bir dosya adı almak bile iki çağrı ister: ekrana bir istem yazmak için write, klavyeden okumak için read. Dosya açma başarısız olabilir: dosya yoktur ya da okuma izni yoktur. Bu durumda open −1 döndürür ve program bir hata iletisi yazıp çıkar. Çıktı dosyası O_WRONLY | O_CREAT | O_EXCL bayraklarıyla açılırsa, aynı adda bir dosya zaten varsa çağrı hata verir; böylece var olan bir dosyanın üzerine yanlışlıkla yazılmaz. Asıl iş döngüdedir: read 0 döndürene, yani dosya sonuna gelinene kadar okunan baytlar write ile yazılır. Bu kadar basit bir program bile onlarca sistem çağrısı yapar; Linux'ta strace -c cp a b komutu, cp programının yaptığı çağrıları türlerine göre sayar.
Döngünün kendisini ayrıntılı izlemek için aşağıdaki kod parçasını ele alalım:
char buf[4096];
int in = open("kaynak", O_RDONLY);
int out = creat("hedef", 0644);
ssize_t n; long toplam = 0;
while ((n = read(in, buf, 4096)) > 0) {
write(out, buf, n);
toplam += n;
}
close(in); close(out);
printf("%ld bayt\n", toplam);Program 4096 baytlık bir tampon ayırır. kaynak dosyasını salt okunur açar ve tanımlayıcısını in'e koyar. creat çağrısı hedef adlı boş bir dosya oluşturur ve izinlerini 0644 (sahibi okur ve yazar, diğerleri yalnız okur) olarak belirler. Döngü koşulu hem okumayı yapar hem de sonucu sınar: read en çok 4096 bayt okur ve okuduğu bayt sayısını n'ye yazar; n pozitif olduğu sürece döngü sürer. Döngünün gövdesi tampondaki n baytı hedefe yazar ve toplam'ı günceller. Sonda iki dosya kapatılır ve kopyalanan bayt sayısı yazdırılır.
kaynak 10 000 baytlık bir dosyadır ve sürecin yalnız 0, 1, 2 tanımlayıcıları açıktır. Kod parçası adım adım şöyle ilerler.
| Adım | Çağrı | n | Dosya konumu | toplam |
|---|---|---|---|---|
| 1 | open("kaynak") → in = 3 | — | 0 | 0 |
| 2 | creat("hedef") → out = 4 | — | 0 | 0 |
| 3 | read, write | 4096 | 4096 | 4096 |
| 4 | read, write | 4096 | 8192 | 8192 |
| 5 | read, write | 1808 | 10000 | 10000 |
| 6 | read | 0 | 10000 | 10000 |
| 7 | close(3), close(4) | 0 | — | 10000 |
| 8 | printf → write(1, …) | 0 | — | 10000 |
İlk open en küçük boş tanımlayıcıyı döndürür; 0, 1, 2 dolu olduğu için in = 3, ardından out = 4 olur. İlk read 4096 bayt okur ve çekirdek dosya konumunu 4096 ilerletir. İkinci tur da 4096 bayt okur; konum 8192'ye gelir. Dosyada artık yalnız 10 000 − 8192 = 1808 bayt kalmıştır. Üçüncü read istenenden azını, 1808 baytı döndürür; bu bir hata değildir. Konum dosya sonundadır ve dördüncü read 0 döndürür. Döngü koşulu yanlış olur ve döngü biter. Program 10000 bayt yazdırır.
Toplam sistem çağrısı sayısı 12'dir: 1 open, 1 creat, 4 read, 3 write, 2 close ve printf'in yaptığı 1 write.
read: kaynağın ilk 4096 baytı tampona alınmıştır. İkinci karede üçüncü read: dosyada kalan 1808 bayt okunmuştur.Döngü gövdesinde write(out, buf, 4096) yazmak. Son turda tamponda yalnız 1808 geçerli bayt vardır; geri kalan 2288 bayt önceki turdan kalan eski veridir. Sabit 4096 yazılırsa hedef dosya 12 288 bayt olur ve sonuna anlamsız veri eklenir. Doğrusu, read'in döndürdüğü n kadar bayt yazmaktır.
Tampon boyutu, yapılan sistem çağrısı sayısını doğrudan belirler. Her sistem çağrısı iki mod geçişi ve çekirdekteki giriş-çıkış işleri yüzünden sıradan bir fonksiyon çağrısından çok daha pahalıdır. Aynı 10 000 bayt 1 baytlık bir tamponla kopyalansaydı yalnız okuma ve yazma için yaklaşık 20 000 çağrı gerekirdi: 10 001 read ve 10 000 write. 4096 baytlık tamponla bu sayı 7'dir.
2.4.5read çağrısının on bir adımı
Tanenbaum'un ders kitabı, read(fd, buffer, nbytes) çağrısını on bir adıma ayırarak anlatır. Örnek, tarihsel 32 bitlik x86 kuralına dayanır: parametreler yığın üzerinden aktarılır. Günümüzün x86-64 sistemleri ilk altı parametreyi yazmaçlarla aktarır, ama adımların sırası ve anlamı değişmez.
- Adım 1–3. Kullanıcı programı parametreleri yığına iter: önce
nbytes, sonrabuffer'ın adresi, en sonfd. C derleyicileri parametreleri sondan başa doğru iter. - Adım 4. Program kütüphanedeki
read()fonksiyonunu sıradan bir fonksiyon gibi çağırır; dönüş adresi yığına girer. - Adım 5. Kütüphane, çağrı numarasını çekirdeğin beklediği yazmaca koyar (Linux x86-64'te
rax = 0). - Adım 6. Tuzak komutu (x86-64'te
syscall) çalışır. İşlemci çekirdek moduna geçer ve çekirdekteki sabit bir adrese atlar. - Adım 7. Çekirdek çağrı numarasına bakar ve sistem çağrısı tablosu üzerinden doğru işleyiciye dallanır.
- Adım 8. İşleyici,
fd'nin gösterdiği dosyadanbuffer'a en çoknbytesbayt kopyalar. Veri hazır değilse, örneğin klavyeden henüz bir satır gelmemişse, süreç bloke olur ve işlemci başka bir sürece verilir. - Adım 9. İş bitince kontrol kullanıcı moduna, tuzak komutundan sonraki komuta döner. Sonuç, yani okunan bayt sayısı, bir yazmaçtadır.
- Adım 10. Kütüphanedeki
read()fonksiyonu, kendisini çağıran kullanıcı programına döner. - Adım 11. Program, yığına ittiği parametreleri temizler (yığın göstericisini artırır) ve sonraki komutla devam eder.
read çağrısının 7. adımı. Parametreler (nbytes, &buffer, fd) ve dönüş adresi kullanıcı yığınındadır. Kütüphane tuzak komutuyla çekirdeğe geçmiş, çekirdekteki dağıtıcı tablo[no] üzerinden sys_read işleyicisine dallanmıştır. Kesikli çizgi kullanıcı alanını çekirdek alanından ayırır.Adımların ilk dördü ve son ikisi tamamen kullanıcı modunda geçer; bunlar sıradan fonksiyon çağrısı adımlarıdır. Çekirdeğe giriş 6. adımda, çıkış 9. adımda olur. 8. adım, sistem çağrısının sıradan bir fonksiyondan en önemli farkını gösterir: çağrı bir olayı bekleyebilir. Beklenen olay gerçekleşene kadar süreç işlemciyi kullanmaz; işletim sistemi bu süreyi başka süreçler için değerlendirir.
2.4.6Parametre aktarımı
Sistem çağrısına parametre aktarmanın üç genel yolu vardır:
- Yazmaçlar. En hızlı yoldur. Linux, bir sistem çağrısına en çok altı parametreyi yazmaçlarla aktarır.
- Bellekte blok ya da tablo. Parametreler bellekte bir yapıya yazılır; yapının adresi bir yazmaçla aktarılır. Parametre sayısı çok olduğunda bu yol kullanılır. Linux'taki
clone3(struct clone_args *)çağrısı buna bir örnektir. - Yığın. Program parametreleri yığına iter, işletim sistemi oradan çeker. Tarihsel 32 bitlik sistemlerde yaygındı.
x86-64 Linux'ta yazmaçların görevleri sabittir:
| Yazmaç | Görev |
|---|---|
rax | çağrı numarası; dönüşte sonuç |
rdi, rsi, rdx | 1., 2. ve 3. parametre |
r10, r8, r9 | 4., 5. ve 6. parametre (C fonksiyon çağrısı kuralındaki rcx yerine r10) |
syscall | çekirdeğe geçiş komutu; rcx ve r11 yazmaçlarının değeri bozulur |
| ARM64 | x8 çağrı numarası, x0–x5 parametreler; geçiş komutu svc #0 |
Dördüncü parametrenin neden rcx yerine r10'da olduğu sorusunun cevabı tablonun son satırındadır. syscall komutu, dönüş adresini rcx'e, bayrak yazmacını da r11'e kaydeder. rcx'e konan bir parametre bu yüzden çekirdeğe ulaşmadan silinirdi.
Hata bildirimi de ortak bir kurala bağlıdır. Çekirdek bir hata olduğunda −1 ile −4095 arasında bir değer döndürür; bu değer, hata kodunun eksi işaretlisidir (−errno). libc sarmalayıcısı bu değeri görünce hata kodunu errno değişkenine yazar ve çağırana −1 verir. Program, bir sistem çağrısının −1 döndürüp döndürmediğine bakarak hatayı fark eder ve nedenini errno'dan öğrenir.
2.4.7write ve read ile kod izleme
Ekrana yazı yazan en küçük programlardan biri, printf yerine doğrudan write kullanan şu programdır:
#include <unistd.h> // write()
#include <string.h> // strlen()
int main(void) {
const char *msg = "Merhaba, Dünya!\n";
size_t len = strlen(msg); // 17 bayt
ssize_t n = write(1, msg, len); // 1: stdout
if (n == -1)
return 1; // hata
return 0;
}msg göstericisi, salt okunur veri kesimindeki dizgeyi gösterir. strlen(msg) dizgenin bayt uzunluğunu verir. write(1, msg, len), 1 numaralı tanımlayıcıya, yani standart çıktıya len bayt yazar. Dönen değer −1 ise bir hata olmuştur ve program 1 ile çıkar; değilse 0 ile çıkar.
Program satır satır şöyle çalışır. Dizgedeki Türkçe karakter, bayt sayısını etkiler.
strlen. UTF-8 kodlamasında “Merhaba, ” 9 bayttır (virgülden sonraki boşluk dahil). “Dünya” 6 bayttır, çünkü ü harfi 2 bayt tutar. “!” ve satır sonu karakteri “\n” 2 bayttır. Toplamlen = 17'dir; karakter sayısı ise 16'dır.- Sarmalayıcı. libc yazmaçları doldurur:
rax = 1(write),rdi = 1,rsi = msg,rdx = 17. Ardındansyscallkomutunu çalıştırır. - Çekirdek. Çekirdek modunda
sys_write, 17 baytı 1 numaralı tanımlayıcının bağlı olduğu terminale aktarır. EkrandaMerhaba, Dünya!görünür. - Dönüş. Kullanıcı moduna dönülür ve
n = 17olur. Bu değer yazılan bayt sayısıdır; bazı durumlarda istenenden küçük olabilir (kısmi yazma). - Hata denetimi.
n−1 değildir, hata yoktur. Hata olsaydıwrite−1 döndürür veerrnoayarlanırdı. - Bitiş.
main0 döndürür; libcexit_group(0)çağrısıyla süreci sonlandırır.
Programcı uzunluğu elle hesaplayıp sabit 15 yazsaydı, son iki bayt (“!” ve “\n”) gönderilmez ve çıktı “Merhaba, Dünya” olarak kesilirdi. Uzunluğu strlen ile hesaplamak bu hatayı önler.
Okuma tarafındaki eş program, klavyeden bir satır okuyup aynen ekrana yazar:
#include <unistd.h>
#include <stdio.h>
int main(void) {
char buf[100];
ssize_t n = read(0, buf, sizeof(buf));
if (n == -1) {
perror("okuma hatası");
return 1;
}
write(1, buf, n); // tam n bayt
return 0;
}Kullanıcı program çalışırken selam yazıp Enter tuşuna basıyor. Program nasıl ilerler?
- Tampon.
bufyığında ayrılan 100 baytlık bir dizidir; içeriği ilklenmemiştir, yani rastgele değerler taşır. - Bloke olma.
read(0, buf, 100)standart girdiden, yani klavyeden okumak ister. Henüz bir satır yazılmamıştır. Süreç bloke olur ve çizelgeleyici başka bir süreci çalıştırır. - Kullanıcı yazar. Kullanıcı
selamyazıp Enter'a basar. Terminal sürücüsü karakterleri ekrana yansıtır ve satırı kendi tamponunda biriktirir. - Uyanma. Satır tamamlanınca 6 bayt (
selamve “\n”)buf'a kopyalanır, süreç uyanır veread6 döndürür. Tamponun sonuna “\0” eklenmez. - Hata denetimi.
n = 6'dır, hata yoktur;perroratlanır. - Yazma.
write(1, buf, 6)tam 6 baytı ekrana yazar. Ekrandaselamiki kez görünür: birincisi terminal sürücüsünün yansıtması, ikincisi programın çıktısıdır. - Bitiş. Program 0 ile sonlanır.
Okunan tamponu printf("%s", buf) ile yazdırmak. read, tamponun sonuna dizge sonu karakteri (“\0”) koymaz; buf bir C dizgesi değildir. printf ilk sıfır baytını bulana kadar okumayı sürdürür ve ilklenmemiş bellekteki rastgele baytları da ekrana döker. Doğrusu, read'in döndürdüğü n değeriyle tam n bayt yazmaktır.
2.4.8Standart C kütüphanesi
C programcısının her gün kullandığı printf, bir sistem çağrısı değildir. Kullanıcı modunda çalışan bir kütüphane fonksiyonudur ve gerektiğinde bir sistem çağrısı yapar. Şekil 2.18 printf("Merhaba\n") çağrısının yolunu gösterir.
printf çağrısının write sistem çağrısına dönüşmesi. Program printf'i çağırır; standart C kütüphanesi metni biçimler ve stdout tamponuna koyar. Satır sonu gelince kütüphane write(1, tampon, 8) sistem çağrısını yapar; çekirdek baytları terminale aktarır ve 8 döndürür.- Program
printf'i çağırır. Bu, kullanıcı modunda çalışan sıradan bir fonksiyon çağrısıdır. - libc biçim dizgesini işler ve sonucu
stdout'un tamponuna koyar. Çıktı bir terminale gidiyorsa tampon satır tamponludur: satır sonu gelince boşaltılır. Çıktı bir dosyaya ya da boruya yönlendirildiyse tampon tam tamponludur: tampon dolunca ya da program bitince boşaltılır. - Metin “\n” ile bittiği için libc
write(1, tampon, 8)çağrısını yapar. “Merhaba” 7 bayt, satır sonu 1 bayttır. write8 döndürür,printfda çağırana döner.strace ./merhabakomutu yalnızwrite'ı gösterir;printfbir sistem çağrısı olmadığı için listede yer almaz.
Standart C kütüphanesinin işlevleri başlık dosyalarına göre gruplanır: stdio.h (printf, fopen), stdlib.h (malloc, exit), string.h (strlen, memcpy), math.h ve time.h. POSIX ayrıca unistd.h başlığında fork, read ve write gibi sistem çağrısı sarmalayıcılarını tanımlar. Kütüphanenin yaygın gerçeklemeleri Linux'ta glibc ve musl, Windows'ta UCRT'dir. Kütüphane işlevleri iki gruba ayrılır. strlen gibi işlevler tamamen kütüphanenin içinde çalışır ve çekirdeğe hiç girmez. write gibileri ise bir sistem çağrısını sarmalar.
2.4.9Sistem çağrısı türleri
Sistem çağrıları yaptıkları işe göre altı grupta toplanır. Tablo 2.3 her grubun görevlerini ve POSIX'teki örneklerini listeler.
| Tür | Görevler | POSIX örnekleri |
|---|---|---|
| Süreç denetimi | oluştur, sonlandır, yükle, bekle, sinyal gönder | fork, execve, exit, waitpid, kill |
| Dosya yönetimi | oluştur, sil, aç, kapat, oku, yaz, öznitelik al | open, read, write, close, stat |
| Aygıt yönetimi | aygıt iste ve bırak, oku, yaz, denetle | ioctl, read, write, mmap |
| Bilgi bakımı | saat, tarih, sistem ve süreç bilgisi | getpid, uname, clock_gettime |
| İletişim | bağlantı kur, ileti gönder ve al, paylaşımlı bellek | pipe, socket, sendmsg, shm_open |
| Koruma | izinleri al ve ayarla, kullanıcı kimliği | chmod, chown, umask, setuid |
Aynı gruplar Windows'ta da vardır; yalnız adlar değişir. Süreç oluşturmak için CreateProcess, dosya okumak için ReadFile, aygıt denetimi için DeviceIoControl, saati okumak için GetSystemTime, boru oluşturmak için CreatePipe ve dosya izinleri için SetFileSecurity kullanılır. Bazı çağrıların birden çok grupta görünmesi doğaldır: read ve write hem dosyalar hem de aygıtlar için kullanılır, çünkü UNIX'te aygıtlar da dosya gibi görünür.
2.5POSIX ve Win32 çağrıları
POSIX standardı yüzlerce çağrı tanımlar, ama bir UNIX sisteminin temel davranışını anlamak için birkaç düzine çağrı yeterlidir. Bu kesim, Tanenbaum'un seçtiği temel çağrıları üç grupta ele alır: süreç yönetimi, dosya yönetimi ve dizin yönetimi. Kesimin sonunda aynı çağrıların Windows'taki karşılıkları verilir. Tablolarda ortak bir gösterim kullanılır: pid süreç kimliğini, fd dosya tanımlayıcısını, n bayt sayısını, s ise çağrının dönüş kodunu gösterir. Hata durumunda çağrıların hepsi −1 döndürür ve errno değişkenini ayarlar.
2.5.1Süreç yönetimi
| Çağrı | Açıklama |
|---|---|
pid = fork() | Ebeveynin kopyası olan bir çocuk süreç oluşturur. |
pid = waitpid(pid, &statloc, options) | Bir çocuğun sonlanmasını bekler; çıkış durumunu statloc'a yazar. |
s = execve(name, argv, envp) | Sürecin bellek görüntüsünü yeni bir programla değiştirir; başarılıysa geri dönmez. |
exit(status) | Süreci sonlandırır ve çıkış durumunu ebeveyne iletir. |
fork, UNIX'te yeni süreç oluşturmanın tek yoludur. Çağrı, ebeveynin bellek görüntüsünün, açık dosyalarının ve yazmaçlarının bir kopyasıyla yeni bir süreç oluşturur. Çağrıdan sonra iki süreç vardır ve ikisi de fork'tan dönen aynı noktadan devam eder. İki sürecin hangisinin hangisi olduğunu ayırt etmenin tek yolu dönüş değeridir: ebeveynde fork çocuğun PID'sini, çocukta 0 döndürür. Çağrı başarısız olursa ebeveyne −1 döner ve çocuk oluşmaz. fork'un “bir kez çağrılıp iki kez döndüğü” bu yüzden söylenir.
Çocuğun çoğu zaman ebeveynden farklı bir iş yapması istenir. Bunun için çocuk execve çağırır. execve, sürecin kodunu, verisini ve yığınını diskteki yeni programın görüntüsüyle değiştirir. Süreç aynı süreçtir, PID'si değişmez; yalnız artık başka bir program çalıştırmaktadır. Başarılı bir execve geri dönmez, çünkü onu çağıran kod artık bellekte yoktur. execve'den sonraki satır ancak çağrı başarısız olursa çalışır.
fork() ile bir kopya oluşturur ve waitpid ile bloke olur. Çocuk execve() ile yeni programa geçer (PID aynı kalır), işini bitirince exit(status) çağırır. Bu anda ebeveynin waitpid çağrısı döner ve çıkış durumu alınır.Windows bu işi iki çağrıya bölmez: tek bir CreateProcess çağrısı hem yeni süreci oluşturur hem de ona yeni programı yükler. UNIX'in iki adımlı yaklaşımının yararı, fork ile execve arasındaki kısa aralıktır. Çocuk bu aralıkta kendi açık dosyalarını değiştirebilir; örneğin standart çıktısını bir dosyaya ya da boruya yönlendirebilir. Kabuğun > yönlendirmesi ve boru hatları tam bu aralıkta kurulur.
2.5.2Basit bir kabuk
Süreç yönetimi çağrılarının birlikte nasıl çalıştığını görmenin en iyi yolu, bir kabuk (shell) yazmaktır. Kabuk, kullanıcıdan komut alıp çalıştıran sıradan bir kullanıcı programıdır. Tanenbaum'un verdiği iskelet, ayrıntıları gizleyerek yalnız asıl yapıyı gösterir:
#define TRUE 1
while (TRUE) { /* sonsuza dek */
type_prompt(); /* istemi yaz */
read_command(command, parameters);
if (fork() != 0) { /* ebeveyn */
waitpid(-1, &status, 0);
} else { /* çocuk süreç */
execve(command, parameters, 0);
}
}Kabuk sonsuz bir döngüde çalışır. Her turda istemi yazar (type_prompt), kullanıcının yazdığı satırı okuyup komut adına ve parametrelere ayırır (read_command). Sonra fork çağırır. Dönüş değeri 0'dan farklıysa bu kod ebeveynde, yani kabuğun kendisinde çalışıyordur; kabuk waitpid ile çocuğun bitmesini bekler. Dönüş değeri 0 ise kod çocukta çalışıyordur; çocuk execve ile kendini istenen programla değiştirir.
PID'si 812 olan kabukta kullanıcı ls yazıyor. Çalışma dizininde notlar.txt ve odev.c dosyaları var. Kabuğun bir turu aşağıdaki tabloda görülür.
| Adım | Satır | Olay | Süreçler | Ekran |
|---|---|---|---|---|
| 1 | 4 | İstem yazılır, kullanıcı beklenir. | kabuk (812) | $ |
| 2 | 5 | Satır ayrıştırılır: command = "/bin/ls", parameters = ["ls", NULL]. | kabuk (812) | $ ls |
| 3 | 7 | fork: ebeveynde 1045, çocukta 0 döner. | kabuk (812), çocuk (1045) | $ ls |
| 4 | 8 | Ebeveyn waitpid(-1, …) ile bloke olur. | kabuk (bekliyor), çocuk (1045) | $ ls |
| 5 | 10 | Çocuk execve ile ls olur; dizini listeler. | kabuk (bekliyor), ls (1045) | notlar.txt odev.c |
| 6 | 8 | ls exit(0) ile biter; waitpid döner, status = 0. | kabuk (812) | |
| 7 | 4 | Döngü başa döner, yeni istem yazılır. | kabuk (812) | $ |
3. adımda fork'tan sonra aynı kod iki süreçte birden çalışır. Kabukta fork() != 0 koşulu doğrudur (1045 ≠ 0) ve kabuk 8. satıra gider. Çocukta koşul yanlıştır (0) ve çocuk else dalındaki 10. satıra gider. 5. adımda çocuğun PID'si 1045 olarak kalır; değişen yalnız çalıştırdığı programdır. waitpid'in ilk parametresi −1 olduğu için kabuk, hangi çocuğu olursa olsun ilk biten çocuğu bekler.
Gerçek kabuklar bu iskeletin çok ötesindedir. Komut adını PATH ortam değişkenindeki dizinlerde arar, & ile arka planda çalışan işleri yönetir, yönlendirmeleri ve boru hatlarını kurar. Ama hepsinin kalbinde aynı fork–execve–waitpid üçlüsü vardır.
execve başarısız olduğunda çocuğun sonlandırılmasını unutmak. Kullanıcı var olmayan bir komut yazarsa execve −1 döndürür ve çocuk, kabuk koduyla çalışmaya devam eder. Çocukta execve'nin ardından bir exit yoksa çocuk döngünün başına döner ve kendisi de bir kabuk gibi davranmaya başlar. Kullanıcı artık iki kabukla karşı karşıyadır.
2.5.3Dosya yönetimi
| Çağrı | Açıklama |
|---|---|
fd = open(file, how, …) | Dosyayı okuma, yazma ya da ikisi için açar; gerekirse oluşturur. |
s = close(fd) | Açık dosyayı kapatır. |
n = read(fd, buffer, nbytes) | Dosyadan tampona en çok nbytes bayt okur. |
n = write(fd, buffer, nbytes) | Tampondan dosyaya veri yazar. |
position = lseek(fd, offset, whence) | Dosya konum göstericisini taşır (SEEK_SET, SEEK_CUR, SEEK_END). |
s = stat(name, &buf) | Dosyanın durum bilgisini (boyut, izinler, zamanlar) alır. |
Çekirdek her açık dosya için bir konum göstericisi tutar. Bu gösterici, bir sonraki read ya da write çağrısının dosyanın hangi baytından başlayacağını belirler. Her okuma ve yazma, göstericiyi işlenen bayt sayısı kadar ilerletir. Kopyalama örneğinde dosyanın baştan sona sırayla okunabilmesi bu sayededir: program hiçbir yerde konum belirtmez, çekirdek konumu kendisi izler.
read ya da write ile sağa ilerler. lseek göstericiyi dosyanın istenen bir noktasına taşır.Dosyanın belirli bir yerine gitmek için lseek kullanılır. whence parametresi, offset'in neye göre ölçüleceğini belirler: SEEK_SET dosyanın başına, SEEK_CUR göstericinin o anki yerine, SEEK_END dosyanın sonuna göre. Çağrı, göstericinin yeni konumunu dosyanın başından itibaren bayt olarak döndürür. Örneğin lseek(fd, 0, SEEK_END) göstericiyi dosya sonuna taşır ve dönüş değeri dosyanın boyutuna eşit olur.
1000 baytlık bir dosya fd = open(…, O_RDONLY) ile açılıyor. Ardından read(fd, buf, 300), lseek(fd, 100, SEEK_CUR), read(fd, buf, 300), lseek(fd, -50, SEEK_END) ve read(fd, buf, 300) çağrıları yapılıyor. Her çağrı ne döndürür?
Çözüm. Gösterici açılışta 0'dır. İlk read 300 bayt okur ve 300 döndürür; gösterici 300'e gelir. lseek(fd, 100, SEEK_CUR) göstericiyi 100 ileri taşır ve 400 döndürür. İkinci read 400–699. baytları okur, 300 döndürür; gösterici 700 olur. lseek(fd, -50, SEEK_END) göstericiyi sondan 50 bayt geriye, 950'ye taşır ve 950 döndürür. Son read 300 bayt ister, ama dosyada yalnız 50 bayt kalmıştır; 50 döndürür.
stat, dosyanın içeriğine dokunmadan onun hakkındaki bilgileri bir yapıya doldurur: boyutu, sahibi, izin bitleri, son erişim ve değişiklik zamanları gibi. Güncel Linux bu temel çağrıların yanında openat, pread / pwrite (konum göstericisini değiştirmeden belirli bir konumdan okuyup yazan çağrılar) ve statx gibi genişletilmiş sürümler de sunar.
2.5.4Dizin yönetimi ve diğer çağrılar
| Çağrı | Açıklama |
|---|---|
s = mkdir(name, mode) | Yeni bir dizin oluşturur. |
s = rmdir(name) | Boş bir dizini siler. |
s = link(name1, name2) | name1'in gösterdiği dosyaya name2 adlı yeni bir dizin girdisi oluşturur. |
s = unlink(name) | Bir dizin girdisini siler. |
s = mount(special, name, flag) | Bir dosya sistemini bağlar (Linux'taki biçimi: mount(kaynak, hedef, tür, bayrak, veri)). |
s = umount(special) | Bir dosya sisteminin bağlantısını kaldırır. |
s = chdir(dirname) | Çalışma dizinini değiştirir. |
s = chmod(name, mode) | Dosyanın koruma (izin) bitlerini değiştirir. |
s = kill(pid, signal) | Bir sürece sinyal gönderir; adına rağmen yalnız süreç öldürmek için kullanılmaz. |
seconds = time(&seconds) | 1 Ocak 1970 00.00 UTC'den (Unix epoch) bu yana geçen saniye sayısını verir. |
Tablodaki son çağrı, yazılım dünyasının bilinen bir sorununa kaynaklık eder. Eski sistemlerde zaman değeri 32 bitlik işaretli bir tamsayıda (time_t) tutuluyordu. Böyle bir tamsayının alabileceği en büyük değer 231 − 1 = 2 147 483 647'dir. Bu kadar saniye, 1970'in başından itibaren sayıldığında 19 Ocak 2038 tarihine denk gelir. O andan sonra sayaç taşar ve tarih 1901'e geri döner. Bu sorun “2038 sorunu” adıyla anılır. Güncel sistemler zamanı 64 bitlik bir time_t ile tutarak sorunu ortadan kaldırır.
chdir çağrısının bir süreç özelliğini değiştirdiğine dikkat etmek gerekir. Çalışma dizini sürece aittir; bir çocuk süreç kendi çalışma dizinini değiştirse bile ebeveynin çalışma dizini aynı kalır. Kabuktaki cd komutunun ayrı bir program olarak değil, kabuğun kendi içinde yerleşik bir komut olarak gerçeklenmesi bu yüzdendir.
2.5.5link ve unlink
UNIX'te bir dosyanın adı ile dosyanın kendisi ayrı şeylerdir. Dosyanın asıl bilgileri (sahibi, izinleri, boyutu, verinin diskteki blokları) i-node adı verilen bir yapıda tutulur. Dizin ise yalnız (i-node numarası, ad) çiftlerinden oluşan bir tablodur. Bir ad, bir i-node'u gösteren bir dizin girdisinden ibarettir.
Bu ayrımın doğal bir sonucu vardır: aynı i-node'u gösteren birden çok dizin girdisi olabilir. Böylece bir dosyanın birden çok adı olur. Bu adların her birine katı bağ (hard link) denir. i-node, kendisini gösteren ad sayısını bağ sayısı adlı bir sayaçta tutar.
/usr/jim dizininde memo adlı bir dosya vardır ve 70 numaralı i-node'u gösterir. Önce link("/usr/jim/memo", "/usr/ast/note"), sonra unlink("/usr/jim/memo") çağrılıyor.
- Başlangıç.
/usr/jimdizininde (31, bin), (70, memo), (59, f.c.), (38, prog1) girdileri vardır. i-node 70'in bağ sayısı 1'dir. link./usr/astdizinine (70, note) girdisi eklenir. Veri kopyalanmaz; yeni girdi aynı i-node'u gösterir. Bağ sayısı 2 olur. Artık/usr/jim/memoile/usr/ast/noteaynı dosyadır: biri üzerinden yapılan değişiklik öteki üzerinden de görülür.unlink./usr/jim'deki (70, memo) girdisi silinir. Dosyanın kendisi silinmez,noteadıyla yaşamaya devam eder. Bağ sayısı 1'e iner.
Bağ sayısı 0'a indiğinde ve dosyayı açık tutan bir süreç kalmadığında çekirdek i-node'u ve dosyanın veri bloklarını serbest bırakır. UNIX'te “dosya silmek” diye ayrı bir çağrının olmaması bundandır; rm komutu aslında unlink çağırır.
link çağrısı. Üstte başlangıç: /usr/jim/memo 70 numaralı i-node'u gösterir, bağ sayısı 1'dir. Altta link'ten sonra /usr/ast'ta da 70'i gösteren note girdisi vardır; bağ sayısı 2'dir.Dosyanın açık olması, bağ sayısından ayrı bir sayaçla izlenir. Bir süreç dosyayı open ile açtığında çekirdek yol adını çözer ve i-node'a ulaşır. Bundan sonra süreç dosyaya adı üzerinden değil, tanımlayıcı üzerinden doğrudan i-node'a giderek erişir. Ad silinse bile açık tanımlayıcı çalışmaya devam eder. Aşağıdaki kod bu durumu izler:
int fd = open("/usr/jim/memo", O_RDONLY);
link("/usr/jim/memo", "/usr/ast/note");
unlink("/usr/jim/memo");
unlink("/usr/ast/note");
n = read(fd, buf, 100); /* hâlâ okunur */
close(fd);memo 85 baytlık bir dosyadır ve sürecin yalnız 0, 1, 2 tanımlayıcıları açıktır. Tablo, her satırdan sonra i-node 70'in bağ sayısını ve açık sayacını verir.
| Satır | Çağrı | Bağ sayısı | Açık | Not |
|---|---|---|---|---|
| — | başlangıç | 1 | 0 | yalnız memo adı var |
| 1 | open | 1 | 1 | fd = 3 |
| 2 | link | 2 | 1 | note eklendi |
| 3 | unlink(memo) | 1 | 1 | yalnız note kaldı |
| 4 | unlink(note) | 0 | 1 | hiçbir yol yok, ama dosya açık |
| 5 | read | 0 | 1 | n = 85 |
| 6 | close | 0 | 0 | i-node ve bloklar serbest |
4. satırdan sonra dosyanın dizin ağacında hiçbir adı yoktur. Yine de 3 numaralı tanımlayıcı onu açık tuttuğu için çekirdek i-node'u ve blokları silmez. 5. satırdaki read, tanımlayıcı üzerinden doğrudan i-node'a gider; ada ihtiyacı yoktur. 100 bayt ister ve dosyada 85 bayt olduğu için 85 döndürür. Son tanımlayıcı 6. satırda kapanınca hem bağ sayısı hem açık sayacı 0 olur ve çekirdek ancak bu anda i-node'u ve veri bloklarını serbest bırakır.
link'ten sonra: /usr/jim/memo ve /usr/ast/note aynı i-node'u gösterir, bağ sayısı 2'dir. İkinci karede iki unlink'ten sonra: dizinlerde dosyanın adı kalmamıştır, ama tanımlayıcı açık olduğu için i-node yaşar.Programlar bu davranıştan bilerek yararlanır. Geçici bir dosya oluşturan program, dosyayı açtıktan hemen sonra unlink ile adını siler. Dosya, program çalıştığı sürece tanımlayıcı üzerinden kullanılabilir ama dizin ağacında görünmez. Program ister normal biçimde bitsin ister çöksün, son tanımlayıcı kapanınca çekirdek dosyayı kendiliğinden temizler.
2.5.6Win32 API karşılıkları
Windows'un programlama arayüzü Win32 API'dir. Bu arayüz binlerce fonksiyon içerir ve POSIX'teki temel çağrıların çoğunun bir karşılığı vardır. Tablo 2.7 Tanenbaum'un listesini güncel karşılıklarıyla verir.
| UNIX | Win32 | UNIX | Win32 |
|---|---|---|---|
fork | CreateProcess | mkdir | CreateDirectory |
waitpid | WaitForSingleObject | rmdir | RemoveDirectory |
execve | (yok: CreateProcess) | link | CreateHardLink |
exit | ExitProcess | unlink | DeleteFile |
open | CreateFile | mount | SetVolumeMountPoint |
close | CloseHandle | umount | DeleteVolumeMountPoint |
read | ReadFile | chdir | SetCurrentDirectory |
write | WriteFile | chmod | (yok: ACL, SetFileSecurity) |
lseek | SetFilePointerEx | kill | (yok: TerminateProcess) |
stat | GetFileAttributesEx | time | GetSystemTime |
Tablodaki “yok” işaretleri tasarım farklarından kaynaklanır. execve'nin karşılığı yoktur, çünkü Windows süreç oluşturma ile program yüklemeyi tek CreateProcess çağrısında birleştirir. chmod'un karşılığı yoktur, çünkü Windows dosya izinlerini UNIX'in dokuz izin biti yerine erişim denetim listeleriyle (access control list, ACL) yönetir. kill'in karşılığı yoktur, çünkü Windows'ta UNIX'teki gibi bir sinyal mekanizması bulunmaz; bir süreci sonlandırmak için TerminateProcess kullanılır. Tanenbaum'un kitabının eski baskılarında karşılığı olmayan link ve mount ise bugün CreateHardLink ve SetVolumeMountPoint ile mümkündür.
Win32 fonksiyonları çekirdeğe doğrudan geçmez. Asıl çekirdek geçişini, ntdll.dll kütüphanesindeki Nt… ile başlayan fonksiyonlar yapar; örneğin CreateFile sonunda NtCreateFile'ı çağırır. Bu fonksiyonların çekirdekteki çağrı numaraları Windows sürümünden sürümüne değişir ve belgelenmez. Programlar bu yüzden her zaman API'yi kullanır; numaralara doğrudan dayanan bir program bir sonraki Windows güncellemesinde çalışmaz hâle gelebilir.
2.6Çekirdek yapıları
Sistem çağrıları, işletim sisteminin dışarıya bakan yüzüdür. Kesimin konusu ise içeridir: çağrıyı karşılayan bileşenler nasıl düzenlenir? Bu soruya verilen cevaplar onlarca yıl içinde birkaç ana yapı etrafında toplanmıştır. Her yapı, 2.1.1'deki ödünleşim üçgeninde farklı bir noktayı seçer.
2.6.1Genel bakış
Yapıları sıralamanın kullanışlı bir yolu, çekirdek modunda ne kadar iş yapıldığına bakmaktır. Şekil 2.23 yapıları bu ölçüte göre bir doğru üzerine dizer.
İki kavram bu doğruya tam oturmaz. Katmanlı yapı bir yerleşim değil, bir düzenleme ilkesidir: bileşenler hiyerarşik katmanlara ayrılır. THE sistemi ve MULTICS'in koruma halkaları bu ilkenin örnekleridir ve ilke, doğrunun herhangi bir noktasındaki bir sisteme uygulanabilir. Sanal makineler ise ayrı bir eksendir: donanımın kendisi sanallaştırılır ve tek bir makinede birden çok işletim sistemi çalışır.
2.6.2Basit yapı: MS-DOS
Bazı işletim sistemlerinin belirgin bir yapısı yoktur. Bu sistemler küçük, basit ve sınırlı donanım için yazılmış, zamanla beklenenin ötesinde büyümüştür. MS-DOS bunun en bilinen örneğidir.
MS-DOS'ta arayüzler ve işlev düzeyleri iyi ayrılmamıştır. Bir uygulama, MS-DOS'un sunduğu hizmetleri kullanmak yerine BIOS rutinlerini doğrudan çağırabilir, hatta ekran belleğine ya da aygıt portlarına doğrudan yazabilir. Bunun temel nedeni donanımdır: MS-DOS'un yazıldığı Intel 8088 işlemcisinde çift mod yoktu. Donanım koruması olmadığı için tek bir hatalı program bütün sistemi çökertebiliyordu. Modüller arasında net sınırlar ve veri gizleme (data hiding) da yoktu: her bileşen ötekinin verisine dokunabiliyordu. Silberschatz bu tür sistemleri “basit yapı” başlığı altında, monolitik çekirdeklerden ayrı ele alır.
2.6.3Monolitik sistem
En yaygın yapı, bütün işletim sisteminin çekirdek modunda çalışan tek bir program olarak bağlandığı yapıdır.
Monolitik çekirdekte (monolithic kernel) bütün işletim sistemi hizmetleri tek bir adres alanında, çekirdek modunda çalışır. Çekirdekteki her prosedür ötekini doğrudan çağırabilir.
“Monolitik” sözcüğü düzensizlik anlamına gelmez. Tanenbaum, monolitik bir sistemin bile temel bir düzeni olduğunu üç katmanla anlatır:
- Ana program. Sistem çağrısının numarasına bakar ve istenen hizmet prosedürünü başlatır.
- Hizmet prosedürleri. Her biri bir sistem çağrısının işini yapar.
- Yardımcı prosedürler. Birden çok hizmetin ortak ihtiyacı olan işleri görür; örneğin kullanıcı programının belleğinden veri almak.
Geleneksel UNIX ve Linux monolitik çekirdeklerdir. Şekil 2.26 geleneksel UNIX'in katmanlarını gösterir. Kullanıcılar ve sistem programları (kabuklar, komutlar, derleyiciler, kütüphaneler) sistem çağrısı arayüzünün üstündedir. Bu arayüzün altındaki çekirdek; sinyalleri, terminal yönetimini, dosya sistemini, takas alanını, disk sürücülerini, işlemci çizelgelemesini ve sanal belleği tek bir bütün olarak barındırır. Çekirdeğin altında, çekirdek ile donanım arasındaki arayüz ve aygıt denetleyicileri bulunur.
Monolitik yapının güçlü yanları açıktır. Bileşenler birbirini doğrudan fonksiyon çağrısıyla kullanır; arada ileti hazırlama, kopyalama ya da mod geçişi yoktur. Bu yüzden monolitik çekirdekler hızlıdır. Tasarımları ve gerçeklemeleri de görece basittir. Zayıf yanları ise aynı yakınlıktan doğar. Bütün bileşenler aynı adres alanında olduğu için tek bir sürücüdeki hatalı bir gösterici, çekirdeğin herhangi bir yapısını bozabilir ve bütün sistemi çökertebilir. Kod tabanı da devasadır: 40 milyon satırı aşan Linux'ta bir hatayı bulmak ve bakım yapmak kolay değildir. Çoğu monolitik çekirdek bugün sürücüleri ve dosya sistemlerini çalışma anında yüklenebilen modüller olarak da alır; bu ek esnekliğe 2.6.9'da değinilir.
2.6.4Katmanlı yapı
Büyük bir sistemi yönetilebilir kılmanın klasik yolu onu katmanlara bölmektir.
Katmanlı yapıda (layered system) işletim sistemi katmanlara bölünür. En alttaki katman donanıma, en üstteki katman kullanıcıya en yakındır. Her katman yalnız kendi altındaki katmanların hizmetlerini kullanır.
Bu yapının ilk örneği, Edsger Dijkstra'nın öğrencileriyle 1968'de geliştirdiği THE sistemidir. Şekil 2.27 THE'nin altı katmanını gösterir.
- Katman 0, kesmeler geldiğinde ve zamanlayıcı dolduğunda süreçler arasında geçiş yapar. Bu katmanın üstündekiler, birden çok sürecin tek işlemciyi paylaştığını düşünmek zorunda kalmaz; her süreç kendi işlemcisi varmış gibi yazılır.
- Katman 1, süreçlere bellek ayırır ve sayfaları o dönemin ikincil belleği olan tambur ile ana bellek arasında taşır. Üst katmanlar, sayfanın bellekte mi tamburda mı olduğunu bilmez.
- Katman 2, her süreç ile operatör konsolu arasındaki iletişimi yönetir. Üstteki her süreç kendi konsolu varmış gibi davranır.
- Katman 3, G/Ç aygıtlarını yönetir. Üst katmanlar gerçek aygıtların ayrıntıları yerine soyut aygıtlarla çalışır.
- Katman 4, kullanıcı programlarıdır. Bu programlar bellek, konsol ve G/Ç ayrıntılarıyla uğraşmaz.
- Katman 5, sistem operatörüdür.
Katmanlı yapının en büyük yararı, hata ayıklamanın ve doğrulamanın katman katman yapılabilmesidir. Katman 0 tek başına sınanır ve doğruluğundan emin olunur. Sonra katman 1 eklenir; bir hata çıkarsa, alttaki katman doğru olduğuna göre hata katman 1'dedir. Soyutlama ve veri gizleme de kolaydır: her katman kendi veri yapılarını üst katmanlardan saklar.
Yapının iki önemli zorluğu vardır. Birincisi, katmanları doğru tanımlamaktır. Bileşenler arasındaki bağımlılıklar çoğu zaman iki yönlüdür. Örneğin bellek yöneticisi diske sayfa yazmak için G/Ç sürücüsüne ihtiyaç duyar; G/Ç sürücüsü de tampon ayırmak için bellek yöneticisine. Hangisinin altta olacağına karar vermek güçtür. İkincisi, performanstır: bir istek her katman geçişinde parametre hazırlama ve denetim gibi ek işler doğurur. Bu yüzden saf katmanlı işletim sistemleri yaygınlaşmamıştır. Fikir ise bugün ağ protokol yığınlarında yaşar: bir ağ paketi uygulama, taşıma, ağ ve bağlantı katmanlarından sırayla geçer.
2.6.5Mikro çekirdek
Monolitik yapının en büyük sorunu, bir bileşendeki hatanın bütün sistemi düşürebilmesidir. Mikro çekirdek yaklaşımı bu soruna kökten bir cevap verir: çekirdekte yalnız gerçekten orada bulunması gereken şeyler kalır.
Mikro çekirdekte (microkernel) çekirdek modunda yalnız süreçler arası iletişim (ileti geçirme), temel çizelgeleme, adres alanı yönetimi ve kesme işleme kalır. Sürücüler, dosya sistemleri ve ağ yığını kullanıcı modunda sunucu süreçler olarak çalışır ve birbirleriyle iletilerle haberleşir.
Yeni bir hizmet eklemek, mikro çekirdekte kullanıcı alanına yeni bir sunucu eklemek demektir; çekirdeğin kendisi değişmez. Mach, MINIX 3, otomotiv sektöründe yaygın QNX Neutrino, L4 ailesi, biçimsel olarak doğruluğu kanıtlanmış seL4 ve Google Fuchsia'nın Zircon çekirdeği bu yaklaşımın örnekleridir. macOS, Mach'tan türemiş olsa da saf bir mikro çekirdek değil, hibrit bir çekirdektir.
Mikro çekirdek tasarımında sık anılan bir ilke, mekanizma ile politikanın ayrılmasıdır (policy / mechanism). Mekanizma bir işin nasıl yapılacağını, politika ne yapılacağını belirler. Örneğin çekirdek, en yüksek öncelikli hazır süreci çalıştırma mekanizmasını sağlar. Süreçlere hangi önceliklerin verileceğine ise kullanıcı modundaki bir bileşen karar verir. Böylece politika değiştirilmek istendiğinde çekirdeğe dokunmak gerekmez.
Mikro çekirdeğin artıları şunlardır. Çöken bir sürücü sistemi düşürmez; yalnız o sürücünün süreci ölür ve yeniden başlatılabilir. Çekirdek küçük olduğu için sınanması, doğrulanması ve başka bir donanıma taşınması kolaydır; seL4'ün birkaç bin satırlık çekirdeğinin doğruluğu matematiksel olarak kanıtlanabilmiştir. Ayrıcalıklı kod az olduğu için saldırganın hedef alabileceği yüzey de küçüktür. Eksisi ise performanstır: bileşenler arası her istek bir ileti ve birkaç mod geçişi gerektirir. L4 ailesinin tasarımları, ileti geçirmeyi son derece hızlandırarak bu ek yükü büyük ölçüde azaltmıştır.
2.6.6Mikro çekirdekte ileti alışverişi
Mikro çekirdekte bir uygulamanın bir dosyadan okuma yapması, monolitik çekirdektekinden çok daha uzun bir yol izler. Şekil 2.29 bu yolun iki anını gösterir.
Uygulama isteğini bir ileti olarak çekirdeğe verir; çekirdek iletiyi kullanıcı modundaki dosya sistemi sunucusuna teslim eder. Dosya sistemi hangi disk bloğunun okunacağını bulur, ama diske kendisi erişemez; bu yüzden yine çekirdek üzerinden disk sürücüsüne bir ileti yollar. Sürücü, çekirdeğin izin verdiği G/Ç işlemleriyle bloğu okur. Yanıtlar da aynı yolu tersinden izler. Monolitik çekirdekte aynı iş, tek bir sistem çağrısının içinde birbirini çağıran fonksiyonların zinciridir.
Yolu daha ayrıntılı görmek için istemci ile dosya sistemi sunucusunun (FS) kodunu izleyelim. Aşağıdaki kod, send ve receive adlı iki ilkel çağrıyla yazılmıştır. send(hedef, &m) iletiyi hedefe gönderir; receive(kaynak, &m) belirtilen kaynaktan (ya da ANY ise herhangi birinden) bir ileti gelene kadar bekler.
/* istemci */
m.tur = OKU; m.dosya = 7; m.ofset = 0;
send(FS, &m);
receive(FS, &m); /* yanıtı bekle */
/* dosya sistemi sunucusu (FS) */
while (1) {
receive(ANY, &m);
blok = blok_bul(m.dosya, m.ofset);
send(DISK, &blok);
receive(DISK, &veri);
m.sonuc = veri;
send(m.kaynak, &m);
}İstemci, 7 numaralı dosyanın 0. baytından okumak istiyor. Dosya sistemi bu konumun 120 numaralı blokta olduğunu biliyor. Başlangıçta FS ve disk sürücüsü receive'de iş bekliyor.
- İstek hazırlanır. İstemci isteğini ileti yapısına yazar:
m.tur = OKU,m.dosya = 7,m.ofset = 0. - 1. ileti.
send(FS, &m)çekirdeğe bir tuzaktır. Çekirdek iletiyi FS'nin tamponuna kopyalar ve FS'yi uyandırır. İstemci, yanıt içinreceive(FS, &m)'de bloke olur. - FS işi alır. FS'nin
receive(ANY, &m)çağrısı döner. İletinin kimden geldiğim.kaynakalanındadır. FS,blok_bulile istenen verinin 120 numaralı blokta olduğunu bulur. - 2. ileti. FS de diske doğrudan erişemez.
send(DISK, &blok)ile sürücüye ikinci iletiyi yollar vereceive(DISK, &veri)ile yanıtı bekler. - 3. ileti. Sürücü, çekirdeğin izin verdiği G/Ç ile bloğu okur ve üçüncü iletiyle veriyi FS'ye gönderir. FS,
m.sonuc = veriile yanıtı hazırlar. - 4. ileti.
send(m.kaynak, &m)yanıtı istemciye taşır; istemci hazır duruma geçer. - FS döngüye döner. FS,
whiledöngüsünün başındakireceive(ANY, &m)'de yeni bir iş beklemeye başlar. - İstemci devam eder. İstemcinin
receiveçağrısı döner; verim.sonucalanındadır.
Tek bir okuma için dört ileti gönderilmiştir. Her iletide en az bir kullanıcı → çekirdek ve bir çekirdek → kullanıcı geçişi olur. Monolitik çekirdekte aynı iş tek bir sistem çağrısı içinde, iki mod geçişi ve birkaç fonksiyon çağrısıyla biter.
Mikro çekirdeğin ek yükünü yalnız ileti sayısıyla ölçmek. Her ileti, iki mod geçişinin yanında genellikle bir bağlam değiştirme de gerektirir. İşlemci istemci sürecinden sunucu sürecine geçer ve sunucunun adres alanı etkin hâle gelir. Önbellekteki veriler de büyük ölçüde işe yaramaz olur. Ek yükün asıl kaynağı çoğu zaman bu bağlam değiştirmelerdir.
2.6.7MINIX 3 ve istemci–sunucu modeli
MINIX 3, Andrew Tanenbaum ve ekibinin geliştirdiği, güvenilirliği ön plana çıkaran bir mikro çekirdek işletim sistemidir. Şekil 2.31 yapısını gösterir.
MINIX 3'ün iki özelliği dikkat çekicidir. Birincisi reenkarnasyon sunucusudur (RS). RS, sürücülerin sağlığını sürekli denetler. Bir sürücü, örneğin terminal sürücüsü (TTY), çökerse ya da yanıt vermeyi keserse RS onu kullanıcı fark etmeden yeniden başlatır. Sistem kendi kendini onarır. İkincisi bölünmüş ayrıcalıktır. Sürücüler kullanıcı modunda çalışır ve G/Ç portlarına yalnız çekirdek çağrılarıyla, kendilerine izin verilen ölçüde erişebilir. Hatalı bir sürücü kendi belleğini bozabilir, ama çekirdeğin ya da başka bir sürecin belleğine dokunamaz.
Mikro çekirdek üzerinde çalışan süreçleri iki sınıfa ayırmak doğaldır: hizmet isteyen istemciler ve hizmet veren sunucular. Haberleşme yalnız iletilerle olur. Bu düzene istemci–sunucu modeli (client–server model) denir.
Modelin önemli bir sonucu vardır. İstemci, iletinin kendi makinesinde mi yoksa ağdaki başka bir makinede mi işlendiğini bilmek zorunda değildir: ileti gönderir ve yanıtı bekler. Bu yüzden model, doğal olarak dağıtık sistemlere genişler. Bugünün web uygulamaları ve mikroservis mimarileri aynı fikrin geniş ölçekli uygulamalarıdır. Yine de ileti alışverişi kendiliğinden güvenli değildir. Güvenlik, çekirdeğin hangi sürecin hangi sunucuya ileti gönderebileceğini denetlemesine bağlıdır.
2.6.8Tanenbaum–Torvalds tartışması
1991'de Linus Torvalds, Linux çekirdeğinin ilk sürümünü yayımladı. Birkaç ay sonra, 1992'nin başında, MINIX'in yazarı Andrew Tanenbaum bir haber grubunda “LINUX is obsolete” (Linux eskimiştir) başlıklı bir ileti yazdı. Bu ileti, işletim sistemi tarihinin en çok anılan tartışmalarından birini başlattı.
Tanenbaum'un savı şuydu: monolitik tasarım 1970'lerin geride kalmış bir yaklaşımıdır. Mikro çekirdek daha modüler, daha esnek ve daha kolay taşınabilirdir; bir bileşendeki hata sınırlı zarar verir. Donanım hızlandıkça ileti alışverişinin ek yükü önemsizleşecektir. Torvalds ise şöyle karşılık verdi: monolitik çekirdeğin tasarımı ve gerçeklemesi daha basittir ve doğrudan fonksiyon çağrıları daha hızlıdır. Süreçler arası ileti alışverişine gerek yoktur; asıl önemli olan pratikte çalışan koddur. Ayrıca açık kaynak geliştirme modeli, Linux'un hızla gelişmesini sağlayacaktır.
Aradan geçen otuz yılda iki taraf da kısmen haklı çıktı. Linux monolitik kaldı ve dünyanın en yaygın çekirdeklerinden biri oldu. Ama esneklik için mikro çekirdek fikirlerinden de yararlandı. Yüklenebilir modüller ve kullanıcı alanında çalışan dosya sistemleri (FUSE) ile sürücüler (VFIO) bunun örnekleridir. Çekirdeğe güvenli biçimde küçük programlar yüklemeyi sağlayan eBPF de aynı yöndedir. Mikro çekirdekler ise güvenilirliğin performanstan önemli olduğu güvenlik-kritik ve gömülü sistemlerde (QNX, seL4) yaygınlaştı.
2.6.9Modüler yapı
Günümüz işletim sistemlerinin çoğu, monolitik çekirdeğin hızını korurken esneklik kazanmak için modüler bir yaklaşım izler.
Modüler yapıda çekirdekte yalnız temel bileşenler bulunur; öteki hizmetler açılışta ya da çalışma anında yüklenebilir çekirdek modülü (loadable kernel module, LKM) olarak çekirdeğe eklenir. Modüller çekirdek modunda ve çekirdeğin adres alanında çalışır.
Linux, FreeBSD ve Solaris bu yapıyı kullanır. Modüler yapı iki ayrı yapıya benzer, ama ikisinden de farklıdır. Katmanlı yapıya benzer, çünkü her modülün tanımlı bir arayüzü vardır; ondan daha esnektir, çünkü her modül herhangi bir modülü çağırabilir. Mikro çekirdeğe benzer, çünkü çekirdeğin özü küçüktür ve hizmetler ayrı parçalar hâlindedir; ondan farkı, modüllerin ileti kullanmadan doğrudan fonksiyon çağrılarıyla haberleşmesi ve çekirdek modunda çalışmasıdır.
usb_storage modülü yüklenir; vfat modülü bellek bağlanırken eklenir.Bir modülün yaşam döngüsü Linux'ta şöyle izlenebilir. Açılışta çekirdek ve başlangıç RAM diskindeki (initramfs) gerekli modüller yüklüdür; lsmod komutu yüklü modülleri listeler. Kullanıcı bir USB bellek takar. Çekirdek bir olay üretir. udev hizmeti bu olayı görür ve modprobe ile usb-storage modülünü bağımlılıklarıyla birlikte yükletir. Bellekteki FAT dosya sistemini okumak için gereken vfat modülü ise bu sırada değil, bellek bağlanırken (mount çağrısı dosya sistemi türünü istediğinde) yüklenir. Modül yüklenince kendini çekirdeğe kaydeder ve bundan sonra doğrudan fonksiyon çağrılarıyla çalışır. Modülü kullanan kalmayınca, yani kullanım sayısı 0'a inince modprobe -r ya da rmmod komutu onu bellekten çıkarır.
Modüler yapının bedeli yalıtımın olmamasıdır. Modül çekirdek modunda çalıştığı için hatalı bir modül çekirdeği çökertebilir; kötü niyetli bir modül sistemin bütün denetimini ele geçirebilir. Ayrıca modüllerin, yüklendikleri çekirdeğin sürümüyle uyumlu derlenmiş olması gerekir.
Yüklenebilir modülleri olduğu için Linux'u bir mikro çekirdek saymak. Mikro çekirdeği tanımlayan, bileşenlerin ayrı parçalar olması değil, kullanıcı modunda ayrı süreçler olarak çalışıp iletilerle haberleşmesidir. Linux modülleri çekirdek modunda, çekirdeğin adres alanında çalışır ve fonksiyon çağrısıyla haberleşir. Linux bu yüzden modüler bir monolitik çekirdektir.
2.6.10Hibrit çekirdek
Bazı sistemler, mikro çekirdek fikirleriyle yola çıkıp performans kaygısıyla birçok hizmeti yeniden çekirdek moduna almıştır. Bu sistemlere hibrit çekirdek (hybrid kernel) denir. Windows ve macOS en bilinen iki örnektir.
ntdll.dll; çekirdek modunda yürütücü, win32k.sys, aygıt sürücüleri, çekirdek ve HAL. Altta macOS ve iOS'un XNU çekirdeği: Mach, BSD ve I/O Kit aynı çekirdek adres alanında birleşmiştir.Windows NT'nin tasarımı, mikro çekirdek fikirlerinden izler taşır: alt sistemler, nesne ve ileti temelli bir yapı, donanımı soyutlayan bir katman (hardware abstraction layer, HAL). Ama performans için G/Ç, bellek, süreç ve nesne yöneticilerini içeren yürütücü (executive), grafik alt sistemi ve aygıt sürücüleri çekirdek moduna alınmıştır. macOS ve iOS'un çekirdeği XNU ise Mach mikro çekirdeği ile BSD UNIX katmanını aynı çekirdek adres alanında birleştirir. Mach IPC'yi, çizelgelemeyi ve sanal belleği, BSD POSIX arayüzünü, ağı ve dosya sistemlerini, I/O Kit de nesne yönelimli sürücü çatısını sağlar. Amaç, mikro çekirdeğin modülerliğini monolitik çekirdeğin hızıyla buluşturmaktır. Apple son yıllarda yeni sürücüleri çekirdeğin dışına, kullanıcı alanındaki DriverKit çatısına taşımaktadır.
2.6.11Exokernel
Yelpazenin en uç noktasında exokernel bulunur. Bu yaklaşım, MIT'de Dawson Engler ve Frans Kaashoek'un 1995'te önerdiği bir araştırma mimarisidir. Çekirdeğin adı Aegis, onunla birlikte çalışan kütüphane işletim sisteminin adı ExOS'tur. Exokernel ticari bir işletim sistemi değil, bir fikirdir.
Exokernel, yalnız donanım kaynaklarını korumalı biçimde dağıtan ve hangi kaynağın kime ait olduğunu izleyen çekirdektir. Dosya ve sanal bellek gibi soyutlamaları çekirdek değil, uygulamalarla birlikte kullanıcı modunda çalışan kütüphane işletim sistemleri (LibOS) sağlar.
Exokernel'i sanal makine yaklaşımıyla karşılaştırmak, fikri anlamayı kolaylaştırır. Sanal makine yöneticisi her sanal makineye bütün makinenin bir kopyasını verir. Sanal makine kendi diskinin 0. bloğundan başladığını sanır; yönetici de bu bloğu gerçek diskteki bir bloğa eşlemek zorunda kalır. Exokernel ise her uygulamaya gerçek kaynağın bir bölümünü verir ve yalnız sahipliği denetler. Uygulama hangi gerçek disk bloklarını aldığını bilir; arada bir yeniden eşleme katmanı gerekmez.
Yaklaşımın sloganı, yönetimi korumadan ayırmaktır. Çekirdek yalnız korur: bir uygulama başkasının bloğuna ya da sayfasına dokunamaz. Kaynakların nasıl yönetileceği ise uygulamaya kalır. Bir veritabanı kendi önbellekleme ve disk yerleşim stratejisini, bir web sunucusu kendi ağ tamponlarını kurabilir. Uygulamaya özgü bu yönetim yüksek başarım sağlar; bedeli, uygulama geliştirmenin karmaşıklaşmasıdır. Exokernel fikri bugün iki biçimde yaşar. Birincisi, tek bir uygulamayı kendi küçük işletim sistemiyle birlikte derleyen unikernel'lerdir. İkincisi, ağ ve depolama G/Ç'sinde çekirdeği atlayıp aygıta kullanıcı alanından erişen DPDK ve SPDK gibi kütüphanelerdir.
2.6.12Yapıların karşılaştırılması
| Yapı | Çekirdekte ne var? | Güçlü yön | Zayıf yön | Örnek |
|---|---|---|---|---|
| Basit | her şey, sınırsız | küçük, basit | koruma yok | MS-DOS |
| Monolitik | bütün hizmetler | hız | hata yalıtımı yok | UNIX, Linux |
| Katmanlı | hiyerarşik katmanlar | doğrulama kolay | ek yük, katman tanımı zor | THE, MULTICS |
| Mikro çekirdek | IPC, çizelgeleme, bellek | güvenilirlik | ileti ek yükü | MINIX 3, QNX, seL4 |
| Modüler | çekirdek özü + modüller | esneklik, hız | modül = tam yetki | Linux, FreeBSD |
| Hibrit | mikro çekirdek + seçili hizmetler | denge | karmaşıklık | Windows, macOS |
| Exokernel | yalnız koruma | uygulamaya özgü hız | geliştirme zor | Aegis/ExOS |
Tablo yapıları saf biçimleriyle verir. Gerçek sistemler ise nadiren saftır. Linux monolitik ve modülerdir; Windows ve macOS hibrittir; hemen hepsi bir ölçüde katmanlıdır. Bir sistemi tek bir kutuya yerleştirmek yerine hangi kararları neden verdiğini sormak daha öğreticidir.
2.7Sanal makineler ve çok işlemcili sistemler
Çekirdek yapıları, bir işletim sisteminin kendi içini nasıl düzenlediğini anlatır. Bu kesimin iki konusu ise işletim sisteminin altındaki donanımla ilgilidir: donanımın yazılımla çoğaltılması ve birden çok işlemcinin aynı belleği paylaşması.
2.7.1Sanal makineler
Hipervizör (hypervisor) ya da sanal makine yöneticisi (virtual machine monitor, VMM), tek bir fiziksel donanım üzerinde her biri bağımsız bir bilgisayar gibi görünen sanal makineler (virtual machine, VM) oluşturan yazılım katmanıdır. Sanal makinenin içinde çalışan işletim sistemine misafir işletim sistemi denir.
Her misafir işletim sistemi kendi işlemcisi, belleği, diski ve G/Ç aygıtları olduğuna inanır. Bu yüzden farklı işletim sistemleri aynı donanımda aynı anda çalışabilir: aynı sunucuda bir sanal makinede Linux, ötekinde Windows çalışabilir. Şekil 2.36 sanal olmayan bir makineyi sanal makinelerle karşılaştırır.
Fikir yeni değildir; IBM'in 1972'de yayımladığı VM/370 sistemine dayanır. Bugün ise bulut bilişimin temelidir. Sanallaştırmanın başlıca yararları şunlardır:
- Sunucu birleştirme. Az kullanılan çok sayıda sunucu tek bir güçlü makinede toplanır.
- Yalıtım. Bir sanal makinedeki sorun ötekilere sıçramaz.
- Anlık görüntü. Sanal makinenin o anki durumu kaydedilir ve gerekirse bu duruma geri dönülür.
- Canlı taşıma. Çalışan bir sanal makine kapatılmadan başka bir fiziksel makineye taşınır.
Hipervizörler çalıştıkları yere göre iki türe ayrılır. Tip 1 hipervizör doğrudan donanımın üzerinde çalışır; altında başka bir işletim sistemi yoktur. Veri merkezlerinde kullanılan hipervizörler bu türdendir. Tip 2 hipervizör ise sıradan bir işletim sisteminin üzerinde bir uygulama gibi çalışır; bir masaüstü bilgisayarda sanal makine çalıştıran programlar bu türdendir.
Sanal makineyle sık karıştırılan bir kavram konteynerdir (container). Konteyner, kendi çekirdeğini taşımaz. Aynı çekirdeği paylaşan bir grup süreci, çekirdeğin ad alanları ve kaynak sınırlama mekanizmalarıyla öteki süreçlerden yalıtır. Konteyner bu yüzden sanal makineden çok daha hafiftir ve saniyenin altında başlatılabilir. Ama aynı çekirdeği paylaştığı için içinde yalnız o çekirdekle uyumlu programlar çalışır ve yalıtımı sanal makineninki kadar güçlü değildir.
Exokernel ile hipervizörü aynı şey sanmak. İkisi de kaynakları birden çok tarafa paylaştırır, ama farklı biçimde. Hipervizör her sanal makineye makinenin tam bir kopyasını verir ve sanal kaynakları gerçek kaynaklara kendisi eşler. Exokernel ise gerçek kaynakların parçalarını dağıtır ve yalnız sahipliği denetler; soyutlamaları uygulamanın LibOS'u kurar.
2.7.2Simetrik çok işlemcili ve çok çekirdekli mimari
Günümüzün hemen her bilgisayarında birden çok işlemci ya da işlemci çekirdeği vardır. Bu durum, işletim sisteminin yapısını da etkiler.
Simetrik çok işlemcili (symmetric multiprocessor, SMP) bir sistemde her işlemcinin kendi yazmaçları ve önbelleği vardır, ama ana bellek ortaktır. “Simetrik” sözcüğü, bütün işlemcilerin eşit haklı olduğunu anlatır: hepsi, işletim sistemi kodu da dahil her işi yapabilir. Bunun önemli bir sonucu vardır. İki işlemci aynı anda çekirdek kodunu çalıştırabildiği için çekirdeğin ortak veri yapıları kilitlerle korunmak zorundadır.
Çekirdekte hazır süreçlerin bağlı bir listesi vardır ve listenin başını bas göstericisi gösterir. Başlangıçta liste yalnız A görevinden oluşur. İki işlemci aynı anda listeye görev ekliyor: CPU0 X görevini, CPU1 Y görevini. Ekleme iki adımdır: (1) g->sonraki = bas; (2) bas = g;. Adımlar CPU0(1), CPU1(1), CPU0(2), CPU1(2) sırasıyla işlenirse ne olur?
Çözüm. CPU0(1): X.sonraki = A. CPU1(1): Y.sonraki = A. CPU0(2): bas = X; liste X → A olur. CPU1(2): bas = Y; liste Y → A olur. X görevi listeden kaybolmuştur: hiçbir göstericiden ulaşılamaz ve hiçbir zaman çizelgelenmez. Sonuç, adımların hangi sırayla işlendiğine bağlıdır; buna yarış durumu (race condition) denir. Çözüm, ekleme işlemini bir kilitle korumaktır: kilidi al, (1) ve (2)'yi yap, kilidi bırak.
İlk çok işlemcili Linux sürümleri bu sorunu kaba bir yolla çözmüştü: bütün çekirdeği tek bir kilitle, “büyük çekirdek kilidiyle” (big kernel lock) koruyorlardı. Aynı anda yalnız bir işlemci çekirdek kodu çalıştırabiliyordu. Bu kilit yıllar içinde daha ince taneli kilitlerle değiştirildi ve 2011'de tamamen kaldırıldı.
Çok çekirdekli yongada (multicore) birden çok çekirdek aynı yonganın üzerindedir. Çekirdekler yakın önbellekleri paylaştığı için ayrı yongalardaki işlemcilere göre daha hızlı ve daha az enerjiyle haberleşir. 2026'da yaygın olan bir tasarım, aynı yongada iki tür çekirdek bulundurmaktır: hızlı ama enerji harcayan performans (P) çekirdekleri ve yavaş ama tutumlu verimlilik (E) çekirdekleri. Bu tasarım çizelgeleyiciye yeni bir görev yükler: hangi işin hangi tür çekirdekte çalışacağına karar vermek.
2.8Bağlama, yükleme ve açılış
Son iki konu, bir programın ve bir işletim sisteminin nasıl çalışmaya başladığıdır. Programcının yazdığı kaynak kodun bir süreç hâline gelmesi birkaç aşamadan geçer. Aynı biçimde, güç düğmesine basıldığı andan giriş ekranının görünmesine kadar da bir dizi adım vardır.
2.8.1Derleme, bağlama ve yükleme
main.c derleyiciyle (gcc -c) main.o nesne dosyasına çevrilir. Bağlayıcı (ld) bunu öteki nesne dosyaları ve statik kütüphanelerle (libm.a) birleştirip çalıştırılabilir program üretir. Yükleyici (execve ve ld.so) programı paylaşımlı libc.so.6 ile birlikte belleğe alır ve süreç çalışmaya başlar.- Derleme. Derleyici kaynak kodu yeniden konumlandırılabilir nesne dosyasına çevirir. Bu dosyadaki adresler henüz kesin değildir, çünkü kodun bellekte nereye yerleşeceği bilinmez. Programın kullandığı ama başka bir dosyada tanımlı semboller, örneğin
printf, de çözülmemiştir. - Bağlama. Bağlayıcı (linker) nesne dosyalarını ve statik kütüphaneleri birleştirir. Her sembolün hangi dosyada tanımlandığını bulur, adresleri düzeltir ve bir çalıştırılabilir dosya (executable file) üretir.
- Yükleme. Kabuk
execveçağırır. Çekirdekteki yükleyici (loader) dosyanın başlığını okur, kod ve veri kesimlerini sürecin adres alanına eşler. Program paylaşımlı kütüphane kullanıyorsa dinamik bağlayıcıyı (ld-linux.so) başlatır. - Dinamik bağlama. Paylaşımlı kütüphaneler (shared library), örneğin
libc.so.6, çalışma anında bağlanır. Kütüphanenin kodu bellekte tek kopya olarak durur ve onu kullanan bütün süreçler bu kopyayı paylaşır.
Bir sunucuda C kütüphanesini kullanan 50 farklı program aynı anda çalışıyor ve kütüphanenin kodu 3 MiB tutuyor. Kütüphane statik bağlanırsa ve dinamik bağlanırsa bellekte kütüphane kodu için ne kadar yer gerekir?
Çözüm. Statik bağlamada kütüphanenin kullanılan kodu her çalıştırılabilir dosyanın içine kopyalanır. Her süreç kendi kopyasını belleğe yükler: 50 × 3 MiB = 150 MiB gerekir. Dinamik bağlamada kütüphanenin kod sayfaları salt okunur olduğundan tek bir kopya bütün süreçlerin adres alanına eşlenir: yaklaşık 3 MiB yeterlidir. Her süreç ayrıca kütüphanenin yazılabilir verisi için küçük, özel bir alan kullanır. Dinamik bağlamanın bir yararı daha vardır: kütüphanedeki bir güvenlik açığı kapatıldığında yalnız libc.so güncellenir. Statik bağlamada ise 50 programın hepsinin yeniden bağlanması gerekir.
Çalıştırılabilir dosyanın biçimi işletim sistemine özgüdür: Linux ELF (Executable and Linkable Format), Windows PE (Portable Executable), macOS ise Mach-O biçimini kullanır. Bir programın bir işletim sisteminden ötekine doğrudan taşınamamasının bir nedeni budur. Aynı işlemci üzerinde bile, biri öteki sistemin yükleyicisinin anlamadığı bir dosya biçimindedir ve iki sistemin sistem çağrıları da farklıdır.
2.8.2Açılış süreci
Açılış ya da önyükleme (booting), bilgisayara güç verildiği andan çekirdeğin belleğe yüklenip ilk kullanıcı sürecini başlatmasına kadar geçen süreçtir.
Eski PC'lerde açılışı 1981'den beri kullanılan BIOS (16 bitlik gerçek kipte çalışan ürün yazılımı) yönetirdi. BIOS diskin ilk sektöründeki ana önyükleme kaydını (MBR) belleğe alıp çalıştırırdı. Günümüzün bilgisayarları ise UEFI kullanır. UEFI bir sektörü değil, bir dosyayı (.efi uzantılı bir program) yükler. Şekil 2.39 UEFI kullanan bir Linux bilgisayarın açılış adımlarını gösterir.
systemd).- Güç ve sıfırlama. Güç verilince işlemci sıfırlanır ve donanımın belirlediği sabit bir adresten, anakart üzerindeki flash bellekte duran ürün yazılımını yürütmeye başlar. Bu anda bellekte henüz işletim sistemi yoktur.
- UEFI. Ürün yazılımı önce açılış öz sınamasını (power-on self-test, POST) yapar: belleği dener, aygıtları bulur. Ardından kalıcı ayarlarında (NVRAM) kayıtlı açılış sırasına bakar ve sırasıyla NVMe disk, USB ya da ağ üzerinden açılış (PXE) seçeneklerini dener. GPT biçimli diskteki EFI sistem bölümünden (FAT32 dosya sistemi) önyükleyiciyi yükler. Güvenli Önyükleme (Secure Boot) açıksa önyükleyicinin dijital imzası doğrulanır; imzasız ya da değiştirilmiş bir önyükleyici çalıştırılmaz.
- Önyükleyici. Önyükleyici (bootloader), GRUB 2 ya da systemd-boot, kullanıcıya bir menü sunar. Seçilen çekirdeği ve başlangıç RAM diskini (initramfs) belleğe koyar ve kontrolü çekirdeğe verir.
- Çekirdek. Çekirdek bellek yönetimini, çizelgeleyiciyi ve sürücüleri başlatır. initramfs içindeki betikler, gerçek kök dosya sistemine ulaşmak için gereken modülleri (örneğin disk denetleyicisi ya da şifreli disk desteği) yükler.
- Kök dosya sistemi. Gerçek kök dosya sistemi bağlanır ve artık gerekmeyen initramfs bellekten bırakılır.
- PID 1. Çekirdek ilk kullanıcı sürecini, PID'si 1 olan
systemd'yi başlatır.systemdhizmetleri, ağı ve giriş ekranını mümkün olduğunca paralel biçimde başlatır.
PID 1'in özel bir yeri vardır. Bundan sonra sistemde oluşan her süreç onun soyundandır; süreç ağacının kökü odur. Ebeveyni ölen süreçleri de PID 1 evlat edinir ve onların çıkış durumlarını toplar. Linux'ta systemd-analyze komutu, açılışın her aşamasının ne kadar sürdüğünü gösterir. Windows'ta aynı zincirin karşılığı winload.efi → ntoskrnl.exe → smss.exe → winlogon.exe biçimindedir.
2.9Özet
- İşletim sistemi yapısı, bileşenlerin nerede çalışacağına (çekirdek ya da kullanıcı modu) ve nasıl haberleşeceğine (fonksiyon çağrısı ya da ileti) dair stratejidir. Her yapı performans, güvenilirlik ve esneklik arasında bir ödünleşim seçer.
- İşletim sistemi kullanıcıya ve programlara arayüz, program yürütme, G/Ç, dosya sistemi, iletişim ve hata algılama; sistemin bütününe kaynak ayırma, kaynak izleme, koruma ve güvenlik hizmetleri sunar. Hizmetlere sistem çağrılarıyla ulaşılır.
- Süreç çalışmakta olan programdır; bilgileri süreç tablosundaki PCB'de tutulur. Süreçler
fork()ile bir ağaç oluşturur. Adres alanı metin, veri, BSS, öbek ve yığın kesimlerinden oluşur. - Dosyalar bir dizin ağacında durur; açık dosyalar küçük tamsayılarla (tanımlayıcılar) anılır ve yeni dosya en küçük boş numarayı alır. Bağlama, bir dosya sistemini ağaçtaki bir dizine iliştirir. Boru, iki süreci bağlayan tek yönlü bir bayt kanalıdır.
- Çift modlu çalışma ayrıcalıklı komutları çekirdek moduna ayırır; zamanlayıcı işlemcinin her zaman işletim sistemine dönmesini sağlar. Çekirdeğe yalnız sistem çağrısı, kesme ve istisnayla girilir.
- Bir sistem çağrısında libc sarmalayıcısı çağrı numarasını ve parametreleri yazmaçlara koyar, tuzak komutunu çalıştırır; çekirdek numarayı denetleyip sistem çağrısı tablosundan işleyiciyi bulur. Hata durumunda çağrı −1 döndürür ve
errnoayarlanır. printfbir kütüphane fonksiyonudur ve tamponu boşaltırkenwritesistem çağrısını yapar. Tampon boyutu, yapılan sistem çağrısı sayısını belirler.- Kabuk,
fork,execvevewaitpidüçlüsüyle çalışır.linkbir i-node'a yeni ad ekler,unlinkbir adı siler; dosya ancak bağ sayısı ve açık sayacı 0 olunca yok olur. - Monolitik çekirdek hızlıdır ama hata yalıtımı yoktur. Katmanlı yapı doğrulamayı kolaylaştırır ama ek yük getirir. Mikro çekirdek güvenilirdir ama ileti ek yükü taşır. Modüler yapı esnekliği hızla birleştirir, hibrit çekirdekler bu fikirleri harmanlar. Exokernel yalnız korur, yönetimi uygulamaya bırakır.
- Hipervizör donanımı sanallaştırıp birden çok işletim sistemini aynı makinede çalıştırır; konteyner ise aynı çekirdeği paylaşan süreçleri yalıtır. SMP sistemlerde çekirdek veri yapıları kilitlerle korunmalıdır.
- Kaynak kod derleyici, bağlayıcı ve yükleyiciden geçerek sürece dönüşür. Açılışta ürün yazılımı, önyükleyici ve çekirdek sırasıyla çalışır ve çekirdek ilk kullanıcı süreci olan PID 1'i başlatır.
2.10Kavram listesi
Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.
- işletim sistemi hizmetleri operating system services
- süreç process
- süreç kontrol bloğu process control block (PCB)
- ebeveyn / çocuk süreç parent / child process
- adres alanı address space
- süreçler arası iletişim interprocess communication (IPC)
- dosya tanımlayıcısı file descriptor
- yol adı path name
- blok / karakter aygıtı block / character device
- bağlama mount
- boru pipe
- kilitlenme deadlock
- kaynak tahsis çizgesi resource allocation graph
- çift modlu çalışma dual-mode operation
- çekirdek modu / kullanıcı modu kernel mode / user mode
- tuzak trap
- zamanlayıcı timer
- sistem çağrısı system call
- API application programming interface
- kabuk shell
- i-node i-node
- katı bağ hard link
- monolitik çekirdek monolithic kernel
- katmanlı yapı layered system
- mikro çekirdek microkernel
- ileti geçirme message passing
- politika / mekanizma policy / mechanism
- istemci–sunucu modeli client–server model
- yüklenebilir çekirdek modülü loadable kernel module (LKM)
- hibrit çekirdek hybrid kernel
- exokernel exokernel
- sanal makine virtual machine (VM)
- hipervizör hypervisor
- misafir işletim sistemi guest operating system
- konteyner container
- simetrik çok işlemci symmetric multiprocessor (SMP)
- yarış durumu race condition
- çalıştırılabilir dosya executable file
- paylaşımlı kütüphane shared library
- önyükleme booting
- önyükleyici bootloader
2.11Kendini Dene
Yeni başlamış bir süreçte yalnız 0, 1, 2 açıktır. Sırasıyla
a = open("x", O_RDONLY),b = open("y", O_RDONLY),close(a),c = open("z", O_RDONLY),close(2)ved = dup(b)çalışıyor.a,b,cvedkaçtır?Cevap
Her çağrı en küçük boş numarayı verir.
a = 3,b = 4.close(a)3'ü boşaltır;c = 3olur.close(2)standart hatayı kapatır ve en küçük boş numara 2 olur;d = dup(4) = 2. Sonuç: 3, 4, 3, 2. Bu süreçteki hata iletileri artıkydosyasına yönelmiştir; ama dosya salt okunur açıldığı için oraya yazma girişimi hata verir.Notta izlenen kopyalama kodu 4096 baytlık tamponla 9000 baytlık bir dosyayı kopyalıyor. Kaç
readve kaçwriteyapılır, herreadne döndürür?open,creat, ikicloseveprintfile birlikte toplam kaç sistem çağrısı yapılır?Cevap
readsırasıyla 4096, 4096, 808 ve 0 döndürür: 4read. Son çağrı dosya sonunu bildirdiği için ardındanwriteyapılmaz: 3write. Toplam 1 + 1 + 4 + 3 + 2 + 1 = 12 sistem çağrısıdır. Program9000 baytyazdırır.write(1, "Günaydın\n", n)çağrısında metnin tamamının yazılması içinnkaç olmalıdır?nkarakter sayısı olarak verilirse ekranda ne görünür?Cevap
Metinde 9 karakter vardır (G, ü, n, a, y, d, ı, n, \n). UTF-8'de ü ve ı ikişer bayt tuttuğu için bayt sayısı 11'dir;
n = 11olmalıdır.n = 9verilirse son iki bayt, yani son “n” harfi ile satır sonu gönderilmez ve ekranda “Günaydı” görünür; imleç aynı satırda kalır.x86-64 Linux'ta
mmap(adres, uzunluk, koruma, bayraklar, fd, ofset)çağrısının altı parametresi ve çağrı numarası hangi yazmaçlara konur? Dördüncü parametre nedenrcx'e konmaz?Cevap
Çağrı numarası
rax'a;adresrdi'ye,uzunlukrsi'ye,korumardx'e,bayraklarr10'a,fdr8'e,ofsetr9'a konur.syscallkomutu dönüş adresinircx'e, bayrak yazmacınır11'e kaydettiği içinrcx'teki bir parametre çekirdeğe ulaşmadan silinirdi; bu yüzden C kuralındakircxyeriner10kullanılır.Bir zamanlayıcının sayacı 1 MHz'lik bir saatle her darbede 1 azalıyor. Çekirdek saniyede 100 tik istiyor. Sayaca hangi değer yüklenmelidir? Her tik kesmesinin işlenmesi 5 µs sürüyorsa işlemci zamanının yüzde kaçı tiklere gider?
Cevap
1 000 000 / 100 = 10 000; sayaca 10 000 yüklenir ve iki tik arası 10 ms olur. Ek yük saniyede 100 × 5 µs = 500 µs'dir; bu, işlemci zamanının %0,05'idir.
Bir süreç
fadlı dosyayı oluşturuyor (bağ sayısı 1). Sonralink("f", "g"),link("g", "h"),unlink("f"),fd = open("h", O_RDONLY),unlink("g")veunlink("h")çağrılıyor. Her adımdan sonra bağ sayısı kaçtır? Dosyanın blokları ne zaman serbest kalır?Cevap
Bağ sayısı sırasıyla 2, 3, 2, 2, 1 ve 0 olur.
link("g", "h"),g'nin gösterdiği i-node'a, yani aynı dosyaya üçüncü bir ad ekler.openbağ sayısını değiştirmez, açık sayacını 1 yapar. Sonunlink'ten sonra dosyanın adı kalmaz, ama tanımlayıcı açık olduğu için bloklar serbest bırakılmaz. Bloklar ancakclose(fd)çağrıldığında (ya da süreç sonlandığında) serbest kalır.Basit kabuk iskeletinde çocuk süreç
execve'den sonraexitçağırmıyor. Kullanıcılssdiye var olmayan bir komut yazarsa ne olur?Cevap
execveprogramı bulamaz ve −1 döndürür. Çocuk, kabuğun koduylaelsebloğundan çıkar vewhiledöngüsünün başına döner: istem yazar ve komut beklemeye başlar. Ebeveyn kabuk ise hâlâwaitpid'de bu çocuğun bitmesini beklemektedir. Kullanıcı farkında olmadan ikinci bir kabukla konuşur; ancak o “kabuktan” çıkıldığında ebeveyn uyanır.Mikro çekirdekli bir sistemde istemcinin istediği dosya bloğu dosya sistemi sunucusunun önbelleğinde bulunuyor. Okuma kaç iletiyle ve en az kaç mod geçişiyle tamamlanır? Monolitik çekirdekte kaç mod geçişi olur?
Cevap
Disk sürücüsüne gitmeye gerek yoktur. İstemcinin isteği ve sunucunun yanıtı olmak üzere 2 ileti yeterlidir. Her ileti bir kullanıcı → çekirdek ve bir çekirdek → kullanıcı geçişi gerektirdiği için en az 4 mod geçişi olur. Monolitik çekirdekte okuma tek bir sistem çağrısıdır: girişte ve dönüşte birer geçiş, toplam 2.
Aşağıdaki işlemlerin hangileri yalnız kütüphane koduyla tamamlanır, hangileri bir sistem çağrısı gerektirir:
strlen,getpid,sqrt,fork,printf("a")(satır sonu yok, çıktı terminalde)?Cevap
strlenvesqrtyalnız sürecin kendi verisiyle çalışır; kütüphane kodudur.getpidveforkçekirdekteki süreç bilgisine dokunur; sistem çağrısıdır.printf("a")bir kütüphane fonksiyonudur ve terminalde satır tamponlu çalıştığı için satır sonu gelmeden çekirdeğe girmez; “a” tamponda bekler. Tampon daha sonra boşaltılırken (satır sonu,fflushya da program sonu)writeçağrılır.Bir sürücünün hatalı bir göstericiyle bellek bozduğunu varsayalım. Bu hata MINIX 3'te, Linux'ta (sürücü yüklenebilir modül olarak) ve exokernel tabanlı bir sistemde (sürücü uygulamanın LibOS'unda) nelere zarar verebilir?
Cevap
MINIX 3'te sürücü kullanıcı modunda ayrı bir süreçtir; yalnız kendi belleğini bozabilir, çöker ve reenkarnasyon sunucusu onu yeniden başlatır. Linux'ta modül çekirdek modunda ve çekirdeğin adres alanında çalışır; çekirdeğin herhangi bir yapısını bozabilir ve bütün sistemi çökertebilir. Exokernel'de LibOS uygulamayla aynı adres alanındadır; hata o uygulamayı bozar, ama exokernel sahipliği denetlediği için öteki uygulamaların kaynaklarına dokunamaz.
2.12İleri okuma
- A. Silberschatz, P. B. Galvin ve G. Gagne, Operating System Concepts (10. baskı, 2018), Bölüm 2: işletim sistemi hizmetlerini, sistem çağrılarını, bağlayıcı ve yükleyiciyi, yapıları ve açılışı bu notun izlediği sırayla, bol şekille anlatır.
- A. S. Tanenbaum ve H. Bos, Modern Operating Systems (4. baskı, 2014), Kesim 1.5–1.7: süreç, adres alanı ve dosya kavramlarını, POSIX çağrılarını,
read'in on bir adımını ve monolitik, katmanlı, mikro çekirdek, istemci–sunucu ve exokernel yapılarını ayrıntılı işler. - R. H. Arpaci-Dusseau ve A. C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces (sürüm 1.10), “Process API” ve “Limited Direct Execution” bölümleri:
fork/exec/waitüçlüsünü ve tuzak ile zamanlayıcıya dayanan korumayı kısa ve açık örneklerle anlatır; çevrim içi ve ücretsizdir. - D. R. Engler, M. F. Kaashoek ve J. O'Toole, “Exokernel: An Operating System Architecture for Application-Level Resource Management” (SOSP 1995): exokernel fikrini ortaya koyan özgün makaledir; yönetimi korumadan ayırma ilkesini Aegis ve ExOS üzerinde ölçümlerle savunur.
2.13Kaynaklar
- R. H. Arpaci-Dusseau, A. C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces, sürüm 1.10, pages.cs.wisc.edu/~remzi/OSTEP.
- D. R. Engler, M. F. Kaashoek, J. O'Toole, “Exokernel: An Operating System Architecture for Application-Level Resource Management”, SOSP, 1995.
- Linux man sayfaları:
syscalls(2),syscall(2),fork(2),pipe(7),mount(8). - A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10. baskı, Wiley, 2018 (Bölüm 2 ve 18).
- A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4. baskı, Pearson, 2014 (Bölüm 1.5–1.7 ve 7).
İlgili sayfalar: Bölüm 2 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.