> İşlemcilerin sayfalama mekanizması: Tıpkı işlemcilerdeki koruma mekanizması gibi, her işlemci de sayfalama mekanizmasına sahip değildir. Yani modern ve kapasiteli işlemcilerde bu mekanizma varken, mikrokontroller düzeyindekilerde mevcut değildir. Bu özelliği, işlemcilerin ilgili ayarları ile oynayarak, aktif veya pasif hale de getirebiliriz. Pekiyi nedir bu sayfalama mekanizması? Anımsayacağımız üzere RAM sıralı baytlardan oluşmaktadır. Bu sıralı baytları bir grup haline getirdiğimiz zaman ise bir sayfa oluşmaktadır. Genel olarak bir sayfa 4096 bayttan oluşmaktadır. Dolayısıyla RAM'in ilk 1-4095 baytlık kısmına sıfırıncı sayfa, ikinci 4096 - 8191'lık kısma birinci sayfa vs. denmektedir. Özetle fiziksel RAM'in, her biri 4K bayt büyüklüğündeki sayfalara ayrılması olayıdır. Pekiyi nedir bu sistem? Derleyiciler bir C kodunu derlerken sanki RAM'de sadece bu program çalışacakmış gibi bir kod üretirler. Yani o program, 4 GB'lik bir RAM'in 4. MB'den itibaren yüklenecekmiş gibi derlenir. Yani ilk 4 MB'lık kısım boş gibi düşünebiliriz. Fakat İşletim sistemi de bunu RAM'e aktarırken parçalara bölerek yükler. Örneğin, bir kısmını RAM'in bir bölgesine yüklerken diğer kısımlarını uygun diğer alanlara yükler fakat ardışık bir yükleme söz konusu değildir. Pekiyi programımız çalışmaya başladığında düzen nasıl sağlanmaktadır? İşte burada devreye Sayfa Tablosu kavramı girmektedir. Sayfa Tablosu iki sütundan oluşan bir tablodur. Sol taraftaki sütunda sayfa numaraları yazarken, sağ taraftaki sütunda ise bu numaralara karşılık gelen RAM'deki gerçek alanlardır. Dolayısıyla Sayfa Tablosunun sol sütunu ardışılken, sağ sütunu karışık vaziyettedir. İş bu nedenden dolayıdır ki programlarımızdaki adresler aslında Sanal Adreslerdir. CPU ise bu adrese erişmek için önce Sayfa Tablosuna bakmakta, oradan hareketle RAM'deki gerçek yerine erişmektedir. PROGRAMIMIZIN SANAL ADRESLERİ ARDIŞILDIR. Bilgisayarın donanımı da Sayfa Tablosuna uyumlu olarak tasarlandığı için sistem geneli bir yavaşlama söz konusu değildir. ÖZETLE; İşletim sistemi prosesin bölümlerini RAM'de farklı yerlere yüklüyor ve bunları ardışılmış gibi göstermek için Sayfa Tablosunu oluşturuyor. CPU'da bu tabloya bakarak çalıştığı için kesiksiz bir çalışma gerçekleşiyor. Sayfa Tablosunun temsili gösterimi aşağıdaki gibidir; Sayfa Tablosu Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 4562 11546 4563 95412 4564 12457 ... ... Şimdi işlemcimiz aşağıdaki kodu çalıştırmak istesin; MOV eax, [5C34782] Buradaki "5C34782" değeri hexadecimal bir değerdir ve sanal bir adres bilgisidir. İşlemci ilk olarak bu adresin kaçıncı sanal sayfada olduğunu hesaplar. Bunun için ilgili sayının 12 kez ötelenmesi gerekmektedir. Yani ilgili sayının sağdan ilk üç rakamını atarsak, geriye kalan değer bize kaçıncı sanal sayfada olduğunu söylemektedir. 5C34782 >> 12 = 5C34 Fakat elde edilen bu sayı yine hexadecimal bir sayıdır. Bunun decimal karşılığı ise 23604 nolu sanal sayfadır. Sayfa Tablomuz da aşağıdaki gibi olsun; Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 23602 47324 23603 52689 23604 29671 ... ... Bu tabloya göre 23604 sanal sayfasına karşılık gelen fiziksel RAM alanı 29671'dir. Pekiyi bu alandaki kaçıncı indise erişecektir? İşte yine devreye ilk başktaki adres bilgimiz girmektedir; "5C34782". Bu adres bilgisinin sağdan üç indisini attığımız zaman geriye kalan adresin sanal sayfa numarası olduğundan bahsetmiştik. İşte atılan bu üç indis ise "offset" numarasıdır. Yani "782". Bu da yine "hexadecimal" dir. Karşılığı ise "1922" değeridir. Zaten "5C34" adresi fiziksel RAM'de "52689" e yönlendirilmişti. "1922" de "offset" değeridir. Her bir sayfa da 4096 bayttan meydana gelmektedir. O vakit gerçek adres değeri "52689 * 4096 + 1922" biçimindedir. Örneğin, programımız şöyle bir fonksiyon çağırmış olsun; foo(); Derleyici de buna karşılık şöyle bir kod üretmiş olsun; CALL 6F14678 Burada 6F14678, "foo" fonksiyonunun sanal adresidir. Program çalışırken işlemci bu adresi ikiye böler. -> 6F14: Sanal Sayfa No(hexadecimal) -> 678: Offset(hexadecimal) Daha sonra sayfa tablosuna bakar ve 6F14'e karşılık gelen fiziksel RAM alanına bakar. Bu alanın da 7C45(hexadecimal) olduğunu varsayalım. O zaman işlemcinin erişeceği fiziksel adresi aşağıdaki gibidir; 7C45 * 4096 + 678 Buraya kadar öğrendiklerimiz şu şekilde; programımız RAM'e yüklenirken parçalara ayrılarak, RAM'in farklı alanlarına yükleniyor. Bu alanların tipik büyüklüğü 4096 bayt biçimindedir. İşletim sistemi de bu farklı alanları koordine edebilmek adına bir sayfa tablosu oluşturuyor. Sol sütunda sanal sayfa numaralarını, sağ tarafta ise bu numaralara karşılık gelen RAM'deki alanları yazmaktadır. Pekiyi sayfa tablosu kim tarafından oluşturuluyor? Tabii ki ilgili program belleğe yüklenirken "exec" fonksiyonları vasıtasıyla oluşturulmaktadır. Buradan hareketle diyebiliriz ki her proses kendine has bir sayfa tablosuna sahiptir. İşlemcinin içindeki bir "register" ise o anda "quanta" süresi işleyen "thread" in sayfa tablosunu göstermektedir. Eğer "task-switch" sonrasında başka bir "thread" atanırsa, artık yeni atanan "thread" in sayfa tablosu da atanacaktır. Dolayısıyla ilgili "register" artık yeni geleninkini gösterecektir. Böylece şöyle bir şey oluyor; farklı proseslerdeki aynı sanal sayfa numaraları, aslında aynı fiziksel adresleri GÖSTERMEMEKTEDİR. İşlemcilerin sayfalama mekanizması sayesinde prosesler birbirlerinin bellek alanlarına erişmesi engellenmiştir. ekiyi 32-bit sistemlerde Sayfa Tablosunun büyüklüğü ne kadar olur? Anımsayacağımız üzere bellek 4096 baytlık alanlara bölünmüştü ve 32-bit bir sistemde de maksimum RAM 4GB büyüklüğünde olacaktır. Dolayısıyla bizler en fazla 1MB adedince sayfa sayısına sahip olabiliriz. Intel mimarilerde sayfa tablosunun her bir satırı 4 bayt'lık yer kaplamaktadır. Dolayısıyla her proses bu mekanizma için 4MB yer kaplamaktadır. İşlemcileri tasarlayanlar bu büyüklüğü azaltmak için bir takım yöntemlere başvurmaktadır. Örneğin, sanal adresleri bizler ikiye ayırmıştık. Bir kısım sayfa tablosundaki indisi, bir kısım da bu indis içerisindeki konum bilgisini taşımaktadır. İşte Intel sanal adresleri üç parçaya ayırmıştır. Öte yandan dörde ayıran mimariler de bulunmaktadır. Fakat bu konunun detayları kurs kapsamında değildir. Derneğin "80X86 ve ARM Sembolik Makina Dilleri" kursunda ele alınmaktadır. Bizler kurs boyunca sanal adresleri ikiye ayıracağız. Pekiyi durum 64-bit sistemlerde nasıldır? Böyle sistemlerde maksimum RAM kapasitesi 2^64 bayt büyüklüğündedir. Dolayısıyla proseslerin sayfa tabloları da aşırı büyük yer kaplayacaktır. İşlemcileri tasarlayanlar sanal adresi dört parçaya ayırmışlardır. Fakat bu konu da kurs kapsamında anlatılmayacaktır. Bütün bu anlatılanlardan sonra bizler neden böyle bir mekanizmaya ihtiyaç duyalım? Anımsanacağınız üzere programlar derlenirken bellekte sadece kendileri çalışacakmış gibi derlenirler. Bu da demektir ki bellekte ardışıl bir biçimde konumlanmaktalar. Fakat bu programı çalıştırdığımız zaman bellekte ardışıl bir biçimde yer KAPLAMIYORLAR. Bunun iki sebebi vardır. Bunlardan birisi "Fragmantation" dediğimiz olgu, diğeri ise "Virtual Memory" dediğimiz sanal bellek kavramıdır. >> "Fragmantation" dediğimiz olgu şudur; proseslerin belleğe ardışıl bir biçimde yüklendiğini ve işleri bittiğinde de bellekten silindiğini varsayalım. Bir zaman sonra bellek üzerinde küçük alanlar oluşacaktır. Çünkü her proses bellekte aynı büyüklükte alan kaplamamaktadır. Bu küçük alanlara da herhangi bir proses sığmamakta ve totalde büyük yer kaplayacaklardır. İşte bu problemi minimize eden şey proseslerin parçalar halinde belleğe aktarılmasıdır. Bellek de zaten bloklara ayrılmış vaziyettedir. Prosesin hangi parçasının belleğin hangi parçacığına yüklendiğinin de bilgisini bir şekilde kaydedilir. Bu yöntem belleklerde olduğu gibi disk yönetiminde de kullanılmaktadır. Fakat bu yöntem de "Fragmantation" olgusunu tamamiyle engellememektedir. Örneğin, prosesimiz eşit 5 parçaya ayrılmış olsun. Bellek de yine eşit parçalı vaziyette. Program belleğe yüklendiğinde, programın ilk dört parçası bellekteki parçaların tamamını kapladığını fakat son beşinci parçanın bellekteki parçanın tamamını kaplamadığını varsayalım. Bu durumda bellekteki bu son parçanın geldiği alanda "Internal Fragmantation" oluşacaktır. Fakat bu tip bir parçalanmanın önüne geçmek mümkün değildir. >>> "Internal Fragmantation": Elimizde 22 kiloluk elma olsun. Depomuzda ise 6 kasalık yer kalmış olsun ve her bir kasa 4 kilo alsın. Bu durumda depodaki 5 kasalık alan ağzına kadar dolu olacaktır. Altıncı kasanın ise sadece yarısı dolmuş olacaktır. İşte bu boş kalan iki kiloluk alan "Internal Fragmantation" denir. Yani her nesnenin son sayfasında kalan boşluk kastedilen şeydir. >> Sanal Bellek Kullanımı: Öyle bir tekniktir ki bir programın tamamını ilk başta belleğe yüklemek yerine sadece belli kısımlarını yüklemek ve gerektiğinde de diğer kısımlarını diskten yüklemek için geliştirilmiştir. Böyle bir mekanizmanın işlevsel olması için de mutlaka sayfalama mekanizmasının olması gerekmektedir. Pekiyi bu tekniği kullanarak "n" tane prosesi belleğe yüklediğimizi ve belleğin de dolduğu düşünelim. Unutmayalım ki bellek de sayfalardan oluşmakta ve prosesin her bir parçası bir sayfaya yüklenmekte. Bu durumda işletim sistemi bir karar veriyor; bellekteki seçtiği bir sayfada bulunan prosesin o parçasını bellekten çıkarıyor ve yerine yeni eklenecek prosesin parçasını koyuyor. Prosesin bellekte olmayan bir parçasının diskten belleğe çekilmesine "swap-in" denir. Bunun tam tersi olaya ise "swap-out" denir. Pekiyi işletim sistemi bellekteki hangi sayfanın boşaltılacağını nasıl belirler? "page swapping algorithms" konusu bu soruya yanıt aramaktadır. Fakat, gelecekte kullanılma olasılığı en düşük olan sayfanın bellekten atılması en iyi örnektir. Fakat bu "swap" işlemi yavaş bir işlem olmasından dolayı sistem genelinde en önemli zayıflatıcı etkilerden birini oluşturmaktadır. Pekiyi bu yavaşlığı nasıl egale edebiliriz? Akla ilk gelen şey bellek büyüklüğünü arttırmak, yani RAM'i büyütmek. Fakat bu da maliyeti beraberinde getirmektedir. İkinci yöntem hard disk yerine SSD kullanımı olacaktır. O programın kodunun küçültülmesi de bir yöntemdir. Bir diğer yöntem ise "page swapping algorithms" tekniklerinden en iyisini kullanmaktır. Bütün bunların yanı sıra şöyle bir senaryo düşünelim; bellekte hiç boş sayfa yok fakat diskten yeni bir sayfa belleğe aktarılacak. Bu durumda bir sayfa bellekten atılmak için seçilecek. İşte bu seçim sırasında o sayfada bir takım değişiklikler oluştuysa durum ne olacaktır? Bu durumda işletim sistemi bu güncellenmiş fakat çıkartılacak sayfayı "swap file" ya da "page file" ismindeki dosyalara yazacaktır. Linux sistemlerinde istediğimiz bir dosyayı bu amaç için kullandırtabiliriz. Eğer kullanılacak bu "swap file" dosyaları da dolarsa artık yapacak bir şey kalmamaktadır. Sistemin sanal bellek limitine ulaştığımız kabul edilir. Artık sadece yeni "swap file" dosyaları sisteme eklenmelidir. Son olarak belirtmekte fayda var ki bu "swap file" dosyalar çalışma zamanı ile ilgilidir. Bilgisayarı kapattığımız zaman buradaki veriler de silinecektir. Pekiyi işlemci sanal sayfa tablosuna baktığında, sayfa numarasına ilişkin bellekte bir yer bulamazsa ne olur? Aşağıdaki biçimde bir sayfa tablomuz olsun; Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 23602 47324 23603 - 23604 29671 ... ... İşte işlemcimiz 23603 numaralı indise baktığında herhangi bir RAM alanının atanmadığı görecektir. Bu durumda "page fault" isimli bir kesme oluşur. Bu noktada işletim sisteminin "page fault handler" isimli aracı devreye giriyor. Bu araç ise yukarıda bahsedilen "swap" işlemlerini gerçekleştiriyor ve tablonun o indisine karşılık bellekte bir alan atıyor. Artık işlemci sayfa tablosu aşağıdaki gibi olacaktır; Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 23602 47324 23603 32145 23604 29671 ... ... Artık işlemci 23603 numaralı indise eriştiğinde bellekteki yerine de erişebilecek. Toparlayacak olursak; işletim sistemi programı belleğe yüklerken sadece belirli kısımlarını belleğe aktarıyor, lüzum gördüğünde diğer kısımlarını diskten çekiyor. Bu noktada devreye "Minimum Working Set" değişkeni girmektedir. Belleğe aktarılan kısımlar için sayfa tablosu dolduruluyor, aktarılmayan kısımlar için "-" işareti koyuluyor. Eğer işlemci prosesin belleğe aktarılmamış sayfalarına erişmek isterse ki bu durumda sayfa tablosundaki bellek sütununun ilgili satırında "-" işareti olacak, "page fault handler" isimli araç devreye giriyor ve diskten ilgili alanı belleğe aktarıyor, yani "swap" gerçekleşiyor ve sayfa tablosunun bellek sütunun ilgili satırı güncelleniyor. "page fault" mekanizması sırasında da ilk olarak işlemcinin erişmek istediği alanın legal bir alan olup olmadığını kontrol etmektedir. Yani o alanın "swap" dosyasında mı yoksa programın bellek alanında mı vs. İşte illegal bir alana erişmek istiyorsa işlemci, "page fault" başarısız olmakta ve proses tümüyle sonlanmaktadır. Eğer rastgele bir adrese erişmek istesek, bu durum "page fault" mekanizması tarafından sonlandırılmaktadır. Öte yandan uygulama programıcısı olarak bu mekanizmanın daha az ya da daha çok devreye girmesini SAĞLATAMAYIZ. Bütün süreç işletim sistemi tarafından yürütülmektedir. Bunun haricinde RAM'i büyüterek, iyi bir "Page Swapping Algo." seçerek, daha fazla "Minimum Working Set" ayarlayarak "page fault" mekanizmasının daha az çalışmasını sağlatabiliriz. Pekiyi işletim sisteminin bellek yönetimi kısmını yazanlar hangi bilgileri tutmak zorundadırlar? İşte işletim sistemleri, tipik olarak bu mekanizmalar için aşağıdaki bilgileri tutmak zorundadırlar: -> Tüm fiziksel RAM'deki bütün sayfaların boş olup olmadığına ilişkin tablo. -> Bir fiziksel sayfa boş değilse hangi proses tarafından kullanıldığı bilgisi. -> "swap" dosyalarının yerleri ve organizasyonu. -> Hangi proseslerin hangi sayfalarının o anda fiziksel RAM'deki hangi fiziksel sayfalarını kullanmakta. -> ... Görüleceği üzere bellek yönetimi bir işletim sisteminin en önemli ve en zor yazılan alt sistemlerinden biridir. Pekiyi bütün bu mekanizma içerisinde işletim sistemi nerededir? Aslında sanal belleğin bir kısmı biz kullanıcılar için, bir kısmı ise "kernel" kodları için ayrılmıştır. Dolayısıyla sayfa tablosunun da bir kısmı biz kullanıcılar, bir kısmı "kernel" kodları için ayrılmıştır. Fakat sayfa tablosunun "kernel" kodları için ayrıldığı bölüm bütün tablolarda aynıdır. "task-switch" esnasında bile o kodlar korunurlar. Peki bizler bir program içerisinde yüksek miktarda dinamik tahsisat yaptığımızda ne olur? Linux sistemlerinde "malloc" fonksiyonu "brk" ya da "sbrk" denilen sistem fonksiyonunu çağırabilmektedir. Ancak arka planda sanal bellek mekanizması açısından şunlar gerçekleşmektedir; -> İşletim sistemi "malloc" ile tahsis edilen alanı sayfa tablosunda oluşturur. Bu aşamada da tahsis edilen alanın toplam "swap" dosyasının büyüklüğünü geçmediğini garanti etmeye çalışır. Çünkü günün sonunda tahsis edilen alan "swap" dosyası içerisinde de bulunacaktır. -> İşletim sistemi, "swap" dosyasının boyutu yeterliyse, tahsisatı kabul etmektedir. Fakat sistemden sisteme değişmekle birlikte, "swap" dosyasında tahsisat yapılabilir ya da yapılmayabilir. Yapılması durumunda "swap" dosyası ciddi manada dolacağı için başka proses aynı işlemi de yapamayabilir. Fakat Linux sistemleri dinamik alan kullanılmaya başlandığı vakit "swap" dosyasında tahsilat yapmaktadır. Aşağıdaki iki kodu çalıştırarak farkını gözlemleyiniz: * Örnek 1, Dinamik Alan henüz kullanılmamaktadır. #include "stdio.h" #include "stdlib.h" int main(int argc, char** argv) { /* # OUTPUT # not enough memory!... */ /* * 5GB büyüklüğünde bir yer ayrılmaya çalışıyor. */ char* pc; pc = (char*) malloc(5000000000); if(pc == NULL) goto NOT_ENOUGH_MEMORY; NOT_ENOUGH_MEMORY: fprintf(stderr, "not enough memory!...\n"); exit(EXIT_FAILURE); } * Örnek 2, Dinamik Alan KULLANILMIŞTIR. #include "stdio.h" #include "stdlib.h" int main(int argc, char** argv) { /* # OUTPUT # not enough memory!... */ /* * 5GB büyüklüğünde bir yer ayrılmaya çalışıyor. */ char* pc; pc = (char*) malloc(5000000000); if(pc == NULL) goto NOT_ENOUGH_MEMORY; for(long long i = 0; i < 5000000000; ++i) pc[i] = 0; goto ENOUGH_MEMORY; NOT_ENOUGH_MEMORY: fprintf(stderr, "not enough memory!...\n"); exit(EXIT_FAILURE); ENOUGH_MEMORY: fprintf(stdout, "Ok!...\n"); exit(EXIT_SUCCESS); } Bu noktada "swap" dosyasının yetersiz gelmesi durumunda "swap" dosyasının büyüklüğünü arttırmamız gerekmektedir. Dolayısıyla yeterli büyüklükteki "swap" dosyası ile daha büyük programları da çalıştırabiliriz. Her bir sayfa tablosu aynı zamanda işletim sisteminin de kodlarını barındırmaktadır ve bu kısım her bir sayfa tablosunda birbirinin aynısıdır. Dolayısıyla sayfa tablosunun bu kısımları fiziksel RAM'deki aynı bölgeyi göstermektedir. Pekiyi prosesler sayfa tablosunun bu kısmına normal yollar ile erişebilirler mi yoksa bir koruma mekanizması da var mıdır? Tabii normal prosesler sayfa tablosunun bu kısmına erişemezler. Çünkü sayfa tablosunun o kısmında bulunan sanal sayfa numaralarına tekabül eden fiziksel RAM sayfaları, "kernel mode" olarak nitelendirilmiştir. Dolayısıyla prosesimizin "user mode" proses değil, "kernel mode" proses olması gerekmektedir. Esasında fiziksel sayfalar "kernel mode" ve "user mode" olarak nitelendirilmiştir. "user mode" prosesler sadece "user mode" olan fiziksel sayfalara erişebilirken, "kernel mode" olan prosesler her iki "mode" daki sayfalara da erişebilmektedir. Eğer "user mode" bir proses "kernel mode" bir fiziksel sayfaya erişmeye çalışırsa işlemci "fault" oluşturur ve işletim sistemi ilgili prosesi sonlandırır. Öte yandan "user mode" sayfalar ekstra bir etikete daha sahiptir. Bu etiketler kabaca şu şekildedir; "read only", "read/write" ve "execute". Bu etiketler yine "user mode" prosesleri ilgilendirmektedir. Bu etiketler bir nevi erişim biçimi olarak da görülebilir. Örneğin, "normal mode" bir proses "normal mode" ve "read only" olarak nitelendirilmiş fiziksel sayfaya yazma yapmak istesin. İşlemci yine içsel kesme("page fault") oluşturacak ve işletim sistemi prosesi sonlandıracaktır. Öte yandan pek çok işlemci ailesinde, bir kodun bir fiziksel sayfada çalışabilmesi için o kodun "executable" niteliğine sahip fiziksel bir sayfada bulunması gerekmektedir. Aksi halde yine "page fault" olacaktır. * Örnek 1, Aşağıdaki programda "read only" olan fiziksel sayfada "write" işlemi yapılmaya çalışılmıştır. #include "stdio.h" int main(int argc, char** argv) { /* # OUTPUT # */ char* str = "ahmo"; /* * C standartlarına göre "string" leri "update" etmek * Tanımsız Davranıştır. Öte yandan "strin" ler RAM'e * aktarılırken "exec" fonksiyonları tarafından * "read only" bir sayfaya yükleniyorlar. Dolayısıyla * bizler aslında "read only" niteliğindeki bir sayfaya * "write" yapmaya çalışıyoruz ve prosesimiz işletim * sistemi tarafından sonlandırılmaktadır. */ *str = 'a'; puts(str); return 0; } * Örnek 2, Bazı derleyiciler, özellikle C++ derleyicileri, "const" olan ve global isim alanındaki değişkenleri de yine fiziksel RAM'in "read only" olarak nitelendirilmiş kısımlarına aktarabilirler. Tabii bu bir zorunluluk değildir... #include "stdio.h" const int x = 100; int main(int argc, char** argv) { /* # OUTPUT # */ /* * Bu dönüşüm ile "const" özelliği düşmüştür. Artık derleme * zamanında bir hata almayacağız. Fakat nesnemiz özü itibariyle * hala "const" bir nesnedir. */ int* ptr = (int*)&x; /* * Fakat "const" ve global isim alanındaki nesneler belleğe * aktarılırken "read only" olan fiziksel sayfalara aktarılabilirler. * Dolayısıyla bizler o alanlarda "write" işlemi yapacağımız * için yine "page fault" oluşacaktır. */ *ptr = 200; printf("%d\n", *ptr); return 0; } * Örnek 3, #include "stdio.h" int main(int argc, char** argv) { /* # OUTPUT # 200 */ const int x = 100; /* * Bu dönüşüm ile "const" özelliği düşmüştür. Artık derleme * zamanında bir hata almayacağız. */ int* ptr = (int*)&x; /* * "const" ve yerel isim alanındaki nesneler "stack" içerisinde * hayata geldiklerinden, bu alanların "read only" bir fiziksel * sayfaya aktarılmaları mümkün değildir. Dolayısıyla aşağıda * gerçektende bir güncelleme yapılmıştır. */ *ptr = 200; printf("%d\n", *ptr); return 0; } Pekiyi bizler bir programı iki defa çalıştırdığımız zaman sayfa tabloları açısından durum nasıl olacaktır? Bu yöndeki beklentimiz iki ayrı sayfa tablosunun hayata gelmesi ve her bir tablonun sağ tarafındaki fiziksel RAM'deki alanları gösteren kısımların da birbirinden farklı olması yönündedir. Fakat işletim sistemi bu noktada bir optimizasyon yapmakta ve her iki prosesin sayfa tablolarının sağ tarafını birbiriyle aynı yapmaktadır. Dolayısıyla iki prosesin sanal sayfaları aslında RAM'de aynı yeri göstermektedir. Ek olarak sayfa tablolarındaki fiziksel sayfaları da "read only" olarak işaretlemektedir. Eğer proseslerden birisi bu alanlardan birisine yazma yapmak isterse "page fault" oluşur ve yeni bir sayfa oluşturulup ilgili tablo da güncellenir. Fakat diğer sayfalar aynı kalır. Böylelikle yazma yapılmayana kadar aslında iki tablo da bellekte aynı alanları gösterir. Bu mekanizmaya da "Copy on Right" denir. Son olarak paylaşılan bellek alanları prosesler arasında haberleşme için de kullanılmaktadır. * Örnek 1, Aşağıdaki şekilde iki tane sayfa tablomuz olsun: Proses - I Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 95134 45321 ... ... Proses - II Sayfa Tablosu (decimal) (decimal) Sanal Sayfa Numaraları RAM'deki Karşılıkları ... ... 32148 45321 ... ... Bu iki sayfa tablosundaki "95134" ve "32148" sanal sayfa numaraları fiziksel RAM'de aynı alanı göstermektedir. Dolayısıyla bu iki proses bellekteki o alanı paylaşımlı olarak kullanmaktadır ve bu iki proses bu bellek alanını kullanarak birbiriyle haberleşmektedir. Bu yöntem, prosesler arası haberleşme yöntemlerinden bir tanesidir. Fakat bu yaklaşım normalde sergilenen bir yaklaşım değildir. Normal şartlarda prosesler arasında tam bir izolasyon söz konusudur. > Hatırlatıcı Notlar: >> Bir program içerisindeki adresler gerçek adres değillerdir. Sanal adreslerdir. >> Donanımsal olarak sayfa tablosuna başvuru işini hızlandırmak için işlemciler bünyelerinde "Translation Lookaside Buffer" denilen bir "cache" sistemi barındırmaktadır. >> "cache": Bir yere erişimi azaltmak için onun bir bölümünün daha hızlı bir yere aktarılması demektir. Örneğin, bizler disk üzerinde bir yere sık sık erişmek isteyelim. Bu bölümü bizler belleğe aktarıyoruz. Lüzum görüldüğünde önce bellekteki bu alana bakıyoruz. Bulamazsak bizzat diske bakıyoruz. İşte "cache" demek budur. >> Kaan Arslan'ın "İntel İşlemcileri Korumalı Mod Yazılım Mimarisi" isimli kitabı, tavsiye edilir. >> Intel işlemciler "core" mantığını şöyle implemente etmektedir: "dual-core" sisteme geçmeden evvel fiziksel sadece bir tane çekirdek vardı. Fakat bu çekirdeğin içeriğindeki bazı parçalar çiftlendi, iki tane oldu. Dolayısıyla aslında fiziken bir tane çekirdek var fakat işlevsellik açısından iki tane gibi. Bu sisteme de "Hyper Threading" denmekte. İşte "dual-core" a geçince artık fiziken iki tane çekirdek var fakat işlevsellik açısından dört tane. Bu yüzdendir ki işlemcilerin dökümanında yazan çekirdek sayısı gerçekte var olan işlemci sayısını, iş parçacığı sayısı ise işlevsellik açısından işlemci sayısını temsil etmektedir. Örneğin, elimizde i7-7700 model bir işlemcimiz olsun. Bu işlemci çekirdek sayısı olarak 4, iş parçacığı sayısı olarak 8 adet çekirdek içermektedir. Yani aslında 4 adet fiziki çekirdek var fakat çakma çekirdek sayısı 8. İşletim sistemi burada 8 çekirdek varmış gibi davranmaktadır. >> "Minimum Working Set" : İşletim sisteminin bir prosesi belleğe yüklerken baştan yükleyeceği en az sayfa sayısıdır. Bu değer önceden belirlenmiştir. Hiçbir programın bu değerden daha az sayıdaki sayfası bellekte bulunamaz. >> "Page Swapping Algo." : https://en.wikipedia.org/wiki/Page_replacement_algorithm >> Linux sistemlerinde bir dosyanın "swap file" olarak kullanmak için: https://acloudguru.com/hands-on-labs/creating-swap-space-on-a-linux-system?utm_source=google&utm_medium=paid-search&utm_campaign=cloud-transformation&utm_term=ssi-global-acg-core-dsa&utm_content=free-trial&gclid=Cj0KCQjwwtWgBhDhARIsAEMcxeBefkHI234CiO9EAcml2sjJ5Dayyo0A7sjVH_xbfw5WFSkfBtFwgRYaApBIEALw_wcB >>> Bu tip sistemlerde o anki toplam "swap" alanlarına "/proc/swaps" dosyasından bakabilir ya da "swapon -s" komutunu "shell" programından çalıştırarak bakabiliriz. >> Windows sistemlerde "swap" dosyasının karşılığı Performans Seçenekleri, Sanal Bellek sekmesindedir. >> Sayfa Tablosunun Fiziksel Sayfa kısmı aslında RAM'deki kısmı göstermektedir. >> Bir bellek alanının başlangıcı "0x0000000..." biçiminde, sonu ise "0xFFFFFFF..." biçimindedir. >> Adres değerinde, sağdan üç basamağı sıfıra çekmek, aşağı yuvarlamaktır eğer sayfa sayısı 4096 ise. Çünkü 10'luk tabandaki 4096, 16'lık tabanda 1000 değeridir. >> Bir adres değerinin sayfa katları olacak şekilde aşağı doğru yuvarlanması: #include #include #include #include char buffer[4096]; int main(void) { /* # OUTPUT # Old : [0x563030815040] New : [0x563030815000] */ printf("Old : [%p]\n", buffer); /* Aşağıda, bir adres değeri, sayfa katları olacak şekilde aşağı doğru, tekrar hizalanmıştır. * @ "uintptr_t" : türü C99 ile dile eklenen ve bir "pointer" uzunluğunda * hizalanmış olan "İşaretsiz Tam Sayı" türüdür. * @ "(uintptr_t)buffer" : Adres değerleri "bit-wise" işlemlere sokulamazlar. Bu * adres değerinin kapladığı alan, sanki işaretsiz tam sayı kullanıldığında kaplayacak * alan biçimine getirilmiştir. * @ "0xFFF" : Sistemimizdeki sayfa uzunluğu "4096" bayt olarak varsayılmıştır. * "4096" baytın 16'lık tabandaki gösterimidir. 2'lik tabandaki "...0000 1111 1111 1111" * ifadesidir. * @ "~0xFFF" : 2'lik tabandaki "...1111 0000 0000 0000" ifadesidir. * @ "(uintptr_t)buffer & ~0xFFF" : İlgili işaretsiz tam sayı değerinin en düşük anlamlı * bitleri sıfırlanmıştır. Böylelikle 10'luk tabandaki karşılığı "4096" nın katları olmuştur. * @ "(void*)((uintptr_t)buffer & ~0xFFF)" : Artık ilgili işaretsiz tam sayı değeri adres olarak * yorumlanacaktır. */ void* addr = (void*)((uintptr_t)buffer & ~0xFFF); printf("New : [%p]\n", addr); return 0; }