İşletim Sistemleri · Ders Notu

Bölüm 8: Bellek Yönetimi

Bir program ancak ana belleğe yüklendikten sonra çalışabilir. İşlemci komutları ve verileri yalnız yazmaçlardan ve ana bellekten okur; disk ancak giriş/çıkış işlemiyle erişilen yavaş bir depodur. Aynı anda birçok süreç çalıştıran bir sistemde ana bellek bu süreçler arasında paylaştırılmak zorundadır. Paylaşım üç soruyu beraberinde getirir. Bir süreç ötekinin belleğine nasıl erişemez? Bir program belleğin herhangi bir yerine yüklendiğinde adresleri nasıl doğru kalır? Boş alan nasıl izlenir ve parçalara bölünmesi nasıl önlenir? Not, önce bellek hiyerarşisini ve önbelleği ele alır. Ardından taban ve limit yazmaçlarıyla başlayan adres çevirisini anlatır. Bağlama ve yükleme, takas, bitişik tahsis ve boş alan yönetimi bunu izler. Ağırlık bugünkü sistemlerin temeli olan sayfalamadadır: sayfa tablosu, TLB, etkin erişim süresi, çok seviyeli, hash ve ters tablolar. Son kesimler kesimlemeyi ve x86-64, ARMv8 ile RISC-V'nin gerçek çözümlerini karşılaştırır.

Öğrenme çıktıları

Soyutlama
Bellek hiyerarşisini, önbellek satırlarını ve ortalama erişim süresini; adres alanını, koruma ve yer değiştirme problemlerini açıklamak.
Adresler
Bağlama zamanlarını, mantıksal ve fiziksel adres ayrımını ve MMU'nun taban-limit denetimini açıklamak.
Bağlama
Dinamik yüklemeyi, statik ve dinamik bağlamayı, PLT/GOT ile paylaşımlı kütüphaneleri ve takası tartışmak.
Bitişik tahsis
Uyan algoritmalarını bir istek dizisine uygulamak; iç ve dış parçalanmayı, biteşlemi ve bağlı listeyi çözümlemek.
Sayfalama
Adresi (p, d) olarak bölmek, sayfa tablosu ve TLB ile çeviri yapmak, etkin erişim süresini hesaplamak.
Gerçek sistemler
Hiyerarşik, hash ve ters tabloları; x86-64'ün dört ve beş seviyeli sayfalamasını, büyük sayfaları, kesimlemeyi ve ARMv8'i karşılaştırmak.

8.1Bellek donanımı

Bellek yönetimini anlamak için önce belleğin kendisine bakmak gerekir. İşletim sistemi belleği soyut bir bayt dizisi olarak görür, ama bu dizinin arkasında hızları ve boyutları çok farklı birkaç katman vardır. Hangi katmanı donanımın, hangisini işletim sisteminin yönettiğini bilmek, sonraki bütün tasarım kararlarını açıklar.

8.1.1Bellek hiyerarşisi ve bellek yöneticisi

İşlemcinin doğrudan erişebildiği depolama yalnız iki yerdedir: yazmaçlar ve ana bellek. Bir komut, işlenenini yazmaçtan ya da bellekten alır; diskteki bir veriye ise doğrudan erişemez. Diskteki veri önce bir giriş/çıkış işlemiyle belleğe getirilmelidir. Bu yüzden çalışan bir programın kodu ve kullandığı veriler ana bellekte bulunmak zorundadır.

Bellek birimi de çok basit bir arayüz sunar. Ona yalnız bir adres akışı gelir: ya “şu adresi oku” ya da “şu adrese şu veriyi yaz”. Bellek birimi adresin nereden geldiğini bilmez. Adres bir komut sayacından, bir dizi indisinden ya da bir işaretçiden üretilmiş olabilir; bellek için hepsi aynıdır. Bu sadelik, adresleri belleğe varmadan önce değiştiren bir donanım katmanının neden mümkün olduğunu da gösterir.

Hız farkı ise çok büyüktür. Yazmaç bir saat turunda okunur. Ana bellekteki bir DRAM erişimi ise yüzlerce tur sürer ve bu sırada işlemci durur (stall). Aradaki boşluğu işlemcinin içindeki küçük ve hızlı bellekler, yani önbellekler (cache) kapatır. Şekil 8.1 bugünkü tipik değerleri gösterir: yazmaçlar bir nanosaniyenin altında, L1 ve L2 önbellek 1–5 ns, L3 10–20 ns, DRAM 80–120 ns, NVMe SSD 10–100 µs, sabit disk 5–10 ms. Yukarıdan aşağı inildikçe gecikme ve kapasite artar, bayt başına maliyet düşer.

Yazmaçlar< 1 ns~1 KBL1 / L2 önbellek1–5 nsKB–MBL3 önbellek10–20 ns10–100+ MBAna bellek (DRAM)80–120 ns8–512 GBSSD (NVMe)10–100 µsTBHDD / teyp5–10 msTB+hızlı, küçük, pahalıyavaş, büyük, ucuz, kalıcıgecikme
Şekil 8.1: Bellek hiyerarşisi. Yazmaçlardan diske doğru gecikme ve kapasite artar. Değerler 2026 itibarıyla tipik mertebelerdir; çekirdek ve modele göre değişir.

Programcının bellekten beklentisi aslında gerçekleşmesi imkânsız bir listedir: belleğin sınırsız, çok hızlı, kalıcı ve korumalı olması. Tek bir teknoloji bu özelliklerin hepsini birden veremez. Gerçek çözüm hiyerarşidir. Küçük ve hızlı katmanlar sık kullanılan veriyi tutar, büyük ve yavaş katmanlar geri kalanını. İyi kurulmuş bir hiyerarşi programcıya “neredeyse en üst katman kadar hızlı, neredeyse en alt katman kadar büyük” bir bellek izlenimi verir.

Hiyerarşinin katmanlarını farklı yöneticiler denetler. Önbellekleri tamamen donanım yönetir; işletim sistemi onların içeriğine karışmaz. Ana belleği ise işletim sistemi yönetir. İşletim sisteminin bu işle uğraşan kısmına bellek yöneticisi denir.

Tanım 8.1

Bellek yönetimi (memory management), ana belleğin süreçler arasında paylaştırılmasıdır. Bellek yöneticisi belleğin hangi bölgelerinin boş, hangilerinin dolu olduğunu izler; süreçlere bellek ayırır, işleri bitince geri alır ve süreçleri birbirinden korur. Bellek yetmediğinde süreçlerin ya da sayfaların bir kısmını diske taşımak da onun işidir.

Bellek yöneticisinin işi ile işlemci çizelgeleyicisinin işi sık karıştırılır. Hazır kuyruğundan sıradaki süreci seçmek çizelgeleyicinin görevidir. Bellek yöneticisi ise “hangi süreç nerede, ne kadar yer kaplıyor, kime ne kadar daha yer verilebilir” sorularıyla ilgilenir.

8.1.2Ana bellek türleri

“Bellek” sözcüğü gündelik dilde birkaç farklı donanım için kullanılır. Tablo 8.1'de dört temel tür karşılaştırılmıştır. Ana bellek olarak kullanılan tür RAM'dir (random access memory, rastgele erişimli bellek). RAM okunur ve yazılır, hızlıdır, ama uçucudur: güç kesilince içeriği kaybolur. DRAM hücreleri küçük kondansatörlerdir ve yük sızdığı için düzenli aralıklarla yenilenmeleri gerekir. SRAM ise yenileme istemez, daha hızlıdır ama daha pahalıdır ve daha çok yer kaplar. Bu yüzden ana bellek DRAM (bugün DDR5, LPDDR5X), önbellekler SRAM'dir.

Tablo 8.1: Ana bellek türleri.
TürÖzellikKullanım
RAM (rastgele erişimli)Okunur ve yazılır, hızlı, uçucu. DRAM yenileme ister; SRAM daha hızlı ve pahalı.Ana bellek (DRAM: DDR5, LPDDR5X), önbellekler (SRAM)
ROM (salt okunur)Üretimde yazılır, değiştirilemez, kalıcı, ucuz.Basit aygıtlar, mikrodenetleyicilerde sabit kod
EEPROM / FlashElektrikle silinip yeniden yazılabilir, kalıcı; yazma yavaş, yazma ömrü sınırlı.UEFI/BIOS ürün yazılımı (SPI flash), SSD, USB bellek, telefon depolaması (NAND flash)
CMOS / NVRAMPil ya da flash ile korunan küçük alan.Saat, önyükleme ayarları

ROM (read-only memory) üretim sırasında yazılır ve sonradan değiştirilemez. Kalıcıdır ve ucuzdur; basit aygıtlarda ve mikrodenetleyicilerde değişmeyecek kodu tutar. EEPROM ve onun bir türü olan flash bellek elektrikle silinip yeniden yazılabilir. Kalıcıdırlar, ama yazma işlemi okumadan çok yavaştır ve bir hücre ancak sınırlı sayıda yazılabilir. Bugünkü bilgisayarlarda BIOS/UEFI kodu ROM'da değil, anakarttaki bir SPI flash yongasında durur; ürün yazılımının güncellenebilmesi bundandır. SSD'ler, USB bellekler ve telefonların depolaması da NAND flash kullanır. Son olarak CMOS ya da NVRAM, bir pil ya da flash ile korunan küçük bir alandır; saat ve önyükleme ayarları burada tutulur.

Bellek yönetiminin konusu bu türlerden yalnız biridir: süreçlerin çalıştığı ana bellek, yani DRAM. Notun geri kalanında “bellek” sözcüğü bu anlamda kullanılır.

8.1.3Önbellek satırları ve yerellik

Önbellek ile ana bellek arasında veri tek tek baytlar hâlinde değil, sabit boyutlu bloklar hâlinde taşınır. Bu bloğa önbellek satırı (cache line) denir. Bugünkü işlemcilerin çoğunda satır 64 bayttır. Bellek bu boyutta ardışık satırlara bölünmüş gibi düşünülür: satır 0, 0–63 adreslerini; satır 1, 64–127 adreslerini kapsar ve böyle sürer.

Bir a adresinin hangi satırda olduğu basit bir bölmeyle bulunur. Satır numarası ⌊a / 64⌋, satır içindeki konum a mod 64'tür. 64 = 26 olduğu için bu bölme gerçekte bir bit ayırma işlemidir: adresin alt 6 biti satır içi konumu, geri kalan üst bitleri satır numarasını verir. Şekil 8.2 200 adresini örnek alır. 200 ikili tabanda 11001000'dır. Alt altı bit 001000, yani 8; üst iki bit 11, yani 3'tür. Gerçekten de 3 × 64 + 8 = 200.

Adres 200 (64 B satır):11001000satır no = 3satır içi ofset = 8 (6 bit)satır 00–63satır 164–127satır 2128–191satır 3192–255
Şekil 8.2: 64 baytlık satırlarda adres 200'ün bölünmesi: üst bitler satır numarası 3, alt 6 bit satır içi konum 8.

İşlemci bir sözcük okurken donanım önce önbelleğe bakar. Satır önbellekte varsa buna isabet (hit) denir ve veri L1'den 4–5 turda gelir. Yoksa ıska (miss) olur; istek bir alt düzeye, en kötü durumda DRAM'e iner ve yüzlerce tur sürer. Önbellek pahalı olduğu için küçüktür. Tek bir büyük önbellek yerine L1, L2, L3 gibi birbirinden büyük ve yavaş düzeyler kurulur.

Küçük bir önbelleğin büyük bir belleğin önünde işe yaramasının nedeni programların davranışıdır. Programlar belleğe rastgele erişmez; erişimlerde iki düzen vardır.

Zamansal yerellik (temporal locality)
Az önce kullanılan bir veri kısa süre içinde yine kullanılır. Döngü sayacı, döngü gövdesinin komutları ve sık çağrılan bir fonksiyon böyledir.
Uzamsal yerellik (spatial locality)
Bir adrese erişen program kısa süre içinde komşu adreslere de erişir. Bir diziyi baştan sona gezmek, ardışık komutları yürütmek böyledir.

Önbellek satırı uzamsal yerelliği kullanır: bir baytlık ıskada 64 baytın tamamı getirildiği için komşu baytlara sonraki erişimler isabet olur. Zamansal yerelliği ise satırın önbellekte kalması kullanır.

8.1.4Bir okumanın hiyerarşideki yolu

İşlemci LOAD R1, [x] komutuyla x değişkenini okumak istesin. Donanım önce en yakın ve en hızlı düzeye, L1'e bakar. Satır L1'de yoksa yaklaşık 1 ns kaybedilmiştir ve arama L2'de sürer. L2'de de, L3'te de ıska olursa, düzeylerin gecikmeleri toplanır: 1 + 4 + 15 = 20 ns. Sonunda veri DRAM'den gelir; bu da yaklaşık 80 ns ekler ve toplam 100 ns'yi bulur (Şekil 8.3).

CPU(yazmaç)L1~1 nsıskasatırL2~4 nsıskasatırL3~15 nsıskasatırDRAM~80 nstoplam gecikme: ~100 ns
Şekil 8.3: Üç düzeyde de ıska olan bir okuma. Veri DRAM'den gelir; tek bir sözcük değil, 64 baytlık satırın tamamı L3, L2 ve L1'e yerleştirilir. Toplam gecikme yaklaşık 100 ns'dir.

DRAM'den gelen tek bir sözcük değil, sözcüğün bulunduğu 64 baytlık satırın tamamıdır. Satır yolda L3'e, L2'ye ve L1'e de yerleştirilir. Aynı satırdaki başka bir veriye ikinci kez erişildiğinde artık L1 isabeti olur ve yaklaşık 1 ns yeter (Şekil 8.4). İlk erişimin 100 ns'lik bedeli, satırın geri kalanına yapılan erişimlerle paylaşılmış olur. Önbelleği işe yarar kılan yerellik budur.

CPU(yazmaç)L1~1 nssatırL2~4 nssatırL3~15 nssatırDRAM~80 nsisabet!toplam gecikme: ~1 ns
Şekil 8.4: Aynı satıra ikinci erişim L1 isabetidir; gecikme yaklaşık 1 ns'ye iner.

8.1.5Kod izleme: doğrudan eşlemeli önbellek

Bir satır önbelleğin hangi yerine konabilir? En basit cevap doğrudan eşlemedir (direct mapping): önbellek birkaç kümeye bölünür ve her blok yalnız tek bir kümeye gidebilir. Blok numarası b, küme sayısı K ise blok b mod K kümesine yerleşir. Kümede o anda başka bir blok varsa yeni gelen onu dışarı atar.

Aşağıdaki C parçası dört kümeli, 64 baytlık satırlı bir doğrudan eşlemeli önbelleği taklit eder. etiket dizisi her kümede o anda hangi bloğun bulunduğunu tutar; −1 boş demektir.

int adres[6] = {0, 8, 64, 256, 0, 72};
int etiket[4] = {-1, -1, -1, -1};
int isabet = 0, iska = 0;
for (int i = 0; i < 6; i++) {
  int blok = adres[i] / 64;
  int kume = blok % 4;
  if (etiket[kume] == blok) isabet++;
  else { etiket[kume] = blok; iska++; }
}

Döngü her adres için önce blok numarasını (adres / 64), sonra kümeyi (blok % 4) hesaplar. Kümedeki etiket bu blokla aynıysa isabet sayacı artar. Değilse blok kümeye yazılır ve ıska sayacı artar. Bu basitleştirilmiş modelde etiket olarak blok numarasının tamamı saklanır. Gerçek donanım küme numarasını adresten zaten bildiği için yalnız üst bitleri, yani gerçek etiketi saklar.

Tablo 8.2: Doğrudan eşlemeli önbellek kodunun izlemesi (4 küme, 64 B satır). Değerler Python ile yeniden hesaplanarak doğrulandı.
iadresblokkümeSonuçetiket[0..3] sonraAçıklama
0000ıska0, −1, −1, −1küme 0 boş
1800isabet0, −1, −1, −18, 0 ile aynı satırda: uzamsal yerellik
26411ıska0, 1, −1, −1küme 1 boş
325640ıska4, 1, −1, −1blok 4, blok 0'ı kovar (çakışma ıskası)
4000ıska0, 1, −1, −1blok 0, blok 4'ü kovar (çakışma ıskası)
57211isabet0, 1, −1, −172, 64 ile aynı satırda

Döngü bittiğinde isabet = 2, iska = 4 olur; isabet oranı %33'tür. İzlemede iki farklı ıska türü görülür. İlk üç ıska kaçınılmazdır: bloklar önbelleğe ilk kez gelir. Bunlara zorunlu ıska denir. 3. ve 4. adımlardaki ıskalar ise başka nedenlidir. 0 ve 256 adresleri farklı bloklardadır (0 ve 4), ama ikisi de 0. kümeye düşer. Önbelleğin 2. ve 3. kümeleri boş durduğu hâlde bu iki blok birbirini kovar. Bu tür ıskaya çakışma ıskası (conflict miss) denir.

Erişim sırası0M8H64M256M0M72HÖnbellek (4 küme)kümeblok (etiket)0blok 01blok 12—3—
Şekil 8.5: Doğrudan eşlemeli önbellek izlemesinin son durumu. M ıskayı, H isabeti gösterir. 0 ile 256 aynı kümeye düştüğü için birbirini kovdu.

Çakışma ıskalarını azaltmanın yolu, bir bloğa birden çok yer tanımaktır. k yollu küme ilişkili (k-way set associative) önbellekte her küme k satır tutar. Blok yine tek bir kümeye gider, ama o kümenin k yerinden herhangi birine yerleşebilir.

Örnek 8.1

Aynı dört satırlık önbellek 2 kümeli ve 2 yollu kurulsun; kümede yer gerekince en uzun süredir kullanılmayan satır çıksın. Aynı 0, 8, 64, 256, 0, 72 dizisinde kaç isabet olur?

  1. Kümeler. Bloklar 0, 0, 1, 4, 0, 1'dir. Küme numarası artık blok mod 2'dir: 0, 0, 1, 0, 0, 1.
  2. İzleme. 0: küme 0 boş, ıska. 8: blok 0 küme 0'da, isabet. 64: küme 1 boş, ıska. 256: blok 4 küme 0'a gelir; kümede bir yer daha boş olduğu için blok 0 yerinde kalır, ıska. 0: blok 0 hâlâ küme 0'da, isabet. 72: blok 1 küme 1'de, isabet.
  3. Sonuç. 3 isabet, 3 ıska. Doğrudan eşlemede 2 isabet vardı. Kazanç tamamen çakışma ıskasının ortadan kalkmasından gelir.
Sık yapılan hata

Küme numarasını adresin kendisinden hesaplamak (ör. 256 mod 4 = 0, 72 mod 4 = 0). Kümeyi belirleyen adres değil, blok numarasıdır. Önce adres satır boyuna bölünür, sonra blok numarasının küme sayısına göre kalanı alınır: 72 / 64 = 1, 1 mod 4 = 1.

8.1.6Kod izleme: ortalama erişim süresi

Bir hiyerarşinin ne kadar iyi çalıştığını tek bir sayıyla özetlemek mümkündür: ortalama bellek erişim süresi. Her erişim önce en üst düzeye uğrar ve o düzeyin süresini harcar. Iska olursa istek bir alt düzeye iner ve onun ortalama süresi eklenir. Bir düzeyin süresi s, ıska oranı m, altındaki düzeyin ortalama süresi Talt ise:

T = s + m · Talt

Formül kendini tekrar eder: alttaki düzeyin süresi de aynı biçimde hesaplanır. En alttaki DRAM her isteği karşıladığı için orada T yalnız DRAM'in süresidir. Aşağıdaki kod hesabı alttan yukarı doğru yapar.

/* L1, L2, L3, DRAM erişim süresi (ns) */
double sure[4] = {1, 4, 20, 80};
double iska[3] = {0.10, 0.40, 0.50};
double t = sure[3];         /* DRAM */
for (int k = 2; k >= 0; k--)
  t = sure[k] + iska[k] * t;
printf("ortalama = %.1f ns\n", t);

t önce DRAM'in 80 ns'siyle başlar. Döngü k = 2'den (L3) k = 0'a (L1) iner ve her turda bir üst düzeyin süresini hesaplar. Tablo 8.3 her turu gösterir.

Tablo 8.3: Ortalama erişim süresi kodunun izlemesi. Değerler Python ile yeniden hesaplanarak doğrulandı.
AdımkDüzeyHesapt (ns)
başlangıç—DRAMt = sure[3]80
12L320 + 0,50 × 8060
21L24 + 0,40 × 6028
30L11 + 0,10 × 283,8
son−1—döngü biter, ortalama = 3.8 ns yazılır3,8
L11 nsıska %10t(L1) = 3.8 nsL24 nsıska %40t(L2) = 28.0 nsL320 nsıska %50t(L3) = 60.0 nsDRAM80 nst(DRAM) = 80.0 nsortalama = 3.8 ns
Şekil 8.6: Dört düzeyin erişim süreleri, ıska oranları ve alttan yukarı hesaplanan ortalama süreler.

Sonuç dikkat çekicidir. Her erişimin %10'u L1'i ıskaladığı hâlde ortalama 3,8 ns'dir. Bu, DRAM'in 80 ns'sinden yaklaşık 21 kat hızlıdır (80 / 3,8 ≈ 21,05). Hiyerarşiyi işe yarar kılan, üst düzeylerdeki yüksek isabet oranıdır.

Formülün biçimi ıska oranlarının ne kadar etkili olduğunu da gösterir. L1'in ıska oranı %10'dan %5'e inerse, alttaki 28 ns aynı kalır ve ortalama 1 + 0,05 × 28 = 2,4 ns olur. L1'in kendi süresini 1 ns'den 0,5 ns'ye indirmek ise yalnız 0,5 ns kazandırır. Ortalamaya ıskalar hâkimdir.

Örnek 8.2

L1 2 ns'de yanıt veriyor ve isteklerin %5'inde ıskalıyor. L2 10 ns sürüyor ve kendisine gelen isteklerin %20'sini ıskalıyor. DRAM 100 ns'dir. Ortalama erişim süresi nedir? L2 olmasaydı ne olurdu?

  1. L2 düzeyi. TL2 = 10 + 0,20 × 100 = 30 ns.
  2. L1 düzeyi. T = 2 + 0,05 × 30 = 2 + 1,5 = 3,5 ns.
  3. L2 olmadan. L1 ıskaları doğrudan DRAM'e gider: 2 + 0,05 × 100 = 7 ns. İkinci düzey ortalamayı yarıya indirmiştir.
Sık yapılan hata

İkinci düzeyin ıska oranını bütün erişimlere oranlamak. L2'nin %20'lik ıska oranı L2'ye gelen istekler içindir; bütün erişimlerin yalnız %5'i L2'ye gelir. Bu yüzden DRAM'e inen erişim oranı %20 değil, 0,05 × 0,20 = %1'dir. Formül bu çarpımı kendiliğinden yapar.

8.1.7Önbellek tasarımının dört sorusu

Her önbellek tasarımı dört soruya cevap vermek zorundadır.

  1. Veri ne zaman önbelleğe konur? Genellikle ıska anında. Donanım ayrıca, erişim düzenini tahmin ederek sıradaki satırları önceden de getirir (prefetch).
  2. Hangi satıra konur? Bu eşleme sorusudur: doğrudan eşlemeli, k yollu küme ilişkili ya da tam ilişkili.
  3. Yer gerekince hangisi çıkar? Bu değiştirme politikasıdır: LRU'ya yakın yaklaşımlar, rastgele seçim ya da FIFO.
  4. Çıkan satır nereye gider? Satır değiştirilmişse (“kirli” ise) çıkarken alt düzeye yazılır (write-back). Diğer seçenek her yazmayı hemen alt düzeye iletmektir (write-through).

Bu dört sorunun yanında üç ödünleşim vardır. Büyük önbellek daha çok isabet verir, ama daha yavaş ve daha pahalıdır. Daha akıllı bir değiştirme politikası isabeti artırır, ama donanımı karmaşıklaştırır. Çok çekirdekli işlemcilerde aynı satırın farklı çekirdeklerdeki kopyalarının güncel tutulması gerekir; buna tutarlılık denir ve MESI gibi protokollerle sağlanır.

Aynı dört soru yalnız donanım önbelleklerine özgü değildir. İşletim sisteminin yönettiği “önbelleklerde” de karşımıza çıkar: dosyaların tutulduğu sayfa önbelleğinde, sanal bellekte ve TLB'de. Bu notta anlatılacak TLB doğrudan bir önbellektir; sanal bellek ise ana belleği diskin önbelleği gibi kullanır.

8.2Adres alanı

Birden çok programın aynı bellekte güvenle çalışabilmesi için iki problemin çözülmesi gerekir. Birincisi korumadır: bir program başka bir programın belleğine erişememelidir. İkincisi yer değiştirmedir: bir program belleğin neresine yüklenirse yüklensin doğru çalışmalıdır. Bu kesim, iki problemin nereden çıktığını ve en basit donanım çözümünü anlatır.

8.2.1Soyutlama olmadan bellek

İlk bilgisayarlarda bellek için hiçbir soyutlama yoktu. 1960'lardan önceki ana çerçevelerde ve 1980'lerden önceki kişisel bilgisayarlarda her program fiziksel belleği olduğu gibi görürdü. MOV REGISTER1, 1000 komutu, fiziksel 1000 adresindeki sözcüğü okurdu. Programın gördüğü adres ile bellek yongasının gördüğü adres aynıydı.

Bu düzende bellekte aynı anda iki program bulunamaz. İkisi de aynı fiziksel adresleri kullanır ve biri ötekinin verisini ezer. Bellek yine de işletim sistemi ile tek bir kullanıcı programı arasında paylaşılabilir. Şekil 8.7 üç yaygın düzeni gösterir. (a)'da işletim sistemi RAM'in alt ucundadır. (b)'de işletim sistemi üst uçta, ROM'dadır. (c)'de aygıt sürücüleri üstte ROM'da, işletim sisteminin geri kalanı altta RAM'dedir. (a) ve (c) düzenlerinde kullanıcı programındaki bir hata işletim sisteminin bulunduğu bölgeye yazıp onu çökertebilir. (b) bazı gömülü sistemlerde, (c) ise MS-DOS çalıştıran ilk PC'lerde kullanıldı; orada BIOS sürücüleri ROM'daydı.

Kullanıcı programıİS (RAM)(a)0İS (ROM)Kullanıcı programı(b)0Sürücüler (ROM)Kullanıcı programıİS (RAM)(c)00xFFF…
Şekil 8.7: Soyutlama olmadan belleği işletim sistemi ve tek bir kullanıcı programı arasında paylaştırmanın üç yolu.

Bu yaklaşım tarihe karışmış değildir. Bellek yönetim birimi olmayan mikrodenetleyiciler bugün de böyle çalışır. Bunların çoğunda yine de bir bellek koruma birimi (memory protection unit, MPU) bulunur. MPU adres çevirisi yapmaz, yalnız belirli bölgelere erişim izinlerini denetler.

8.2.2Yer değiştirme problemi

İki programı aynı anda belleğe koymaya çalışınca ortaya çıkan sorunu Tanenbaum'un örneği açıkça gösterir. A ve B, 16 KB'lık iki programdır ve ikisi de 0 adresinden başlayacağı varsayılarak derlenmiştir. A'nın ilk komutu JMP 24, B'nin ilk komutu JMP 28'dir. İşletim sistemi A'yı 0 adresine, B'yi hemen üstüne, 16384 adresine yükler.

A sorunsuz çalışır. B ise ilk komutunda çöker. JMP 28, B'nin 28. baytındaki CMP komutuna atlamak ister. Ama 28 mutlak bir adrestir ve fiziksel 28 adresinde A'nın ADD komutu vardır. B, kendi kodu yerine A'nın ortasına atlar (Şekil 8.8).

(a) A programıJMP 240ADD4MOV8⋮MOV24ADD28⋮…16380(b) B programıJMP 280MOV REG1, 284ADD8⋮MOV24CMP28⋮…16380(c) ikisi birlikteJMP 240ADD4MOV8⋮MOV24ADD28⋮JMP 2816384MOV REG1, 2816388ADD16392⋮MOV16408CMP16412⋮A’yaatlar!
Şekil 8.8: Yer değiştirme problemi. (a) ve (b) 0'dan başlayacak biçimde derlenmiş iki program, (c) ikisi birlikte bellekte. B'nin JMP 28 komutu A'nın içine atlar.

Bu probleme yer değiştirme (relocation) problemi denir. Programdaki adresler, programın belleğe yüklendiği yere göre düzeltilmelidir. IBM 360'ın bulduğu ilk çözüm statik yer değiştirmedir: yükleyici programı belleğe koyarken her adres sözcüğüne programın başlangıç adresini ekler. B 16384'e yüklendiği için JMP 28, JMP 16412 olur (Şekil 8.9).

(a) A programıJMP 240ADD4MOV8⋮MOV24ADD28⋮…16380(b) B programıJMP 280MOV REG1, 284ADD8⋮MOV24CMP28⋮…16380(c) ikisi birlikteJMP 240ADD4MOV8⋮MOV24ADD28⋮JMP 1641216384MOV REG1, 2816388ADD16392⋮MOV16408CMP16412⋮+16384sabit 28 değişmemeli!
Şekil 8.9: Statik yer değiştirme. Yükleyici atlama adresine 16384 ekler; ama MOV REG1, 28 komutundaki 28 bir sabittir ve değişmemelidir.

Statik yer değiştirmenin iki zayıf yanı vardır. Birincisi yükleme yavaşlar, çünkü programın her sözcüğüne bakılması gerekir. İkincisi daha ince bir sorundur: yükleyici hangi sözcüğün adres, hangisinin sıradan bir sayı olduğunu bilemez. B'nin ikinci komutu MOV REG1, 28, yazmaca 28 sayısını yükler. Bu 28 bir adres değildir; 16384 eklenirse program yanlış çalışır. Yükleyicinin doğru karar verebilmesi için derleyicinin, programın hangi sözcüklerinin adres olduğunu gösteren ek bir yer değiştirme bilgisi üretmesi gerekir.

8.2.3Taban ve limit yazmaçları

Koruma ve yer değiştirme problemlerini birlikte çözen soyutlama adres alanıdır.

Tanım 8.2

Adres alanı (address space), bir sürecin bellekte kullanabileceği adreslerin kümesidir. Her sürecin kendine ait, diğer süreçlerinkinden bağımsız bir adres alanı vardır.

Adres alanını gerçeklemenin en basit donanımı iki yazmaçtır. Taban yazmacı (base register) sürecin bellekteki başlangıç adresini, limit yazmacı (limit register) bölgenin uzunluğunu tutar. Program bellekte bitişik bir alana olduğu gibi yüklenir. Şekil 8.10 örneğinde taban 300040, limit 120900'dür. Süreç 300040 ile 300040 + 120900 − 1 = 420939 arasındaki adreslere erişebilir.

İşletim sistemi0süreç256000süreç (aktif)300040süreç4209408800001024000300040taban120900limit
Şekil 8.10: Taban ve limit yazmaçları. Etkin süreç 300040 ile 420939 arasındaki adreslere erişebilir.

Bu iki yazmaç ayrıcalıklı komutlarla, yalnız çekirdek modunda yüklenebilir. Kullanıcı programı kendi tabanını ya da limitini değiştiremez; değiştirebilseydi koruma anlamsız olurdu. İşletim sistemi bağlam değiştirirken yeni sürecin taban ve limit değerlerini bu yazmaçlara koyar.

Tarihte bu düzenin iki uç örneği vardır. CDC 6600 süper bilgisayarı tam olarak böyle çalışıyordu. Intel 8088'in kesim yazmaçları ise yalnız taban işlevi görüyordu; limit yazmacı olmadığı için yer değiştirme vardı ama koruma yoktu.

8.2.4Donanımsal adres koruması

Taban ve limit yazmaçları korumayı her bellek erişiminde iki karşılaştırmayla sağlar. İşlemcinin ürettiği adres önce tabanla karşılaştırılır: adres tabandan küçükse tuzak oluşur. Sonra taban + limit ile karşılaştırılır: adres bu değerden küçük değilse yine tuzak oluşur. İki koşul da sağlanırsa erişim belleğe gider (Şekil 8.11). Karşılaştırmaları paralel çalışan özel donanım yapar ve erişimi yavaşlatmaz.

CPUadres420.940≥ taban ?evet< taban+limit ?Bellekİşletim sistemine tuzak: adresleme hatasıhayırhayırtaban = 300.040limit = 120.900geçerli: 300.040 … 420.939
Şekil 8.11: Donanımsal adres koruması. 420940 = taban + limit olduğu için ikinci karşılaştırma başarısız olur ve işletim sistemine tuzak üretilir.
Tablo 8.4: Taban = 300040, limit = 120900 iken beş adresin denetimi (geçerli aralık 300040 … 420939).
Adres≥ taban?< taban + limit?Sonuç
300040eveteveterişim (ilk geçerli adres)
356000eveteveterişim
420939eveteveterişim (son geçerli adres)
420940evethayırtuzak
299999hayır—tuzak; başka sürecin alanı korunur

Tuzak oluşunca denetim çekirdeğe geçer. Çekirdek bunu adresleme hatası sayar ve süreci genellikle sonlandırır; Unix sistemlerinde süreç SIGSEGV sinyali alır. Denetim yalnız kullanıcı modundaki erişimler için yapılır. Çekirdek modunda denetim yoktur, çünkü işletim sisteminin bütün belleğe erişebilmesi gerekir: bir süreci belleğe yüklemek, bir sistem çağrısının argümanını kullanıcı tamponundan okumak bunu gerektirir.

Sık yapılan hata

Taban + limit adresini geçerli saymak. Limit bölgenin uzunluğudur; geçerli adresler taban, taban + 1, …, taban + limit − 1'dir. 120900 baytlık bir bölgenin son baytı 300040 + 120899 = 420939'dadır. 420940 artık bölgenin dışındadır.

8.2.5Adres bağlama zamanları

Bir program kaynak koddan bellekteki sürece giden yolda birkaç aşamadan geçer (Şekil 8.12). Derleyici main.c'yi nesne dosyasına (main.o) çevirir. Bağlayıcı bu dosyayı diğer nesne dosyaları ve statik kütüphanelerle birleştirip çalıştırılabilir dosyayı (a.out) üretir. Yükleyici bu dosyayı belleğe alır, dinamik kütüphaneleri ekler ve süreç çalışmaya başlar.

Kaynakmain.cNesnemain.oÇalıştırılabilira.outBellektekisüreçderleyicibağlayıcıyükleyici+ diğer .o, statik .a+ dinamik .so (ld.so)MMUher erişimdederleme zamanıyükleme zamanıyürütme zamanı
Şekil 8.12: Programın derleyici, bağlayıcı ve yükleyiciden geçerek belleğe yerleşmesi. Adres bağlama bu üç aşamadan birinde yapılabilir.

Adresler bu yolculuk boyunca biçim değiştirir. Kaynak kodda adresler semboliktir: programcı count adlı bir değişken yazar. Derleyici bu sembolü göreli bir adrese bağlar: “bu modülün başından 14 bayt sonra”. Bağlayıcı ya da yükleyici de göreli adresi mutlak bir adrese çevirir, örneğin 74014. Bir sembolün mutlak adrese ne zaman bağlandığı, programın bellekte taşınıp taşınamayacağını belirler. Tablo 8.5 üç seçeneği karşılaştırır.

Tablo 8.5: Adres bağlama zamanları.
Bağlama zamanıÜretilen kodKonum değişirseDonanım
DerlemeMutlak kod (ör. MS-DOS .COM)yeniden derlenirgerekmez
YüklemeYeri değiştirilebilir kod; yükleyici adresleri düzeltiryeniden yüklenirgerekmez
YürütmeMantıksal adresler; süreç çalışırken taşınabilirhiçbir şeyMMU (taban-limit, sayfalama)

Derleme zamanında bağlamada derleyici programın belleğe nereye yükleneceğini önceden bilir ve mutlak kod üretir. MS-DOS'un .COM dosyaları böyleydi. Başlangıç adresi değişirse program yeniden derlenmelidir. Yükleme zamanında bağlamada derleyici yeri değiştirilebilir kod üretir; mutlak adresler yükleme anında, statik yer değiştirmeyle belirlenir. Başlangıç adresi değişirse program yeniden yüklenir. Yürütme zamanında bağlamada ise adres çevirisi her erişimde, program çalışırken yapılır. Süreç çalışırken bir bellek bölgesinden ötekine taşınabilir. Bunun için özel donanım gerekir. Bugünkü genel amaçlı işletim sistemlerinin hepsi yürütme zamanında bağlama kullanır.

8.2.6Mantıksal ve fiziksel adres, MMU

Yürütme zamanında bağlama, iki tür adresi birbirinden ayırır.

Tanım 8.3

Mantıksal adres (logical address), işlemcinin ürettiği adrestir; sanal adres da denir. Fiziksel adres (physical address), bellek biriminin gördüğü, bellek adres yazmacına (MAR) yüklenen adrestir. Bir programın ürettiği bütün mantıksal adreslerin kümesi mantıksal adres alanı, bunlara karşılık gelen fiziksel adreslerin kümesi fiziksel adres alanıdır.

Derleme ve yükleme zamanında bağlamada iki adres aynıdır. Yürütme zamanında bağlamada ise farklıdır ve aralarındaki dönüşümü çalışma anında bir donanım yapar: bellek yönetim birimi (memory management unit, MMU). Program yalnız mantıksal adresleri görür ve onlarla çalışır; fiziksel adresleri hiç görmez. Bir işaretçinin değerini ekrana yazdıran bir C programı, bu yüzden her zaman bir mantıksal adres yazdırır.

CPU yongasıCPUMMUsanalfiziksel adresveriyoluBellekDisk denetleyici
Şekil 8.13: MMU işlemci yongasının içindedir. İşlemci sanal adresi MMU'ya verir; MMU fiziksel adresi veriyoluna koyar.

Şekil 8.13 MMU'nun yerini gösterir. Eski sistemlerde MMU ayrı bir yongaydı. Bugünkü işlemcilerde MMU, TLB'leri ve sayfa tablosu yürüyücüsüyle birlikte her çekirdeğin içindedir. Bellek veriyoluna çıkan her adres zaten fiziksel adrestir.

8.2.7MMU ile dinamik yer değiştirme

Taban-limit düzeninin yürütme zamanındaki biçiminde MMU iki iş yapar. Önce mantıksal adresi limitle karşılaştırır; adres limitten küçük değilse tuzak üretir. Sonra adrese taban değerini ekler ve fiziksel adresi bulur. Silberschatz bu yazmaca yer değiştirme yazmacı (relocation register) der; Tanenbaum'un taban yazmacıyla aynı şeydir.

CPUmantıksal346< limit ?3.000limit yazmacıevet+14.000taban (yer değiştirme)fiziksel14.346Bellekhayırtuzak: adresleme hatası
Şekil 8.14: MMU ile dinamik yer değiştirme. Mantıksal 346, limit 3000'den küçüktür; tabanla toplanır ve fiziksel 14346 bulunur.
Örnek 8.3

Limit 3000, taban 14000 olsun. Mantıksal 346, 2999 ve 3500 adresleri nereye gider?

  1. 346. 346 < 3000, geçerli. Fiziksel adres 346 + 14000 = 14346.
  2. 2999. 2999 < 3000, son geçerli mantıksal adres. Fiziksel 2999 + 14000 = 16999.
  3. 3500. 3500 ≥ 3000: toplama yapılmadan tuzak oluşur. Karşılaştırma mantıksal adres üzerinden yapıldığı için sonuç, taban değeri ne olursa olsun aynıdır.

Program kendini 0 ile 2999 arasında görür. İşletim sistemi süreci başka bir yere taşırsa yalnız taban yazmacını değiştirir; programın tek bir adresi değişmez.

Bu düzen, yer değiştirme probleminin örneğindeki B programını da kurtarır. B için taban = 16384, limit = 16384 olsun. JMP 28 komutu bellekte olduğu gibi kalır. Komut çalışınca MMU 28'i limitle karşılaştırır, geçerli bulur ve 28 + 16384 = 16412 fiziksel adresini üretir (Şekil 8.15). Statik yer değiştirmeye de, ek yer değiştirme bilgisine de gerek kalmaz; MOV REG1, 28 içindeki sabit hiç değişmez, çünkü yalnız belleğe giden adresler çevrilir.

CPUmantıksal28< limit ?16.384limit yazmacıevet+16.384taban (yer değiştirme)fiziksel16.412Bellekhayırtuzak: adresleme hatası
Şekil 8.15: B programı taban = limit = 16384 ile çalışır. JMP 28 değişmeden kalır; MMU 16412 üretir.

Bunun bir bedeli vardır: her bellek erişiminde bir toplama ve bir karşılaştırma gerekir. Karşılaştırma hızlıdır. Toplama ise, elde yayılımı yüzünden, özel devreler kullanılmadıkça bir erişime göre yavaş kalır. Bu bedel, ileride sayfalamayla birlikte daha da büyüyecek bir sorunun ilk işaretidir: adres çevirisi her erişimin yolu üzerindedir ve çok hızlı olmak zorundadır.

8.3Bağlama ve yükleme

Bir programın belleğe yüklenmesi “dosyanın tamamını belleğe kopyala” kadar basit değildir. Program kütüphanelere başvurur; bu kütüphaneler programla birlikte mi, ayrı mı yüklenecek? Kullanılmayabilecek büyük bir kod parçası baştan mı yüklenmeli? Bu kesim bu soruların bugünkü cevaplarını anlatır.

8.3.1Dinamik yükleme

Dinamik yükleme (dynamic loading), bir rutinin çağrılana kadar belleğe alınmamasıdır. Programın ana kısmı yüklenir ve çalışmaya başlar. Bir rutine ihtiyaç duyulduğunda program onu kendisi yükletir. Hiç çağrılmayan kod hiç yüklenmez. Bu, hata işleme kodu gibi nadiren çalışan ama büyük kısımlar için yararlıdır.

Dinamik yükleme için özel donanım gerekmez; program buna göre tasarlanır. İşletim sistemi yalnız gerekli kütüphane işlevlerini sağlar. POSIX sistemlerinde bunlar dlopen, dlsym ve dlclose'dur; Windows'taki karşılıkları LoadLibrary ve GetProcAddress'tir. Aşağıdaki program matematik kütüphanesini çalışırken yükler ve içindeki sqrt işlevini çağırır.

#include <dlfcn.h>
#include <stdio.h>

int main(void) {
  void *h = dlopen("libm.so.6", RTLD_LAZY);
  if (!h) { puts(dlerror()); return 1; }
  double (*kok)(double) =
      (double (*)(double)) dlsym(h, "sqrt");
  printf("%.3f\n", kok(2.0));
  dlclose(h);
  return 0;
}

Program satır satır şöyle çalışır.

  1. Program başladığında adres alanında yalnız main'in kodu, C kütüphanesi (libc), öbek ve yığın vardır. Matematik kütüphanesi (libm) yüklü değildir.
  2. dlopen("libm.so.6", RTLD_LAZY) çağrısı dinamik bağlayıcıyı (ld.so) çalıştırır. Dinamik bağlayıcı dosyayı bulur ve mmap ile sürecin adres alanına eşler (Şekil 8.16). Yükleme programın isteğiyle, şimdi olur. Dönen h kütüphaneyi temsil eden bir tanıtıcıdır; dosya bulunamazsa NULL döner ve dlerror() nedenini bildirir.
  3. dlsym(h, "sqrt") kütüphanenin sembol tablosunda sqrt adını arar ve bu sürecin adres alanındaki adresini döndürür. Dönen void * değeri, double alıp double döndüren bir işlev işaretçisine dönüştürülür.
  4. kok(2.0) çağrısı işlev işaretçisi üzerinden yapılır ve ekrana 1.414 yazılır (%.3f biçimiyle √2).
  5. dlclose(h) kütüphanenin referans sayısını bir azaltır. Sayı 0'a inince kütüphane adres alanından çıkarılır.
mainöbeklibmlibcyığınsürecin adres alanı0yüksek
Şekil 8.16: dlopen sonrası adres alanı: libm, main ile libc'nin arasına eşlenmiştir.

Eski glibc sürümlerinde bu programı derlerken -ldl seçeneği gerekirdi. glibc 2.34'ten beri bu işlevler libc'nin içindedir ve ek seçenek gerekmez.

8.3.2Statik ve dinamik bağlama

Program bir kütüphane işlevini çağırdığında, o işlevin kodunun bir yerde bulunması gerekir. Bunun iki yolu vardır. Statik bağlamada bağlayıcı (ld), derlemeden sonra kütüphane rutinlerini çalıştırılabilir dosyanın içine kopyalar; örneğin ld *.o -lc -lm. Dinamik bağlamada ise dosyada yalnız küçük bir taslak (stub) kalır. Rutinin gerçek adresi yürütme zamanında çözülür. Tablo 8.6 iki yaklaşımı karşılaştırır.

Tablo 8.6: Statik ve dinamik bağlama.
Statik bağlamaDinamik bağlama
Kim, ne zaman?Bağlayıcı (ld), derlemeden sonraDinamik bağlayıcı (ld.so), yüklemede ya da çalışırken
Dosya.a, .lib.so, .dll, .dylib
BellekHer programda ayrı kopyaKod sayfaları bir kez bellekte, paylaşılır
GüncellemeYeniden bağlamak gerekirKütüphaneyi değiştirmek yeter (sürüm: soname)
RiskBüyük dosya; güvenlik yaması gecikirSürüm uyuşmazlığı (“DLL cehennemi”)

Dinamik bağlamanın en büyük kazancı bellektedir. Yüzlerce süreç aynı C kütüphanesini kullanır. Statik bağlamada her sürecin içinde ayrı bir kopya bulunurdu. Dinamik bağlamada kütüphanenin kod sayfaları fiziksel bellekte bir kez bulunur ve her sürecin adres alanına eşlenir (Şekil 8.17). Bu tür kütüphaneye paylaşımlı kütüphane (shared library) denir.

Süreç 1kodverilibc.soyığınSüreç 2kodverilibc.soyığınlibc kodutek kopyafiziksel bellek
Şekil 8.17: İki süreç aynı libc kod sayfalarını kendi adres alanlarına eşler; fiziksel bellekte tek kopya bulunur.

Güncelleme de kolaylaşır. C kütüphanesinde bir güvenlik açığı kapatıldığında yeni kütüphaneyi kurmak yeter; onu kullanan bütün programlar bir sonraki çalıştırmada yamalı sürümü kullanır. Statik bağlanmış programlar ise yeniden bağlanana kadar açığı taşır. Uyumsuz sürümler karışmasın diye kütüphanelerin bir sürüm adı (soname) vardır; örneğin libm.so.6'daki 6 böyle bir sürümdür. Bu önlem alınmadığında yaşanan karmaşa Windows dünyasında “DLL cehennemi” adıyla anılır.

8.3.3Tembel bağlama: PLT ve GOT

Ders kitaplarında dinamik bağlama genellikle şöyle anlatılır: taslak ilk çağrıldığında rutinin adresini bulur ve kendini bu adresle değiştirir. Bu anlatım kavramsal olarak doğrudur, ama Linux gibi ELF biçimli dosya kullanan sistemlerde değişen şey kod değildir. Gerçek düzenek iki tabloya dayanır.

PLT (procedure linkage table)
Çalıştırılabilir dosyadaki küçük atlama taslaklarının tablosudur. Program printf'i doğrudan değil, printf@plt taslağını çağırır. Kitaplardaki “taslak” budur.
GOT (global offset table)
Sürece özel, yazılabilir bir adres tablosudur. PLT taslağı, GOT'taki ilgili girdinin gösterdiği adrese dolaylı olarak atlar.

İlk çağrıda GOT girdisi gerçek printf'i değil, dinamik bağlayıcının çözücüsünü gösterir. Çözücü sembolü yüklü kütüphanelerde arar, bulduğu adresi GOT girdisine yazar ve printf'e atlar. Bundan sonraki çağrılar PLT → GOT → printf yolunu izler; araya yalnız bir dolaylı atlama girer. Bu düzene tembel bağlama (lazy binding) denir, çünkü bir sembol ancak ilk kullanıldığında çözülür. Tablo 8.7 adımları sıralar.

maincall printf@pltPLT girdisijmp *GOT[printf]GOT[printf]→ çözücüld.so çözücü_dl_runtime_resolvelibc: printfgerçek kod1. çağrı
Şekil 8.18: Tembel bağlamada ilk çağrı: PLT, GOT'taki adres üzerinden çözücüye gider.
Tablo 8.7: printf'in ilk ve sonraki çağrıları.
AdımNe olur?GOT[printf]
1main, call printf@plt ile PLT taslağına gider.çözücü
2PLT, jmp *GOT[printf] ile GOT'taki adrese atlar; bu adres çözücüdür.çözücü
3ld.so çözücüsü sembolü arar, bulduğu adresi GOT girdisine yazar.printf
4Çözücü printf'e atlar. Kod sayfaları değişmedi; yalnız sürece özel GOT sayfası değişti.printf
5Sonraki çağrılar: PLT → GOT → printf, tek dolaylı atlama.printf

Değişenin kod değil GOT olması, paylaşım için kritiktir. Kod sayfaları hiç yazılmadığı için bütün süreçler arasında paylaşılabilir kalır. Her sürecin yalnız kendi GOT sayfası farklıdır. Bu işi işletim sistemi değil, kullanıcı alanında çalışan dinamik bağlayıcı yapar.

Tembel bağlamanın güvenlik açısından bir zayıflığı vardır: GOT yazılabilir olduğu için bir saldırgan bellek taşmasıyla oradaki bir adresi değiştirip denetimi ele geçirebilir. Bu yüzden birçok dağıtım programları -z now seçeneğiyle derler. Bu “tam RELRO” düzeninde bütün semboller yükleme anında çözülür ve GOT ardından salt okunur yapılır.

Sık yapılan hata

İlk çağrıdan sonra call printf@plt komutunun hedefinin değiştiğini ya da libc'nin printf gövdesinin yeniden yazıldığını düşünmek. İkisi de olmaz: kod sayfaları salt okunurdur ve süreçler arasında paylaşılır. Değişen tek yer sürece özel GOT girdisidir.

8.3.4Statik ve paylaşımlı kütüphane oluşturma

Bir C kaynak dosyasından (func.c) iki tür kütüphane üretilebilir. Statik kütüphane şu komutlarla oluşturulur ve kullanılır:

$ gcc -c func.c -o func.o
$ ar rcs libfunc.a func.o
$ gcc main.c -o main -static -L. -lfunc
$ ./main

ar aracı nesne dosyalarını tek bir arşivde (libfunc.a) toplar. Bağlayıcı bu arşivden yalnız programın gerçekten kullandığı nesneleri kopyalar. -static seçeneği, C kütüphanesinin de statik sürümünün (libc.a) kullanılmasını ister; sonuçta hiçbir paylaşımlı kütüphaneye bağımlı olmayan bir dosya çıkar.

Paylaşımlı kütüphane için adımlar şöyledir:

$ gcc -fPIC -c func.c -o func.o
$ gcc -shared -o libfunc.so func.o
$ gcc main.c -o main -L. -lfunc
$ export LD_LIBRARY_PATH=$(pwd)
$ ./main
$ ldd ./main   (libfunc.so => ./libfunc.so)

Buradaki kilit seçenek -fPIC'tir: konumdan bağımsız kod (position-independent code). Böyle derlenen kod mutlak adres kullanmaz. Atlamalar program sayacına göreli yapılır, global veriye ise GOT üzerinden erişilir. Bu sayede aynı fiziksel sayfa her süreçte farklı bir sanal adrese eşlenebilir. Adres alanı düzeninin rastgele seçilmesi (address space layout randomization, ASLR) de buna dayanır. ldd komutu, programın hangi paylaşımlı kütüphaneleri hangi dosyalardan alacağını gösterir.

statik: her programda ayrı kopyamain1funcmain2funcpaylaşımlı: tek kopya, iki eşlememain1main2libfunc.so
Şekil 8.19: Statik bağlamada kütüphane kodu her programın içine kopyalanır; paylaşımlı kütüphanede tek kopya iki sürecin adres alanına eşlenir.

LD_LIBRARY_PATH değişkeni deneme için kullanışlıdır ama kalıcı bir çözüm değildir. Kalıcı çözüm ya kütüphanenin arama yolunu çalıştırılabilir dosyaya gömmek (-Wl,-rpath,'$ORIGIN') ya da kütüphaneyi sistem dizinine kurup ldconfig çalıştırmaktır. Bugünkü dağıtımlar programların kendisini de konumdan bağımsız (position-independent executable, PIE) derler. Böylece programın kendi kodu da ASLR ile her çalıştırmada farklı bir adrese yüklenir.

8.4Takas

Bellekteki süreçlerin toplam ihtiyacı fiziksel belleği aşarsa ne olur? Bir çözüm, o an çalışmayan süreçleri geçici olarak diske taşımaktır. Bu fikre takas (swapping) denir.

8.4.1Standart takas

Standart takasta bir sürecin tamamı geçici olarak bir yedekleme deposuna (backing store), genellikle hızlı bir diske yazılır. Süreç yeniden çalışacağı zaman belleğe geri getirilir (Şekil 8.20). Takas sayesinde süreçlerin toplam bellek ihtiyacı fiziksel belleği aşabilir ve aynı anda daha çok süreç yürütülebilir; yani çoklu programlama derecesi artar.

İşletim sistemikullanıcı alanıP1(P2 için yer)ana bellekP1P2yedekleme deposu① dışarı② içeri
Şekil 8.20: Standart takas: P1 sürecinin tamamı yedekleme deposuna yazılır, P2 belleğe okunur.

Takastan dönen süreç, belleğin daha önceki yerine değil, o an boş olan başka bir yere yerleşebilir. Bu yüzden takas, yürütme zamanında bağlama gerektirir. Derleme ya da yükleme zamanında bağlanmış bir süreç ancak tam olarak eski yerine dönebilir.

Standart takasın bedeli aktarım süresidir. Silberschatz'ın örneğinde 100 MB'lık bir süreç, 50 MB/s aktarım hızlı bir diske yazılır. Dışarı yazmak 100 / 50 = 2 s, geri okumak da 2 s sürer; gidiş-dönüş 4 saniyedir. Bu süre boyunca takas edilen süreç çalışamaz.

Takas edilecek süreç seçilirken bir kısıt daha vardır: bekleyen giriş/çıkış işlemi olan bir süreç takas edilmemelidir. Süreç bir aygıttan veri bekliyorsa, aygıt veriyi doğrudan bellek erişimiyle (DMA) sürecin tamponuna yazacaktır. Süreç takas edilip yerine başka bir süreç yüklenirse, DMA aktarımı eski fiziksel adrese, yani artık başka sürecin belleğine yazar. Çare ya böyle bir süreci takas etmemek ya da giriş/çıkışı her zaman çekirdek tamponları üzerinden yapmaktır.

Örnek 8.4

200 MB'lık bir süreç 50 MB/s aktarım hızlı bir diske takas ediliyor; gecikmeler ihmal ediliyor. Gidiş-dönüş ne kadar sürer?

  1. Dışarı. 200 / 50 = 4 s.
  2. İçeri. Yine 4 s.
  3. Toplam. 8 s. Bir zaman diliminin onlarca milisaniye olduğu düşünülürse, bütün süreci taşımanın neden bugün kabul edilmediği açıktır.

8.4.2Sayfa takası ve bugünkü sistemler

Bugün süreçlerin boyutları gigabaytlarla ölçülür. 1 GiB'lık bir süreç, 3 GB/s okuma-yazma hızlı bir NVMe SSD'de bile gidiş-dönüşte yaklaşık 2 × 1,07 / 3 ≈ 0,72 s tutar. Bu yüzden Linux, Windows ve macOS süreçleri bütün olarak değil, sayfa sayfa takas eder (Şekil 8.21). Yalnız uzun süredir kullanılmayan, yani “soğuk” sayfalar diske gider. 4 KiB'lık bir sayfa onlarca mikrosaniyede geri gelir.

ana bellekP1 s0P2 s3P1 s2P3 s1takas alanıP1 s1P1 s4P2 s0P3 s0P2 s1sayfa dışarısayfa içeri
Şekil 8.21: Sayfalamalı takas: süreçlerin tamamı değil, tek tek sayfaları bellek ile takas alanı arasında taşınır.

Bugünkü sistemler diske gitmeden önce bir ara adım daha kullanır: sıkıştırma. Soğuk sayfalar önce bellekte sıkıştırılıp küçültülür. Linux'ta zswap ve zram, Windows'ta bellek sıkıştırma, macOS'ta sıkıştırıcı bunu yapar. Sıkıştırılmış bir sayfayı açmak, onu diskten okumaktan çok daha hızlıdır.

Mobil sistemlerde durum farklıdır. Flash belleğin yazma ömrü sınırlıdır ve her yazma enerji harcar. Bu yüzden telefonlarda geleneksel takas ya hiç yoktur ya da çok sınırlıdır. Android zram ile sıkıştırma yapar; bellek yine yetmezse lmkd arka plandaki uygulamaları sonlandırır. iOS da sıkıştırma kullanır ve gerekince uygulamaları sonlandırır (jetsam). Kullanıcı açısından bu, arka plandaki bir uygulamaya dönüldüğünde onun baştan başlaması olarak görülür.

8.5Bitişik tahsis

Taban ve limit yazmaçlarıyla çalışan bir sistemde her süreç bellekte tek parça, kesintisiz bir bölge kaplar. Bu kesim, böyle bir sistemde boş bölgelerin nasıl seçildiğini ve zamanla neden küçük, işe yaramaz parçalara bölündüğünü anlatır.

8.5.1Bitişik bellek tahsisi

Bellek önce iki bölgeye ayrılır: biri işletim sistemine, öteki kullanıcı süreçlerine. İşletim sistemi belleğin alt ya da üst ucuna konabilir. Linux ve Windows çekirdeği yüksek adreslere koyar. Kullanıcı bölgesinde ise her süreç tek bir bitişik bölgededir ve koruma taban-limit yazmaçlarıyla sağlanır.

Tanım 8.4

Bitişik tahsis (contiguous allocation), her sürece tek parça, ardışık adreslerden oluşan bir bellek bölgesi verilmesidir.

Kullanıcı bölgesini süreçlere bölmenin iki yolu vardır. Sabit bölümlemede bellek baştan belirli sayıda ve boyda bölüme ayrılır; IBM OS/360'ın MFT sürümü böyle çalışırdı. Her bölüme bir süreç girer, dolayısıyla bölüm sayısı aynı anda bellekte bulunabilecek süreç sayısını, yani çoklu programlama derecesini belirler. Küçük bir süreç büyük bir bölüme girerse bölümün geri kalanı boşa gider. Değişken bölümlemede ise (OS/360 MVT) her sürece tam ihtiyacı kadar yer verilir. Süreçlerin arasında kalan boş bloklara delik (hole) denir.

Bir sorun daha vardır: süreçler büyür. Bir program malloc ile öbekten yer ister, fonksiyon çağrıları yığını derinleştirir. Bitişik bir bölgenin hemen üstünde başka bir süreç varsa büyüme imkânsızdır. Bu yüzden süreçlere baştan bir büyüme payı bırakılır (Şekil 8.22). (a)'da yalnız veri kesimi büyür. (b)'de yığın yukarıdan aşağı, veri aşağıdan yukarı büyür ve pay ikisinin arasında kalır. Süreç takas edilip geri getirildiğinde yalnız kullandığı kısım taşınır, boş pay taşınmaz.

(a)B (büyüme payı)BA (büyüme payı)AİS(b)B yığını ↓büyüme alanıB veri + kodA yığını ↓büyüme alanıA veri + kod
Şekil 8.22: Büyüyen süreçler için pay bırakmak: (a) veri kesimi büyür, (b) yığın aşağı, veri yukarı doğru büyür.

8.5.2Değişken bölümleme

Değişken bölümlemede işletim sistemi bir tablo ya da liste tutar: hangi bölgeler hangi süreçlere ait, hangi bölgeler delik. Bir süreç geldiğinde onu alabilecek kadar büyük bir delik bulunur. Süreç deliğin bir ucuna yerleşir, artan kısım daha küçük bir delik olarak kalır. Süreç bittiğinde alanı deliğe döner ve komşu deliklerle hemen birleştirilir.

1024 K'lık bir bellekte ilk 128 K işletim sistemine ait olsun ve süreçler ilk uygun deliğe yerleşsin. Tablo 8.8 olayları sırayla izler.

Tablo 8.8: Değişken bölümlemede bir olay dizisi (1024 K bellek, İS 0–128 K). Delikler “boy @ başlangıç” biçiminde; Python ile yeniden izlendi.
OlayYerleşimDelikler sonra
başlangıç—896 K @ 128
A (320 K) gelir128–448576 K @ 448
B (224 K) gelir448–672352 K @ 672
C (288 K) gelir672–96064 K @ 960
A sonlanır—320 K @ 128, 64 K @ 960
D (256 K) gelir128–38464 K @ 384, 64 K @ 960
B sonlanır—288 K @ 384, 64 K @ 960
E (192 K) gelir384–57696 K @ 576, 64 K @ 960

D, A'nın bıraktığı 320 K'lık deliğe girer ve geriye 64 K'lık küçük bir delik kalır. B sonlanınca onun 224 K'lık alanı solundaki 64 K'lık delikle birleşir ve 288 K'lık tek bir delik oluşur. Bitişik delikler birleştirilmeseydi, 288 K'lık bir isteğe yer bulunamazdı. E bu deliğe girdikten sonra toplam boş alan 96 + 64 = 160 K'dır, ama iki parçaya dağılmıştır. 100 K'lık bir süreç gelse, toplam yer yetmesine karşın sığmaz.

01024 KİS128 KD256 KE192 K96 KC288 K64 Kdelikler: 96 K @ 576, 64 K @ 960
Şekil 8.23: Olay dizisinin sonu: D, E ve C bellekte; 96 K ve 64 K'lık iki delik birbirinden ayrı.

8.5.3Yerleştirme algoritmaları

Gelen bir isteği alabilecek birden çok delik varsa hangisi seçilmeli? Bu sorunun klasik cevapları şunlardır.

İlk uyan (first fit)
Liste baştan taranır ve yeterince büyük ilk delikte durulur. Hızlıdır. Zamanla listenin başında küçük delikler birikir.
Sonraki uyan (next fit)
İlk uyanın bir türüdür; arama baştan değil, bir önceki aramanın kaldığı yerden sürer. Bays'in 1977'deki benzetimlerine göre ilk uyandan biraz kötü sonuç verir.
En iyi uyan (best fit)
Bütün liste taranır ve isteğe en yakın boydaki delik seçilir. Liste boyuna sıralı tutulmuyorsa yavaştır. Geriye işe yaramayacak kadar küçük delikler bırakır.
En kötü uyan (worst fit)
En büyük delik seçilir; amaç, artan parçanın başka bir istek için kullanılabilecek kadar büyük kalmasıdır. Benzetimlerde iyi sonuç vermez.
Hızlı uyan (quick fit)
Sık istenen boyutlar (4 K, 8 K, 12 K …) için ayrı listeler tutulur. Uygun deliği bulmak çok hızlıdır; ama serbest kalan bir bloğun komşularını bulup birleştirmek pahalıdır.

Bütün algoritmalar süreçler ve delikler için ayrı listeler tutularak hızlandırılabilir: arama yalnız delik listesinde yapılır. Bugünkü malloc gerçeklemeleri (jemalloc, tcmalloc) hızlı uyanın genelleştirilmiş bir biçimini, boyut sınıflarını kullanır. Silberschatz ve Knuth'un aktardığı benzetimlerin sonucu açıktır. Hem zaman hem bellek kullanımı açısından ilk uyan ve en iyi uyan, en kötü uyandan iyidir. İlk uyan genellikle daha hızlıdır.

8.5.4Kod izleme: en iyi uyan

Aşağıdaki fonksiyon, delik boylarını tutan bir dizide en iyi uyan aramasını yapar. Bulduğu deliği istek kadar küçültür ve deliğin indisini döndürür; yer yoksa −1 döndürür.

int en_iyi(int delik[], int n, int istek) {
  int en = -1;
  for (int i = 0; i < n; i++)
    if (delik[i] >= istek &&
        (en == -1 || delik[i] < delik[en]))
      en = i;
  if (en != -1) delik[en] -= istek;
  return en;
}
/* delik = 120, 450, 230, 300, 180 (K) */
/* en_iyi(delik, 5, 210); en_iyi(delik, 5, 160); */

en değişkeni o ana kadar bulunan en iyi adayın indisidir; −1 “henüz aday yok” demektir. Döngü her delik için iki koşula bakar. Birincisi deliğin isteğe yetmesidir (delik[i] >= istek). İkincisi ya henüz aday olmaması ya da bu deliğin mevcut adaydan küçük olmasıdır. İki koşul birlikte sağlanırsa aday değişir. Döngü bitince seçilen delik istek kadar küçültülür. Fonksiyon her çağrıda bütün listeyi gezer, yani n delik için O(n) zaman harcar.

Tablo 8.9: en_iyi fonksiyonunun iki çağrısı (delikler 120, 450, 230, 300, 180 K). Değerler Python ile doğrulandı.
Çağrıidelik[i]Kararen
istek = 2100120120 < 210, sığmaz−1
1450sığar, ilk aday1
2230sığar, 450'den küçük2
3300sığar ama 230'dan büyük2
4180sığmaz2
Dönüş 2; delik[2] = 230 − 210 = 20. Delikler: 120, 450, 20, 300, 180.
istek = 1600120sığmaz−1
1450sığar, ilk aday1
220sığmaz1
3300sığar, 450'den küçük3
4180sığar, 300'den küçük4
Dönüş 4; delik[4] = 180 − 160 = 20. Delikler: 120, 450, 20, 300, 20.
istek = 160 K120[0]450[1]20[2]300[3]20[4]
Şekil 8.24: İki çağrıdan sonra delik listesi. En iyi uyan iki kez 20 K'lık işe yaramaz delik bıraktı.

İzleme en iyi uyanın tipik davranışını gösterir: her seferinde isteğe en yakın deliği seçtiği için geriye çok küçük artıklar kalır. Aynı iki istek ilk uyanla karşılansaydı ikisi de 450'lik deliğe girerdi: önce 240, sonra 80 K kalırdı. En kötü uyanla 210 K yine 450'ye girer (240 kalır), 160 K ise o an en büyük delik olan 300'e girerdi (140 kalır). Hangi artıkların gelecekte işe yarayacağı, sonraki isteklere bağlıdır.

8.5.5Dört algoritma aynı istek dizisinde

Silberschatz'ın klasik alıştırması, algoritmaların farkını tek bir örnekte gösterir.

Örnek 8.5

Delikler adres sırasıyla 100, 500, 200, 300 ve 600 K'dır. İstekler sırayla 212, 417, 112 ve 426 K'dır. İlk uyan, sonraki uyan, en iyi uyan ve en kötü uyan her isteği nereye koyar?

  1. 212 K. İlk uyan ve sonraki uyan ilk yeterli delik olan 500'e koyar (288 kalır). En iyi uyan yetenlerin (500, 300, 600) en küçüğü 300'e koyar (88 kalır). En kötü uyan 600'e koyar (388 kalır).
  2. 417 K. İlk uyanda 288 yetmez, 600'e girer (183 kalır). Sonraki uyan 500'den sonra aramayı sürdürür; 200 ve 300 yetmez, 600'e girer (183 kalır). En iyi uyanda yetenler 500 ve 600'dür; 500'e girer (83 kalır). En kötü uyanda o anki en büyük delik 500'dür (600 artık 388); 500'e girer (83 kalır).
  3. 112 K. İlk uyan baştan arar: 100 yetmez, 288 yeter (176 kalır). Sonraki uyan en son kaldığı 600'lük deliğin artığı 183'te durur (71 kalır). En iyi uyan yetenlerin en küçüğü olan 200'e koyar (88 kalır). En kötü uyan en büyük delik 388'e koyar (276 kalır).
  4. 426 K. İlk uyanda en büyük delik 300, sonraki uyanda 300, en kötü uyanda 300'dür: yer yoktur, istek bekler. Yalnız en iyi uyanda 600'lük delik hâlâ bütündür; istek oraya girer (174 kalır).

Bu dizide dört isteğin hepsini yalnız en iyi uyan yerleştirebilir. Diğerlerinde toplam boş alan 426 K'dan fazladır, ama bu alan tek parça değildir.

100 K500 K200 K300 K600 Kİlk uyan212112176417183426 K beklerSonraki uyan21228841711271426 K beklerEn iyi uyan417831128821288426174426 K → 600 K boşluk (kalan 174 K)En kötü uyan41783212112276426 K bekler
Şekil 8.25: Dört algoritmanın aynı istek dizisinden sonraki durumu. 426 K'yı yalnız en iyi uyan yerleştirebildi.

Bu sonuç en iyi uyanın her zaman üstün olduğu anlamına gelmez. Örnek, büyük deliği bütün tutmanın işe yaradığı bir dizidir. Başka dizilerde sonuç değişir. Çalışma sorularındaki diziyi ele alalım: delikler 150, 400, 80, 250, 600 K; istekler 230, 120, 380, 90 K. Bu kez en kötü uyan 380 K'yı yerleştiremez. İlk uyan ve en iyi uyan ise dört isteği de karşılar. Aynı karşılaştırmayı farklı dizilerle denemek için Bellek Tahsisi Simülatörü kullanılabilir.

Sık yapılan hata

En kötü uyanda ilk isteğin seçtiği deliği sonraki isteklerde de en büyük delik saymak. Delikler her yerleştirmeden sonra küçülür. 212 K'dan sonra 600'lük delik 388'e iner; 417 K için o anki en büyük delik artık 500'dür.

8.5.6İç ve dış parçalanma

Değişken bölümlemede ortaya çıkan ve önceki örneklerde gördüğümüz sorunun adı dış parçalanmadır.

Tanım 8.5

Dış parçalanma (external fragmentation), bir isteği karşılayacak kadar toplam boş bellek bulunduğu hâlde bu belleğin bitişik olmamasıdır. İç parçalanma (internal fragmentation) ise sürece verilen bloğun istenenden büyük olması ve aradaki farkın bloğun içinde boşa gitmesidir.

Dış parçalanmanın büyüklüğünü Knuth'un %50 kuralı özetler. İlk uyanla çalışan bir sistemde N tahsisli blok varken, ortalamada yaklaşık 0,5 N blok parçalanma yüzünden boşa gider. Boşa giden blokların payı 0,5 N / (N + 0,5 N) = 1/3'tür; yani belleğin üçte biri kullanılamaz hâle gelebilir.

İç parçalanma ise sabit boyutlu bloklarla çalışan her düzende görülür: sabit bölümlemede, sayfalamada ve aşağıda anlatılan buddy sisteminde. Tanenbaum'un örneği ilginç bir nedeni de gösterir (Şekil 8.26). 18.464 baytlık bir delik varken 18.462 baytlık bir süreç gelirse, geriye 2 baytlık bir delik kalacaktır. Bu 2 baytı ayrı bir delik olarak listede izlemenin maliyeti, deliğin kendisinden büyüktür. Bu yüzden süreç 18.464 baytın tamamını alır ve 2 bayt bloğun içinde boşa gider.

Dış parçalanmaP1P2P3403040110 K boş, ama en büyük parça 40 Kistek 60 K ✗İç parçalanmasüreç 18.462 B2 B18.464 B blok verildi; 2 B boşa (artığı izlemekdelikten pahalı olduğundan)
Şekil 8.26: Üstte dış parçalanma: 110 K boş, ama en büyük parça 40 K olduğu için 60 K'lık istek sığmaz. Altta iç parçalanma: verilen bloğun 2 baytı kullanılmaz.

Dış parçalanmanın iki çözümü vardır. Birincisi sıkıştırmadır: süreçler kaydırılıp delikler tek bir blokta toplanır. İkincisi daha köklüdür: bir sürecin adres alanının fiziksel bellekte bitişik olma zorunluluğunu kaldırmak. Sayfalama ve kesimleme bu ikinci yolu izler.

İç parçalanmanın iyi bilinen bir kaynağı, fiziksel çerçeveleri ayırmak için Linux'ta da kullanılan buddy (kardeş) sistemidir (buddy system). Bu düzende bloklar hep ikinin kuvveti boyundadır. Bir istek, ona yeten en küçük ikinin kuvvetine yuvarlanır. Uygun boyda boş blok yoksa daha büyük bir blok ikiye bölünür; oluşan iki yarıya birbirinin “kardeşi” denir. Bir blok serbest kalınca kardeşi de boşsa ikisi yeniden birleşir. Kardeşin adresi tek bir XOR işlemiyle bulunur: adres ⊕ boy.

Örnek 8.6

1024 K'lık boş bir blokla başlayan bir buddy sisteminde sırayla A = 100 K, B = 240 K, C = 64 K ve D = 256 K istensin. Bloklar nereye yerleşir ve ne kadar iç parçalanma oluşur?

  1. A = 100 K. 128 K'ya yuvarlanır. 1024 bölünür (512 + 512), soldaki 512 bölünür (256 + 256), soldaki 256 bölünür (128 + 128). A, 0 adresindeki 128 K'lık bloğu alır. İç parçalanma 28 K. Boş: 128 K @ 128, 256 K @ 256, 512 K @ 512.
  2. B = 240 K. 256 K'ya yuvarlanır ve 256'daki boş bloğa girer. İç parçalanma 16 K.
  3. C = 64 K. Tam 64 K'dır. 128'deki boş blok bölünür (64 + 64); C, 128'deki yarıyı alır. Boş: 64 K @ 192, 512 K @ 512.
  4. D = 256 K. 256 K boş blok yoktur; 512'deki blok bölünür ve D, 512'deki yarıyı alır. Boş: 64 K @ 192, 256 K @ 768.
  5. Serbest bırakma. A bırakılınca kardeşi (0 ⊕ 128 = 128) C'nin içinde olduğundan birleşme olmaz. C bırakılınca kardeşi 192 boştur: 128 K @ 128 oluşur; onun kardeşi 0'daki A bloğu da boştur: 256 K @ 0 oluşur. B ve D de bırakılınca bloklar 512'ye, sonra 1024'e kadar birleşir.

Toplam iç parçalanma 28 + 16 = 44 K'dır. (Python ile adım adım izlendi.)

8.5.7Sıkıştırma

Tanım 8.6

Sıkıştırma (compaction), bellekteki süreçleri bir uca doğru kaydırıp bütün delikleri tek bir büyük blokta toplamaktır.

Şekil 8.27 başlangıç durumunu gösterir. İşletim sistemi 0–128 K'dadır. P1 128'de (160 K), P2 384'te (192 K), P3 640'ta (128 K), P4 928'de (64 K) başlar. Aralarda 96, 64, 160 ve 32 K'lık dört delik vardır; toplam 352 K boş alan bulunduğu hâlde en büyük delik 160 K'dır.

01024 KİS128 KP1160 K96 KP2192 K64 KP3128 K160 KP464 K
Şekil 8.27: Sıkıştırmadan önce: dört süreç arasında dört delik, toplam 352 K.

Sıkıştırma süreçleri sırayla aşağı kaydırır. P1 zaten işletim sisteminin hemen üstündedir. P2, 384'ten 288'e (128 + 160) taşınır. P3, 640'tan 480'e (288 + 192), P4 de 928'den 608'e (480 + 128) taşınır. Sonunda 672 ile 1024 arasında tek bir 352 K'lık delik kalır (Şekil 8.28). Bu iş için 192 + 128 + 64 = 384 K kopyalanmıştır. Her taşınan sürecin taban yazmacı yeni adresle güncellenir.

01024 KİS128 KP1160 KP2192 KP3128 KP464 K
Şekil 8.28: Sıkıştırmadan sonra: süreçler bitişik, tek bir 352 K'lık delik.

Sıkıştırma yalnız yürütme zamanında bağlama kullanan sistemlerde mümkündür. Süreçteki adresler yükleme anında sabitlenmişse, süreç taşındıktan sonra yanlış yerleri gösterir. Taban yazmacı kullanan sistemde ise yalnız yazmacın değeri değişir.

Sıkıştırma pahalıdır. Tanenbaum'un hesabına göre 8 baytı 8 ns'de kopyalayan bir makine, yani saniyede 1 GB, 16 GB'lık belleği yaklaşık 16 s'de sıkıştırır. Bugünkü yaklaşık 20 GB/s'lik kopyalama hızıyla bile iş yaklaşık bir saniye sürer (16 / 20 = 0,8 s; 16 GiB için ≈ 0,86 s). Bu sırada süreçler durur. Sıkıştırma (compaction), veri sıkıştırma (compression) ile karıştırılmamalıdır. Amaç veriyi küçültmek değil, blokları bitiştirmektir.

Sıkıştırma tamamen tarihe karışmış da değildir. Linux, büyük sayfalar için bitişik fiziksel çerçeve üretmek amacıyla arka planda sayfa göçü yapar; bu işi kcompactd çekirdek iş parçacığı yürütür.

8.6Boş alan yönetimi

Yerleştirme algoritmalarının hepsi aynı bilgiye dayanır: hangi bölgeler boş? Bu bilginin nasıl tutulduğu, aramanın ve serbest bırakmanın ne kadar hızlı olacağını belirler. İki temel yapı vardır: biteşlem ve bağlı liste.

8.6.1Biteşlem ve bağlı liste

Bellek önce sabit boyutlu tahsis birimlerine bölünür; birim birkaç sözcük de olabilir, birkaç kilobayt da.

Tanım 8.7

Biteşlem (bitmap), her tahsis birimi için bir bit tutan dizidir: 0 birimin boş, 1 dolu olduğunu gösterir. Bağlı liste yaklaşımında ise her süreç ve her delik için bir düğüm tutulur; düğümde türü (S: süreç, D: delik), başlangıç birimi ve uzunluk bulunur.

Şekil 8.29 aynı belleğin iki gösterimini verir. 32 birimlik bellekte A, B, C, D ve E süreçleri ile üç delik vardır. Biteşlemin ilk baytı 11111000'dır: 0–4 arası birimler A'ya ait, 5–7 boştur. Bağlı listede aynı bilgi “S 0 5, D 5 3, S 8 6, …” düğümleriyle tutulur.

(a) bellek, 32 birimABCDE08162432(b) biteşlem11111000111111111100111111111000(c) bağlı liste (S = süreç, D = delik, başlangıç, uzunluk)S 0 5D 5 3S 8 6S 14 4D 18 2S 20 6S 26 3D 29 3
Şekil 8.29: (a) Beş süreç ve üç delikli 32 birimlik bellek, (b) aynı durumun biteşlemi, (c) bağlı listesi.
Tablo 8.10: Biteşlem ile bağlı listenin karşılaştırılması.
BiteşlemBağlı liste
YapıHer tahsis birimine 1 bitHer süreç ya da delik için bir düğüm
BoyutSabit: birim 4 B ise belleğin 1/33'ü; 16 GiB bellek ve 4 KiB birimle 512 KiBSüreç ve delik sayısıyla orantılı
Aramak ardışık 0 bulmak yavaşDelik listesi gezilir; adrese göre sıralıysa birleştirme kolay

Biteşlemin boyutu birim büyüklüğüne bağlıdır ve sabittir. Birim 4 bayt, yani 32 bit ise her 32 bitlik bellek için 1 bit harcanır; biteşlem belleğin 1/33'ünü kaplar. 16 GiB bellek 4 KiB'lık birimlere bölünürse 234 / 212 = 222 birim olur. Biteşlem de 222 bit, yani 512 KiB tutar. Birim küçüldükçe biteşlem büyür; birim büyüdükçe son birimdeki iç parçalanma artar.

Örnek 8.7

512 MiB'lık bellek 4 KiB'lık birimlerle bir biteşlemde izleniyor. Biteşlem kaç bayttır?

  1. Birim sayısı. 229 / 212 = 217 birim.
  2. Bayt. Her birime 1 bit: 217 bit / 8 = 214 bayt = 16 KiB.

Sık yapılan yanlış, bit sayısını (128 Ki) bayt sanıp 128 KiB bulmaktır.

8.6.2Kod izleme: biteşlemde boş alan arama

Biteşlemin zayıf yanı aramadır. k birimlik bir süreç için yer bulmak, dizide art arda k tane 0 bulmak demektir. Aşağıdaki fonksiyon bunu doğrusal bir taramayla yapar.

/* k ardışık boş (0) birim ara */
int bul(const char bit[], int n, int k) {
  int bas = 0, uz = 0;
  for (int i = 0; i < n; i++) {
    if (bit[i] == 0) {
      if (uz == 0) bas = i;  /* yeni koşu */
      if (++uz == k) return bas;
    } else
      uz = 0;                /* koşu bozuldu */
  }
  return -1;
}

uz o anda sürmekte olan sıfır koşusunun uzunluğunu, bas da koşunun başladığı indisi tutar. Bir 0 görülünce, koşu yeni başlıyorsa (uz == 0) başlangıç kaydedilir ve uzunluk bir artar. Uzunluk k'ye ulaşınca başlangıç döndürülür. Bir 1 görülünce koşu bozulur ve uz sıfırlanır. Tablo 8.11, 10 birimlik 1 1 0 0 1 0 0 0 1 1 biteşleminde k = 3 aramasını izler.

Tablo 8.11: bul(bit, 10, 3) izlemesi. Python ile doğrulandı.
ibit[i]basuzAçıklama
0100dolu birim
1100dolu birim
2021yeni koşu
3022koşu uzar, henüz k'ye ulaşmadı
41202 birimlik koşu bozuldu; bu delik süreç için küçük
5051yeni koşu
6052koşu uzar
7053uz = k: 5 döndürülür
10110203140506071819turuncu: süren boş koşu · yeşil: bulunan alan
Şekil 8.30: Arama sonucu: 5, 6 ve 7 numaralı birimler süreç için ayrılır.

Fonksiyon 5 döndürür. Ayırıcı ardından 5, 6 ve 7 numaralı bitleri 1 yapar. Arama her seferinde dizinin başından başlar ve en kötü durumda bütün biteşlemi gezer: n birim için O(n). Büyük bir biteşlemde bu yavaştır. Gerçek gerçeklemeler biteşlemi sözcük sözcük tarayarak ve donanımın “ilk sıfır biti bul” komutlarını kullanarak hızlandırır, ama doğrusal yapı değişmez.

Örnek 8.8

18 birimlik belleğin biteşlemi 1 1 0 1 0 0 1 1 0 0 0 1 1 0 0 0 0 1'dir (birim 0'dan başlar). İlk uyanla sırayla 3, 4 ve 5 birimlik istekler geliyor. Her biri nereye yerleşir?

  1. Sıfır koşuları. [2] (1 birim), [4–5] (2 birim), [8–10] (3 birim), [13–16] (4 birim).
  2. 3 birim. İlk iki koşu yetmez; 8'den başlayan koşu tam yeter. 8–10 dolar.
  3. 4 birim. Kalan koşular [2], [4–5], [13–16]; yalnız 13 yeter. 13–16 dolar.
  4. 5 birim. Geriye 3 boş birim kalmıştır ve en uzun koşu 2'dir: istek karşılanamaz. Başlangıçta da 4'ten uzun koşu yoktu.

8.6.3Bağlı listede serbest bırakma

Bağlı listenin güçlü yanı serbest bırakmadır. Liste adrese göre sıralı tutulursa, sonlanan bir X sürecinin iki komşusu listede hemen önünde ve arkasındadır. Komşuların türüne göre dört durum vardır (Tablo 8.12).

Tablo 8.12: X sürecinin serbest bırakılmasında dört durum.
Sol komşuSağ komşuNe olur?Düğüm sayısı
süreçsüreçX'in düğümü D'ye çevrilirdeğişmez
süreçdelikX sağdaki delikle birleşir1 azalır
deliksüreçsoldaki delik X kadar uzar1 azalır
delikdeliküç düğüm tek deliğe iner2 azalır
X sonlanmadan öncedelikXdeliksonradelik
Şekil 8.31: İki yanında delik bulunan X sonlanınca üç düğüm tek bir deliğe iner.

Birleştirme yalnız komşulara bakarak, sabit zamanda yapılır. Son durumda önceki düğümü değiştirmek gerektiği için çift bağlı liste işi kolaylaştırır. Delikler ayrı bir listede, boyuta göre sıralı tutulursa en iyi uyan hızlanır. Liste baştan tarandığında ilk yeten delik zaten en küçük yetendir; ilk uyan en iyi uyana dönüşür. Ama bu kez serbest bırakma yavaşlar, çünkü komşu delikler artık listede yan yana değildir.

Bir ayrıntı daha önemlidir: delik listesini tutmak için ayrıca bellek gerekmez. Liste deliklerin kendi içinde saklanabilir. Her deliğin ilk sözcükleri o deliğin boyunu ve sonraki deliğin adresini tutar. Delik zaten boş olduğu için bu yer bedavadır.

8.6.4malloc'un içi: başlık ve gömülü boş liste

Aynı fikirler kullanıcı alanında da geçerlidir. C kütüphanesinin malloc ve free işlevleri, öbek denilen bölgeyi bir boş liste ile yönetir. Bunun için özel bir sorun çözülmelidir: boş listenin düğümleri için ayrı bir veri yapısı kurulamaz, çünkü o yapıya yer ayırmak için yine malloc çağırmak gerekir. Çözüm, düğümü boş parçanın kendi içine gömmektir.

Örnek ayırıcıda her parçanın başında 16 baytlık bir başlık vardır. Parça kullanımdaysa başlıkta boyu ve sabit bir sihirli sayı, boşsa boyu ve sonraki boş parçanın adresi bulunur. Tablo 8.13 4096 baytlık bir öbek üzerindeki işlemleri izler.

Tablo 8.13: 4096 baytlık öbekte 16 baytlık başlıklı ayırıcı. Python ile doğrulandı.
İşlemHesapBoş liste sonra
başlangıç4096 − 16 = 4080[4080]
p1 = malloc(200)200 + 16 = 216 B kullanılır; 4080 − 216 = 3864[3864]
p2 = malloc(100), p3 = malloc(300)3864 − 116 − 316 = 3432[3432]
free(p2)başlık (char *)p2 − 16'dan boy 100 okunur[100] → [3432]
free(p1)komşu boş parçayla birleşir: 216 + 116 = 332, boy 332 − 16 = 316[316] → [3432]
öbek: 4.096 BBp1: 200Bboş: 100Bp3: 300Bboş: 3.432B = 16 B başlık: kullanımdaysa {boy, sihirli sayı}, boşsa {boy, sonraki}boş liste:head → [100] → [3.432] → NULLfree(p2): başlık = (char *)p2 − 16 → boy 100 okunur, parça listenin başına eklenir
Şekil 8.32: free(p2) sonrası öbek: 100 baytlık boş parça listenin başına eklenmiştir. 150 baytlık bir istek artık oraya sığmaz.

free işlevi boyut almaz; boyu, kendisine verilen adresin 16 bayt gerisindeki başlıktan okur. free(p2)'den sonra 100 baytlık bir boş parça vardır, ama 150 baytlık bir istek ona sığmaz: kullanıcı alanında da dış parçalanma oluşur. free(p1) ile p1'in parçası adres sırasındaki komşusu olan boş parçayla birleşir ve liste kısalır.

Sihirli sayı
Başlıktaki sabit desen bir tampon taşması, aynı işaretçinin iki kez serbest bırakılması ya da yanlış bir işaretçi yüzünden bozulabilir. Kütüphane bunu fark edip programı durdurabilir.
Gerçek istek
malloc(n) en az n + başlık kadar bayt tüketir. Ayrıca adresler hizalanmak zorunda olduğu için boy yukarı yuvarlanır; glibc'de hizalama 16 bayttır.
Sistem çağrısı mı?
Hayır, malloc bir kütüphane işlevidir. Öbekte yer bitince brk/sbrk sistem çağrılarıyla öbeği büyütür. Büyük istekleri (glibc'de 128 KiB ve üstü) doğrudan mmap ile alır.
Örnek 8.9

Aynı 16 bayt başlıklı ayırıcıda 4096 baytlık öbekten üç kez malloc(1000) yapılıyor. Boş parçanın boyu kaçtır? Dördüncü bir malloc(1040) karşılanabilir mi?

  1. Her çağrı. 1000 + 16 = 1016 bayt tüketir.
  2. Kalan. 4080 − 3 × 1016 = 4080 − 3048 = 1032 bayt.
  3. Dördüncü istek. 1040 > 1032 olduğu için bu öbekten karşılanamaz. Ayırıcı öbeği sbrk ya da mmap ile büyütür.
Sık yapılan hata

free'ye malloc'un döndürmediği bir adres vermek, örneğin free(p + 4). Ayırıcı başlığı p + 4 − 16 adresinde arar; orada geçerli bir başlık yoktur. Sonuç tanımsız davranıştır. Sihirli sayı denetimi olan bir ayırıcı programı durdurur; olmayan bir ayırıcı boş listeyi bozar ve hata çok sonra, ilgisiz bir yerde ortaya çıkar.

8.7Sayfalama

Bitişik tahsisin bütün sorunları tek bir varsayımdan doğar: sürecin fiziksel bellekte tek parça durması. Sayfalama bu varsayımı kaldırır. Süreç küçük, eşit parçalara bölünür ve her parça belleğin boş olan herhangi bir yerine konabilir. Bugünkü bütün genel amaçlı işletim sistemleri sayfalama kullanır.

8.7.1Temel fikir

Tanım 8.8

Sayfalamada (paging) fiziksel bellek sabit boyutlu bloklara, çerçevelere (frame) bölünür. Mantıksal bellek de aynı boyda bloklara, sayfalara (page) bölünür. Her sürecin bir sayfa tablosu (page table) vardır; tablo, sürecin her sayfasının hangi çerçevede bulunduğunu tutar.

Bu düzenin sonuçları hemen görülür. Sürecin fiziksel bellekteki yeri bitişik olmak zorunda değildir; ardışık sayfalar birbirinden uzak çerçevelerde durabilir (Şekil 8.33). Bu yüzden dış parçalanma yoktur: her boş çerçeve, her sürecin her sayfası için uygundur. N sayfalık bir süreç için herhangi N boş çerçeve yeter. İç parçalanma ise yalnız sürecin son sayfasında kalır, çünkü süreç boyu sayfa boyunun tam katı olmayabilir.

mantıksal belleksayfa 0sayfa 1sayfa 2sayfa 3sayfa tablosu01142337fiziksel bellek01sayfa 023sayfa 24sayfa 1567sayfa 3
Şekil 8.33: Sayfalama. Mantıksal sayfalar, sayfa tablosu üzerinden bitişik olmayan fiziksel çerçevelere eşlenir: 0 → 1, 1 → 4, 2 → 3, 3 → 7.

İşletim sistemi hangi çerçevelerin boş olduğunu izler ve her sürecin sayfa tablosunu kurar. Takas için kullanılan yedekleme deposu da aynı boyda bloklara bölünür; böylece bir sayfa diskteki herhangi bir bloğa yazılıp herhangi bir çerçeveye geri okunabilir.

8.7.2Adresin (p, d) olarak bölünmesi

Sayfalamada işlemcinin ürettiği her mantıksal adres iki parçaya bölünür: sayfa numarası p ve sayfa içi konum d (offset). Mantıksal adres alanı 2m bayt, sayfa boyutu 2n bayt ise adresin alt n biti konumu, üst m − n biti sayfa numarasını verir (Şekil 8.34).

p: sayfa numarasım − n bitd: sayfa içi konumn bitmantıksal adres alanı 2^m, sayfa boyutu 2^n32 bit, 4 KiB:p = 20 bitd = 12 bit
Şekil 8.34: Mantıksal adres sayfa numarası p ve sayfa içi konum d olarak ikiye bölünür. 32 bit adres ve 4 KiB sayfada p 20 bit, d 12 bittir.

Sayfa tablosu p'yi bir çerçeve numarası f'ye çevirir. Konum hiç değişmez. Fiziksel adres çerçevenin başlangıcına konumun eklenmesiyle bulunur:

p = ⌊a / sayfa boyu⌋,   d = a mod sayfa boyu,   fiziksel = f × sayfa boyu + d

Sayfa boyu ikinin bir kuvveti olduğu için bu bölme ve kalan, birer bit kaydırma ve maskeleme işlemidir. 4 KiB'lık sayfada p = a >> 12, d = a & 0xFFF'tir. Donanım bu işlemleri bedavaya yapar; ikinin kuvveti olmayan bir sayfa boyu gerçek bir bölücü devre gerektirirdi.

Tablo 8.14: Adres bölme örnekleri (Python ile doğrulandı).
ÖrnekHesapSonuç
32 bit adres, 4 KiB sayfa4096 = 212 → d 12 bit, p = 32 − 12 = 20 bit220 = 1.048.576 sayfa
0x12345678p = 0x12345678 >> 12, d = 0x12345678 & 0xFFFp = 0x12345 (74.565), d = 0x678 (1.656)
16 bit adres, 4 KiB (Tanenbaum)8196 = 2 · 4096 + 4p = 2, d = 4
48 bit (x86-64), 4 KiBp = 48 − 1236 bit sayfa numarası

Onaltılık yazımda bölme özellikle kolaydır: her onaltılık hane 4 bittir, dolayısıyla 12 bitlik konum son üç hanedir. 0x12345678'in son üç hanesi 0x678 konumu, geri kalanı 0x12345 sayfa numarasını verir.

Sık yapılan hata

Fiziksel adresi bulurken konumu unutmak ya da mantıksal adresin tamamını çerçeve başına eklemek. 2 KiB'lık sayfada mantıksal 9000 ve 4 → 7 eşlemesi için doğru hesap şudur: p = ⌊9000 / 2048⌋ = 4, d = 9000 − 8192 = 808, fiziksel = 7 × 2048 + 808 = 15144. Yalnız 7 × 2048 = 14336 yazmak konumu unutmaktır; 14336 + 9000 = 23336 ise bütün adresi eklemektir.

8.7.3Sayfa tablosuyla çeviri

Silberschatz'ın küçük örneği çeviriyi elle izlemeye uygundur. Sayfa boyu 4 bayt, fiziksel bellek 32 bayttır; yani 8 çerçeve vardır. Sürecin 16 baytlık mantıksal belleği a'dan p'ye kadar harflerle doludur ve dört sayfa kaplar. Sayfa tablosu 0 → 5, 1 → 6, 2 → 1, 3 → 2'dir.

Tablo 8.15: 4 baytlık sayfalarla çeviri (Silberschatz Şekil 9.10).
MantıksalHarfpdÇerçeveFiziksel
0a0055 × 4 + 0 = 20
3d0355 × 4 + 3 = 23
4e1066 × 4 + 0 = 24
13n3122 × 4 + 1 = 9
mantıksala0b1c2d3e4f5g6h7i8j9k10l11m12n13o14p15sayfa tablosu05162132fiziksel bellek (32 bayt)0çerçeve 04ijklçerçeve 18mnopçerçeve 212çerçeve 316çerçeve 420abcdçerçeve 524efghçerçeve 628çerçeve 7adres 13 → p = 3, d = 12 × 4 + 1 = 9
Şekil 8.35: 4 baytlık sayfalarla çeviri: mantıksal 13 (n), sayfa 3, konum 1'dir; çerçeve 2'de fiziksel 9'a karşılık gelir.

Örnek iki önemli noktayı gösterir. Birincisi, sürecin harfleri fiziksel bellekte sırasını korumaz: i…l çerçeve 1'de, m…p çerçeve 2'de, a…d çerçeve 5'tedir. Program yine de belleğini a'dan p'ye ardışık görür. İkincisi, sayfa tablosu girdisinin indisi sayfa numarasıdır; sayfa numarası tabloda ayrıca saklanmaz. Tablonun 3. girdisine bakmak “sayfa 3 nerede?” sorusunun cevabıdır.

8.7.4MMU içinde çeviri

Tanenbaum'un örneğinde sanal adres 16 bittir (64 KiB, 16 sayfa), fiziksel bellek 32 KiB'tır (8 çerçeve). Sayfa boyu 4 KiB olduğu için sanal adresin üst 4 biti sayfa numarası, alt 12 biti konumdur. Çerçeve numarası 3 bittir. Aşağıdaki komutlar beş farklı adrese erişir; sayfa tablosu da yorumlarda verilmiştir.

/* 16 bit sanal adres, 4 KiB sayfa */
MOV REG, 0
MOV REG, 8192
MOV REG, 20500
MOV REG, 8196
MOV REG, 32780

/* sayfa tablosu (sayfa → çerçeve) */
/* 0→2 1→1 2→6 3→0 4→4 5→3 */
/* 9→5 11→7, diğerleri: yok */

MMU her adreste aynı işi yapar. Üst 4 biti tablo indisi olarak kullanır ve girdideki 3 bitlik çerçeve numarasını alır. Alt 12 biti hiç değiştirmeden bu numaraya ekler. Tablo 8.16 beş adresin çevirisini gösterir.

Tablo 8.16: MMU çevirisi (16 bit sanal adres, 4 KiB sayfa). Python ile doğrulandı.
Sanalİkili (p | d)pdÇerçeveFiziksel
00000 0000000000000028192
81920010 00000000000020624576
205000101 000000010100520312288 + 20 = 12308
81960010 00000000010024624576 + 4 = 24580
327801000 000000001100812yoksayfa hatası
sanal adres 205000101000000010100p = 5d = 20çerçeve 3 + d → fiziksel 12308011000000010100
Şekil 8.36: 20500 adresinin çevirisi: üst 4 bit (0101 = 5) çerçeve numarasıyla (011 = 3) değiştirilir, alt 12 bit aynen kopyalanır.

Son adres ilginçtir. 32780 = 8 × 4096 + 12'dir ve sayfa 8 eşlenmemiştir; tablodaki girdisinin mevcut/yok biti 0'dır. MMU fiziksel adres üretemez ve bir sayfa hatası (page fault) tuzağı oluşturur. Denetim işletim sistemine geçer. Sayfa diskteyse işletim sistemi onu bir çerçeveye getirip tabloyu günceller; sayfa hiç geçerli değilse süreci sonlandırır.

8.7.5Sayfa boyutu ve iç parçalanma

Sayfalamada iç parçalanma yalnız son sayfada oluşur, ama sayfa boyu seçimini etkileyecek kadar önemlidir.

Örnek 8.10

Sayfa boyu 2048 bayt, süreç 72.766 bayttır. Süreç kaç çerçeve kaplar ve ne kadar iç parçalanma oluşur?

  1. Bölme. 72.766 = 35 × 2048 + 1086. Süreç 35 tam sayfa ve 1086 baytlık bir artık kaplar.
  2. Çerçeve. Artık için de bir çerçeve gerekir: 36 çerçeve.
  3. İç parçalanma. Son çerçevede 2048 − 1086 = 962 bayt boşa gider.

En kötü durumda süreç bir sayfadan yalnız bir bayt taşar ve iç parçalanma bir sayfa eksi bir bayt olur. Süreç boyları rastgele dağılıyorsa ortalama iç parçalanma yarım sayfadır. Bu, küçük sayfa lehine bir gerekçedir. Buna karşılık küçük sayfa daha çok sayfa ve dolayısıyla daha büyük bir sayfa tablosu, daha çok TLB ıskası demektir. Büyük sayfa ise disk ile bellek arasında daha verimli aktarım ve daha az tablo girdisi sağlar.

Tanenbaum bu ödünleşimi basit bir modelle ölçer. Ortalama süreç boyu s bayt, bir tablo girdisinin boyu e bayt, sayfa boyu p olsun. Süreç başına yaklaşık s / p sayfa ve s·e / p bayt tablo gerekir; iç parçalanma da ortalama p / 2'dir. Toplam ek yük:

ek yük = s·e / p + p / 2

p küçülünce birinci terim, büyüyünce ikinci terim artar. Türev alınıp sıfıra eşitlenirse −s·e / p2 + 1/2 = 0, yani p = √(2·s·e) bulunur. s = 1 MiB, e = 8 bayt için p = √(2 × 220 × 8) = √(224) = 212 = 4096 bayt çıkar (Şekil 8.37). Bu değerde ek yük 2048 + 2048 = 4096 bayttır; 2048 ve 8192 baytlık sayfalarda ise 5120 bayta çıkar.

10 K20 K30 K5121 K2 K4 K8 K16 K32 K64 Ken az: 4 KiBsayfa boyutu p (log ölçek)
Şekil 8.37: Ek yük s·e / p + p / 2 eğrisi; s = 1 MiB, e = 8 bayt için en küçük değer 4 KiB'ta.

Gerçek sistemler de bu civarda kalır. x86-64'te temel sayfa 4 KiB'tır; ayrıca 2 MiB ve 1 GiB'lık büyük sayfalar vardır. ARM64 4, 16 ya da 64 KiB'lık sayfa kullanabilir; Apple Silicon 16 KiB kullanır.

8.7.6Boş çerçeveler

Yeni bir süreç geldiğinde işletim sistemi boş çerçeve listesinden sürecin sayfa sayısı kadar çerçeve alır ve sayfa tablosunu doldurur. Şekil 8.38 örneğinde boş liste 14, 13, 18, 20, 15'tir. Dört sayfalık süreç listenin ilk dört çerçevesini alır: sayfa 0 → 14, 1 → 13, 2 → 18, 3 → 20. Listede yalnız 15 kalır.

önceboş çerçeve listesi:14, 13, 18, 20, 15yeni süreç: 4 sayfasayfa 0sayfa 1sayfa 2sayfa 3131415161718192021sonraboş liste: 15sayfa tablosu01411321832013sayfa 114sayfa 015161718sayfa 21920sayfa 321
Şekil 8.38: Yeni sürecin dört sayfası boş çerçeve listesindeki 14, 13, 18 ve 20 numaralı çerçevelere yerleşir.

İşletim sistemi bunun için bir çerçeve tablosu tutar: her fiziksel çerçeve için çerçevenin boş olup olmadığı, doluysa hangi sürecin hangi sayfasını tuttuğu. Bu tablo yalnız tahsiste değil, başka işlerde de gerekir. Örneğin bir süreç read sistem çağrısıyla bir tampon adresi verdiğinde bu adres mantıksaldır. Çekirdek veriyi oraya yazabilmek için adresi sürecin sayfa tablosuyla fiziksel adrese çevirir.

Her sürecin ayrı bir sayfa tablosu olduğu için bağlam değiştirmede donanıma yeni sürecin tablosunu göstermek gerekir. Dağıtıcı bunu bir yazmacı güncelleyerek yapar; x86'da bu yazmaç CR3'tür. Bu ek iş bağlam değiştirme süresini uzatır.

8.7.7Sayfa tablosu girdisi

Sayfa tablosu girdisi (page table entry, PTE) yalnız çerçeve numarasını tutmaz. Yanında, donanımın ve işletim sisteminin kullandığı bir dizi bit bulunur. Şekil 8.39 x86-64'ün 64 bitlik girdisini gösterir.

NX63PK62–59—58–52çerçeve numarası51–12—11–9G8PAT7D6A5PCD4PWT3U/S2R/W1P0x86-64 sayfa tablosu girdisi (64 bit, 4 KiB sayfa)
Şekil 8.39: x86-64 sayfa tablosu girdisi (64 bit, 4 KiB sayfa): NX, koruma anahtarı, çerçeve numarası, G, PAT, D, A, önbellek bitleri, U/S, R/W ve P.
Tablo 8.17: Sayfa tablosu girdisinin bitleri.
BitAnlamı (Tanenbaum adı)Kim yazar?
PMevcut/yok (present/absent): 0 ise erişim sayfa hatası üretirİS
R/W, U/S, NXKoruma: yazılabilir mi, kullanıcı modu erişebilir mi, çalıştırılabilir miİS
AErişildi (referenced): okuma ya da yazmada 1 olur; sayfa değiştirmede kullanılırdonanım (İS sıfırlar)
DDeğiştirildi (modified, kirli): yazmada 1 olur; kirli sayfa çıkarılırken diske yazılırdonanım
PCD, PWT, PATÖnbelleği devre dışı bırakma ve yazma politikası: bellek eşlemeli G/Ç içinİS
GGlobal: bağlam değiştirmede TLB'den silinmez (çekirdek sayfaları)İS

Erişildi ve değiştirildi bitleri işletim sistemine çok değerli bilgi verir. Donanım bir sayfaya her erişimde A'yı, her yazmada D'yi 1 yapar. İşletim sistemi A bitlerini periyodik olarak sıfırlayıp hangi sayfaların son zamanlarda kullanıldığını öğrenir. D biti ise bir sayfa bellekten çıkarılırken onu diske yazmanın gerekip gerekmediğini söyler: değişmemiş bir sayfanın diskteki kopyası zaten günceldir.

Önbellek bitleri, bellek eşlemeli giriş/çıkış için gereklidir. Bir aygıtın yazmacı bellek adresine eşlenmişse, o adresten okunan değer önbellekten değil aygıttan gelmelidir. Aksi hâlde program aygıtın eski durumunu okur.

Bir sayfa bellekte değilse diskteki yeri nerede tutulur? Disk adresi PTE'de tutulmaz; donanım onu kullanmaz. P biti 0 olan bir girdinin geri kalan bitleri tamamen işletim sistemine aittir. Linux takas alanındaki yeri bu bitlere kodlar.

8.7.8Geçerli / geçersiz biti

Sayfa tablosu, sürecin adres alanının tamamı için girdi içerir; ama süreç bu alanın hepsini kullanmaz. Kullanılmayan sayfaların girdileri geçersiz olarak işaretlenir. Silberschatz'ın örneğinde adres 14 bittir (0 … 16383), sayfa 2 KiB'tır, yani 8 sayfa vardır. Süreç yalnız 0 ile 10468 arasını kullanır: sayfa 0–5 geçerlidir ve 2, 3, 4, 7, 8, 9 numaralı çerçevelerdedir; sayfa 6 ve 7 geçersizdir.

Tablo 8.18: Geçerli/geçersiz biti ile çeviri (2 KiB sayfa). Python ile doğrulandı.
Adresp, dGirdiSonuç
3.0001, 952geçerli, çerçeve 33 × 2048 + 952 = 7.096
10.4685, 228geçerli, çerçeve 99 × 2048 + 228 = 18.660 (sürecin son adresi)
12.0005, 1760geçerli, çerçeve 920.192 (program dışı, ama yakalanmaz)
12.5006, 212geçersiztuzak
süreç: 0 … 10.468sayfa 0sayfa 1sayfa 2sayfa 3sayfa 4sayfa 5sayfa 6sayfa 7sayfa tablosu02g13g24g37g48g59g60ğ70ğ14 bit adres: 0 … 16.383sayfa 2 KiB → 8 sayfag = geçerli, ğ = geçersizadres 12.000= 5 × 2048 + 1760 → sayfa 5geçerli → çerçeve 9fiziksel = 9 × 2048 + 1760 = 20.192
Şekil 8.40: 12.000 adresi programın dışındadır ama sayfa 5'in içinde kaldığı için donanım onu yakalamaz.

Üçüncü satır sayfalamanın bir sınırını gösterir. Sürecin son adresi 10.468'dir; 12.000 programa ait değildir. Ama sayfa 5, 10.240 ile 12.287 arasını kapsar ve geçerli işaretlidir. Donanım koruması sayfa ayrıntı düzeyindedir: bir sayfanın içindeki her adres ya geçerlidir ya da hiçbiri geçerli değildir. Sürecin son sayfasındaki kullanılmayan kısım, iç parçalanmanın bir başka yüzüdür. 12.500 ise sayfa 6'dadır ve geçersiz girdi bir tuzak üretir. İşletim sistemi bu tuzağın bir programlama hatası mı (SIGSEGV), yoksa henüz belleğe getirilmemiş bir sayfaya erişim mi olduğuna karar verir.

Bütün tabloyu tutmak yerine bir sayfa tablosu uzunluk yazmacı (page-table length register, PTLR) da kullanılabilir. Bu yazmaç tablonun kaç girdi içerdiğini tutar; daha büyük bir sayfa numarası doğrudan tuzak üretir. Böylece kullanılmayan sayfaların girdileri için yer harcanmaz.

8.7.9Koruma bitleri

Sayfalamada koruma da sayfa düzeyinde yapılır. İzin bitleri her PTE'de ayrı tutulur ve her erişimde, TLB girdisiyle birlikte denetlenir.

Okuma / yazma (R/W)
Kod sayfaları salt okunur eşlenir; koda yazma girişimi koruma hatası üretir.
Kullanıcı / denetçi (U/S)
Çekirdek sayfalarına kullanıcı modunda erişilemez. Böylece aynı tabloda hem çekirdeğin hem sürecin eşlemeleri bulunabilir.
Çalıştırma yasağı (NX / XD)
Veri, yığın ve öbek sayfalarında kod çalıştırılamaz. İşletim sistemleri W^X kuralını uygular: bir sayfa ya yazılabilir ya çalıştırılabilirdir, ikisi birden olamaz. Böylece yığına yazılan saldırı kodu çalıştırılamaz.
SMEP / SMAP
Çekirdek modundayken kullanıcı sayfalarındaki kodun çalıştırılmasını (SMEP) ve kullanıcı verisine izinsiz erişimi (SMAP) engeller.
Koruma anahtarları (PKU)
PTE'deki 4 bitlik anahtar ve bir kullanıcı yazmacı (PKRU) ile, sistem çağrısı yapmadan bir sayfa grubunun iznini değiştirmeyi sağlar.
Geçerli / geçersiz
Erişim izninin en kaba biçimidir: sayfa adres alanında hiç yoksa her erişim tuzaktır.

8.7.10Paylaşımlı sayfalar

Sayfalamanın önemli bir kazancı, ortak kodun kolayca paylaşılabilmesidir. Yeniden girilir (reentrant), yani çalışırken kendini değiştirmeyen kod birçok süreç arasında paylaşılabilir. Üç kullanıcı aynı metin düzenleyicisini çalıştırırsa düzenleyicinin kod sayfaları bellekte yalnız bir kez bulunur. Her sürecin sayfa tablosu bu çerçeveleri gösterir; yalnız veri sayfaları her süreç için ayrıdır (Şekil 8.41).

P1ed 1 → 3ed 2 → 4ed 3 → 6P1 veri → 1P2ed 1 → 3ed 2 → 4ed 3 → 6P2 veri → 7P3ed 1 → 3ed 2 → 4ed 3 → 6P3 veri → 2fiziksel bellek (çerçeve no. altta)0P11P32ed 13ed 245ed 36P27
Şekil 8.41: Üç süreç düzenleyici kodunun üç sayfasını aynı fiziksel çerçevelere (3, 4, 6) eşler; her sürecin verisi ayrı çerçevededir.

Paylaşımlı kütüphaneler de bu yolla paylaşılır. Paylaşımın bir sonucu vardır: sayfanın “sahibi” olan süreç bittiğinde çerçeve hemen boşaltılamaz, çünkü başka süreçler onu kullanıyor olabilir. Bu yüzden her paylaşılan çerçeve için bir referans sayısı tutulur.

Bugünkü sistemlerde paylaşımın başka biçimleri de vardır. fork() çağrısında ebeveyn ve çocuk bütün sayfaları yazınca kopyala (copy-on-write, COW) yöntemiyle paylaşır. Sayfalar iki süreçte de salt okunur işaretlenir; süreçlerden biri bir sayfaya yazmaya çalışınca koruma hatası olur ve çekirdek yalnız o sayfanın kopyasını çıkarır. Bellek eşlemeli dosyalar (mmap) ve paylaşımlı bellek (shm_open) ise iki sürecin aynı çerçeveleri görmesini sağlar; süreçler veri kopyalamadan iletişim kurar.

8.8TLB

Sayfalama dış parçalanmayı ortadan kaldırır, ama bir bedeli vardır: her bellek erişimi önce bir tablo erişimi gerektirir. Bu kesim, bu bedelin nasıl neredeyse sıfıra indirildiğini anlatır.

8.8.1Sayfa tablosu nerede durur?

Sayfa tablosu küçük olsaydı işlemcinin içinde, özel yazmaçlarda tutulabilirdi. Ama 32 bit adres ve 4 KiB sayfada bir tablonun 220, yani bir milyondan fazla girdisi vardır. Bu kadar girdi yazmaçlara sığmaz. Tablo bu yüzden ana bellekte durur. Başlangıç adresini sayfa tablosu taban yazmacı (page-table base register, PTBR) tutar; bu yazmaç x86'da CR3, ARM'da TTBR0/TTBR1'dir. Bağlam değiştirmede yalnız bu yazmaç değişir.

Bu çözümün sorunu açıktır: her veri erişimi artık iki bellek erişimidir. Önce PTBR + p adresindeki girdi okunur, sonra veriye erişilir. Bellek erişim süresi iki katına çıkar. Çözüm, son kullanılan çevirileri tutan küçük bir donanım önbelleğidir.

Tanım 8.9

TLB (translation look-aside buffer), MMU içindeki küçük, hızlı ve ilişkisel bir önbellektir. Son kullanılan (sayfa numarası, çerçeve numarası) çiftlerini tutar. İlişkisel olması, bir sayfa numarasının bütün girdilerle aynı anda, paralel olarak karşılaştırılması demektir.

Çeviri şöyle yürür (Şekil 8.42). İşlemci bir adres ürettiğinde sayfa numarası önce TLB'de aranır. Bulunursa (TLB isabeti) çerçeve numarası hemen alınır ve tablo erişimi yapılmaz. Bulunmazsa (TLB ıskası) PTBR üzerinden bellekteki tabloya gidilir, girdi okunur ve bir sonraki erişim için TLB'ye yüklenir. TLB doluysa bir girdi çıkarılır.

CPUpdTLB (ilişkisel bellek)sayfaçerçeveisabetfdfiziksel adresFiziksel bellekıskasayfa tablosu (bellekte)PTBRgirdi TLB’ye yüklenir
Şekil 8.42: TLB ile sayfalama donanımı. Sayfa numarası önce TLB'de aranır; ıskada PTBR ile bellekteki tabloya gidilir ve girdi TLB'ye yüklenir.

Bugünkü işlemcilerde tipik olarak her çekirdekte 64 girdilik bir L1 TLB ve 1–3 bin girdilik bir L2 TLB bulunur. TLB girdileri bir adres alanı kimliğiyle (address-space identifier, ASID; Intel'de PCID) etiketlenebilir. Böylece her girdi hangi sürece ait olduğunu bilir ve bağlam değiştirmede TLB'nin boşaltılması gerekmez.

8.8.2TLB isabet ve ıska izlemesi

Tanenbaum'un MMU örneğindeki sayfa tablosu (0 → 2, 2 → 6, 5 → 3, 9 → 5, 11 → 7) bu kez bir TLB ile kullanılsın. TLB 4 girdiliktir ve yer gerekince en uzun süredir kullanılmayan (LRU) girdiyi çıkarır. TLB başlangıçta boştur. Aşağıdaki sözde kod her erişimde yapılanları özetler.

p = adres / 4096;  d = adres % 4096;
if (p TLB'de) {           /* isabet */
  f = TLB[p];  p'yi en yeni yap;
} else {                  /* ıska */
  f = sayfa_tablosu[p];
  if (TLB dolu) en eski girdiyi çıkar;
  TLB'ye (p, f) ekle;
}
fiziksel = f * 4096 + d;
Tablo 8.19: 4 girdilik LRU TLB izlemesi. TLB sütununda girdiler en eskiden en yeniye sıralıdır. Python ile doğrulandı.
ErişimpSonuçFizikselÇıkanTLB sonra
0x00100ıska0x2010—0
0x20042ıska0x6004—0, 2
0x00200isabet0x2020—2, 0
0x50105ıska0x3010—2, 0, 5
0x90009ıska0x5000—2, 0, 5, 9
0x20082isabet0x6008—0, 5, 9, 2
0xB00411ıska0x700405, 9, 2, 11
0x00300ıska0x203059, 2, 11, 0
erişim dizisi (16 bit, 4 KiB sayfa)0x0010p = 0ıska0x2004p = 2ıska0x0020p = 0isabet0x5010p = 5ıska0x9000p = 9ıska0x2008p = 2isabet0xB004p = 11ıska0x0030p = 0ıskaTLB (4 girdi, LRU; soldan sağa en eskiden en yeniye)9 → 52 → 611 → 70 → 2fiziksel2·4096 + 48 = 0x2030çıkarılan: sayfa 5
Şekil 8.43: İzlemenin son adımı: 0x0030 için sayfa 0 TLB'de yoktur, en uzun süredir kullanılmayan sayfa 5 çıkarılır.

Sekiz erişimde yalnız 2 isabet vardır. İsabetlerde sayfa tablosuna hiç gidilmez. 0x0020 isabetinden sonra sayfa 0 “en yeni” konuma geçer. Ama 0xB004 geldiğinde TLB doludur ve en uzun süredir kullanılmayan girdi yine sayfa 0'dır, çünkü 0x0020'den sonra 5, 9 ve 2 kullanılmıştır. Hemen ardından gelen 0x0030 bu yüzden ıskalar. Kısa örneklerde oran düşük çıkar; gerçek programlarda ise yerellik sayesinde TLB isabet oranı %99'u aşar.

Farklı adreslerle TLB isabet ve ıskalarını, sayfa hatalarını denemek için Sayfalama ve Adres Çevirisi Simülatörü kullanılabilir.

8.8.3Etkin erişim süresi

TLB'nin kazancı tek bir sayıyla ölçülür.

Tanım 8.10

Etkin erişim süresi (effective access time, EES), TLB isabet ve ıskalarının olasılıkları hesaba katılarak bulunan ortalama bellek erişim süresidir. TLB arama süresi ε, bellek erişim süresi t, TLB isabet oranı α ise tek seviyeli tabloda:

EES = (t + ε) · α + (2t + ε) · (1 − α) = 2t + ε − t · α

Formülün mantığı basittir. İsabette TLB'ye bakılır ve tek bir bellek erişimi yapılır: t + ε. Iskada TLB'ye bakılır, sonra tablo girdisi için bir, veri için bir bellek erişimi yapılır: 2t + ε. Ortalama, bu iki sürenin olasılıklarla ağırlıklı toplamıdır.

Örnek 8.11

ε = 20 ns, t = 100 ns olsun. α = %80 ve α = %98 için etkin erişim süresi nedir?

  1. İki durum. İsabet: 20 + 100 = 120 ns. Iska: 20 + 100 + 100 = 220 ns.
  2. α = %80. 120 × 0,80 + 220 × 0,20 = 96 + 44 = 140 ns. Kapalı biçimle de 2 × 100 + 20 − 100 × 0,80 = 140 ns.
  3. Yorum. TLB olmasaydı her erişim 200 ns sürerdi; 140 ns bundan iyidir, ama yalın bellek süresinden hâlâ %40 yavaştır.
  4. α = %98. 120 × 0,98 + 220 × 0,02 = 117,6 + 4,4 = 122 ns. Yavaşlama %22'ye iner. Gerçek iş yüklerinde oran genellikle %99'un üzerindedir; α = %99'da EES 121 ns'dir.

Çok seviyeli tablolarda ıska çok daha pahalıdır. x86-64'ün dört seviyeli tablosunda bir ıska dört tablo erişimi ve bir veri erişimi gerektirir: 20 + 4 × 100 + 100 = 520 ns. Bu durumda α = %98 için EES = 120 × 0,98 + 520 × 0,02 = 117,6 + 10,4 = 128 ns, α = %99 için 118,8 + 5,2 = 124 ns olur. Iska ne kadar pahalıysa isabet oranı o kadar önemlidir. Bu yüzden donanım tablo yürüyüşünün ara seviyelerini de ayrı bir önbellekte tutar (page walk cache). Gerçek bir ıska böylece çoğu zaman dört değil, bir ya da iki bellek erişimine iner.

Örnek 8.12

Bellek erişimi 80 ns, TLB araması 10 ns, tablo tek seviyeli, isabet oranı %95'tir. (a) EES nedir? (b) EES'nin 92 ns'ye inmesi için isabet oranı en az kaç olmalıdır?

  1. İki durum. İsabet 10 + 80 = 90 ns, ıska 10 + 80 + 80 = 170 ns.
  2. (a) 0,95 × 90 + 0,05 × 170 = 85,5 + 8,5 = 94 ns.
  3. (b) 90α + 170(1 − α) ≤ 92 → 170 − 80α ≤ 92 → α ≥ 78 / 80 = 0,975. İsabet oranı en az %97,5 olmalıdır.

TLB'nin etkisini bir döngü üzerinde saymak da öğreticidir.

Örnek 8.13

for (i = 0; i < 1000; i++) a[i] = 0; döngüsü aşağıdaki dört komuta derlenir. Dizi a sanal 40.000 adresinde başlar, int 4 bayttır, sayfa 4 KiB'tır ve kod tek bir sayfadadır. Doğrusal sayfa tablosu var. TLB yokken ve varken toplam kaç bellek erişimi olur?

dongu: movl $0,(%edi,%eax,4)  /* a[i] = 0 */
       incl %eax              /* i++      */
       cmpl $1000,%eax
       jne  dongu
  1. Bir tur. Dört komut getirilir ve movl belleğe bir kez yazar: 5 erişim.
  2. TLB yok. Her erişimden önce bir PTE okunur: tur başına 10, toplam 10 × 1000 = 10.000 erişim.
  3. TLB var. 5 × 1000 = 5000 erişime yalnız TLB ıskalarındaki PTE okumaları eklenir. Dizi 40.000 ile 43.999 arasındadır ve ⌊40.000 / 4096⌋ = 9 ile ⌊43.999 / 4096⌋ = 10 numaralı sayfalara yayılır. Kod sayfasıyla birlikte üç ıska olur: 5000 + 3 = 5003 erişim.

8.8.4TLB yönetimi

TLB'nin nasıl doldurulduğu ve temizlendiği mimariden mimariye değişir. Tablo 8.20 başlıca konuları özetler.

Tablo 8.20: TLB yönetimi.
KonuAçıklama
Donanım yönetimlix86-64, ARMv8, RISC-V: ıskada MMU sayfa tablosunu kendisi yürür (page walk); işletim sistemi yalnız tabloyu kurar.
Yazılım yönetimliMIPS, SPARC, Alpha gibi eski RISC işlemcilerde ıska işletim sistemine tuzak olur; çekirdek girdiyi bulup TLB'ye yazar. Bu bütün RISC'ler için geçerli değildir: ARMv8 ve RISC-V donanım yönetimlidir.
Yumuşak / sert ıskaYumuşak ıska: sayfa bellekte, yalnız TLB'de yok (nanosaniyeler). Sert ıska: sayfa diskte, G/Ç gerekir (mikro- ya da milisaniyeler).
ASID / PCIDGirdi süreç kimliğiyle etiketlenir; bağlam değiştirmede TLB temizlenmez. Meltdown (2018) sonrası getirilen KPTI'nin maliyetini PCID azaltır.
Global girdilerÇekirdek eşlemeleri G biti ile işaretlenir ve bütün süreçlerde geçerli kalır.
TLB vurusuÇok çekirdekte bir eşleme değişince diğer çekirdeklerin TLB'leri işlemciler arası kesmelerle (IPI) geçersizlenir; yeni işlemcilerde yayın komutları vardır (AMD INVLPGB).
TLBASID 7 · sayfa 0x400 → 0x1A2ASID 9 · sayfa 0x400 → 0x3F0ASID — · sayfa 0xFFFF8 → 0x001 · Gaynı sanal sayfa, iki süreç: girdiler ASID ile ayrılırG: tüm süreçlerde geçerli (çekirdek)
Şekil 8.44: ASID etiketli TLB: iki sürecin aynı sanal sayfası (0x400) farklı çerçevelere eşlenir; G işaretli çekirdek girdisi bütün süreçlerde geçerlidir.

ASID'nin değeri bağlam değiştirmede ortaya çıkar. Etiket yoksa, yeni süreç eski sürecin çevirilerini kullanmasın diye bütün TLB boşaltılmalıdır; yeni süreç de ilk erişimlerinde art arda ıskalar. ASID ile iki sürecin aynı sanal sayfası TLB'de yan yana durabilir (Şekil 8.44). TLB vurusu (TLB shootdown) ise çok çekirdekli sistemlerin sorunudur: bir çekirdek bir eşlemeyi değiştirdiğinde, aynı eşlemeyi TLB'sinde tutan öteki çekirdeklere haber vermek zorundadır. Bu iletişim pahalıdır ve büyük sistemlerde ölçeklenmeyi sınırlar.

8.9Sayfa tablosu yapıları

Tek seviyeli, doğrusal bir sayfa tablosu küçük adres alanlarında işe yarar. Adres alanı büyüdükçe tablonun kendisi bir bellek sorununa dönüşür. Bu kesim, tabloyu küçültmenin üç yolunu anlatır: hiyerarşik, hash ve ters tablolar.

8.9.1Hiyerarşik sayfa tablosu

32 bit adres ve 4 KiB sayfada tek seviyeli tablonun 220 girdisi vardır; girdi 4 bayt ise tablo 4 MiB tutar. Bu tablo her süreç için ayrı ve bellekte bitişik olmalıdır. 100 süreçlik bir sistemde yalnız sayfa tabloları 400 MiB eder. 64 bitte durum çok daha kötüdür. 4 KiB sayfa ve 8 baytlık girdiyle tek seviyeli tablo 252 × 8 B = 32 PiB, yani yaklaşık 33,6 milyon GiB tutar. Adresin yalnız 48 biti kullanılsa bile 236 × 8 B = 512 GiB gerekir.

Çözüm, tablonun kendisini sayfalamaktır. Sanal adres iki indise ve bir konuma bölünür. 32 bit x86'da 10 bit üst tabloyu (sayfa dizini), 10 bit ikinci seviye tabloyu indisler, 12 bit konumdur (Şekil 8.45). Burada bölünen şey tablo girdisi değil, sanal adrestir.

Tanım 8.11

Hiyerarşik sayfa tablosu (hierarchical page table), sayfa tablosunun çok seviyeli bir ağaç olarak tutulmasıdır. Üst tablonun her girdisi bir alt seviye tabloyu gösterir; yalnız kullanılan bölgelerin alt tabloları vardır.

PT110 bitPT210 bitkonum12 bit32 bit sanal adresüst tablo (1024)0: kod1: veri2⋮1023: yığın2. seviye: kod2. seviye: veri2. seviye: yığıngeri kalan 1021 üst girdi: “yok”
Şekil 8.45: İki seviyeli sayfa tablosu. Üst tablonun yalnız kod, veri ve yığın girdileri ikinci seviye tablolara işaret eder; kalan 1021 üst girdi “yok”tur.

Kazanç seyrek adres alanlarından gelir. Bir sürecin 4 MiB kod, 4 MiB veri ve 4 MiB yığın kullandığını düşünelim. Her ikinci seviye tablo 1024 sayfa × 4 KiB = 4 MiB'lık bir bölgeyi kapsar. Bu süreç için bir üst tablo ve üç alt tablo yeter: 4 × 4 KiB = 16 KiB. Tek seviyeli tablo 4 MiB tutardı. Kullanılmayan bölgelerin alt tabloları hiç oluşturulmaz; üst tablodaki girdileri yalnız “yok” olarak işaretlenir.

8.9.2İki seviyeli çeviri örneği

Örnek 8.14

Tanenbaum'un örneğinde sanal adres 0x00403004'tür (4.206.596). Bu adres 4 MiB ile 8 MiB arasında, veri bölgesindedir. İki seviyeli tabloda nasıl çevrilir?

  1. Bölme. 0x00403004 = 0000000001 0000000011 000000000100. Üst 10 bit PT1 = 1, orta 10 bit PT2 = 3, konum = 4.
  2. Üst tablo. Üst tablonun 1. girdisi, 4–8 MiB bölgesinin ikinci seviye tablosunu gösterir. Her üst girdi 4 MiB'lık bir bölgeyi kapsar.
  3. İkinci seviye. O tablonun 3. girdisine bakılır. Bu girdi bölge içinde 12.288–16.383 bayt aralığındaki sayfadır; mutlak olarak 4.206.592–4.210.687. Girdinin mevcut biti 0 olsaydı sayfa hatası oluşurdu.
  4. Fiziksel adres. Girdi çerçeve numarasını verir (örnekte 0x1F3). Konum eklenir: 0x1F3004.

TLB ıskasında bu çeviri iki tablo erişimi ve bir veri erişimi gerektirir. Örnekteki çerçeve numaraları açıklama amaçlıdır.

sanal adres 0x00403004 = 4.206.59600000000010000000011000000000100PT1 = 1PT2 = 3konum = 4üst tablo00–4 MiB14–8 MiB28–12 MiB3…2. seviye tablo (4–8 MiB)00x0A210x1C02yok30x1F340x0B7çerçeve 0x1F3fiziksel 0x1F3004
Şekil 8.46: 0x00403004 adresinin çevrilmesi: PT1 = 1, PT2 = 3, konum = 4; çerçeve 0x1F3, fiziksel 0x1F3004.

32 bit x86'da üst tablonun adresi CR3'tedir. İki seviyedeki tabloların her biri tam bir 4 KiB'lık sayfayı doldurur: 1024 girdi × 4 bayt. Bu seçim tesadüf değildir. Tablonun bir sayfaya tam sığması, alt tabloların da sıradan sayfalar gibi herhangi bir çerçeveye konmasını, gerekirse diske takas edilmesini sağlar.

8.9.3Kod izleme: iki seviyeli çeviri

MMU'nun yaptığı iş C ile şöyle yazılabilir:

/* 32 bit: 10 + 10 + 12 bit */
uint32_t cevir(uint32_t va) {
  uint32_t p1 = va >> 22;
  uint32_t p2 = (va >> 12) & 0x3FF;
  uint32_t d  = va & 0xFFF;
  pte_t e1 = ust[p1];
  if (!e1.p) sayfa_hatasi(va);
  pte_t e2 = e1.tablo[p2];
  if (!e2.p) sayfa_hatasi(va);
  return (e2.cerceve << 12) | d;
}

va >> 22 üst 10 biti alır. (va >> 12) & 0x3FF önce konumu atar, sonra alt 10 biti, yani orta indisi bırakır. va & 0xFFF konumdur. Fonksiyon üst tablonun girdisine, oradan ikinci seviye tablonun girdisine gider; iki adımda da mevcut biti 0 ise sayfa hatası üretir. Sonunda çerçeve numarasını 12 bit sola kaydırıp konumla birleştirir.

Tablo 8.21: cevir fonksiyonunun iki adresle izlenmesi. Üst tabloda yalnız girdi 2 geçerli; onun tablosunda girdi 1 → çerçeve 0x2F. Python ile doğrulandı.
Değişkenva = 0x00801A3Cva = 0x01403004
p1 = va >> 2225
p2 = (va >> 12) & 0x3FF0x801 & 0x3FF = 10x1403 & 0x3FF = 3
d = va & 0xFFF0xA3C0x004
e1 = ust[p1]P = 1 (1. bellek erişimi)P = 0 → sayfa hatası
e2 = e1.tablo[p2]çerçeve 0x2F, P = 1 (2. erişim)—
dönüş(0x2F << 12) | 0xA3C = 0x0002FA3C—
va = 0x014030040000000101p1 = 50000000011p2 = 3000000000100d = 0x004ust[5]: yokP = 0 → sayfa hatası (tuzak)
Şekil 8.47: 0x01403004 için üst tablonun 5. girdisi geçersizdir: 20–24 MiB bölgesi için ikinci seviye tablo hiç yoktur.

İlk adreste üçüncü bellek erişimi veriye yapılır. İkinci adres 20–24 MiB bölgesindedir. Bu bölge için ikinci seviye tablo hiç oluşturulmamıştır; MMU üst tabloda tuzak üretir. Tablo için bellek harcanmamış olması, hiyerarşik tablonun bütün kazancıdır.

Örnek 8.15

Aynı 10 + 10 + 12 bölünmesinde bir süreç adres alanının en altındaki 12 MiB'ı ve en üstteki 4 MiB'ı kullanıyor. Kaç sayfa tablosu gerekir?

  1. Alt tablolar. Her alt tablo 4 MiB kapsar: alttaki 12 MiB için 3, üstteki 4 MiB için 1 tablo.
  2. Toplam. 4 alt tablo + 1 üst tablo = 5 tablo = 20 KiB. Doğrusal tablo 4 MiB olurdu.

8.9.4x86-64: dört ve beş seviyeli sayfalama

x86-64'te her sayfa tablosu 512 girdi × 8 bayt = 4 KiB'tır; yani yine tam bir sayfadır ve her seviye 9 bitlik bir indis kullanır. Dört seviye ve 12 bitlik konumla 4 × 9 + 12 = 48 bitlik sanal adres elde edilir; bu 256 TiB'lık bir adres alanıdır (Şekil 8.48). Seviyelerin adları üstten alta PML4, PDPT, PD ve PT'dir.

x86-64, 4 seviye (48 bit sanal adres, 256 TiB)işaret63–48PML49 bitPDPT9 bitPD9 bitPT9 bitkonum12 bitx86-64, 5 seviye (LA57: 57 bit, 128 PiB)işaret63–57PML59PML49PDPT9PD9PT9konum12
Şekil 8.48: x86-64 sanal adresinin dört ve beş seviyeli sayfalamada bölünmesi: her seviye 9 bit, konum 12 bit.

64 bitlik adresin üst 16 biti kullanılmaz ama rastgele de olamaz: hepsi bit 47'nin kopyası olmalıdır. Böyle adreslere kanonik adres denir. Sonuçta adres alanı ikiye ayrılır: alt yarı kullanıcıya, üst yarı çekirdeğe verilir. Beş seviyeli sayfalama (LA57) bir PML5 seviyesi ekler ve 57 bitlik, 128 PiB'lık sanal adres verir. Intel Ice Lake sunucu işlemcileri ve AMD Zen 4 ile sonrası bunu destekler; Linux 4.14'ten beri kullanabilir.

Hiyerarşik tablonun 64 bitte de işe yaramasının nedeni budur: 64 bitin hepsi kullanılmaz ve kullanılan adres alanı da seyrektir. Bugünkü x86-64 ve ARMv8 tam olarak hiyerarşik tablo kullanır. Bir orta seviye girdisinde PS biti 1 ise yürüyüş orada biter: PD seviyesinde 2 MiB'lık, PDPT seviyesinde 1 GiB'lık büyük bir sayfa bulunmuş olur.

8.9.5Dört seviyeli yürüyüş

Örnek 8.16

Kullanıcı yığınına yakın bir adres olan VA = 0x00007FFE12345678 dört seviyede nasıl çevrilir?

  1. Bölme (9 + 9 + 9 + 9 + 12). PML4 = (VA >> 39) & 0x1FF = 255, PDPT = (VA >> 30) & 0x1FF = 504, PD = (VA >> 21) & 0x1FF = 145, PT = (VA >> 12) & 0x1FF = 325, konum = 0x678.
  2. PML4. CR3, PML4 tablosunun çerçevesini verir (0x1A2B). Girdi 255 okunur; PDPT tablosunun çerçevesi 0x3C4'tür.
  3. PDPT. PDPT[504] sayfa dizininin (PD) çerçevesini verir: 0x7D10. Burada PS = 1 olsaydı 1 GiB'lık bir sayfa bulunmuş olurdu.
  4. PD. PD[145] son seviye tablonun çerçevesini verir: 0x2E95. PS = 1 olsaydı 2 MiB'lık bir sayfa bulunurdu.
  5. PT. PT[325] çerçeve 0x5A3F7'yi verir; konum eklenir: fiziksel 0x5A3F7678.

TLB ıskasında 4 tablo erişimi ve 1 veri erişimi yapılır. Çerçeve değerleri örnektir; indisler Python ile hesaplandı.

VA = 0x00007FFE12345678PML4[255]PDPT[504]PD[145]PT[325]konum 0x678CR30x1A2BPML4 tablosu[255] → 0x3C4çerçeve 0x1A2BPDPT tablosu[504] → 0x7D10çerçeve 0x3C4PD tablosu[145] → 0x2E95çerçeve 0x7D10PT tablosu[325] → 0x5A3F7çerçeve 0x2E95fiziksel 0x5A3F7678
Şekil 8.49: x86-64 dört seviyeli sayfa tablosu yürüyüşünün sonu: PT[325] çerçeve 0x5A3F7'yi verir.
Sık yapılan hata

x86-64'te de 32 bit x86'daki gibi 10 bitlik indis kullanmak. Girdi 8 bayta çıktığı için 4 KiB'lık bir tabloya 512 girdi sığar; indis 9 bittir. İndisler 0x1FF ile maskelenir, 0x3FF ile değil.

8.9.6Büyük sayfalar

Büyük çalışma kümesiyle çalışan programlar, örneğin veritabanları, sanal makineler ve yapay zekâ iş yükleri, 4 KiB'lık sayfalarla sürekli TLB ıskası yaşar. Sorunun ölçüsü TLB erişim alanıdır (TLB reach): girdi sayısı × sayfa boyu. 1536 girdilik bir TLB, 4 KiB'lık sayfalarla yalnız 6 MiB'ı kapsar; 2 MiB'lık sayfalarla aynı TLB 3 GiB'ı kapsar (Şekil 8.50).

TLB erişim alanı (reach) = girdi sayısı × sayfa boyutu, 1536 girdi4 KiB sayfa6 MiB2 MiB sayfa3 GiB(log ölçek)
Şekil 8.50: TLB erişim alanı: 1536 girdi ile 4 KiB sayfada 6 MiB, 2 MiB sayfada 3 GiB.

Tablo boyutu da küçülür. 1 GiB'ı 4 KiB'lık sayfalarla eşlemek 230 / 212 = 262.144 PTE, yani 2 MiB'lık tablo ister. 2 MiB'lık sayfalarla yalnız 512 PD girdisi, yani 4 KiB yeter.

Linux büyük sayfaları üç biçimde sunar. hugetlbfs, önceden ayrılmış 2 MiB ya da 1 GiB'lık sayfaları açıkça isteyen programlar içindir. Şeffaf büyük sayfalarda (transparent huge pages, THP) çekirdek uygun bölgeleri kendiliğinden büyük sayfalarla eşler. Son sürümlerdeki mTHP ise 16–64 KiB'lık ara boyutlar kullanır. Windows'ta aynı işi “large pages” görür.

Büyük sayfaların bedeli iki tanedir: iç parçalanma artar ve 2 MiB'lık bitişik bir fiziksel bölge bulmak gerekir. Bitişik bölge bulma sorunu, buddy ayırıcının ve sıkıştırmanın bugün hâlâ neden önemli olduğunu açıklar. ARMv8 bu soruna ara bir çözüm getirir: contiguous biti işaretli 16 komşu 4 KiB'lık girdi tek bir TLB girdisiyle, 64 KiB olarak tutulabilir.

8.9.7Hash sayfa tablosu

32 bitten büyük adres alanları için bir başka yaklaşım hash tablosudur.

Tanım 8.12

Hash sayfa tablosunda (hashed page table) sanal sayfa numarası bir hash fonksiyonundan geçirilir. Tablonun her kovası, aynı hash değerine düşen (sanal sayfa, çerçeve, sonraki) girdilerinin bağlı listesidir.

Örnek 8.17

8 kovalı bir tabloda h(p) = p mod 8'dir. Kova 2'nin zinciri (p = 10 → f = 7), (p = 26 → f = 3)'tür. Sanal adres 0x1A0AB nereye çevrilir? (4 KiB sayfa)

  1. Bölme. p = 0x1A = 26, d = 0x0AB.
  2. Hash. h(26) = 26 mod 8 = 2.
  3. Zincir. İlk girdi p = 10: eşleşmez, sonrakine geçilir. İkinci girdi p = 26: eşleşir, çerçeve 3.
  4. Fiziksel adres. 3 × 4096 + 0xAB = 0x30AB.
sanal adres 0x1A0AB → p = 26 (0x1A), d = 0xABh(p) = p mod 8h(26) = 2hash tablosu0p=8 → f=4p=16 → f=612p=10 → f=7p=26 → f=3345p=13 → f=167p=31 → f=2çerçeve 3fiziksel 0x30AB
Şekil 8.51: Hash sayfa tablosunda 26 numaralı sayfanın aranması: kova 2'nin zincirinde ikinci girdi eşleşir.

Hash tablosunun boyutu kullanılan sayfa sayısıyla orantılıdır, adres alanının büyüklüğüyle değil. Bu, 64 bitlik seyrek adres alanları için çekici bir özelliktir. Kümelenmiş türünde her girdi tek bir sayfa yerine birden çok (örneğin 16) ardışık sayfanın çerçevelerini tutar. Bu tür, seyrek ama yer yer yoğun adres alanlarına uygundur.

8.9.8Ters sayfa tablosu

Şimdiye kadarki bütün tablolarda süreç başına bir tablo vardı ve tablonun boyu sanal adres alanına bağlıydı. Ters tablo bakış açısını tersine çevirir.

Tanım 8.13

Ters sayfa tablosu (inverted page table), fiziksel çerçeve başına bir girdi tutan, sistemde tek olan tablodur. Her girdi o çerçevede hangi sürecin hangi sayfasının bulunduğunu, yani (pid, sayfa) çiftini tutar. Girdinin indisi çerçeve numarasıdır.

Çeviri bir arama işidir. (pid = 2, p = 5, d = 0x123) adresi için tablo baştan taranır. Girdi 0 (1, 0), girdi 1 (2, 3), girdi 2 (1, 5), girdi 3 (3, 1) eşleşmez. Girdi 4 (2, 5) eşleşir. İndisin kendisi çerçeve numarası olduğu için fiziksel adres 4 × 4096 + 0x123 = 0x4123'tür (Şekil 8.52).

mantıksal adres: (pid = 2, p = 5, d = 0x123)ters tablo: çerçeve başına bir girdi0pid 1, sayfa 01pid 2, sayfa 32pid 1, sayfa 53pid 3, sayfa 14pid 2, sayfa 55pid 1, sayfa 26pid 3, sayfa 07pid 2, sayfa 0çerçeveeşleşme: indis 4 = çerçevefiziksel 0x4123
Şekil 8.52: Ters sayfa tablosunda (pid 2, sayfa 5) aranması: indis 4'te eşleşme bulunur ve fiziksel adres 0x4123'tür.

Tablonun boyu fiziksel bellekle orantılıdır ve süreç sayısından bağımsızdır. 16 GiB RAM ve 4 KiB'lık çerçevelerle 4.194.304 girdi vardır; girdi başına 8 bayt ile tablo 32 MiB tutar. Bu bütün sistem için tek bir tablodur.

Bedeli üç tanedir. Birincisi aramadır: doğrusal tarama her erişimde kabul edilemeyecek kadar yavaştır. Gerçek sistemler bu yüzden (pid, p) çiftini hash'leyip doğrudan ilgili girdiye gider ve önüne bir TLB koyar. İkincisi paylaşımdır: bir çerçeve tek bir (pid, sayfa) çifti tutabildiği için paylaşımlı bellek zorlaşır. Üçüncüsü sayfa hatalarıdır: bellekte olmayan bir sayfanın diskteki yeri ters tabloda bulunmaz; bunun için süreç başına ayrı, dış bir tablo gerekir. PowerPC ve IBM POWER mimarileri hash'li ters tablo kullanır; IA-64 de bu yaklaşımı benimsemişti.

8.10Kesimleme

Sayfalama, programcıdan tamamen gizli bir mekanizmadır: adres alanı tek boyutlu bir bayt dizisidir ve sayfa sınırları programın mantıksal yapısıyla hiç ilgili değildir. Kesimleme ise adres alanını programın kendi parçalarına göre böler.

8.10.1Neden kesimleme?

Tanenbaum'un örneği bir derleyicidir. Derleyici çalışırken birçok tablo kurar: kaynak metin, sembol tablosu, sabitler tablosu, ayrıştırma ağacı ve çağrı yığını. Bu tabloların hepsi çalışma boyunca büyüyüp küçülür. Tek boyutlu bir adres alanında her tabloya ardışık bir bölge ayrılır. Bir tablo dolunca, komşusunda boş yer olsa bile ona çarpar (Şekil 8.53). Sembol tablosu kaynak metnin bölgesine taşar; derleyici ya durur ya da tabloları elle kaydırmak zorunda kalır.

(a) tek boyutlu uzayçağrı yığınıayrıştırma ağacısabitlerkaynak metinsembol tablosusembol tablosu büyüdü,kaynak metne çarpar!(b) ayrı kesimlersembol0kaynak1sabit2ağaç3yığın4her kesim 0’dan başlar, ayrı büyür
Şekil 8.53: (a) Tek boyutlu adres alanında derleyici tabloları birbirine çarpar. (b) Ayrı kesimlerde her tablo 0'dan başlar ve bağımsız büyür.
Tanım 8.14

Kesimleme (segmentation), sürece birbirinden bağımsız, değişken uzunluklu birden çok adres alanı vermektir. Her adres alanına kesim (segment) denir. Bir adres iki parçalıdır: (kesim numarası, kesim içi konum).

Kesimleme programcının bakışına uyar. Programcı programını tek bir bayt dizisi olarak değil; ana program, fonksiyonlar, yığın, diziler ve sembol tablosu gibi ayrı birimler olarak düşünür. Kesimleme bu birimlerin her birine kendi adres alanını verir. Her kesim 0'dan başlar ve öteki kesimleri etkilemeden büyüyüp küçülebilir.

8.10.2Kesim tablosuyla çeviri

Çeviri bir kesim tablosu ile yapılır. Tablonun her girdisi bir kesimin tabanını (fiziksel başlangıç adresi) ve limitini (uzunluğunu) tutar. Tablonun bellekteki yerini kesim tablosu taban yazmacı (STBR), girdi sayısını kesim tablosu uzunluk yazmacı (STLR) tutar. (s, d) adresi için donanım önce d < limit[s] koşuluna bakar. Koşul sağlanırsa fiziksel adres taban[s] + d'dir; sağlanmazsa tuzak oluşur. Kesimleme aslında her kesim için ayrı bir taban-limit çiftidir.

Tablo 8.22: Silberschatz'ın beş kesimli örneği. Kesim tablosu (limit, taban): 0 → (1000, 1400), 1 → (400, 6300), 2 → (400, 4300), 3 → (1100, 3200), 4 → (1000, 4700). Python ile doğrulandı.
(s, d)d < limit?Sonuç
(2, 53)53 < 4004.300 + 53 = 4.353
(3, 852)852 < 1.1003.200 + 852 = 4.052
(1, 399)399 < 4006.300 + 399 = 6.699
(0, 1.222)1.222 ≥ 1.000tuzak
kesim tablosulimittaban01.0001.40014006.30024004.30031.1003.20041.0004.700fiziksel bellekkesim 01.400kesim 16.300kesim 24.300kesim 33.200kesim 44.700(s, d) = (0, 1.222)d < limit ? 1.222 < 1.000hayır → tuzak (adresleme hatası)
Şekil 8.54: (0, 1.222) adresi: kesim 0'ın boyu yalnız 1.000 bayttır, donanım tuzak üretir.

Son satırda denetim olmasaydı adres 1.400 + 1.222 = 2.622 olurdu. Bu adres kesim 0'ın dışındadır; sürecin başka bir kesimine ya da başka bir sürecin belleğine ait olabilir. Limit denetimi tam olarak bunu engeller.

Sık yapılan hata

Konum limite eşitken erişimi geçerli saymak. Geçerli konumlar 0 … limit − 1'dir. Limiti 400 olan bir kesimde (1, 399) son geçerli baytı gösterir; (1, 400) artık tuzaktır.

8.10.3Kesimi seçen bitler, aşağı büyüyen yığın

Kesim numarası adresin ayrı bir parçası olmak zorunda değildir; sanal adresin üst bitleri de kesimi seçebilir. OSTEP'tekine benzer bir örnekte sanal adres 14 bittir (16 KiB). Üst iki bit kesimi seçer: 00 kod, 01 öbek, 11 yığın. Kalan 12 bit konumdur, dolayısıyla bir kesim en çok 4 KiB olabilir. Kesim yazmaçları şöyledir:

Tablo 8.23: Kesim yazmaçları (14 bit sanal adres).
KesimTabanBoyYönKoruma
00 kod24.5763.072+r-x
01 öbek40.9602.048+rw-
11 yığın32.7682.048−rw-

Yığın kesiminin “yön” biti eksidir: yığın aşağı doğru büyür. Böyle bir kesimde konum negatif yorumlanır. Konumdan en büyük kesim boyu (4.096) çıkarılır; elde edilen negatif sayının mutlak değeri boydan büyük olmamalıdır. Fiziksel adres taban + negatif konumdur.

Örnek 8.18

Sanal 1.000, 4.500, 16.000 ve 7.000 adresleri nereye gider?

  1. 1.000. İkili 00 001111101000: kesim 00 (kod), konum 1.000. 1.000 < 3.072, geçerli. Fiziksel 24.576 + 1.000 = 25.576.
  2. 4.500. 01 000110010100: kesim 01 (öbek), konum 4.500 − 4.096 = 404. 404 < 2.048, geçerli. Fiziksel 40.960 + 404 = 41.364. Sanal adresin tamamını tabana eklemek (40.960 + 4.500 = 45.460) yanlış olurdu.
  3. 16.000. 11 111010000000: kesim 11 (yığın), konum 3.712. Yığın aşağı büyüdüğü için 3.712 − 4.096 = −384. |−384| ≤ 2.048, geçerli. Fiziksel 32.768 − 384 = 32.384.
  4. 7.000. 01 101101011000: kesim 01 (öbek), konum 2.904. 2.904 ≥ 2.048: donanım tuzak üretir, işletim sistemi süreci SIGSEGV ile sonlandırır.
kesimtabanboyyönkoruma00 kod24.5763.072+r-x01 öbek40.9602.048+rw-11 yığın32.7682.048−rw-sanal adres 16.00011111010000000kesimkonum (12 bit) = 3.712kesim 11 → yığınaşağı büyür: negatif konum = 3.712 − 4.096 = −384|−384| ≤ 2.048 → geçerli32.768 − 384 = 32.384fiziksel bellek24 K42 Kkodöbekyığın32.384
Şekil 8.55: 16.000 adresi: üst bitler 11 yığını seçer; yığın aşağı büyüdüğü için konum −384 olarak yorumlanır ve fiziksel adres 32.384'tür.
Örnek 8.19

Aynı yazmaçlarla sanal 13.000 ve 14.500 adresleri nereye gider?

  1. 13.000. 13.000 − 12.288 = 712; kesim 11 (yığın), konum 712. 712 − 4.096 = −3.384; |−3.384| > 2.048 olduğu için tuzak.
  2. 14.500. Kesim 11, konum 2.212. 2.212 − 4.096 = −1.884; |−1.884| ≤ 2.048, geçerli. Fiziksel 32.768 − 1.884 = 30.884.

Koruma bitleri paylaşımı da mümkün kılar. r-x izinli bir kod kesimi birçok süreçte aynı fiziksel bölgeye eşlenebilir. Kesim salt okunur olduğu için süreçlerden biri ona yazmaya çalışırsa koruma hatası oluşur ve ötekiler etkilenmez.

8.10.4Kesimlemenin artıları ve eksileri

Kesimlemenin avantajları programın mantıksal yapısından gelir:

  • Büyüyen ya da küçülen tablolar birbirini etkilemez.
  • Kesimler ayrı ayrı derlenebilir. Bir fonksiyon kendi kesimindeyse, fonksiyon değişince öteki kesimlerin adresleri değişmez.
  • Bir kütüphane ayrı bir kesime konup süreçler arasında paylaşılabilir.
  • Koruma kesim başına ayarlanır: kod yalnız çalıştırılabilir, sabitler yalnız okunabilir, veri okunup yazılabilir.

Dezavantajları ise bitişik tahsisten tanıdıktır. Kesimler değişken uzunlukludur; bellekte yer değiştirdikçe aralarında delikler oluşur. Bu dış parçalanmaya, deseni yüzünden dama tahtası (checkerboarding) da denir (Şekil 8.56). Çaresi yine sıkıştırmadır. Ayrıca bir kesimin bütünüyle bellekte bulunması gerekir; çok büyük bir kesim belleğe sığmayabilir. Bu son sorunun çözümü kesimleri de sayfalamaktır; MULTICS ve Intel 386 bu yolu izledi.

(a)K0 4KK1 8KK2 5KK3 8KK4 7K(b)K0 4KK7 5K3KK2 5KK3 8KK4 7K(c)K0 4KK7 5K3KK2 5KK5 4K4KK4 7K(d)K0 4KK7 5KK2 5KK5 4KK4 7K7K(b)–(c) dış parçalanma (dama tahtası), (d) sıkıştırma
Şekil 8.56: Kesimlerin yer değiştirmesiyle oluşan dama tahtası: (b)–(c) dış parçalanma, (d) sıkıştırma.

8.10.5Sayfalama ile karşılaştırma

Tablo 8.24, Tanenbaum'un karşılaştırma tablosunu bugünkü donanıma uyarlayarak verir.

Tablo 8.24: Sayfalama ve kesimleme (Tanenbaum Şekil 3-32'den uyarlandı). * Tanenbaum burada sayfalama için “hayır” der; bugünkü R/W/NX bitleri ve paylaşımlı sayfalar aynı işi sayfa düzeyinde yapar.
DurumSayfalamaKesimleme
Programcı tekniğin farkında olmalı mı?HayırEvet
Kaç doğrusal adres alanı var?1Çok
Toplam adres alanı fiziksel belleği aşabilir mi?EvetEvet
Kod ve veri ayrılıp ayrı korunabilir mi?Sayfa bitleriyle evet*Evet
Boyu değişen tablolar kolayca yerleşir mi?HayırEvet
Kullanıcılar arasında işlev paylaşımı kolay mı?Paylaşımlı sayfalarla*Evet
Parçalanmaİç (son sayfa)Dış
Neden icat edildi?Fiziksel bellekten büyük doğrusal adres alanıProgram ve veriyi bağımsız mantıksal uzaylara ayırmak; paylaşım, koruma
sayfalama: tek doğrusal alan, eşit sayfalarkesimleme: ayrı mantıksal alanlarkodveriyığınsembol
Şekil 8.57: Sayfalamada tek doğrusal adres alanı eşit sayfalara bölünür; kesimlemede kod, veri ve yığın ayrı boyutlu kesimlerdir.

İki teknik farklı problemleri çözmek için doğdu. Sayfalama fiziksel bellekten büyük, tek bir doğrusal adres alanı sağlamak; kesimleme ise programı mantıksal parçalara ayırmak, bu parçaları ayrı korumak ve paylaşmak için icat edildi. Tarih sayfalamadan yana sonuçlandı: kesimlemenin koruma ve paylaşım avantajları sayfa bitleriyle elde edilebiliyor, sayfalamanın dış parçalanmadan kurtulma avantajı ise başka bir yolla elde edilemiyor.

8.11Gerçek mimariler ve uygulama

8.11.1x86: kesimleme, sayfalama ve düz model

x86 ailesi kesimlemenin bütün tarihini taşır. 8086'nın gerçek modunda fiziksel adres kesim × 16 + konum formülüyle hesaplanır. Sonuç 20 bittir, yani 1 MiB'lık bir adres alanı verir; koruma yoktur. 80386'nın korumalı modunda ise mantıksal adres bir seçici ile bir konumdan oluşur. 16 bitlik seçicinin 13 biti indis, bir biti tablo seçimi (GDT ya da LDT), iki biti istenen ayrıcalık düzeyidir (RPL). Seçici, tablodan 8 baytlık bir tanımlayıcı seçer; tanımlayıcıda kesimin tabanı ve limiti bulunur. Taban + konum doğrusal adresi verir. Doğrusal adres ardından iki seviyeli sayfalamadan geçerek fiziksel adrese çevrilir (Şekil 8.58). Bu, MULTICS'in “sayfalanmış kesimler” fikrinin donanımdaki karşılığıdır.

seçicikonum (32 bit)mantıksal adres (seçici : konum)GDT / LDTtanımlayıcı: taban, limit+doğrusal adreskesimlemesayfalamaCR3 →sayfa tablolarıfiziksel
Şekil 8.58: Intel 32 bit mimaride kesimleme mantıksal adresi doğrusal adrese, sayfalama doğrusal adresi fiziksel adrese çevirir.

Donanım bu kadar zengin olmasına karşın Linux ve Windows, 32 bitte bile kesimlemeyi fiilen kapattı. Bütün kesimlerin tabanı 0, limiti 4 GiB yapıldı. Böylece doğrusal adres mantıksal adresle aynı olur; buna düz model denir. Koruma tamamen sayfa bitleriyle sağlandı. Bunun bir nedeni taşınabilirliktir: kesimleme yalnız x86'da vardı, sayfalama ise her mimaride.

x86-64'ün uzun modu bu durumu donanıma yansıttı. CS, DS, ES ve SS kesimlerinin tabanı donanımca 0 sayılır ve limit denetlenmez. Yalnız FS ve GS kesimlerinin tabanı hâlâ kullanılır: kullanıcı alanında iş parçacığına yerel depolamayı (TLS), çekirdekte çekirdek başına veriyi göstermek için. CS ise ayrıca o anki ayrıcalık düzeyini taşır.

8.11.2ARMv8-A ve RISC-V

Bugünkü öteki iki büyük mimari aynı fikirleri farklı alan adlarıyla uygular (Tablo 8.25).

Tablo 8.25: ARMv8-A ve RISC-V'de bellek yönetimi.
ARMv8-A (AArch64)RISC-V
Sayfa (granül)4, 16 ya da 64 KiB; Apple Silicon ve bazı Android 15+ aygıtlar 16 KiB4 KiB (+ 2 MiB, 1 GiB, 512 GiB büyük sayfa)
Sanal adres48 bit (4 KiB ile 4 seviye, L0–L3); LVA/LPA2 ile 52 bitSv39 (3 seviye), Sv48 (4), Sv57 (5)
Tablo tabanıTTBR0_EL1 alt yarı (kullanıcı), TTBR1_EL1 üst yarı (çekirdek)satp (mod + ASID + kök çerçeve)
Süreç etiketiASID 8/16 bitASID (16 bite kadar)
KorumaAP bitleri, UXN/PXN (çalıştırma yasağı), PAN (≈ SMAP)R/W/X/U bitleri, SUM
TLBDonanım yürüyüşü; A/D bitleri isteğe bağlı olarak donanımda (v8.1); contiguous bitiDonanım yürüyüşü (çoğu gerçeklemede); sfence.vma
işaret uzantısı (25)VPN[2] 9VPN[1] 9VPN[0] 9konum 12Sv39: 3 seviye × 512 girdi; satp kök tablonun çerçevesini ve ASID’i tutar
Şekil 8.59: RISC-V Sv39 sanal adresi: üç 9 bitlik indis (VPN[2], VPN[1], VPN[0]) ve 12 bit konum; üst 25 bit bit 38'in kopyasıdır.

İki mimaride de kesimleme yoktur. Sanal adres düzdür; çeviri çok seviyeli sayfa tablosu, TLB ve ASID ile yapılır. ARMv8'in iki tablo taban yazmacı, x86-64'ün kanonik adres bölünmesinin karşılığıdır: kullanıcı ve çekirdek adresleri ayrı tablolardan çevrilir. RISC-V'nin Sv39'u 3 × 9 + 12 = 39 bitlik, 512 GiB'lık bir adres alanı verir; daha büyük alanlar için Sv48 ve Sv57 vardır. Kısacası x86-64 ile aynı fikirler, farklı adlarla uygulanır.

8.11.3Politika ve mekanizma ayrımı

İşletim sistemi sayfalamaya dört anda katılır. Süreç oluşturulurken sayfa tablosu kurulur. Bağlam değiştirmede yeni sürecin tablosu etkinleştirilir ve TLB ayarlanır. Sayfa hatasında eksik sayfa getirilir. Süreç sonlanınca çerçeveler ve takas alanı bırakılır.

Bu işlerin bir kısmı makineye bağlıdır (MMU'nun tablo biçimi), bir kısmı değildir (hangi sayfanın ne zaman nereye konacağı). Mach çekirdeği bu ayrımı açıkça yaptı. Mekanizma çekirdekte kaldı: makineye bağlı bir MMU işleyicisi ve makineden bağımsız bir sayfa hatası işleyicisi. Politika ise kullanıcı alanında çalışan bir dış sayfalayıcıya verildi (Şekil 8.60). Sayfa hatası olunca çekirdekteki işleyici, gereken sayfayı dış sayfalayıcıdan ister. Sayfalayıcı sayfayı diskten okur ve geri verir. MMU işleyicisi de sayfayı sürecin adres alanına eşler.

kullanıcı alanıçekirdek alanıKullanıcı süreciDış sayfalayıcıHata işleyiciMMU işleyicisiDisk1 sayfa hatası2 gereken sayfa3 iste4 geldi5 işte sayfa6 eşle
Şekil 8.60: Dış sayfalayıcı ile sayfa hatası: çekirdekteki hata işleyici, kullanıcı alanındaki sayfalayıcıdan sayfayı ister; MMU işleyicisi sayfayı eşler.

Bu tasarım esneklik ve modülerlik kazandırır: farklı uygulamalar farklı sayfalama politikaları kullanabilir. Bedeli, her sayfa hatasında kullanıcı ve çekirdek sınırını birkaç kez geçmektir. Fikrin bugünkü karşılıkları da vardır. Linux'un userfaultfd düzeneği sayfa hatalarını kullanıcı alanındaki bir programa iletir; sanal makinelerin canlı göçü ve süreçlerin dondurulup başka makinede sürdürülmesi (CRIU) buna dayanır. seL4 ve Fuchsia gibi mikro çekirdeklerde ise sayfalayıcı zaten kullanıcı alanındadır.

8.12Özet

  • İşlemci yalnız yazmaçlara ve ana belleğe doğrudan erişir. Bellek hiyerarşisinde önbellekleri donanım, ana belleği işletim sistemi yönetir. Önbellek 64 baytlık satırlarla çalışır ve zamansal ile uzamsal yerellik sayesinde işe yarar.
  • Doğrudan eşlemeli önbellekte blok b, b mod K kümesine gider; aynı kümeye düşen bloklar birbirini kovar (çakışma ıskası). Ortalama erişim süresi T = s + m · Talt formülüyle alttan yukarı hesaplanır; örnekte 3,8 ns, DRAM'den yaklaşık 21 kat hızlı.
  • Adres alanı koruma ve yer değiştirme problemlerini çözer. Taban ve limit yazmaçlarıyla geçerli adresler taban … taban + limit − 1'dir; dışarısı tuzak üretir.
  • Adres bağlama derleme, yükleme ya da yürütme zamanında yapılabilir. Bugünkü sistemler yürütme zamanında bağlar: işlemci mantıksal adres üretir, MMU onu fiziksel adrese çevirir.
  • Dinamik yükleme (dlopen) kodu çağrılınca yükler. Dinamik bağlamada kütüphane kodu bir kez bellekte bulunur ve paylaşılır; ELF'te ilk çağrıda değişen kod değil, sürece özel GOT girdisidir. -fPIC konumdan bağımsız kod üretir.
  • Standart takas bütün süreci taşır ve yavaştır; bugünkü sistemler soğuk sayfaları takas eder ve önce sıkıştırır.
  • Bitişik tahsiste delik seçimi ilk uyan, sonraki uyan, en iyi uyan ya da en kötü uyanla yapılır. Dış parçalanmada toplam boş alan yeter ama bitişik değildir; %50 kuralına göre belleğin üçte biri kaybolabilir. İç parçalanma bloğun içindeki kullanılmayan kısımdır. Sıkıştırma delikleri birleştirir ama pahalıdır.
  • Boş alan biteşlem (birim başına bir bit; k ardışık sıfır aramak doğrusal) ya da bağlı liste (adrese göre sıralıysa birleştirme sabit zamanda) ile izlenir. malloc boş listeyi boş parçaların içine gömer.
  • Sayfalamada mantıksal bellek sayfalara, fiziksel bellek çerçevelere bölünür; adres (p, d) olur ve fiziksel = f × sayfa boyu + d. Dış parçalanma yoktur; iç parçalanma ortalama yarım sayfadır. Optimum sayfa boyu yaklaşık √(2se)'dir.
  • Sayfa tablosu girdisi çerçeve numarasıyla birlikte P, R/W, U/S, NX, A, D ve G bitlerini tutar. Geçerli/geçersiz biti adres alanı dışını yakalar, ama koruma sayfa ayrıntı düzeyindedir.
  • TLB son çevirileri tutar. Etkin erişim süresi EES = (t + ε) α + (2t + ε)(1 − α); ε = 20, t = 100 ns için α = %80'de 140 ns, %98'de 122 ns. Çok seviyeli tabloda ıska çok daha pahalıdır.
  • Hiyerarşik tablo yalnız kullanılan bölgelerin alt tablolarını tutar; x86-64 dört (48 bit) ya da beş (57 bit) seviye, her seviyede 9 bit kullanır. Büyük sayfalar TLB erişim alanını büyütür. Hash tablonun boyu kullanılan sayfa sayısıyla, ters tablonunki fiziksel bellekle orantılıdır.
  • Kesimleme adres alanını (kesim, konum) biçiminde mantıksal parçalara böler; her kesim bir taban-limit çiftidir ve dış parçalanmaya yol açar. x86-64'te kesimlemeden yalnız FS/GS tabanları kalmıştır; ARMv8 ve RISC-V'de kesimleme yoktur.

8.13Kavram listesi

Terimlerin İngilizce karşılıkları ve öteki bölümlerdeki kullanımları terim sözlüğünde bulunur.

8.14Kendini Dene

  1. 4 kümeli, 64 bayt satırlı, başlangıçta boş doğrudan eşlemeli bir önbellekte sırayla 16, 80, 40, 272 ve 20 adreslerine erişiliyor. Hangi erişimler isabettir?

    Cevap

    Bloklar 0, 1, 0, 4, 0; kümeler 0, 1, 0, 0, 0'dır. 16: ıska. 80: ıska. 40: blok 0 küme 0'da, isabet. 272: blok 4 blok 0'ı kovar, ıska. 20: blok 0 artık yok, ıska. Toplam 1 isabet, 4 ıska.

  2. Bir sürecin taban yazmacı 5000, limit yazmacı 2000'dir. MMU önce limit denetimi yapıp sonra tabanı ekliyor. Mantıksal 1999 ve 2000 adresleri ne olur?

    Cevap

    1999 < 2000 olduğu için geçerlidir ve fiziksel 6999'a gider. 2000 limite eşittir; geçerli konumlar 0 … 1999 olduğu için tuzak oluşur.

  3. Delikler adres sırasıyla 300, 120, 500 ve 200 K'dır. İstekler sırayla 180, 100 ve 450 K'dır. İlk uyan, en iyi uyan ve en kötü uyan hangisinde bir istek karşılanamaz?

    Cevap

    İlk uyan: 180 → 300 (120 kalır), 100 → aynı deliğin 120'lik artığı (20 kalır), 450 → 500 (50 kalır); hepsi yerleşir. En iyi uyan: 180 → 200 (20), 100 → 120 (20), 450 → 500 (50); hepsi yerleşir. En kötü uyan: 180 → 500 (320), 100 → 320 (220); 450 için en büyük delik 300'dür, istek karşılanamaz.

  4. Sayfa boyu 1 KiB'tır ve sayfa 4, çerçeve 9'a eşlenmiştir. Mantıksal 5000 adresinin fiziksel karşılığı nedir?

    Cevap

    p = ⌊5000 / 1024⌋ = 4, d = 5000 − 4096 = 904. Fiziksel = 9 × 1024 + 904 = 9216 + 904 = 10120.

  5. TLB araması 5 ns, bellek erişimi 100 ns, tablo tek seviyeli ve isabet oranı %90'dır. Etkin erişim süresi nedir? EES'nin 125 ns olması için isabet oranı kaç olmalıdır?

    Cevap

    İsabet 105 ns, ıska 205 ns. EES = 0,9 × 105 + 0,1 × 205 = 94,5 + 20,5 = 115 ns. 205 − 100α = 125 için α = 0,8, yani %80.

  6. 32 bit x86 iki seviyeli sayfalamasında (10 + 10 + 12) sanal 0x00C05123 adresi için üst indis, alt indis ve konum nedir?

    Cevap

    Üst indis 0x00C05123 >> 22 = 3, alt indis (0x00C05123 >> 12) & 0x3FF = 0xC05 & 0x3FF = 5, konum 0x123'tür. Adres 12–16 MiB bölgesindedir.

  7. 8 GiB RAM, 4 KiB çerçeve ve 8 baytlık girdiyle bir ters sayfa tablosu ne kadar yer kaplar? Sistemde 200 süreç olsaydı bu değer değişir miydi?

    Cevap

    233 / 212 = 221 = 2.097.152 girdi; × 8 bayt = 16 MiB. Değişmezdi: ters tablo fiziksel bellekle orantılıdır ve sistemde tektir, süreç sayısına bağlı değildir.

  8. Silberschatz'ın kesim tablosunda (kesim 2: limit 400, taban 4300; kesim 4: limit 1000, taban 4700) (4, 999) ve (2, 400) adresleri nereye gider?

    Cevap

    (4, 999): 999 < 1000, fiziksel 4700 + 999 = 5699. (2, 400): konum limite eşittir; geçerli konumlar 0 … 399 olduğu için tuzak.

  9. 1024 K'lık bir buddy sisteminde 70 K'lık bir istek kaç K'lık blok alır ve ne kadar iç parçalanma oluşur?

    Cevap

    70 K'yı alan en küçük ikinin kuvveti 128 K'dır. Süreç 128 K'lık bloğu alır; iç parçalanma 128 − 70 = 58 K'dır.

  10. Dinamik bağlanmış bir programda puts ilk kez çağrıldıktan sonra hangi sayfa değişir ve bu neden önemlidir?

    Cevap

    Yalnız sürece özel GOT sayfasındaki puts girdisi değişir; çözücü oraya gerçek adresi yazar. Programın ve libc'nin kod sayfaları değişmediği için salt okunur kalır ve bütün süreçler arasında paylaşılmaya devam eder.

8.15İleri okuma

  • R. H. Arpaci-Dusseau ve A. C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces, “Address Translation”, “Segmentation”, “Free-Space Management”, “Paging: Faster Translations (TLBs)” ve “Paging: Smaller Tables” bölümleri: kesim yazmaçları ve TLB konularını kısa, çalıştırılabilir benzetimlerle anlatır. Kitap ostep.org'da ücretsizdir.
  • A. S. Tanenbaum ve H. Bos, Modern Operating Systems, Bölüm 3: yer değiştirme problemini, biteşlem ve bağlı listeyi, MMU ve çok seviyeli tablo örneklerini bu nottaki şekillerin özgün biçimleriyle anlatır.
  • U. Drepper, “What Every Programmer Should Know About Memory”, 2007: önbellek, TLB ve NUMA'nın program performansına etkisini ölçümlerle gösteren uzun bir makaledir.
  • Linux çekirdek belgeleri, “Transparent Hugepage Support”: şeffaf büyük sayfaların nasıl çalıştığını, ne zaman kazanç sağladığını ve nasıl ayarlandığını anlatır.

8.16Kaynaklar

  1. A. Silberschatz, P. B. Galvin, G. Gagne, Operating System Concepts, 10. baskı, Wiley, 2018 — Bölüm 9: Main Memory.
  2. A. S. Tanenbaum, H. Bos, Modern Operating Systems, 4. baskı (2014) / 5. baskı (2023), Pearson — Bölüm 3: Memory Management (§3.1–3.3, 3.5–3.7).
  3. R. H. Arpaci-Dusseau, A. C. Arpaci-Dusseau, Operating Systems: Three Easy Pieces (OSTEP), ostep.org — “Address Spaces”, “Address Translation”, “Segmentation”, “Free-Space Management”, “Paging”, “TLBs”, “Smaller Tables”.
  4. D. E. Knuth, The Art of Computer Programming, Cilt 1, §2.5 Dynamic Storage Allocation (%50 kuralı).
  5. Intel 64 and IA-32 Architectures Software Developer's Manual, Cilt 3A, Bölüm 4–5 (Paging, Protection); Arm Architecture Reference Manual for A-profile (VMSAv8-64); RISC-V Privileged Specification (Sv39/48/57).
  6. U. Drepper, “How To Write Shared Libraries”, 2011; U. Drepper, “What Every Programmer Should Know About Memory”, 2007; Linux çekirdek belgeleri: mm/, “Transparent Hugepage Support”, userfaultfd.

İlgili sayfalar: Bölüm 8 sunumu · Çalışma soruları · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.