=============================================================== Bellek Yönetimi - 2. Bölüm :raw-html:`
` Tahsisat İşlemleri =============================================================== Bu bölümde Linux çekirdeğinin sayfa düzeyindeki ve byte düzeyindeki tahsisat mekanizmalarını ele alacağız. Bölüm içerisinde önce çekirdeğin boş sayfaları nasıl belirlediğini ve sayfa düzeyinde tahsisatları nasıl yaptığını açıklayacağız. Sonra da byte düzeyindeki tahsisat işlemleri üzerinde duracağız. Linux çekirdeğinde iki düzeyli tahsisat sistemi vardır: 1. Sayfa düzeyinde tahsisat sistemi (*buddy allocator*) 2. Byte düzeyinde tahsisat sistemi (*slab allocator*) Linux çekirdeğindeki sayfa düzeyinde tahsisat sistemine *ikiz blok tahsisat sistemi* ya da İngilizcesiyle *buddy allocator*, byte düzeyinde tahsisat sistemine ise *dilimli tahsisat sistemi* ya da İngilizcesiyle *slab allocator* denilmektedir. Dilimli tahsisat sistemi, ikiz blok tahsisat sistemini kullanrarak tahsis ettiği sayfaları byte düzeyinde tahsisatlar için organize etmektedir. Dilimli tahsisat sisteminin ikiz blok tahsisat sisteminin üzerine oturtulduğunu söyleyebiliriz: .. image:: _static/buddy-slab-layers.png :alt: Dilimli ve İkiz Blok Tahsisat Sistemi şeması :align: center :width: 45% Biz bu bölümde önce ikiz blok tahsisat sistemini sonra da dilimli tahsisat sistemini inceleyeceğiz. İkiz Blok Tahsisat Sistemi (Buddy Allocator) ============================================ Çekirdek tüm fiziksel sayfaları bir heap alanıymış gibi ele alıp onların tahsisatlarını yönetmektedir. Fiziksel sayfaları temsil eden ``page`` nesneleri ilgili sayfanın tahsis edilip edilmediği gibi bilgileri de tutmaktadır. Biz bir aygıt sürücü yazarken ya da çekirdeğe bir modül eklerken bir fiziksel sayfayı doğrudan kullanamayız. Çünkü o sayfa başka amaçlarla başka kaynaklar tarafından kullanılıyor olabilir. Biz önce sayfa düzeyinde tahsisat yapan fonksiyonlarla sayfayı tahsis edip ondan sonra o sayfayı kullanabiliriz. Yukarıda da belirttiğimiz gibi Linux çekirdeğinde sayfa düzeyinde tahsisat yapan bir tahsisat sistemi bulunmaktadır. Biz bu sisteme Türkçe *ikiz blok tahsisat sistemi* diyeceğiz. Bu sistemin İngilizce ismi *buddy allocator* biçimindedir. İzleyen paragraflarda ikiz blok tahsisat sisteminin algoritmik yapısını açıklayacağız. İşletim sistemlerinde sayfa düzeyinde tahsisatların hızlı yapılması gerekir. Aynı zamanda sayfa tahsisat sisteminin mümkün olduğunca bellek bölünmesi (*fragmentation*) olgusuna dirençli olması da istenir. İkiz blok tahsisat sistemi ilk kez Knowlton tarafından ortaya atılmıştır. Knowlton bu algoritmayı 1965 yılında *Communications of the ACM* dergisinde *A Fast Storage Allocator* başlıklı makalesinde açıklamıştır. Bu sistem Linux çekirdeklerine 1.2 versiyonuyla (1995) eklenmiştir. Linux çekirdeklerinde zamanla ikiz blok tahsisat sistemi daha karmaşık hale getirilmiştir. Bu karmaşıklık tahsisat algoritmasının kendisinden değil *boş listelerin (free lists)* ve bölgelerin (zones) *fallback* denilen gözden geçirilmesi mekanizmasından kaynaklanmaktadır. Biz burada önce ikiz blok tahsisat sisteminin algoritmik yapısını açıklayacağız sonra Linux çekirdeğindeki gerçekleştirimi üzerinde duracağız. İkiz Blok Tahsisat Sisteminin Algoritmik Yapısı ------------------------------------------------ İkiz blok tahsisat sisteminde boş bloklar 2'nin kuvvetlerine ilişkin ardışıl fiziksel sayfalardan oluşan bloklar biçiminde organize edilmektedir. Buradaki 2'nin kuvvetine ilişkin boş blok listelerine İngilizce *order* denilmektedir. Biz *order* sözcüğü yerine Türkçe *düzey* sözcüğünü kullanacağız. Aşağıda 5 düzeyli bir ikiz blok sisteminin çizimi yapılmıştır: .. image:: _static/buddy-levels.png :align: center :width: 70% Tabii buradaki listeler boş sayfa listeleridir. Tahsis edilen sayfalar bu listelerden çıkartılmaktadır. Tahsisat her zaman 2ⁿ sayfa olacak biçimde düzey belirtilerek yapılmaktadır. Örneğin 4 sayfanın (2² sayfanın) tahsis edilmek istendiğini düşünelim. Tahsisat 2'inci düzeydeki boş listeden sağlanacaktır. Peki ya istenilen düzeyde hiç boş sayfa bloğu yoksa ne olur? İşte bu durumda daha yüksek düzeylere başvurulup onlar parçalanmaktadır. Örneğin 2'inci düzeyde boş sayfa bloğu bulunmuyor olsun. Algoritma bu durumda 3'üncü düzeye bakar. Eğer 3'üncü düzeyde 8 sayfalık boş bir sayfa bloğu varsa onu 2 parçaya ayırır. Parçalardan birini 2'inci düzeydeki sayfa bloğu listesine ekler, diğerini verir. Peki biz sayfa tahsis etmek istediğimizde 2'inci düzeyde de 3'üncü düzeyde de boş sayfa bloğu yoksa ne olacaktır? İşte bu durumda gittikçe yukarı çıkılır, ilk boş sayfa bloğu olan düzeyden blok tahsis edilir. Sonra blok bölüne bölüne aşağıya inilir. Örneğin 4 sayfa tahsis etmek istediğimizde 3'üncü düzeyde boş sayfa bloğu yoksa ancak 4'üncü düzeyde boş sayfa bloğu varsa bu düzeydeki 16 sayfalık blok yarıya bölünür. Bunun 8'lik kısmı 3'üncü düzeydeki boş listeye eklenir, diğer 8'lik kısmı yine bölünür, onun 4'lük kısmı 2'inci düzey listeye eklenir, diğeri de tahsis edilir. Şimdi bu süreci şekillerle adım adım gösterelim. Başlangıç durumu şöyledir: .. image:: _static/buddy-initial.png :align: center :width: 60% 2'inci düzeyde ve 3'üncü düzeyde boş sayfa bloğu olmadığı için 4'üncü düzeydeki boş sayfa bloklarının biri alınıp bölünür. Bölünen bloklar 8 sayfalık olacaktır. Bunlardan biri 3'üncü düzeydeki boş listeye eklenir, diğeri bölünmeye devam eder: .. image:: _static/buddy-split-step1.png :align: center :width: 70% Burada 3'üncü düzeydeki 8 sayfanın yeniden ikiye bölünmesiyle bunlardan biri 2'inci düzeydeki boş listeye eklenir: .. image:: _static/buddy-split-step2.png :align: center :width: 70% İşte 8 sayfanın 4'lük kısmı 2'inci düzeydeki bağlı listeye eklenip kalan 4'lük kısmı da çağrıyı yapana verilmektedir. Boş listelerin son hali şöyle olacaktır: .. image:: _static/buddy-final-state.png :align: center :width: 70% Peki tahsis edilen bu 4 sayfalık blok free hale getirildiğinde ne olmaktadır? İşte algoritma bu durumda tersten işletilmektedir. Yani bu 4 sayfalık blok 2'inci düzeye yerleştirilir. Ancak bu 2'inci düzeyde onun *ikizi (buddy'si)* varsa free hale getirilenle bu ikizi birleştirilerek üst düzeydeki boş listeye eklenir. Tabii aynı durum üst düzey için de yapılacaktır. Burada bir noktaya dikkatinizi çekmek istiyoruz: Free hale getirme algoritmasından amaç en yüksek sayfa içeren ardışıl bloğun oluşturulmasıdır. Free edilen blokla onun *ikizinin (buddy'sinin)* birleştirilip üst bloğa taşınmasının temel amacı budur. Şimdi bu birleştirme işlemini yine adım adım gösterelim. Free işlemi öncesindeki durum şöyledir: Şimdi 4 sayfalık bloğu free hale getirelim. Algoritma önce bu bloğu 2'inci düzeye eklerken onun ikizi (buddy'si) o düzeyde var mı diye bakar. Eğer varsa onları birleştirip bir yukarıdaki düzeye eklemeye çalışır. Bizim örneğimizde onun ikizi 2'inci düzeyde bulunmaktadır: .. image:: _static/buddy-coalesce.png :align: center :width: 70% Birleştirme sonucunda şu durum oluşacaktır: .. image:: _static/buddy-coalesce-lv3.png :align: center :width: 70% İşte 3'üncü düzeyde de birleştirilmiş bloğun ikizi bulunduğu için o ikiz blok da birleştirilip üst boş blok listesine (4'üncü düzeydeki boş blok listesine) yerleştirilecektir: .. image:: _static/buddy-final-merge.png :align: center :width: 70% Görüldüğü gibi her şey ters sırada eski haline gelmiştir. Linux çekirdeklerinde maksimum düzey ``include/linux/mmzone.h`` dosyasındaki ``MAX_ORDER`` sembolik sabitiyle belirtiliyordu. Ancak en son çekirdeklerde (>= 6.8) artık ``MAX_ORDER`` yerine aşağıdaki iki sembolik sabit kullanılmaya başlanmıştır: .. code-block:: c #define MAX_PAGE_ORDER 10 /* en yüksek düzey indeksi */ #define NR_PAGE_ORDERS (MAX_PAGE_ORDER + 1) /* listelerin sayısı = 11 */ ``MAX_PAGE_ORDER`` maksimum düzeyin indeks numarasını, ``NR_PAGE_ORDERS`` ise bunların sayısını belirtmektedir. Linux çekirdeklerinde maksimum düzey 10'dur. (Yani toplam 11 tane düzey listesi bulunmaktadır.) 6.1 çekirdeği ve öncesinde ``MAX_ORDER`` toplam liste sayısını belirtirken, daha sonra en yüksek düzey indeksini belirtir hale getirilmiştir. Nihayet yukarıda da belirttiğimiz gibi 6.8 ile birlikte bu sembolik sabitlerin isimleri değiştirilmiştir. .. list-table:: :header-rows: 1 :widths: 22 78 * - Versiyon - Durum * - ≤ 6.1 - ``MAX_ORDER = 11``; maksimum düzey indeksi 10. * - 6.2 – 6.7 - ``MAX_ORDER = 10``; anlam düzeltildi (artık indeks numarasını belirtiyor). * - ≥ 6.8 - ``MAX_ORDER`` kaldırıldı → ``MAX_PAGE_ORDER = 10``, ``NR_PAGE_ORDERS = 11``. Linux çekirdeklerinde en yüksek düzey 10 olduğuna göre ve 2\ :sup:`10` = 1024 olduğuna göre, en yüksek düzeydeki sayfa blokları 1024 sayfadan oluşmaktadır. 1024 sayfa da 4 MB yer kaplamaktadır. Bellek Bölgeleri ve Göç Türleri ------------------------------- Biz yukarıda ikiz blok tahsisat sisteminin temel algoritmasını açıkladık. Ancak Linux çekirdeğinde ikiz blok tahsisat sistemi bir tane değildir. Her NUMA düğümü bellek bölgelerinden (memory zones), her bellek bölgesi de göç türlerine (migration types) ilişkin ikiz blok tahsisat sistemlerinden (buddy allocators) oluşmaktadır. Yani bir bellek bölgesinde bile birden fazla ikiz blok tahsisat sistemi bulunmaktadır. .. figure:: _static/numa-buddy-node0.png :alt: NUMA Düğümü 0 — Zone ve göç türü hiyerarşisi :align: center :width: 80% .. figure:: _static/numa-buddy-node1.png :alt: NUMA Düğümü 1 — Zone ve göç türü hiyerarşisi :align: center :width: 80% Görüldüğü gibi NUMA düğümleri bellek bölgelerinden, bellek bölgeleri ise göç türlerine göre birden fazla ikiz blok tahsisat sisteminden oluşmaktadır. NUMA düğümleri içerisindeki her bölgenin *zone* isimli bir yapıyla temsil edildiğini belirtmiştik. İşte ``zone`` yapısının ``free_area`` elemanı o bölgedeki ikiz blok tahsisat sistemlerini tutmaktadır: .. code-block:: c struct zone { /* ... */ struct free_area free_area[NR_PAGE_ORDERS]; int nr_zones; /* ... */ }; Görüldüğü gibi ``free_area`` dizisi *free_area* isimli yapı türündendir ve uzunluğu maksimum düzey sayısına eşittir. (Yani dizi 11 elemanlıdır; ilk elemanı 0'ıncı düzeyi, son elemanı 10'uncu düzeyi belirtmektedir.) ``free_area`` yapısı şöyle tanımlanmıştır: .. code-block:: c struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; }; Görüldüğü gibi ``free_area`` aslında her göç türü için bağlı listelerden oluşmaktadır. Aşağıdaki şekil bu veri yapısını daha iyi anlaşılmasına yardımcı olacaktır: .. code-block:: text zone (örn. ZONE_NORMAL): │ ├── free_area[0] (düzey-0, 4 KB bloklar) │ ├── free_list[MIGRATE_UNMOVABLE] → [pg1] → [pg4] → [pg9] → NULL │ ├── free_list[MIGRATE_MOVABLE] → [pg2] → [pg7] → NULL │ ├── free_list[MIGRATE_RECLAIMABLE] → [pg3] → NULL │ ├── free_list[MIGRATE_HIGHATOMIC] → NULL │ ├── free_list[MIGRATE_CMA] → NULL │ └── free_list[MIGRATE_ISOLATE] → NULL │ ├── free_area[1] (düzey-1, 8 KB bloklar) │ ├── free_list[MIGRATE_UNMOVABLE] → [pg16,17] → NULL │ ├── free_list[MIGRATE_MOVABLE] → [pg32,33] → [pg64,65] → NULL │ └── ... │ ├── free_area[2] (düzey-2, 16 KB bloklar) │ └── ... │ ... │ └── free_area[10] (düzey-10, 4 MB bloklar) └── ... Buradaki ``free_area``'nın düzeylerden oluşan bir dizi olduğuna, dizinin her elemanının her göç türü için ayrı listeler barındırdığına dikkat ediniz. ``free_area`` listesinin her elemanı aslında bir bağlı liste dizisidir. ``free_area[n]`` bağlı liste dizisi *n*'inci düzeyin her göç türü için bağlı listelerini tutmaktadır. (Örneğin ``free_area[0]`` bağlı liste dizisi her göç türü için 0'ıncı düzeyin bağlı listelerini, ``free_area[1]`` bağlı liste dizisi her göç türü için 1'inci düzeyin bağlı listelerini tutmaktadır.) Göç türlerinin ne amaçla kullanıldığını izleyen paragraflarda açıklayacağız. Güncel çekirdeklerde ``free_area`` nesnelerinin içerisindeki ``free_list`` bağlı listeleri ``buddy_list`` elemanı yoluyla *page* nesnelerini tutar durumdadır: .. code-block:: c struct page { memdesc_flags_t flags; union { struct { union { /* ... */ struct list_head buddy_list; /* free_list tarafından kullanılan düğüm */ /* ... */ }; /* ... */ }; /* ... */ } } _struct_page_alignment; Sayfa Tahsisat Fonksiyonları ---------------------------- ``alloc_pages`` fonksiyonu ikiz blok tahsisat sisteminden sayfa tahsis eden en temel fonksiyondur. Fonksiyonun parametrik yapısı şöyledir: .. code-block:: c struct page *alloc_pages(gfp_t gfp_mask, unsigned int order); ``alloc_pages`` eskiden ``include/linux/gfp.h`` dosyasında ``CONFIG_NUMA`` konfigürasyon parametresine göre makro ya da inline fonksiyon biçiminde tanımlanıyordu. Ancak daha sonra makro haline getirilmiştir. ``alloc_pages`` fonksiyonun birinci parametresi tahsisatın nereden yapılacağını belirtmektedir. İkinci parametresi ise tahsisat için düzey belirtmektedir. (Yani örneğin 1 sayfa tahsis edilecekse bu parametre 0, iki sayfa tahsis edilecekse 1, 4 sayfa tahsis edilecekse 2 girilmelidir.) Fonksiyonun birinci parametresindeki ``gfp_t`` türü tipik olarak ``unsigned int`` biçiminde typedef edilmektedir. Bu parametreye çeşitli mask bayrakları bit düzeyinde OR işlemine sokularak verilmektedir. Mask bayrakları ``include/linux/gfp_types.h`` dosyası içerisinde define edilmiştir. Bunları aşağıda bir tablo biçiminde veriyoruz: .. list-table:: :header-rows: 1 * - Bayraklar - İşlevi * - ``__GFP_DMA`` - Tahsisatı ZONE_DMA'dan yap (eski ISA DMA uyumu için) * - ``__GFP_HIGHMEM`` - Tahsisatı ZONE_HIGHMEM'den yap * - ``__GFP_DMA32`` - Tahsisatları 32-bit adreslenebilir ZONE_DMA32'den yap * - ``__GFP_MOVABLE`` - ZONE_MOVABLE'a izin ver; sayfa göç ile taşınabilir * - ``__GFP_RECLAIMABLE`` - Sayfa shrinker'lar aracılığıyla geri alınabilir (dilim için) * - ``__GFP_WRITE`` - Sayfa kirletilecek; bölgeler arasında dağıtılır (fair policy) * - ``__GFP_HARDWALL`` - cpuset bellek tahsisat politikasını zorla uygula * - ``__GFP_THISNODE`` - Yalnızca belirtilen NUMA düğümlerini ayır, fallback yok * - ``__GFP_ACCOUNT`` - Tahsisatı kmemcg'ye hesapla (kernel memory cgroup) * - ``__GFP_HIGH`` - Yüksek öncelikli; atomic rezervlere erişebilir * - ``__GFP_MEMALLOC`` - Tüm belleğe (rezervler dahil) erişime izin ver * - ``__GFP_NOMEMALLOC`` - Acil rezervlere erişimi açıkça yasakla * - ``__GFP_IO`` - Bellek geri almak için fiziksel I/O başlatabilir * - ``__GFP_FS`` - Bellek geri almak için dosya sistemi çağrısı yapabilir * - ``__GFP_DIRECT_RECLAIM`` - Çağıran doğrudan geri alıma girebilir * - ``__GFP_KSWAPD_RECLAIM`` - Low watermark'ta kswapd'yi uyandırabilir * - ``__GFP_RECLAIM`` - ``__GFP_DIRECT_RECLAIM | __GFP_KSWAPD_RECLAIM`` kısayolu * - ``__GFP_RETRY_MAYFAIL`` - İlerleme varsa geri alımı tekrar dene; OOM'u tetiklemez * - ``__GFP_NOFAIL`` - Sonsuz yineleme; asla başarısız olamaz, bloke olabilir * - ``__GFP_NORETRY`` - Yalnızca hafif geri alım dene; "OOM killer" çağrılmaz * - ``__GFP_NOWARN`` - Ayırma başarısız olursa çekirdek uyarı mesajını bastır * - ``__GFP_COMP`` - Bileşik sayfalar için metadata ekle (büyük sayfa grupları için) * - ``__GFP_ZERO`` - Başarılı tahsisatlarda sıfırlanmış sayfa döndür * - ``__GFP_ZEROTAGS`` - Bellek sıfırlanırken KASAN HW bellek etiketlerini de sıfırla * - ``__GFP_SKIP_ZERO`` - KASAN HW etiket sıfırlamasını atla * - ``__GFP_SKIP_KASAN`` - KASAN sayfa zehirleme/çözme kontrollerini atla * - ``__GFP_NOLOCKDEP`` - GFP context takibi için lockdep denetimini devre dışı bırak Linux çekirdek kodlamasında başı ``__`` ile başlayan değişkenlerin "aşağı seviyeli kodlar tarafından kullanıldığını" anımsayınız. Yukarıdaki bayraklar ince ayar için kullanılmaktadır. Bunların her bileşimi anlamlı değildir. Yani örneğin bazı bayraklar bazı bayraklarla kullanılamamaktadır. Aslında çekirdek içerisinde yukarıdaki bayraklar kullanılarak oluşturulmuş başı ``__`` ile başlamayan daha yüksek seviyeli bayraklar da vardır. Bunların listesini de aşağıdaki tabloda veriyoruz: .. list-table:: :width: 70% :header-rows: 1 * - Bileşke Bayraklar - Bileşen Bayraklar * - ``GFP_ATOMIC`` - ``__GFP_HIGH | __GFP_KSWAPD_RECLAIM`` * - ``GFP_KERNEL`` - ``__GFP_RECLAIM | __GFP_IO | __GFP_FS`` * - ``GFP_KERNEL_ACCOUNT`` - ``GFP_KERNEL | __GFP_ACCOUNT`` * - ``GFP_NOWAIT`` - ``__GFP_KSWAPD_RECLAIM`` * - ``GFP_NOIO`` - ``__GFP_RECLAIM`` * - ``GFP_NOFS`` - ``__GFP_RECLAIM | __GFP_IO`` * - ``GFP_USER`` - ``__GFP_RECLAIM | __GFP_IO | __GFP_FS | __GFP_HARDWALL`` * - ``GFP_HIGHUSER`` - ``GFP_USER | __GFP_HIGHMEM`` * - ``GFP_HIGHUSER_MOVABLE`` - ``GFP_HIGHUSER | __GFP_MOVABLE | __GFP_SKIP_KASAN`` * - ``GFP_DMA`` - ``__GFP_DMA`` * - ``GFP_DMA32`` - ``__GFP_DMA32`` * - ``GFP_TRANSHUGE`` - ``GFP_HIGHUSER_MOVABLE | __GFP_COMP | __GFP_NOMEMALLOC |`` ``__GFP_NORETRY | __GFP_NOWARN | __GFP_KSWAPD_RECLAIM`` * - ``GFP_TRANSHUGE_LIGHT`` - ``GFP_HIGHUSER_MOVABLE | __GFP_COMP | __GFP_NOMEMALLOC |`` ``__GFP_NORETRY | __GFP_NOWARN`` Programcılar genellikle bu yüksek seviyeli bayrakları kullanmaktadır. Örneğin ``GFP_ATOMIC``, ``GFP_KERNEL``, ``GFP_USER``, ``GFP_DMA``, ``GFP_DMA32`` en çok kullanılan yüksek seviyeli bayraklardır. Biz bu bayraklar hakkında izleyen paragraflarda daha fazla bilgi vereceğiz. ``alloc_pages`` fonksiyonu başarı durumunda tahsis edilen sayfaların ilkine ilişkin ``page`` yapı nesnesinin adresiyle, başarısızlık durumunda ``NULL`` adresle geri dönmektedir. Anımsanacağı gibi zaten çekirdek tüm sayfaları doğrudan ya da dolaylı biçimde bir dizi içerisinde tutmaktadır. ``alloc_pages`` bize ilgili ``page`` nesnesinin bu dizideki adresini vermektedir. Burada bize verilen ``page`` adresini biz ilerleterek ilgili dizinin sonraki elemanına erişiriz. İkiz blok tahsisat sisteminde her zaman fiziksel bellekte ardışıl fiziksel sayfaların tahsis edildiğini anımsayınız. Tabii ``alloc_pages`` fonksiyonun bize verdiği adres sanal adrestir. Anımsanacağı gibi bir ``page`` nesnesinin adresi bilindiğinde bunun fiziksel bellekteki kaç numaralı sayfaya ilişkin olduğu ``page_to_pfn`` fonksiyonuyla elde edilebilmekteydi. ``alloc_pages`` fonksiyonuyla tahsis edilen sayfaların ``__free_pages`` fonksiyonuyla serbest bırakılması gerekir: .. code-block:: c void __free_pages(struct page *page, unsigned int order); Fonksiyon ``mm/page_alloc.c`` dosyası içerisinde tanımlanmıştır. Fonksiyonun birinci parametresi tahsisata ilişkin ilk ``page`` nesnesinin adresini, ikinci parametresi ise düzey bilgisini belirtmektedir. Örneğin 2'inci düzeyden 4 sayfa tahsis etmiş olalım. Bu sayfaları serbest bırakırken yine düzey bilgisini 2 olarak girmeliyiz. Örneğin: .. code-block:: c struct page *pages; pages = alloc_pages(GFP_KERNEL, 2); /* 2'inci düzeyden 4 ardışıl fiziksel sayfa tahsis ediliyor */ if (pages == NULL) return -ENOMEM; /* ... */ __free_pages(pages, 2); /* 2'inci düzeyden yapılan tahsisat iade ediliyor */ ``alloc_pages`` ve ``__free_pages`` fonksiyonları export edildiği için aygıt sürücüler tarafından da kullanılabilmektedir. ``alloc_pages`` fonksiyonun başında ``__`` yokken yapılan tahsisatı serbest bırakan ``__free_pages`` fonksiyonun başında ``__`` olması bir uyumsuzluk oluşturmaktadır. Çekirdekte aslında ``free_pages`` isimli başka bir fonksiyon da vardır. Bu fonksiyon sayfaları onların sanal adreslerini (``page`` adreslerini değil sanal adreslerini) alarak serbest bırakmaktadır. ``alloc_pages`` ile tahsis edilen sayfaları farklı miktarlarda iade etmeye çalışmayınız. Bu durumda dilimli tahsisat sistemini bozabilirsiniz: .. code-block:: c __free_pages(pages + 0, 0); /* dikkat! yanlış kullanım */ __free_pages(pages + 1, 0); /* dikkat! yanlış kullanım */ __free_pages(pages + 2, 0); /* dikkat! yanlış kullanım */ __free_pages(pages + 3, 0); /* dikkat! yanlış kullanım */ ``__get_free_pages`` fonksiyonunun tek sayfayı serbest bırakan ``__get_free_page`` isimli makrosu da bulunmaktadır: .. code-block:: c #define __get_free_page(gfp_mask) __get_free_pages((gfp_mask), 0) ``alloc_pages`` fonksiyonunun bize fiziksel sayfanın sanal adresini vermediğine, o sayfayı yönetmekte kullanılan ``page`` nesnesinin adresini verdiğine dikkat ediniz. Biz eğer ilgili sayfanın içeriğine erişmek istiyorsak ``page_to_virt`` makrosunu ya da ``page_address`` fonksiyonunu kullanmalıyız. Eğer tek sayfalık tahsisat yapılacaksa ``alloc_pages`` yerine ``alloc_page`` makrosu kullanılabilir. Bu makro ``include/linux/gfp.h`` dosyasında şöyle yazılmıştır: .. code-block:: c #define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0) Yukarıda da belirttiğimiz gibi artık ``alloc_pages`` de çekirdeklerde bir süredir makro olarak yazılmaktadır. Burada çekirdekteki makroların ve ``static inline`` fonksiyonların aygıt sürücülerde kullanımına ilişkin bir noktayı belirtmek istiyoruz. Bir makro ya da ``inline`` fonksiyon koda açılmaktadır. Açılan koddaki makrolar da yeniden açılmaktadır. Makroların ve ``inline`` fonksiyonların export edilmesi söz konusu değildir. Makroların ve ``inline`` fonksiyonların çekirdek modülleri ve aygıt sürücüler tarafından kullanılabilmesi için onların açımları sonucunda çağrılan fonksiyonların export edilmiş olması gerekir. Aşağıda ``alloc_pages`` ve ``__free_pages`` fonksiyonlarının kullanımına ilişkin basit bir aygıt sürücü örneği verilmiştir. Aygıt sürücünün ``init`` fonksiyonunda sayfa tahsisatı yapılmış ve sayfanın sanal adresi bir global değişkende saklanmıştır. ``write`` işleminde bu sayfaya yazma yapılıp, ``read`` işleminde de yazılanlar okunmuştur. Aygıt sürücünün ``exit`` fonksiyonunda da tahsis edilen sayfa serbest bırakılmıştır. Aygıt sürücünün ``init`` fonksiyonunda sayfa tahsisatı şöyle yapılmıştır: .. code-block:: c static void *g_pageaddr; static int __init test_driver_init(void) { struct page *page; if ((page = alloc_pages(GFP_KERNEL, 0)) == NULL) { printk(KERN_ERR "cannot alloc pages!..\n"); cdev_del(&g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } g_pageaddr = page_address(page); return 0; } Aygıt sürücünün ``exit`` fonksiyonunda tahsisat şöyle geri alınmıştır: .. code-block:: c static void __exit test_driver_exit(void) { __free_pages(virt_to_page(g_pageaddr), 0); /* ... */ } Aygıt sürücünün ``read`` ve ``write`` fonksiyonları da şöyledir: .. code-block:: c static ssize_t test_driver_read(struct file *filp, char *buf, size_t size, loff_t *off) { if (copy_to_user(buf, g_pageaddr, size) != 0) return -EFAULT; return size; } static ssize_t test_driver_write(struct file *filp, const char *buf, size_t size, loff_t *off) { if (copy_from_user(g_pageaddr, buf, size) != 0) return -EFAULT; return size; } Aygıt sürücünün tam kaynak kodu aşağıda verilmiştir. Aygıt sürücüyü yükledikten sonra ``test-page.c`` programı ile test edebilirsiniz. ``test-driver.c`` .. code-block:: c #include #include #include #include #include #include MODULE_LICENSE("GPL"); MODULE_AUTHOR("Kaan Aslan"); MODULE_DESCRIPTION("test-driver"); static int test_driver_open(struct inode *inodep, struct file *filp); static int test_driver_release(struct inode *inodep, struct file *filp); static ssize_t test_driver_read(struct file *filp, char *buf, size_t size, loff_t *off); static ssize_t test_driver_write(struct file *filp, const char *buf, size_t size, loff_t *off); static dev_t g_dev; static struct cdev g_cdev; static struct file_operations g_fops = { .owner = THIS_MODULE, .open = test_driver_open, .read = test_driver_read, .write = test_driver_write, .release = test_driver_release, }; static void *g_pageaddr; static int __init test_driver_init(void) { int result; struct page *page; printk(KERN_INFO "test-driver module initialization...\n"); if ((result = alloc_chrdev_region(&g_dev, 0, 1, "test-driver")) < 0) { printk(KERN_INFO "cannot alloc char driver!...\n"); return result; } cdev_init(&g_cdev, &g_fops); if ((result = cdev_add(&g_cdev, g_dev, 1)) < 0) { unregister_chrdev_region(g_dev, 1); printk(KERN_ERR "cannot add device!...\n"); return result; } if ((page = alloc_pages(GFP_KERNEL, 0)) == NULL) { printk(KERN_ERR "cannot alloc pages!..\n"); cdev_del(&g_cdev); unregister_chrdev_region(g_dev, 1); return -ENOMEM; } g_pageaddr = page_address(page); return 0; } static void __exit test_driver_exit(void) { __free_pages(virt_to_page(g_pageaddr), 0); cdev_del(&g_cdev); unregister_chrdev_region(g_dev, 1); printk(KERN_INFO "test-driver module exit...\n"); } static int test_driver_open(struct inode *inodep, struct file *filp) { return 0; } static int test_driver_release(struct inode *inodep, struct file *filp) { return 0; } static ssize_t test_driver_read(struct file *filp, char *buf, size_t size, loff_t *off) { if (copy_to_user(buf, g_pageaddr, size) != 0) return -EFAULT; return size; } static ssize_t test_driver_write(struct file *filp, const char *buf, size_t size, loff_t *off) { if (copy_from_user(g_pageaddr, buf, size) != 0) return -EFAULT; return size; } module_init(test_driver_init); module_exit(test_driver_exit); ``makefile`` .. code-block:: makefile obj-m += ${file}.o all: make -C /lib/modules/$(shell uname -r)/build M=${PWD} modules clean: make -C /lib/modules/$(shell uname -r)/build M=${PWD} clean ``load`` .. code-block:: bash #!/bin/bash module=$1 mode=666 /sbin/insmod ./${module}.ko ${@:2} || exit 1 major=$(awk "\$2 == \"$module\" {print \$1}" /proc/devices) rm -f $module mknod -m $mode $module c $major 0 ``unload``` .. code-block:: bash #!/bin/bash module=$1 /sbin/rmmod ./${module}.ko || exit 1 rm -f $module ``page-test.c`` .. code-block:: c #include #include #include #include #include void exit_sys(const char *msg); int main(void) { int fd; char wbuf[] = "this is a test"; char rbuf[4096 + 1]; size_t result; if ((fd = open("test-driver", O_RDWR)) == -1) exit_sys("open"); if (write(fd, wbuf, strlen(wbuf)) == -1) exit_sys("write"); if ((result = read(fd, rbuf, strlen(wbuf))) == -1) exit_sys("read"); rbuf[result] = '\0'; printf("%s\n", rbuf); close(fd); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } Çekirdekteki ``__get_free_pages`` fonksiyonu ``alloc_pages`` gibi sayfa tahsisatı yapmakla birlikte bize ``page`` nesnesinin adresini değil doğrudan tahsis edilen fiziksel sayfanın sanal bellek adresini vermektedir. (Çekirdek alanındaki fiziksel sayfaların ardışıl biçimde sanal adrese haritalandığını anımsayınız.) ``__get_free_pages`` aslında ``include/linux/gfp.h`` dosyasında bir makro olarak yazılmıştır. Biz burada anlatımı kolaylaştırmak için ona fonksiyon diyeceğiz. Fonksiyonun parametrik yapısı şöyledir: .. code-block:: c unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order); Fonksiyon tahsis edilen fiziksel sayfaların sanal adresine geri dönmektedir. Geri dönüş değerinin ``unsigned long`` türünden olması sizi şaşırtmasın. ``__get_free_pages`` fonksiyonu ile tahsis edilen sayfalar ``free_pages`` fonksiyonu ile serbest bırakılabilir. (Bu fonksiyonu ``__free_pages`` fonksiyonu ile karıştırmayınız.) ``free_pages`` fonksiyonunun parametrik yapısı şöyledir: .. code-block:: c void free_pages(unsigned long addr, unsigned int order); Tabii aslında bu fonksiyon da ``__free_pages`` fonksiyonunu çağırmaktadır. Şöyle yazılmıştır: .. code-block:: c void free_pages(unsigned long addr, unsigned int order) { if (addr != 0) { VM_BUG_ON(!virt_addr_valid((void *)addr)); __free_pages(virt_to_page((void *)addr), order); } } EXPORT_SYMBOL(free_pages); Örneğin: .. code-block:: c unsigned long addr; void *buf; if ((addr = __get_free_pages(GFP_KERNEL, 1)) != 0) return -ENOMEM; buf = (void *)addr; memset(buf, 0, PAGE_SIZE * 2); free_pages(addr, 1); Çekirdekteki ``get_zeroed_page`` fonksiyonu içi sıfırlanmış tek bir sayfanın tahsisatını yapmaktadır. Fonksiyon tahsis edilen sayfanın sanal bellek adresiyle geri dönmektedir: .. code-block:: c unsigned long get_zeroed_page(gfp_t gfp_mask); Çekirdekte ``alloc_pages_exact`` isimli ilginç bir sayfa tahsisat fonksiyonu (aslında bir makro) da vardır. Fonksiyonun parametrik yapısı şöyledir: .. code-block:: c void *alloc_pages_exact(size_t size, gfp_t gfp_mask); Fonksiyonun birinci parametresi byte cinsinden büyüklük belirtmektedir. Fonksiyon parametresiyle belirtilen büyüklüğü kapsayan en küçük sayfa miktarını tahsis eder. Ancak ikiz blok tahsisat sisteminde tahsisatlar 2'nin kuvvetlerine göre yapıldığı için artan sayfalar oluşabilecektir. Bunlar fonksiyon tarafından geri bırakılmaktadır. Örneğin biz bu fonksiyonun birinci parametresine 25000 değerini girmiş olalım. 25000 byte'ı karşılayabilecek sayfa sayısı 6'dır. Ancak ikiz blok tahsisat sisteminde 6 sayfa tahsis edilememektedir, ancak 8 sayfa tahsis edilebilmektedir. İşte fonksiyon 8 sayfayı tahsis edip 2 sayfayı geri bırakmaktadır. Fonksiyonun doğrudan sanal adresle geri döndüğüne dikkat ediniz. ``alloc_pages_exact`` fonksiyonuyla tahsis edilen sayfalar ``free_pages_exact`` fonksiyonuyla serbest bırakılmaktadır: .. code-block:: c void free_pages_exact(void *virt, size_t size); NUMA mimarisinde ``alloc_pages`` gibi sayfa tahsis eden fonksiyonlar çağrı hangi işlemcideki ya da çekirdekteki koddan yapılmışsa o işlemcinin ya da çekirdeğin NUMA düğümünden tahsisatı yapmaya çalışmaktadır. Ancak ilgili düğümde boş yer bulunamazsa *fallback* mekanizması devreye sokularak diğer düğümlere de bakılabilmektedir. *Fallback* mekanizması izleyen paragraflarda ele alınmaktadır. İşte ayrıca çekirdekte belli NUMA düğümlerinden sayfa tahsisatı yapan bir fonksiyon da bulundurulmuştur: .. code-block:: c struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order); Fonksiyonun birinci parametresi NUMA düğümünün indeksini belirtmektedir. Sayfa Tahsisatlarında Fallback Mekanizması ------------------------------------------ Şimdi de ikiz blok tahsisat sistemindeki *fallback* mekanizması üzerinde duracağız. (*fallback* Türkçe "yedek plan", "B planı" gibi anlamlara gelmektedir.) *Fallback* "belli bir NUMA düğümünde ya da belli bir bölgede ya da belli bir göç türünde tahsisat yapılamazsa diğer başka düğümlere, bölgelere ve göç türlerine de bakılması" anlamına gelmektedir. Bellek yönetiminin giriş bölümünde de açıkladığımız gibi Linux çekirdeği fiziksel belleği NUMA düğümlerinden (node), NUMA düğümlerini bellek bölgelerinden (zones), bellek bölgelerini de göç türlerinden (migration type) oluşan bir sistem biçiminde ele almaktadır. Linux çekirdeğinde her göç türünün ayrı bir ikiz blok tahsisat sistemi vardır. Bu tahsisat sisteminin kullandığı veri yapılarını yukarıda açıklamıştık. Yeniden anımsatmak istiyoruz: .. code-block:: c typedef struct pglist_data { /* ... */ struct zonelist node_zonelists[MAX_ZONELISTS]; int nr_zones; /* ... */ }; struct zone { /* ... */ struct free_area free_area[NR_PAGE_ORDERS]; int nr_zones; /* ... */ }; struct free_area { struct list_head free_list[MIGRATE_TYPES]; unsigned long nr_free; }; .. code-block:: none zone (örn. ZONE_NORMAL): │ ├── free_area[0] (düzey-0, 4 KB bloklar) │ ├── free_list[MIGRATE_UNMOVABLE] → [pg1] → [pg4] → [pg9] → NULL │ ├── free_list[MIGRATE_MOVABLE] → [pg2] → [pg7] → NULL │ ├── free_list[MIGRATE_RECLAIMABLE] → [pg3] → NULL │ ├── free_list[MIGRATE_HIGHATOMIC] → NULL │ ├── free_list[MIGRATE_CMA] → NULL │ └── free_list[MIGRATE_ISOLATE] → NULL │ ├── free_area[1] (düzey-1, 8 KB bloklar) │ ├── free_list[MIGRATE_UNMOVABLE] → [pg16,17] → NULL │ ├── free_list[MIGRATE_MOVABLE] → [pg32,33] → [pg64,65] → NULL │ └── ... │ ├── free_area[2] (düzey-2, 16 KB bloklar) │ └── ... │ ... │ └── free_area[10] (düzey-10, 4 MB bloklar) └── ... Aslında sayfa tahsisatları "belli bir düğümün, belli bir bölgesinin, belli bir göç türünü" hedef alarak süreci başlatmaktadır. İşte ``alloc_pages`` gibi fonksiyonların birinci parametresindeki bayraklar bu tespitin yapılmasını sağlamaktadır. Aşağıda hangi bayraklar kullanıldığında işlemlerin hangi bölgeden ve hangi göç türünden başlatılacağı bilgisi bir tablo halinde verilmiştir: .. list-table:: :header-rows: 1 * - GFP Kombinasyonu - Hedef Bölge - Göç Türü - Tipik Kullanım * - ``GFP_KERNEL`` - ``ZONE_NORMAL`` - ``MIGRATE_UNMOVABLE`` - kmalloc, kzalloc, genel kernel * - ``GFP_ATOMIC`` - ``ZONE_NORMAL`` - ``MIGRATE_UNMOVABLE`` - IRQ handler, spinlock tutan kod * - ``GFP_USER`` - ``ZONE_NORMAL`` - ``MIGRATE_MOVABLE`` - kullanıcı alanı tahsisatları * - ``GFP_HIGHUSER_MOVABLE`` - ``ZONE_HIGHMEM`` - ``MIGRATE_MOVABLE`` - anonim kullanıcı sayfaları * - ``GFP_NOFS`` - ``ZONE_NORMAL`` - ``MIGRATE_UNMOVABLE`` - FS kritik yol (deadlock önlemi) * - ``GFP_NOIO`` - ``ZONE_NORMAL`` - ``MIGRATE_UNMOVABLE`` - I/O kritik yol (deadlock önlemi) * - ``GFP_TRANSHUGE_MOVABLE`` - ``ZONE_NORMAL`` - ``MIGRATE_MOVABLE`` - transparent huge page (THP) * - ``GFP_KERNEL | __GFP_RECLAIMABLE`` - ``ZONE_NORMAL`` - ``MIGRATE_RECLAIMABLE`` - slab cache (kmem_cache) * - ``GFP_KERNEL | __GFP_DMA`` - ``ZONE_DMA`` - ``MIGRATE_UNMOVABLE`` - eski ISA/legacy DMA aygıtları * - ``GFP_ATOMIC | __GFP_DMA`` - ``ZONE_DMA`` - ``MIGRATE_UNMOVABLE`` - IRQ ctx'te DMA tamponu * - ``GFP_KERNEL | __GFP_DMA32`` - ``ZONE_DMA32`` - ``MIGRATE_UNMOVABLE`` - 32-bit DMA aygıtları (PCIe vs.) * - ``GFP_ATOMIC | __GFP_DMA32`` - ``ZONE_DMA32`` - ``MIGRATE_UNMOVABLE`` - IRQ ctx'te 32-bit DMA tamponu * - ``GFP_DMA`` - ``ZONE_DMA`` - ``MIGRATE_UNMOVABLE`` - ``GFP_KERNEL | __GFP_DMA`` kısayolu * - ``GFP_DMA32`` - ``ZONE_DMA32`` - ``MIGRATE_UNMOVABLE`` - ``GFP_KERNEL | __GFP_DMA32`` kısayolu Biz daha önce bellek bölgelerinin anlamlarını açıklamıştık. Ancak göç türleri hakkında ayrıntılı bir açıklama yapmamıştık. Önce bölgelerdeki göç türleri üzerinde açıklamalar yapalım. Linux çekirdeğinde kullanılan göç türleri şunlardır: .. code-block:: c enum migratetype { MIGRATE_UNMOVABLE, /* 0 — taşınamaz, geri alınamaz */ MIGRATE_MOVABLE, /* 1 — taşınabilir */ MIGRATE_RECLAIMABLE, /* 2 — geri alınabilir */ MIGRATE_PCPTYPES, /* 3 — PCP listelerinin sonu (marker) */ MIGRATE_HIGHATOMIC = MIGRATE_PCPTYPES, /* 3 — acil rezerv */ MIGRATE_CMA, /* 4 — Contiguous Memory Allocator */ MIGRATE_ISOLATE, /* 5 — izole edilmiş, tahsisat yapılmaz */ MIGRATE_TYPES /* toplam tür sayısı */ }; ``MIGRATE_UNMOVABLE`` ve ``MIGRATE_MOVABLE`` göç türleri tahsis edilen sayfanın yerinin çekirdek tarafından değiştirilip değiştirilmeyeceği anlamına gelmektedir. Çekirdek ikiz blok tahsisat sisteminde ardışıl yeteri kadar blok bulunamadığında (yani *fragmentation* durumunda) ardışıl sayfa elde etmek için sayfaların fiziksel bellekteki yerlerini değiştirebilmektedir. İşte bu tür sayfalara Linux çekirdeğinde *movable sayfalar* denilmektedir. Tabii çekirdek fiziksel sayfanın yerini değiştirdiğinde bu sayfaya referans eden öğelerin fiziksel adreslerini de sayfa tablolarında değiştirmektedir. Örneğin ikiz blok tahsisat sisteminde 1 sayfalık çok sayıda blok bulunduğu halde yan yana 2 sayfalık hiç blok bulunmasın. İşte çekirdek 1 sayfalık bloğun yanındaki ikizinin fiziksel bellekte yerini değiştirerek (yani onu boş sayfalardan birine taşıyarak) yan yana iki fiziksel sayfa oluşturabilmektedir. Tabii bu durum oldukça seyrek gerçekleşir. Bir sayfanın taşınabilmesi için onun *reversible* özelliklere sahip olması gerekmektedir. İşte ``MIGRATE_MOVABLE`` bayrağı sayfayı taşınabilir hale getirmektedir. Görüldüğü gibi çekirdekte taşınabilen sayfalarla, taşınamayan sayfalar ayrı ikiz blok tahsisat sisteminde tutulmaktadır. Çekirdekte sayfa taşıma işlemi şu aşamalardan geçilerek yapılmaktadır: .. code-block:: none migrate_page(old_page, new_page): 1. new_page için fiziksel frame al 2. old_page içeriğini new_page'e kopyala 3. rmap üzerinden tüm PTE'leri bul 4. Her old_page sayfa tablosu girişini new_page'e yönlendir (TLB flush) 5. old_page'i iade et C'deki ``malloc`` fonksiyonu gerektiğinde ``brk`` ya da ``mmap`` sistem fonksiyonlarını çağırarak tahsisatları yapmaktadır. Bunlar için tahsis edilen sayfalar ``MIGRATE_MOVABLE`` biçimdedir. ``MIGRATE_RECLAIMABLE`` göç türü fiziksel olarak taşınamaz ancak çekirdek tarafından *swap out* amacıyla boşaltılabilen sayfaların bulunduğu ikiz blok sistemini belirtmektedir. Bu göç türünden işlemleri başlatmak için ``__GFP_RECLAIMABLE`` bayrağının da eklenmesi gerekmektedir. Dilimli tahsisat sisteminden (slab allocator) tahsis edilen sayfalar bu özelliğe sahiptir. Çekirdekteki pek çok nesne zaten dilimli tahsisat sistemi ile tahsis edilmektedir. Örneğin: .. code-block:: none kmem_cache_alloc() │ ├─► inode ──┐ ├─► dentry ──┤ ──► Hepsi dilimli tahsisat sistemi ile ├─► vm_area_struct ──┤ MIGRATE_RECLAIMABLE kullanılarak └─► task_struct ──┘ tahsis ediliyor Aşağıdaki tabloda ``MIGRATE_UNMOVABLE`` , ``MIGRATE_MOVABLE`` ve ``MIGRATE_RECLAIMABLE`` göç türlerini karşılaştırıyoruz: .. list-table:: :header-rows: 1 :widths: 22 26 26 26 * - Özellik - ``MIGRATE_UNMOVABLE`` - ``MIGRATE_RECLAIMABLE`` - ``MIGRATE_MOVABLE`` * - Fiziksel taşıma - Hayır - Hayır - Evet * - Geri alma (reclaim) - Hayır - Evet - Dolaylı * - Compaction katkısı - Hayır - Dolaylı - Doğrudan * - Tipik tahsisat - kmalloc, DMA, modül kodu, IRQ - kmem_cache, page/buffer cache - malloc, mmap, THP, KSM ``MIGRATE_HIGHATOMIC`` göç türü yüksek öncelikli, bloke olmaması gereken kodların kullanması amacıyla oluşturulmuş özel bir ikiz blok tahsisat sistemidir. ``MIGRATE_CMA`` (*Contiguous Memory Allocator*) göç türü: multimedya SoC'ları (kamera, video codec, GPU) büyük fiziksel ardışıl belleklere gereksinim duymaktadır. Bu alanların boot anında rezerve edilmesi israfa yol açabilmektedir. CMA göç türü bu bölgeleri normalde ``MIGRATE_MOVABLE`` gibi kullanır; CMA tahsisatı gerektiğinde ise bölgedeki taşınabilir sayfaları başka yere taşıyarak ardışıl alan açar. ``MIGRATE_ISOLATE`` göç türü geçici olarak ikiz blok tahsisat sisteminden izole edilmiş sayfaları barındırmak için kullanılmaktadır. Buradan hiçbir zaman yeni tahsisat yapılmaz. Çekirdek bu alanı bazı önlemler için geçici olarak oluşturmaktadır. Başlangıçta tüm göç türlerine ilişkin ikiz blok tahsisat sistemleri dolu olmak zorunda değildir. Zaten *fallback* mekanizması "eğer bu liste boşsa başka listeden al" anlamına gelmektedir. Yukarıda da belirttiğimiz gibi *fallback* mekanizması "burada boş yer bulamazsan şuralara da bak" anlamına gelen bir mekanizmadır. *Fallback* mekanizmasının bazı ayrıntıları vardır. Ancak mekanizma temel olarak şöyle yürütülmektedir: 1. ``alloc_pages`` gibi bir fonksiyonla sayfa tahsisatı yapılmak istensin. 2. Tahsisat önce çağrıyı yapan işlemci ya da çekirdeğin NUMA düğümünden hareketle yapılmaya çalışılır. 3. İlgili NUMA düğümünde GFP bayraklarına bakılarak başlangıç bölgesi (zone) ve göç türü (migration type) belirlenir. 4. Önce ilgili bölgedeki belirlenen göç türünden tahsisat yapılmaya çalışılır; eğer orada boş yer yoksa belirlenmiş olan (ayrıntıları var) diğer göç türlerine de bakılır. 5. Eğer ilgili bölgedeki göç türlerinde boş sayfa bulunamazsa bu kez aynı NUMA düğümündeki diğer bölgelere (ayrıntıları var) geçilir. Arama o bölgelerin ilgili göç türlerinde de benzer biçimde yapılır. 6. Eğer ilgili NUMA düğümünde aranan hiçbir bölgenin göç türünde boş yer bulunamazsa belirlenen diğer NUMA düğümlerine geçilir. Aşağıda bu süreç şekilsel olarak da gösterilmiştir: .. figure:: _static/fallback-mechanism.png :alt: Fallback mekanizması akış diyagramı :align: center :width: 70% Güncel çekirdeklerde göç türlerine ilişkin *fallback* sırası ``mm/page_alloc.c`` dosyasında ``fallbacks`` isimli iki boyutlu bir dizide belirtilmiştir. Bu dizi şöyle tanımlanmıştır: .. code-block:: c static int fallbacks[MIGRATE_PCPTYPES][MIGRATE_PCPTYPES - 1] = { [MIGRATE_UNMOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE }, [MIGRATE_MOVABLE] = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE }, [MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE, MIGRATE_MOVABLE }, }; Matrisin satırları her göç türü için "eğer orada boş sayfa bulunamazsa sırasıyla hangi göç türlerine bakılacağını" belirtmektedir. Örneğin ``MIGRATE_UNMOVABLE`` göç türünden tahsisat yapılmak istensin. İşte burada boş sayfa bulunamazsa sırasıyla ``MIGRATE_RECLAIMABLE`` ve ``MIGRATE_MOVABLE`` göç türlerine de bakılacaktır. Bu *fallback* sırasının statik bir biçimde çekirdek kodlarında belirtildiğine dikkat ediniz. Burada önemli bir noktayı vurgulamak istiyoruz. Çekirdek belli bir göç türünde boş blok bulamayıp diğer göç türüne başvurup oradan boş blok aldığında artık kopardığı blokları hedef göç türüne taşımaktadır. Bu sayfalar free hale getirildiğinde alınan yere iade edilmemektedir, taşınan yere iade edilmektedir. Ayrıca önemli bir ayrıntı da vardır. Diğer göç türlerinden arama her zaman en yüksek düzeyden (order'dan) başlanarak aşağıya doğru yapılmaktadır. (En yüksek düzeyin 10 olduğunu ve 4 MB sayfa bloğu belirttiğini anımsayınız.) Örneğin biz 4 sayfa tahsis etmek isteyelim. Ancak *fallback* durumu oluşup bu sayfa başka bir göç türünde aranıyor olsun. İşte arama 10'uncu düzeydeki bağlı listeden başlatılıp aşağıya doğru inecektir. Böylece yalnızca 4 sayfa değil ilgili sayfa bloğunun tüm sayfaları göçe tabi tutulacaktır. Örneğin ilgili göç türünde 10'uncu düzeyde boş sayfa bloğu olmasın, 9'uncu düzeyde de olmasın ama 8'inci düzeyde olsun. İşte 8'inci düzeydeki 1 MB'lik sayfa bloğu göçe tabi tutularak hedefe taşınıp oradan 4 sayfa verilmektedir. Böylece bir göç türünde boş sayfa bulunamayınca diğer göç türünden daha büyük bir parçanın alınması sağlanmıştır. Fallback Mekanizmasına İlişkin Veri Yapıları ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Şimdi *fallback* mekanizmasının işletildiği veri yapıları üzerinde dikkatimizi yoğunlaştıralım. NUMA düğümlerini temsil eden ``pglist_data`` yapısının (bu yapı ``pg_data_t`` olarak da typedef edilmiştir) ``node_zones`` dizisi o düğümün bölgelerini, ``node_zonelists`` dizisi ise o düğümün fallback bölge listesini tutmaktadır. .. code-block:: c typedef struct pglist_data { /* ... */ struct zone node_zones[MAX_NR_ZONES]; struct zonelist node_zonelists[MAX_ZONELISTS]; /* fallback amaçlı */ /* ... */ } pg_data_t; ``node_zonelists`` dizisi ``zonelist`` isimli bir yapı türündendir. Bu yapı şöyle tanımlanmıştır: .. code-block:: c struct zonelist { struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1]; }; Görüldüğü gibi bu yapı da ``zoneref`` türünden bir yapı dizisi içermektedir. ``zoneref`` yapısı da şöyle tanımlanmıştır: .. code-block:: c struct zoneref { struct zone *zone; /* Pointer to actual zone */ int zone_idx; /* zone_idx(zoneref->zone) */ }; Görüldüğü gibi burada bölgeyi temsil eden nesnenin adresi ve onun ``node_zones`` dizisindeki indeksi tutulmaktadır. İşte aslında ``node_zonelists`` fallback amaçlı kullanılmaktadır. Bölge türlerini de yeniden anımsatmak istiyoruz: .. code-block:: c enum zone_type { ZONE_DMA, // İlk 16 MB — eski ISA DMA için ZONE_DMA32, // İlk 4 GB — 32-bit DMA için ZONE_NORMAL, // Normal kernel sayfaları ZONE_MOVABLE, // Taşınabilir sayfalar (hugepage, migration için) ZONE_DEVICE, // Kalıcı bellek (PMEM) için __MAX_NR_ZONES }; ``node_zonelists`` dizisinin ``MAX_ZONELISTS`` kadar elemanı içerdiğine dikkat ediniz. Bu sembolik sabit şöyle tanımlanmıştır: .. code-block:: c enum { ZONELIST_FALLBACK, /* zonelist with fallback */ #ifdef CONFIG_NUMA /* * The NUMA zonelists are doubled because we need zonelists that * restrict the allocations to a single node for __GFP_THISNODE. */ ZONELIST_NOFALLBACK, /* zonelist without fallback (__GFP_THISNODE) */ #endif MAX_ZONELISTS }; Görüldüğü gibi aslında ``node_zonelists`` en fazla iki boyutlu bir dizidir. Dizinin birinci elemanı fallback listesini tutmaktadır. İkinci elemanı ise düğüm temelinde fallback yapılmaması durumunda kullanılmaktadır. Biz burada dizinin 0'ıncı indeksli ilk elemanı ile ilgileniyoruz. O halde aslında NUMA düğümünün fallback listesi ``zoneref`` dizisi biçimindedir. ``zoneref`` nesnesi de ilgili bölgeyi belirtmektedir. (Başka bir deyişle NUMA düğümünün fallback listesi aslında bir bölge listesinden oluşmaktadır.) ``node_zonelists`` dizisinin uzunluğunun ``MAX_ZONES_PER_ZONELIST`` ile belirtildiğine dikkat ediniz. Bu sembolik sabit şöyle tanımlanmıştır: .. code-block:: c #define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES) Buradaki ``MAX_NUMNODES`` ve ``MAX_NR_ZONES`` değerleri çeşitli etmenlere göre değişebilmektedir. Burada önemli bir noktayı belirtmek istiyoruz. ``node_zonelists`` elemanının 0'ıncı indeksindeki (``ZONELIST_FALLBACK``) bölge dizisi aslında yalnızca o NUMA düğümünün bölgelerini belirtmemektedir; diğer NUMA düğümlerinin bölgeleri de bu dizi içerisindedir. ``node_zonelists`` dizisinin temsili görüntüsü şöyledir: .. code-block:: none pg_data_t (Node 0) ├── node_zonelists[0] ← ZONELIST_FALLBACK │ └── _zonerefs[] │ [0]: zone=&node0.zone[NORMAL], zone_idx=ZONE_NORMAL │ [1]: zone=&node0.zone[DMA32], zone_idx=ZONE_DMA32 │ [2]: zone=&node0.zone[DMA], zone_idx=ZONE_DMA │ [3]: zone=&node1.zone[NORMAL], zone_idx=ZONE_NORMAL ← komşu node │ [4]: zone=&node1.zone[DMA32], zone_idx=ZONE_DMA32 │ [5]: zone=&node2.zone[NORMAL], zone_idx=ZONE_NORMAL ← uzak node │ [6]: zone=&node3.zone[NORMAL], zone_idx=ZONE_NORMAL ← en uzak │ [7]: zone=NULL ← LİSTE SONU │ └── node_zonelists[1] ← ZONELIST_NOFALLBACK (__GFP_THISNODE için) └── _zonerefs[] [0]: zone=&node0.zone[NORMAL], zone_idx=ZONE_NORMAL [1]: zone=&node0.zone[DMA32], zone_idx=ZONE_DMA32 [2]: zone=&node0.zone[DMA], zone_idx=ZONE_DMA [3]: zone=NULL Bu temsili çizimde örnek olarak 0'ıncı NUMA düğümünün ``node_zonelists`` dizisi gösterilmiştir. Görüldüğü gibi bu dizinin 0'ıncı elemanı bölgelerden oluşmaktadır; ancak bölgeler yalnızca 0'ıncı düğümün bölgelerini içermemektedir, diğer düğümlerin bölgelerini de içermektedir. Örneğin 0'ıncı NUMA düğümünde ``alloc_pages`` ile ``ZONE_DMA32`` bölgesinden ``MIGRATE_UNMOVABLE`` tahsisatı yapılmak istensin. İşte bölge listesinin dolaşılmasına 0'ıncı düğümdeki DMA32'den başlatılacaktır: .. code-block:: none pg_data_t (Node 0) ├── node_zonelists[0] ← ZONELIST_FALLBACK │ └── _zonerefs[] │ [0]: zone=&node0.zone[NORMAL], zone_idx=ZONE_NORMAL │ Bölge araması buradan başlatılacak ──► [1]: zone=&node0.zone[DMA32], zone_idx=ZONE_DMA32 │ [2]: zone=&node0.zone[DMA], zone_idx=ZONE_DMA │ [3]: zone=&node1.zone[NORMAL], zone_idx=ZONE_NORMAL ← komşu node │ [4]: zone=&node1.zone[DMA32], zone_idx=ZONE_DMA32 │ [5]: zone=&node2.zone[NORMAL], zone_idx=ZONE_NORMAL ← uzak node │ [6]: zone=&node3.zone[NORMAL], zone_idx=ZONE_NORMAL ← en uzak │ [7]: zone=NULL ← LİSTE SONU │ └── node_zonelists[1] ← ZONELIST_NOFALLBACK (__GFP_THISNODE için) └── _zonerefs[] [0]: zone=&node0.zone[NORMAL], zone_idx=ZONE_NORMAL [1]: zone=&node0.zone[DMA32], zone_idx=ZONE_DMA32 [2]: zone=&node0.zone[DMA], zone_idx=ZONE_DMA [3]: zone=NULL Eğer bu bölgenin göç *fallback* listesinin hiçbir yerinde talep edilen miktarda boş sayfa bulunamazsa bundan sonra arama 0'ıncı düğümün ``ZONE_DMA`` bölgesinden devam edecek, orada da bulunamazsa 1'inci düğümün ``ZONE_NORMAL`` bölgesinden devam edecektir. ``node_zonelists[1]`` elemanının (``ZONELIST_NOFALLBACK``) ne işe yaradığını merak edebilirsiniz. Bu elemanda belirtilen bölge dizisi aramanın kesinlikle belirli bir NUMA düğümünde kalması gerektiği durumlar için bulundurulmuştur. Bu dizi elemanı ``__GFP_THISNODE`` bayrağı girildiğinde kullanılmaktadır. Bu dizi elemanında yalnızca ilgili düğümün bölümleri vardır. Yani ``alloc_pages_node`` çağrısında GFP bayrakları içinde ``__GFP_THISNODE`` varsa ``node_zonelists[1]`` (``ZONELIST_NOFALLBACK``) seçilir. Bu liste yalnızca o düğümün kendi bölgelerini içerdiği için arama diğer düğümlere hiçbir koşulda taşmaz. Bölgelerdeki Boş sayfa Listelerinin Başlangıç Durumu ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Şimdi de başlangıçta (yani boot işleminden sonra) boş sayfa listelerinin durumu hakkında bilgi verelim. Başlangıçta her sayfa kendi bellek bölgesinin en yüksek düzeyli ``MIGRATE_MOVABLE`` göç türüne ilişkin ikiz blok tahsisat sistemindedir. Örneğin UMA x86-64 mimarisindeki başlangıç durumu şöyledir: .. code-block:: none node 0 (pgdat) ├── ZONE_DMA (0–16MB) │ free_area[10].free_list[MIGRATE_MOVABLE] → ~birkaç adet 4MB blok │ (diğer tüm göç listeleri boş) │ ├── ZONE_DMA32 (16MB–4GB) │ free_area[10].free_list[MIGRATE_MOVABLE] → ~binlerce sayfa │ (diğer tüm göç listeleri boş) │ └── ZONE_NORMAL (4GB–16GB) free_area[10].free_list[MIGRATE_MOVABLE] → belleğin ana gövdesi (diğer tüm göç listeleri boş) Örneğin ARM64 kullanılan Raspberry Pi modelleri için başlangıç durumu şöyledir: .. code-block:: none node 0 (pgdat) — RPi, UMA ├── ZONE_DMA (0 – 1GB) │ free_area[10].free_list[MIGRATE_MOVABLE] → ~1GB'ın boş kısmı, çoğu 4MB'lık blok │ (diğer tüm göç listeleri boş) ├── ZONE_DMA32 (1GB – 4GB) │ free_area[10].free_list[MIGRATE_MOVABLE] → ~3GB │ (diğer tüm göç listeleri boş) └── ZONE_NORMAL (4GB – 8GB) free_area[10].free_list[MIGRATE_MOVABLE] → ~4GB (diğer tüm göç listeleri boş) ARM32 kullanan BeagleBone modelleri için de başlangıç durumu şöyledir: .. code-block:: none node 0 (pgdat) — BBB, UMA └── ZONE_NORMAL (0x80000000 – 0x9FFFFFFF, 512MB) free_area[10].free_list[MIGRATE_MOVABLE] → tüm boş bellek burada, çoğu 4MB'lık blok Fallback Mekanizmasında NUMA Düğümlerinin Dolaşım Sırası ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Peki bölgelerde *fallback* yapılırken NUMA düğümlerine hangi sırada bakılmaktadır? İşte *fallback* işlemlerinde NUMA düğümlerine NUMA uzaklık matrisindeki uzaklık değerleri dikkate alınarak bakılmaktadır. Yani node_zonelists dizisinde NUMA düğümleri NUMA uzaklıklarına göre küçükten büyüğe sort edilmiş durumdadır. Peki NUMA uzaklık matrisi nedir? İzleyen paragraflarda NUMA uzaklık matrisinin ne anlama geldiğini açıklıyoruz. Bilgisayar ana kartında CPU'lar için soketler bulunmaktadır. Bu soketlere takılan entegre devrelere *paket (package)* denilmektedir. Biz bu paketlere *işlemci paketleri* de diyeceğiz. Bu işlemci paketleri içerisinde İngilizce *die* denilen silikon kalıplar bulunmaktadır. Çekirdekler bu silikon kalıplar üzerindedir. Her çekirdek bağımsız bir işlemci gibi davranmaktadır. Bir kasa içerisindeki soketlerin sayısında da belli bir sınır vardır. Bu sınır donanımsal kısıtlardan kaynaklanmaktadır. Eğer kasa soket sayısını kaldıramıyorsa bu durumda kasa sayısı artırılmaktadır. Tabii kasalar arasındaki RAM iletişimi de yüksek hızlı iletkenlerle sağlanmaktadır. Böylece büyük NUMA sistemlerinde dağıtık RAM blokları bulunabilmektedir ve her çekirdek bu RAM bloklarına (yani düğümlerine) farklı hızlarda erişebilmektedir. Linux çekirdeği her soket ve dolayısıyla çekirdek için NUMA düğümlerine *NUMA uzaklığı (NUMA distance)* denilen bir uzaklık derecesi atamaktadır. Tabii çekirdek bu bilgiyi de aslında donanımdan, yani modern sistemlerde ACPI tablosundan elde etmektedir. NUMA uzaklık matrisindeki değerler gerçek bir gecikme değeri değil göreli bir değer belirtmektedir. Örneğin soketlerin NUMA düğümlerine uzaklıkları aşağıdakine benzer olabilmektedir: .. list-table:: NUMA Erişim Türlerine Göre Tipik Uzaklık Skorları :header-rows: 1 :widths: 60 40 * - Erişim Türü - Tipik Skor Aralığı * - Kendi düğümü (local) - 10 * - Aynı soket, farklı die (SNC modu) - 12–16 * - Farklı soket, doğrudan bağlı - 20–22 * - Farklı soket, 2 hop - 30–32 * - Farklı kasa (multi-chassis) - 40–100+ NUMA uzaklık matrisi aşağıdaki gibi temsil edilebilir: .. code-block:: none Node0 Node1 Node2 Node3 Soket0 [ 10 21 31 41 ] Soket1 [ 21 10 21 31 ] Soket2 [ 31 21 10 21 ] Soket3 [ 41 31 21 10 ] Tipik bazı NUMA donanımlarındaki gecikmeler nanosaniyeler mertebesinde şöyledir: .. code-block:: none 2 soketli Intel Xeon (UPI): Local RAM erişimi: ~80–90 ns Remote RAM erişimi: ~130–150 ns Oran: ~1.7x yavaş 2 soketli AMD EPYC (Infinity Fabric): Local RAM erişimi: ~75–85 ns Remote RAM erişimi: ~140–170 ns Oran: ~1.9x yavaş 4 soketli Intel (eski, 2-hop mümkün): Local: ~80 ns 1-hop remote: ~150 ns 2-hop remote: ~220 ns Örneğin 2 soketli bir NUMA sistemi aşağıdaki gibi bir mimariye sahip olabilmektedir: .. figure:: _static/numa-two-socket.png :alt: 2 soketli NUMA mimarisi :align: center Her soketteki çekirdek o sokete ilişkin RAM bank'ına (yani NUMA düğümüne) daha hızlı erişmektedir. Yukarıdaki sistemde her sokette 64 çekirdekli bir işlemci paketi, 2 sokette toplamda 128 çekirdek bulunmaktadır. Dilimli Tahsisat Sistemi (Slab Allocator) ========================================= Biz Linux çekirdeklerindeki bellek tahsisat sistemini iki kısma ayırmıştık: sayfa düzeyinde tahsisat ve byte düzeyinde tahsisat. Sayfa düzeyinde tahsisatların "ikiz blok tahsisat sistemi (buddy allocator)" ile yapıldığını gördük. Şimdi de çekirdeğin byte düzeyinde tahsisat sistemi üzerinde duracağız. Daha önceden de belirttiğimiz gibi çekirdeğin byte düzeyinde tahsisat sistemine "dilimli tahsisat sistemi (slab allocator)" denilmektedir. Dilimli tahsisat sistemi 1994 yılında Jeff Bonwick tarafından önerilmiştir. Bonwick'in orijinal makalesine aşağıdaki bağlantıdan erişebilirsiniz: `The Slab Allocator: An Object-Caching Kernel Memory Allocator (Bonwick, 1994) `_ Dilimli tahsisat sistemi ilk kez Solaris sistemlerinde kullanılmıştır. Bunu FreeBSD sistemleri izlemiştir. Sonra da Linux'un 2.2 kararlı sürümüyle çekirdekte yerini almıştır. Klasik Tahsisat Algoritması: Boş Blokların Bağlı Listede Saklanması ------------------------------------------------------------------- Dilimli tahsisat sistemini açıklamadan önce klasik byte düzeyinde tahsisat işleminin (yani ``malloc`` gibi bir fonksiyonun) nasıl gerçekleştirildiği üzerinde duralım. Klasik byte düzeyinde tahsisat algoritması oldukça basittir. Bellekte bir bölge *heap* olarak ayrılır. Bu bölgedeki "yalnızca boş alanlar" bir bağlı listede tutulur. Tahsis edilmiş alanlar için bir kayıt tutulmaz. Tahsisat yapılmak istendiğinde boş blokları tutan bağlı liste üzerinde istenilen uzunlukta ilk blokla karşılaşılana kadar (buna İngilizce *first fit* yöntemi de denilmektedir) sıralı arama yapılır. Klasik tahsis algoritması D. Ritchie ve B. Kernighan'ın ünlü *"The C Programming Language"* kitabında "8.7 Example - A Storage Allocator (Sayfa 163)" başlığı altında da açıklanmıştır. Pek çok ``malloc``/``realloc``/``free`` benzeri tahsisat sistemi burada belirtilen algoritmayı temel almıştır. Örneğin Windows sistemlerindeki ``HeapAlloc``, ``HeapFree`` gibi API fonksiyonlarının temeli de bu algoritmadır. Linux sistemlerinde ``malloc``/``realloc``/``free`` fonksiyonlarında da uzun süre bu klasik algoritmanın iyileştirilmiş biçimleri kullanılmıştır. D. Ritchie ve B. Kernighan tarafından *The C Programming Language* kitabında verilen örnek ``malloc`` ve ``free`` algoritmaları aşağıda verilmiştir. .. code-block:: c #include #include typedef long Align; /* alignment for longs */ union header { struct { union header *ptr; /* next block if on free list */ unsigned size; /* size of this block */ } s; Align x; /* force alignment of blocks */ }; typedef union header Header; static Header base; /* empty list to get started */ static Header *freep = NULL; /* start of free list */ void free(void *ap) { Header *bp, *p; bp = (Header *)ap - 1; /* point to block header */ for (p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr) if (p >= p->s.ptr && (bp > p || bp < p->s.ptr)) break; /* freed block at start or end of arena */ if (bp + bp->s.size == p->s.ptr) { /* join to upper nbr */ bp->s.size += p->s.ptr->s.size; bp->s.ptr = p->s.ptr->s.ptr; } else bp->s.ptr = p->s.ptr; if (p + p->s.size == bp) { /* join to lower nbr */ p->s.size += bp->s.size; p->s.ptr = bp->s.ptr; } else p->s.ptr = bp; freep = p; } #define NALLOC 1024 /* minimum #units to request */ static Header *morecore(unsigned nu); void *malloc(unsigned nbytes) { Header *p, *prevp; unsigned nunits; nunits = (nbytes + sizeof(Header) - 1) / sizeof(Header) + 1; if ((prevp = freep) == NULL) { /* no free list yet */ base.s.ptr = freep = prevp = &base; base.s.size = 0; } for (p = prevp->s.ptr; ; prevp = p, p = p->s.ptr) { if (p->s.size >= nunits) { /* big enough */ if (p->s.size == nunits) /* exactly */ prevp->s.ptr = p->s.ptr; else { /* allocate tail end */ p->s.size -= nunits; p += p->s.size; p->s.size = nunits; } freep = prevp; return (void *)(p + 1); } if (p == freep) /* wrapped around free list */ if ((p = morecore(nunits)) == NULL) return NULL; /* none left */ } } *"The C Programming Language"* kitabında belirtilen klasik tahsisat algortimasındaki işlemlerin algoritma karmaşıklıkları şöyledir: .. figure:: _static/malloc-complexity-table.png :alt: Klasik malloc/free algoritması karmaşıklık tablosu :align: center :width: 70% Zaman içerisinde pek çok sistem kullanıcı modundaki tahsisatlar için bu klasik tahsisat algoritmasının iyileştirilmiş varyasyonlarını kullanmaya başlamıştır. Biz kitabımızda bunları incelemeyeceğiz. Dilimli Tahsisat Sisteminin Ana Fikri ------------------------------------- Yukarıda açıkladığımız klasik tahsisat sistemindeki bağlı listede tutulan boş bloklar aynı uzunlukta olsaydı tahsisat ve serbest bırakma işlemleri O(1) karmaşıklıkta yapılabilirdi. Çünkü boş bağlı listedeki tüm bloklar eşit uzunlukta olduğuna göre tahsisat sırasında hemen listenin başındaki blok verilebilirdi. Benzer biçimde serbest bırakma işleminde de hemen blok listenin başına O(1) karmaşıklıkta eklenebilirdi. Ancak böyle bir yöntemde blok uzunluğunun belirlenmesi sorunlu bir noktayı oluşturmaktadır. Örneğin boş bağlı listedeki blokların 64 byte uzunlukta olduğunu düşünelim. Bu durumda biz 100 byte'lık bir tahsisat yapamayız. 30 byte tahsisat yapmak istediğimizde de bloktaki 34 byte boşa gidecektir. (Blok içerisinde kullanılmayan alanların oluşması durumuna "içsel bölünme (internal fragmentation)" dendiğini anımsayınız.) Bu durumda ilk akla gelecek yöntem değişik uzunlukta birden fazla boş bağlı liste bulundurmaktır. Böylece tahsisat hangi uzunluğa en yakınsa o listeden yapılabilir. Tabii yine "içsel bölünme" kaçınılmazdır ancak daha tolere edilebilir bir noktaya indirgenmiştir. Çekirdekte aynı türden pek çok nesne yaratılmaktadır. Örneğin bir proses yaratıldığında ``task_struct`` nesnesi, bir dosya açıldığında ``file`` nesnesi, ``dentry`` nesnesi, duruma göre de ``inode`` nesnesi tahsis edilmektedir. Bu nesnelerin uzunlukları farklıdır. İşte bu farklı uzunluktaki nesneler için tam o uzunlukta farklı boş blok listeleri oluşturulursa hem bu nesnelerin tahsis edilmesi hızlandırılır hem de "içsel bölünme" ortadan kaldırılır. Modern işletim sistemlerinin büyük çoğunluğunda bu teknik kullanılmaktadır. Dilimli tahsisat sistemi de bu tekniği temel almaktadır. Linux çekirdeğindeki dilimli tahsisat sistemi zaman içerisinde iyileştirilmiştir. İlk kullanılan gerçekleştirimin adı SLAB'dır. Bunun iyileştirilmiş biçimine de SLUB denilmektedir. Bir noktaya kadar çekirdek kodlarında her iki gerçekleştirim de bulunuyordu ve hangi gerçekleştirimin kullanılacağı konfigürasyon parametreleriyle seçilebiliyordu. Ancak çekirdeğin 6.5 sürümüyle birlikte ilk SLAB gerçekleştirimi çekirdek kodlarından atılmıştır. Yani bugünkü sistemler SLUB gerçekleştirimini kullanmaktadır. Ayrıca 6.2 versiyonuna kadar çekirdekte bir de SLOB gerçekleştirimi bulunuyordu. Bu SLOB gerçekleştirimi yukarıda açıkladığımız klasik tahsisat algoritmasını kullanıyordu. Bellek kısıtı olan gömülü sistemlerde kullanılmak üzere çekirdekte bulunduruluyordu. Bu SLOB gerçekleştirimi de çekirdek kodlarından 6.2 sürümüyle çıkartılmıştır. Yani güncel çekirdeklerde artık yalnızca SLUB gerçekleştirimi kullanılmaktadır. .. figure:: _static/slab-implementations-table.png :alt: SLAB, SLUB ve SLOB gerçekleştirimlerinin karşılaştırması :align: center Her ne kadar güncel gerçekleştirimin adı SLUB olsa da sisteme genel olarak yine İngilizce *slab allocator* denilmektedir. SLUB ismi *"Unqueued SLAB"* sözcüklerinden, SLOB ismi ise *"Simple List Of Blocks"* sözcüklerinden çağrışımla uydurulmuştur. Burada bir noktaya dikkatinizi çekmek istiyoruz. Çekirdek kaynak kodlarında var olan her şey derlemede çekirdek imajına yansıtılmamaktadır. Konfigürasyon aşamasında "yalnızca seçilen özellikler" çekirdek imajına yansıtılmaktadır. Zaten konfigürasyon işleminin amaçlarından biri de budur. SLUB gerçekleştirimi oldukça ayrıntılıdır. Biz kitabmızda bu gerçekleştirimin ana hatları üzerinde duracağız. Ancak SLUB sözcüğü yerine "dilimli tahsisat sistemi (slab allocator)" ve "dilim (slab)" terimlerini kullanacağız. Dilimli Tahsisat Sistemine İlişkin Veri Yapıları ve Algoritmalar ---------------------------------------------------------------- Dilimli tahsisat sisteminde üç önemli kavram vardır: Dilim Önbellek (Slab Cache), Dilim (Slab) ve Nesne (Object). Dilim önbelleği bu terminolojide tahsisat sistemini belirtmektedir. Dilim önbelleği dilimlerden, dilimler de nesnelerden oluşmaktadır. Tahsis edilecek öğeler eşit uzunluktaki nesnelerdir. Dilimli tahsisat sistemindeki dilim önbelleği (slab cache) ana taşıyıcıdır. Tahsisat sistemi ile ilgili önemli bilgiler burada tutulmaktadır. Dilimler (slabs) ardışıl sayfalardan oluşan bellek bloklarıdır. Nesneler dilimlerin içerisindedir. Tahsisat sistemi dilimleri dilim önbelleği içerisindeki bir bağlı listede tutmaktadır. SLUB gerçekleştiriminde her dilim önbelleği (slab cache) her NUMA düğümü için ayrı bir dilim listesi tutmaktadır. Yani aslında dilim önbelleği NUMA düğümlerinden, NUMA düğümleri dilimlerden, dilimler de nesnelerden oluşmaktadır. Bu sistemi şekille şöyle gösterebiliriz: .. figure:: _static/slab-cache-structure.png :alt: Dilim önbelleği hiyerarşisi :align: center Bu şekilde iki NUMA düğümü vardır. Her NUMA düğümünde dilimler bulunmaktadır. Dilimler de tahsis edilecek blokları içermektedir. Dilim Önbelleği ve kmem_cache Yapısı ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Şimdi bu sistemin veri yapısı üzerinde duralım. Dilim önbelleği ``mm/slab.h`` dosyası içerisindeki ``kmem_cache`` yapısıyla temsil edilmiştir. Güncel çekirdeklerde bu yapı şöyledir: .. code-block:: c struct kmem_cache { struct slub_percpu_sheaves __percpu *cpu_sheaves; /* Used for retrieving partial slabs, etc. */ slab_flags_t flags; unsigned long min_partial; unsigned int size; /* Object size including metadata */ unsigned int object_size; /* Object size without metadata */ struct reciprocal_value reciprocal_size; unsigned int offset; /* Free pointer offset */ unsigned int sheaf_capacity; struct kmem_cache_order_objects oo; /* Allocation and freeing of slabs */ struct kmem_cache_order_objects min; gfp_t allocflags; /* gfp flags to use on each alloc */ int refcount; /* Refcount for slab cache destroy */ void (*ctor)(void *object); /* Object constructor */ unsigned int inuse; /* Offset to metadata */ unsigned int align; /* Alignment */ unsigned int red_left_pad; /* Left redzone padding size */ const char *name; /* Name (only for display!) */ struct list_head list; /* List of slab caches */ #ifdef CONFIG_SYSFS struct kobject kobj; /* For sysfs */ #endif #ifdef CONFIG_SLAB_FREELIST_HARDENED unsigned long random; #endif #ifdef CONFIG_NUMA /* * Defragmentation by allocating from a remote node. */ unsigned int remote_node_defrag_ratio; #endif #ifdef CONFIG_SLAB_FREELIST_RANDOM unsigned int *random_seq; #endif #ifdef CONFIG_KASAN_GENERIC struct kasan_cache kasan_info; #endif #ifdef CONFIG_HARDENED_USERCOPY unsigned int useroffset; /* Usercopy region offset */ unsigned int usersize; /* Usercopy region size */ #endif #ifdef CONFIG_SLUB_STATS struct kmem_cache_stats __percpu *cpu_stats; #endif struct kmem_cache_node *node[MAX_NUMNODES]; }; Yapının pek çok elemanının çeşitli konfigürasyon parametreleri seçildiğinde yapıya dahil edildiğine dikkat ediniz. Yapının ``object_size`` elemanı dilimlerde tutulan nesnelerin büyüklüğünü belirtmektedir. Ancak aslında sistem çeşitli konfigürasyon parametrelerine de bağlı olarak nesneler için metadata bilgileri nedeniyle daha büyük yer ayırabilmektedir. Yapının ``size`` elemanı nesneler için dilim içerisinde ayrılan gerçek alanı belirtmektedir. Nesneler için kullanılan ek metadata bilgileri şöyledir: .. figure:: _static/object-layout.png :alt: SLUB nesne bellek düzeni :align: center Buradaki ``flags`` elemanı tahsisat sırasındaki davranışı belirtmektedir. Bu eleman aşağıdaki bayrakların bileşimlerinden oluşabilmektedir: .. list-table:: :header-rows: 1 * - Flag - Anlamı * - ``SLAB_HWCACHE_ALIGN`` - Nesneleri CPU önbelleğine göre hizala * - ``SLAB_PANIC`` - Tahsisat başarısız olursa kernel panic yap * - ``SLAB_POISON`` - Serbest nesneleri bilinen bir byte ile doldur (debug) * - ``SLAB_RED_ZONE`` - Nesne etrafına kırmızı bölge ekle (debug) * - ``SLAB_ACCOUNT`` - Tahsisatları cgroup'a say * - ``SLAB_RECLAIM_ACCOUNT`` - Dilimleri geri alınabilir (reclaimable) olarak işaretle * - ``SLAB_TYPESAFE_BY_RCU`` - RCU grace period'u bitmeden slab'ı serbest bırakma Yapının ``allocflags`` elemanı ise ``alloc_pages`` fonksiyonuyla tahsisat yapılırken kullanılan bayrakları içermektedir. Zaten bu bayraklar izleyen paragraflarda göreceğimiz ``kmem_cache_create`` fonksiyonuna argüman olarak verilmektedir. Her dilim önbelleğinin bir ismi vardır. Bu isim yapının ``name`` elemanında tutulmaktadır. Biz dilim önbelleğinin düğümlerden, düğümlerin dilimlerden ve dilimlerin de nesnelerden oluştuğunu belirtmiştik. İşte dilim önbelleğindeki düğümler yapının ``node`` elemanında tutulmaktadır. ``node`` elemanının dilim önbelleğindeki düğümleri belirten ``kmem_cache_node`` türünden nesnelerin adreslerini tutan bir dizi olduğuna dikkat ediniz: .. code-block:: c struct kmem_cache_node *node[MAX_NUMNODES]; SLUB gerçekleştiriminde her nesneden sonra yukarıda açıkladığımız bazı metadata bilgileri tutulmaktadır. Yapının ``inuse`` elemanında nesnelerin metadata alanlarının "hangi offset'ten itibaren başladığı" bilgisi bulundurulmaktadır. Aşağıdaki şekli inceleyiniz: .. figure:: _static/slab-page-layout.png :alt: Dilim için ayrılan sayfa düzeni :align: center Yapının ``align`` elemanı yukarıdaki şekilden de görüldüğü gibi nesneler için ayrılan alanın kaçın katlarına göre hizalanacağını belirtmektedir. Yapının ``min_partial`` elemanı dilimlerin sisteme iadesi için gereken minimum dilim sayısını belirtmektedir: .. code-block:: none Dilim önbelleğindeki dilim sayısı > min_partial → boşalan dilimin sayfaları iade edilir Dilim önbelleğindeki dilim sayısı <= min_partial → boşalan dilimin sayfaları iade edilmez ``min_partial`` elemanının değeri şöyle tespit edilmektedir: .. code-block:: c #define MIN_PARTIAL 5 #define MAX_PARTIAL 10 static inline unsigned long slub_min_partial(void) { return ilog2(nr_cpu_ids); } static void set_min_partial(struct kmem_cache *s, unsigned long min) { if (min < MIN_PARTIAL) min = MIN_PARTIAL; else if (min > MAX_PARTIAL) min = MAX_PARTIAL; s->min_partial = min; } ``min_partial`` bu algoritmaya göre şu değerlerden biri olabilmektedir: .. list-table:: :header-rows: 1 * - CPU Sayısı - ilog2(nr_cpus) - min_partial * - 1 - 0 - 5 (MIN_PARTIAL alt sınırı) * - 2 - 1 - 5 (MIN_PARTIAL alt sınırı) * - 4 - 2 - 5 (MIN_PARTIAL alt sınırı) * - 8 - 3 - 5 (MIN_PARTIAL alt sınırı) * - 16 - 4 - 5 (MIN_PARTIAL alt sınırı) * - 32 - 5 - 5 (MIN_PARTIAL alt sınırı) * - 64 - 6 - 6 * - 128 - 7 - 7 * - 256 - 8 - 8 * - 512 - 9 - 9 * - 1024 - 10 - 10 (MAX_PARTIAL üst sınırı) * - 2048+ - 11+ - 10 (MAX_PARTIAL üst sınırı) ``min_partial`` elemanının amacı dilim önbelleğinde hazır durumda tutulacak belli miktarda boş dilimlerin bulundurulmasını sağlamaktır. ``kmem_cache`` yapısının ``ctor`` elemanında aşağıdaki gibi bir fonksiyon göstericisi tutulmaktadır: .. code-block:: c void (*ctor)(void *object); Ne zaman sistemden bir nesne tahsis edilmek istense önce o nesne bu ``ctor`` fonksiyonuna verilir. Bu fonksiyon nesnenin içerisine ilkdeğerlerini verir; bu işlemden sonra nesne tahsis edene iletilir. Bu elemanda ``NULL`` adresi varsa böyle bir işlem yapılmamaktadır. Buraya yerleştirilecek fonksiyon ``kmem_cache_create`` fonksiyonuna argüman olarak girilmektedir. Dilimlerin ardışıl fiziksel sayfalardan oluştuğunu söylemiştik. Peki bir dilim ardışıl kaç fiziksel sayfadan oluşmaktadır? Soruyu şöyle de sorabiliriz: Bir dilim ikiz blok tahsisat sisteminin hangi düzeyinden yapılmaktadır? İşte dilimlerin sayfa büyüklüklerinin belirlenmesinde ``kmem_cache`` yapısının iki elemanı etkili olmaktadır: .. code-block:: c struct kmem_cache { /* ... */ struct kmem_cache_order_objects oo; /* optimal order + nesne sayısı */ struct kmem_cache_order_objects min; /* fallback: minimum order + nesne sayısı */ /* ... */ }; İlk denemede yapının ``oo`` elemanına başvurulmaktadır. Eğer ikiz blok sisteminden ``oo`` elemanında belirtilen sayfa düzeyinde (order) tahsisat yapılamazsa bu kez yapının ``min`` elemanına başvurulmaktadır. Yapının ``min`` elemanına nesne boyutunu içeren en küçük düzey değeri (genellikle 0) atanmaktadır. Yani en az tahsisat değeri 1 sayfadır. ``oo`` elemanına atanacak düzey değeri dilim önbelleği yaratılırken ``kmem_cache_create`` fonksiyonunun çağrı zincirindeki ``calculate_sizes`` fonksiyonu tarafından verilmektedir. Ancak değerin asıl hesaplandığı yer ``calculate_order`` fonksiyonudur. ``calculate_order`` fonksiyonu ``oo`` için sayfa büyüklüğü değerini şu faktörlere bağlı olarak hesaplar: - **CPU sayısı:** Çok CPU'lu sistemde aynı nesne boyutu daha büyük düzeye yol açabilmektedir. Ayrıntılara burada girmeyeceğiz. Çekirdek kaynak kodlarına başvurabilirsiniz. - **Sınırlar:** Elde edilen değer ``slub_min_order`` ile ``slub_max_order`` değişkenlerinin arasına çekilir. Başlangıçta ``slub_min_order = 0`` ve ``slub_max_order = 3`` durumundadır. (Yani en fazla bir dilim 8 sayfadan oluşabilmektedir.) - **İstisnai Durum:** Nesne boyutu ``slub_max_order``'lık slab'a tek başına bile sığmıyorsa sınır aşılır ve nesnenin boyutuna uygun en küçük düzey kullanılır. Aşağıda somut *x86-64, 16 CPU* için çeşitli nesne boyutlarına göre ``min`` ve ``oo`` değerlerini veriyoruz: .. list-table:: :header-rows: 1 * - size (bayt) - oo:order - oo:objects - Slab boyutu - İsraf - min:order * - 64 - 0 - 64 - 4 KB - %0.0 - 0 * - 96 - 0 - 42 - 4 KB - %1.6 - 0 * - 192 - 1 - 42 - 8 KB - %1.6 - 0 * - 256 - 1 - 32 - 8 KB - %0.0 - 0 * - 512 - 2 - 32 - 16 KB - %0.0 - 0 * - 1024 - 3 - 32 - 32 KB - %0.0 - 0 * - 2048 - 3 - 16 - 32 KB - %0.0 - 0 * - 4096 - 3 - 8 - 32 KB - %0.0 - 0 * - 5000 - 3 - 6 - 32 KB - %8.4 - 1 * - 8192 - 3 - 4 - 32 KB - %0.0 - 1 * - 12000 - 3 - 2 - 32 KB - %26.8 - 2 * - 40960 - 4 - 1 - 64 KB - %37.5 - 4 Bu tabloda sütunlarda neden ``oo:order`` ve ``min:order`` yazıldığını merak edebilirsiniz. Aslında ``oo`` ve ``min`` elemanları yalnızca dilim için yapılacak tahsisatın düzey bilgisini değil aynı zamanda bir dilimde kaç nesnenin yer aldığı bilgisini de tutmaktadır. Bu elemanların ``kmem_cache_order_objects`` türünden olduğuna dikkat ediniz. Bu yapı şöyle tanımlanmıştır: .. code-block:: c struct kmem_cache_order_objects { unsigned int x; }; Görüldüğü gibi yapının ``x`` isminde tek bir elemanı vardır. İşte bu ``x`` elemanının düşük anlamlı 16 biti nesne sayısını, yüksek anlamlı 16 biti de düzey değerini tutmaktadır. .. figure:: _static/oo-bitfield.png :alt: kmem_cache_order_objects x alanının bit düzeni :align: center kmem_cache_node Yapısı ~~~~~~~~~~~~~~~~~~~~~~~ Bir dilim önbelleğinin düğümlerden, düğümlerin dilimlerden ve dilimlerin de nesnelerden oluştuğunu söylemiştik. Dilim önbelleğinin düğümleri ``kmem_cache_node`` isimli yapıyla temsil edilmektedir. Bu yapı güncel çekirdeklerde ``mm/slab.h`` dosyası içerisinde şöyle tanımlanmıştır: .. code-block:: c struct kmem_cache_node { spinlock_t list_lock; #ifdef CONFIG_SLAB struct list_head slabs_partial; /* partial list first, better asm code */ struct list_head slabs_full; struct list_head slabs_free; unsigned long total_slabs; /* length of all slab lists */ unsigned long free_slabs; /* length of free slab list only */ unsigned long free_objects; unsigned int free_limit; unsigned int colour_next; /* Per-node cache coloring */ struct array_cache *shared; /* shared per node */ struct alien_cache **alien; /* on other nodes */ unsigned long next_reap; /* updated without locking */ int free_touched; /* updated without locking */ #endif #ifdef CONFIG_SLUB unsigned long nr_partial; struct list_head partial; #ifdef CONFIG_SLUB_DEBUG atomic_long_t nr_slabs; atomic_long_t total_objects; struct list_head full; #endif #endif }; Güncel çekirdekler derlenirken yalnızca ``CONFIG_SLUB`` define edilmiş durumdadır. Eski tip SLAB ve SLOB gerçekleştirimlerinin çekirdekten çıkartıldığını belirtmiştik. Dolayısıyla aslında yukarıdaki yapı güncel çekirdeklerde aşağıdaki hale gelmektedir: .. code-block:: c struct kmem_cache_node { spinlock_t list_lock; /* CONFIG_SLUB */ unsigned long nr_partial; struct list_head partial; #ifdef CONFIG_SLUB_DEBUG atomic_long_t nr_slabs; atomic_long_t total_objects; struct list_head full; #endif }; Yapının ``partial`` elemanı bu düğümdeki dilimlerin listesini, ``nr_partial`` elemanı ise bunların sayısını tutmaktadır. UMA mimarisinde zaten tek bir düğümün olduğunu anımsayınız. Dilimler ve slab Yapısı ~~~~~~~~~~~~~~~~~~~~~~~~ Güncel çekirdeklerde dilimler ``mm/slab.h`` dosyası içerisindeki ``slab`` isimli yapıyla temsil edilmektedir. Ancak bir süre önceye kadar bu yapı yerine doğrudan dilim bilgileri ``page`` nesnelerinin içerisinde saklanıyordu. Güncel çekirdeklerdeki (7'li çekirdeklerdeki) ``slab`` yapısı şöyle tanımlanmıştır: .. code-block:: c struct slab { memdesc_flags_t flags; struct kmem_cache *slab_cache; union { struct { struct list_head slab_list; /* Double-word boundary */ struct freelist_counters; }; struct rcu_head rcu_head; }; unsigned int __page_type; atomic_t __page_refcount; #ifdef CONFIG_SLAB_OBJ_EXT unsigned long obj_exts; #endif }; Buradaki elemanları ve işlevlerini aşağıdaki tabloda listeliyoruz: .. list-table:: :width: 80% :header-rows: 1 * - Eleman - Tür - Açıklama * - ``__page_flags`` - ``memdesc_flags_t`` - Fiziksel sayfa bayrakları. ``struct page`` ile bellekte örtüşür; ``PG_slab`` gibi bayraklar burada tutulur. * - ``slab_cache`` - ``struct kmem_cache *`` - Bu dilimin ait olduğu önbelleği gösterir. * - ``slab_list`` - ``struct list_head`` - ``kmem_cache_node.partial`` listesine bağlanan düğüm. (``rcu_head`` ile union içindedir) * - ``rcu_head`` - ``struct rcu_head`` - Dilim RCU ile serbest bırakılırken kullanılır. (``slab_list`` ile union içindedir) * - ``struct freelist_counters`` - ``struct freelist_counters`` - 7.x ile ayrı bir yapıya taşınan freelist+counters bloğu. Aşağıdaki alanları içerir. * - ``freelist`` - ``void *`` - Dilim içindeki boş nesnelerin tek yönlü listesi. * - ``counters`` - ``unsigned long`` - ``inuse``, ``objects``, ``frozen``'ı tek atomik işlemle güncellemek için hepsini kapsayan birlik alanı. * - ``inuse`` - ``unsigned`` (16 bit) - Şu an tahsis edilmiş nesne sayısı. * - ``objects`` - ``unsigned`` (15 bit) - Dilimin toplam nesne kapasitesi. * - ``frozen`` - ``unsigned`` (1 bit) - 1 → dilim bir CPU'nun per-CPU listesine "dondurulmuş". ``SLUB_DEBUG`` açıksa: slab bozulma göstergesi olarak yeniden kullanılır. * - ``__page_type`` - ``unsigned int`` - Sayfa tipi bilgisi; ``struct page`` ile örtüşür. * - ``__page_refcount`` - ``atomic_t`` - Fiziksel sayfanın referans sayacı. Doğrudan erişilmez, page allocator yönetir. * - ``obj_exts`` - ``unsigned long`` - Nesne uzantılarına gösterici. Yalnızca ``CONFIG_SLAB_OBJ_EXT`` ile derlenir; slab profiling ve ``memory_failure`` gibi özellikler için kullanılır. ``slab`` yapısının önemli elemanları ``freelist_counters`` yapısına taşınmıştır. Yani bir süre önceye kadar aslında bu ``freelist_counters`` yapısının elemanları ``slab`` yapısının içindeydi. Fakat zaten yukarıdaki anonim birlik ve yapı tanımlaması ile sanki bu yapının elemanları ``slab`` yapısının içerisindeymiş gibi ele alınmaktadır. Yani elemanlara erişim bakımından bir farklılık oluşmamaktadır. 6'lı çekirdeklerde ``slab`` yapısı şöyledir: .. code-block:: c struct slab { memdesc_flags_t flags; struct kmem_cache *slab_cache; union { struct { union { struct list_head slab_list; struct { /* For deferred deactivate_slab() */ struct llist_node llnode; void *flush_freelist; }; #ifdef CONFIG_SLUB_CPU_PARTIAL struct { struct slab *next; int slabs; /* Nr of slabs left */ }; #endif }; /* Double-word boundary */ union { struct { void *freelist; /* first free object */ union { unsigned long counters; struct { unsigned inuse:16; unsigned objects:15; /* * If slab debugging is enabled then the * frozen bit can be reused to indicate * that the slab was corrupted */ unsigned frozen:1; }; }; }; #ifdef system_has_freelist_aba freelist_aba_t freelist_counter; #endif }; }; struct rcu_head rcu_head; }; unsigned int __page_type; atomic_t __page_refcount; #ifdef CONFIG_SLAB_OBJ_EXT unsigned long obj_exts; #endif }; Burada C standartları bağlamında bir noktaya dikkatinizi çekmek istiyoruz. C11'den sonra aşağıdaki gibi bir yapı tanımlaması geçerlidir: .. code-block:: c struct Sample { union { struct { int x; int y; }; int z; }; int k; }; Burada birlik içerisindeki yapının elemanları sanki birliğin elemanları gibi, birliğin elemanları da sanki ``Sample`` yapısının elemanları gibi işlem görmektedir. Örneğin: .. code-block:: c struct Sample s; s.x = 10; // geçerli Ancak yukarıdaki ``slab`` yapısında aşağıdakine benzer bir tanımlama yapılmıştır: .. code-block:: c struct S { int x; int y; }; struct Sample { union { struct S; /* C11'de geçersiz, gcc eklentisi */ int z; }; int k; }; Bu örnekte anonim birliğin içerisinde ``S`` yapısına ilişkin bir değişken ismi belirtilmemiştir. Bu durum C11'de geçerli değildir. Ancak gcc'de bir eklenti olarak desteklenmektedir. gcc derleyicileri bu tanımlamayla öncekini eşdeğer gibi kabul etmektedir. Güncel çekirdeklerdeki ``slab`` yapısının ``slab_cache`` elemanı geri doğru bu dilimin içinde bulunduğu ``kmem_cache`` nesnesini göstermektedir. Yapının ``slab_list`` elemanı dilimleri (yani ``slab`` nesnelerini) birbirine bağlayan bağlı liste düğümünü belirtmektedir. ``rcu_head`` elemanı dilim RCU mekanizmasıyla serbest bırakılırken kullanılmaktadır. Yukarıda da belirttiğimiz gibi aslında ``slab`` yapısının önemli elemanlarının çoğu yapının ``freelist_counters`` elemanına ilişkin yapının içerisindedir. Ancak anonim birlik ve yapı kuralları nedeniyle bunlara sanki ``slab`` yapısının elemanlarıymış gibi erişebilmektedir. ``freelist_counters`` yapısı şöyle tanımlanmıştır: .. code-block:: c struct freelist_counters { union { struct { void *freelist; union { unsigned long counters; struct { unsigned inuse:16; unsigned objects:15; /* * If slab debugging is enabled then the * frozen bit can be reused to indicate * that the slab was corrupted */ unsigned frozen:1; #ifdef CONFIG_64BIT /* * Some optimizations use free bits in 'counters' field * to save memory. In case ->stride field is not available, * such optimizations are disabled. */ unsigned int stride; #endif }; }; }; #ifdef system_has_freelist_aba freelist_full_t freelist_counters; #endif }; }; Yapının ``freelist`` elemanı o dilimdeki ilk boş nesnenin adresini belirtmektedir. Dilim içerisindeki nesneler tek bağlı liste (single linked list) ile birbirine bağlanmıştır. Dolayısıyla dilimden bir nesne tahsis edilmek istendiğinde tahsisat fonksiyonu (``kmem_cache_alloc``) hemen bu göstericinin gösterdiği yerdeki nesneyi O(1) karmaşıklıkta vermektedir. Yapının ``inuse`` elemanı ilgili dilimdeki tahsis edilmiş nesne sayısını belirtmektedir. ``objects`` elemanı ise dilim içerisinde toplam kaç nesne bulunduğunu belirtmektedir. Aşağıda ``slab`` veri yapısına ilişkin örnek bir çizim verilmiştir: .. figure:: _static/slab-struct-diagram.png :alt: Dilim için ayrılan sayfa düzeni :align: center :width: 60% Dilim Önbelleklerinin Yaratılması --------------------------------- Bir dilim önbelleği oluşturmak için yani ``kmem_cache`` türünden bir nesne oluşturmak için ``kmem_cache_create`` isimli çekirdek fonksiyonu kullanılmaktadır. ``kmem_cache_create`` fonksiyonunun parametrik yapısı şöyledir: .. code-block:: c struct kmem_cache *kmem_cache_create( const char *name, unsigned int size, unsigned int align, slab_flags_t flags, void (*ctor)(void *) ); Fonksiyonun birinci parametresi (``name``) yaratılacak dilim önbelleğinin ismini, ikinci parametresi (``size``) tahsis edilecek nesnenin uzunluğunu belirtmektedir. Üçüncü parametre (``align``) nesnelerin hangi değerin katlarına hizalanacağını belirtmektedir. Dördüncü parametre (``flags``) yukarıda belirttiğimiz dilim bayraklarına ilişkindir. Son parametre (``ctor``) ise dilimden nesne tahsis edildiğinde o nesneye ilkdeğerlerinin verilmesi için kullanılacak fonksiyonun adresini belirtmektedir. Fonksiyonun hizalama belirten üçüncü parametresine (``align``) 0 değeri girilebilir. Bu durumda o sisteme ilişkin varsayılan hizalama kullanılmaktadır. Çekirdek kodlarında bu varsayılan hizalama ``ARCH_KMALLOC_MINALIGN`` sembolik sabiti ile belirtilmektedir. Bu sembolik sabit şu değerlerden biri olabilmektedir: .. list-table:: :header-rows: 1 * - Mimari - ARCH_KMALLOC_MINALIGN * - x86_64 - 8 byte * - arm64 - 8 byte * - x86 (32) - 8 byte * - DMA varsa - 64 byte ``kmem_cache_create`` fonksiyonunun kullanımına şöyle bir örnek verebiliriz: .. code-block:: c struct myobject { int a; int b; char name[64]; }; /* ... */ if ((g_myobject_cachep = kmem_cache_create("myobject_cache", sizeof(struct myobject), 0, SLAB_HWCACHE_ALIGN, NULL)) == NULL) return -ENOMEM; Eskiden ``kmem_cache_create`` fonksiyonu ``mm/slab.c`` dosyası içerisinde normal bir fonksiyon biçiminde tanımlanıyordu. Çekirdeğin 6.12 sürümü ile birlikte artık bu fonksiyon bir makro biçimine getirilmiştir. Çekirdeğin 6.10 versiyonunda ``kmem_cache_create`` fonksiyonu ``mm/slab_common.c`` dosyasında şöyle tanımlanmıştı: .. code-block:: c struct kmem_cache *kmem_cache_create(const char *name, unsigned int size, unsigned int align, slab_flags_t flags, void (*ctor)(void *)) { return kmem_cache_create_usercopy(name, size, align, flags, 0, 0, ctor); } EXPORT_SYMBOL(kmem_cache_create); Buradaki ``kmem_cache_create_usercopy`` fonksiyonu asıl işlemi yapan fonksiyondur. Bu fonksiyon da şöyle tanımlanmıştır: .. code-block:: c struct kmem_cache * kmem_cache_create_usercopy(const char *name, unsigned int size, unsigned int align, slab_flags_t flags, unsigned int useroffset, unsigned int usersize, void (*ctor)(void *)) { struct kmem_cache *s = NULL; const char *cache_name; int err; #ifdef CONFIG_SLUB_DEBUG /* * If no slab_debug was enabled globally, the static key is not yet * enabled by setup_slub_debug(). Enable it if the cache is being * created with any of the debugging flags passed explicitly. * It's also possible that this is the first cache created with * SLAB_STORE_USER and we should init stack_depot for it. */ if (flags & SLAB_DEBUG_FLAGS) static_branch_enable(&slub_debug_enabled); if (flags & SLAB_STORE_USER) stack_depot_init(); #endif mutex_lock(&slab_mutex); err = kmem_cache_sanity_check(name, size); if (err) { goto out_unlock; } /* Refuse requests with allocator specific flags */ if (flags & ~SLAB_FLAGS_PERMITTED) { err = -EINVAL; goto out_unlock; } /* * Some allocators will constraint the set of valid flags to a subset * of all flags. We expect them to define CACHE_CREATE_MASK in this * case, and we'll just provide them with a sanitized version of the * passed flags. */ flags &= CACHE_CREATE_MASK; /* Fail closed on bad usersize of useroffset values. */ if (!IS_ENABLED(CONFIG_HARDENED_USERCOPY) || WARN_ON(!usersize && useroffset) || WARN_ON(size < usersize || size - usersize < useroffset)) usersize = useroffset = 0; if (!usersize) s = __kmem_cache_alias(name, size, align, flags, ctor); if (s) goto out_unlock; cache_name = kstrdup_const(name, GFP_KERNEL); if (!cache_name) { err = -ENOMEM; goto out_unlock; } s = create_cache(cache_name, size, calculate_alignment(flags, align, size), flags, useroffset, usersize, ctor, NULL); if (IS_ERR(s)) { err = PTR_ERR(s); kfree_const(cache_name); } out_unlock: mutex_unlock(&slab_mutex); if (err) { if (flags & SLAB_PANIC) panic("%s: Failed to create slab '%s'. Error %d\n", __func__, name, err); else { pr_warn("%s(%s) failed with error %d\n", __func__, name, err); dump_stack(); } return NULL; } return s; } EXPORT_SYMBOL(kmem_cache_create_usercopy); Burada ``kmem_cache`` nesnesinin asıl yaratımı ``create_cache`` fonksiyonunda yapılmaktadır. Bu fonksiyon da şöyle tanımlanmıştır: .. code-block:: c static struct kmem_cache *create_cache(const char *name, unsigned int object_size, unsigned int align, slab_flags_t flags, unsigned int useroffset, unsigned int usersize, void (*ctor)(void *), struct kmem_cache *root_cache) { struct kmem_cache *s; int err; if (WARN_ON(useroffset + usersize > object_size)) useroffset = usersize = 0; err = -ENOMEM; s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL); if (!s) goto out; s->name = name; s->size = s->object_size = object_size; s->align = align; s->ctor = ctor; #ifdef CONFIG_HARDENED_USERCOPY s->useroffset = useroffset; s->usersize = usersize; #endif err = __kmem_cache_create(s, flags); if (err) goto out_free_cache; s->refcount = 1; list_add(&s->list, &slab_caches); return s; out_free_cache: kmem_cache_free(kmem_cache, s); out: return ERR_PTR(err); } Aşağıda bu sürümdeki ``kmem_cache_create`` fonksiyonunun çağrı zinciri şekilsel olarak verilmiştir: .. figure:: _static/kmem-cache-create-callchain.png :alt: kmem_cache_create çağrı zinciri :align: center :width: 30% Peki çekirdek içerisindeki nesne tahsisatları dilimli tahsisat sistemiyle yapıldığına göre ``kmem_cache_create`` fonksiyonu ``kmem_cache`` nesnesini nasıl tahsis etmektedir? İşte ``kmem_cache`` nesneleri için de ayrı bir dilim önbelleği oluşturulmuştur: .. code-block:: c s = kmem_cache_zalloc(kmem_cache, GFP_KERNEL); Burada tahsisat çekirdek tarafından yaratılmış olan ``kmem_cache`` dilim önbelleğinden yapılmaktadır. 6.12 çekirdekleriyle birlikte ``kmem_cache_create`` fonksiyonu ``include/linux/slab.h`` dosyasında bir makro biçimine getirilmiştir: .. code-block:: c #define kmem_cache_create(__name, __object_size, __args, ...) \ _Generic((__args), \ struct kmem_cache_args *: __kmem_cache_create_args, \ void *: __kmem_cache_default_args, \ default: __kmem_cache_create)(__name, __object_size, __args, __VA_ARGS__) ``_Generic`` makrosu bir çeşit *function overloading* mekanizmasının makro düzeyinde sağlanması için C11 ile C'ye eklenmiştir. Bu makro bir parametreye sahiptir. Bu parametrenin türüne göre açım yapar. Eğer parametrenin türü belirtilen türlerden biri değilse ``default`` ile belirtilen açımı yapmaktadır. Örneğin: .. code-block:: c #define abs(x) _Generic((x), \ int: abs_int, \ long: abs_long, \ float: abs_float, \ double: abs_double, \ default: abs_default \ )(x) Bu örnekte ``abs`` makrosunu ``int`` türünden bir argümanla çağırmışsak bu durumda aslında ``abs_int`` açımı yapılacaktır. Dolayısıyla ``abs_int`` çağrılmış olacaktır. Eğer ``abs`` makrosunu ``long`` bir parametreyle çağırırsak bu durumda aslında ``abs_long`` fonksiyonu çağrılmış olacaktır. ``abs`` makrosunu ``int``, ``long``, ``float``, ``double`` türlerinin dışında bir türden argümanla çağırmışsak bu durumda ``abs_default`` fonksiyonu çağrılacaktır. Şimdi yukarıdaki ``kmem_cache_create`` makrosuna yeniden dönelim. ``_Generic`` makrosuna aktarılan parametre ``__args`` parametresidir. Bu parametre aslında eski biçimdeki ``align`` parametresidir. Biz bu ``align`` parametresi için gerçekten hizalama amaçlı ``int`` bir değer girersek (örneğin 0) bu durumda ``default`` kısım devreye girecek ve çağrı ``__kmem_cache_create`` haline gelecektir. Güncel çekirdeklerde ``__kmem_cache_create`` de yukarıda vermiş olduğumuz 6.10'daki ``kmem_cache_create_usercopy`` fonksiyonuna benzemektedir. Dilim Önbelleklerinin Birleştirilmesi ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Dilim önbelleği yaratılırken "birleştirme (merge)" denilen bir işlem de yapılabilmektedir. Çekirdek bir dilim önbelleği yaratılmak istendiğinde zaten o uzunlukta (bazı ayrıntıları var) daha önce yaratılmış olan bir dilim önbelleği varsa gerçekte yeni bir dilim önbelleği yaratmamakta, daha önce yaratılmış olanı sanki yeni yaratılmış gibi vermektedir. Birleştirme (merge) işlemi ``kmem_cache_create`` fonksiyonunun çağırdığı ``find_mergeable`` fonksiyonu tarafından yapılmaktadır. Bu fonksiyonun çağırma akışı şöyledir: .. figure:: _static/find-mergeable-flow.png :alt: find_mergeable çağrı akışı :align: center :width: 60% Yeni yaratılmak istenen bir dilim önbelleğinin zaten yaratılmış olana referans etmesinin (yani birleştirme işleminin) bazı koşulları vardır: .. list-table:: :header-rows: 1 * - Ölçüt - Açıklama * - Boyut uyumu - Mevcut dilim önbelleğinin ``size`` değeri yeni dilim önbelleğinin normalize edilmiş ``size`` değerine eşit ya da küçük bir tolerans payı kadar büyük olmalı. * - Constructor yok - Her iki dilim önbelleğinde de ``ctor == NULL`` olmalı. Constructor olan dilim önbellekleri hiçbir zaman merge edilmez; her nesne farklı başlatma gerektirebilir. * - ``SLAB_MERGE_SAME`` bayrakları - Aşağıdaki bayraklar her iki dilim önbelleğinde de aynı olmalıdır. * - ``SLAB_NEVER_MERGE`` yok - Yeni dilim önbelleğinde birleştirmeyi engelleyen flag olmamalı. * - Hizalama uyumu - Mevcut dilim önbelleğinin ``align`` değeri yeni dilim önbelleğinin ``align`` değerinden küçük olmamalı. Aşağıdaki bayrakların herhangi biri iki dilim önbelleğinde farklıysa birleştirme yapılmamaktadır: .. list-table:: :header-rows: 1 * - Bayrak - Neden Birleştirmeyi Engeller * - ``SLAB_RECLAIM_ACCOUNT`` - Biri reclaimable diğeri unreclaimable ise birleştirme yapılmaz. * - ``SLAB_CACHE_DMA`` - DMA cache ile normal önbellekler birleşemez; farklı zone'lardan tahsis yapılır. * - ``SLAB_CACHE_DMA32`` - ``ZONE_DMA32`` ile diğer zone'lar aynı önbelleği paylaşamaz. * - ``SLAB_ACCOUNT`` - memcg (memory cgroup) takibi farklıysa hesaplama bozulur. Birleştirmeyi kesin engelleyen bayraklar da şunlardır: .. list-table:: :header-rows: 1 :widths: 30 70 * - Bayrak - Neden * - ``SLAB_RED_ZONE`` - Nesne layout'u değiştirir; debug baytları başka önbelleğin nesnelerini bozar. * - ``SLAB_POISON`` - Poison deseni tüm nesne alanına uygulanır; birleştirilmiş önbellekte yanlış alarmlar üretir. * - ``SLAB_STORE_USER`` - Her nesneye stack trace saklanır; layout uyumu bozulur. * - ``SLAB_TRACE`` - Her alloc/free için ayrı log tutulur; birleştirilince hangi cache'e ait olduğu belirsiz kalır. * - ``SLAB_TYPESAFE_BY_RCU`` - RCU grace period serbest bırakma semantiği tüm önbellek genelinde tutarlı olmalı. * - ``SLAB_NO_MERGE`` - Kullanıcının açıkça birleştirmeyi engellediği durum. Önceden Yaratılmış Dilim Önbellekleri ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Çekirdeğin ``task_struct`` gibi, ``file`` gibi, ``dentry`` gibi, ``inode`` gibi nesneleri tahsis etmekte kullandığı önceden yaratılmış dilimli önbellek nesneleri (yani ``kmem_cache`` nesneleri) vardır. Aşağıda bunların isimlerini ve bunlara erişmekte kullanılan ``kmem_cache`` türünden göstericilerin isimlerini veriyoruz: .. list-table:: :header-rows: 1 :widths: 28 36 36 * - Nesne Tipi - ``kmem_cache`` Göstericisi - İsim * - ``struct kmem_cache`` - ``kmem_cache`` - ``"kmem_cache"`` * - ``struct task_struct`` - ``task_struct_cachep`` - ``"task_struct"`` * - ``struct mm_struct`` - ``mm_cachep`` - ``"mm_struct"`` * - ``struct vm_area_struct`` - ``vm_area_cachep`` - ``"vm_area_struct"`` * - ``struct file`` - ``filp_cachep`` - ``"filp"`` * - ``struct dentry`` - ``dentry_cache`` - ``"dentry"`` * - ``struct inode`` - (her fs kendi önbelleğini yaratır) - ``"ext4_inode_cache"`` vb. * - ``struct socket`` - ``sock_inode_cachep`` - ``"sock_inode_cache"`` * - ``struct sk_buff`` - ``skbuff_head_cache`` - ``"skbuff_head_cache"`` * - ``struct sk_buff`` (fclone) - ``skbuff_fclone_cache`` - ``"skbuff_fclone_cache"`` * - ``struct sigqueue`` - ``sigqueue_cachep`` - ``"sigqueue"`` * - ``struct cred`` - ``cred_jar`` - ``"cred_jar"`` * - ``struct pid`` - ``pid_cachep`` - ``"pid"`` * - ``struct fs_struct`` - ``fs_cachep`` - ``"fs_struct"`` * - ``struct files_struct`` - ``files_cachep`` - ``"files_cache"`` * - ``struct nsproxy`` - ``nsproxy_cachep`` - ``"nsproxy"`` * - ``struct signal_struct`` - ``signal_cachep`` - ``"signal_cache"`` * - ``struct sighand_struct`` - ``sighand_cachep`` - ``"sighand_cache"`` * - ``struct bio`` - ``bio_slab`` (her queue için) - ``"bio"`` * - ``struct request`` - ``request_cachep`` - ``"blkdev_requests"`` * - ``struct buffer_head`` - ``bh_cachep`` - ``"buffer_head"`` * - ``struct anon_vma`` - ``anon_vma_cachep`` - ``"anon_vma"`` Peki yukarıdaki dilim önbellek nesneleri çekirdekte hangi aşamada yaratılmaktadır? Biz kursumuzda çekirdeğin başlatılma sürecini ayrı bir bölümde ele alacağız. Linux çekirdek imajı belleğe yüklendiğinde ``start_kernel`` isimli fonksiyon çağrılmaktadır. Bu fonksiyonu çekirdeğin main fonksiyonu gibi düşünebilirsiniz. Bu fonksiyon içerisinde pek çok alt sistem ilklendirilmektedir. İşte yukarıdaki dilim önbellekleri bu alt sistemlerin ilklendirildiği (initialize edildiği) yerlerde yaratılmaktadır. Bunların yaratıldığı yerleri aşağıdaki tablolarda veriyoruz: .. list-table:: :header-rows: 1 * - Nesne Türü - Yaratan Fonksiyon * - ``struct pid`` - ``pidmap_init()`` * - ``struct anon_vma`` - ``anon_vma_init()`` * - ``struct buffer_head`` - ``buffer_init()`` * - ``struct vm_area_struct`` - ``mmap_init()`` / ``vma_init()`` * - ``struct mm_struct`` - ``mm_init()`` → ``mm_cache_init()`` * - ``struct fs_struct`` - ``proc_caches_init()`` * - ``struct files_struct`` - ``proc_caches_init()`` * - ``struct task_struct`` - ``proc_caches_init()`` → ``fork_init()`` * - ``struct signal_struct`` - ``proc_caches_init()`` * - ``struct sighand_struct`` - ``proc_caches_init()`` * - ``struct nsproxy`` - ``nsproxy_cache_init()`` * - ``struct cred`` - ``cred_init()`` * - ``struct sigqueue`` - ``signals_init()`` * - ``struct dentry`` - ``dcache_init()`` (VFS) * - ``struct inode`` - ``inode_init()`` (VFS — genel inode cache) * - ``struct file`` - ``files_init()`` (VFS) * - ``struct socket`` / ``sock_inode`` - ``sock_init()`` (ağ alt sistemi) * - ``struct sk_buff`` - ``skb_init()`` (ağ — ``net_dev_init`` içinde) * - ``struct bio`` - ``bio_init()`` (blok katmanı) * - ``struct request`` - ``blk_mq_init()`` / ``request_cachep`` yaratılır * - ``struct inode`` (fs) - ``ext4_init_inodecache()`` vb. — modül yüklemede Bu nesnelerin yaratıldığı yerlere ilişkin çağrı zincirini de aşağıda veriyoruz: .. code-block:: none start_kernel() │ ├── mm_init() │ └── kmem_cache_init() → [Aşama 1] kmem_cache │ ├── proc_caches_init() → [Aşama 2] task_struct, mm_struct, │ fs_struct, files_struct, │ signal, sighand ├── cred_init() → [Aşama 2] cred ├── fork_init() → [Aşama 2] task_struct (tamamlanır) ├── signals_init() → [Aşama 2] sigqueue ├── pidmap_init() → [Aşama 2] pid ├── anon_vma_init() → [Aşama 2] anon_vma ├── buffer_init() → [Aşama 2] buffer_head │ └── do_initcalls() → [Aşama 3] ├── dcache_init() → dentry ├── inode_init() → inode (genel) ├── files_init() → file ├── sock_init() → socket ├── skb_init() → sk_buff ├── bio_init() → bio ├── blk_mq_init() → request └── ext4_init() vb. → fs'e özel inode cache Dilim Önbelleklerinden Tahsisat İşlemleri ----------------------------------------- Yaratılmış olan bir dilim önbelleğinden tahsisat yapmak için kullanılan temel fonksiyonlar şunlardır: .. list-table:: :header-rows: 1 * - Fonksiyon - İşlevi * - ``kmem_cache_alloc`` - Standart tekli tahsisat * - ``kmem_cache_zalloc`` - Sıfırlanmış tahsisat (``GFP_ZERO``) * - ``kmem_cache_alloc_node`` - NUMA düğümüne özgü tahsisat * - ``kmem_cache_alloc_bulk`` - Toplu (çok nesne) tahsisat ``kmem_cache_alloc`` ve ``kmem_cache_zalloc`` en çok kullanılan tahsisat fonksiyonlarıdır. Bu fonksiyonlar belli bir dilim önbelleğinden nesne tahsis etmektedir. Fonksiyonların prototipleri şöyledir: .. code-block:: c void *kmem_cache_alloc(struct kmem_cache *cachep, int flags); void *kmem_cache_zalloc(struct kmem_cache *k, gfp_t flags); Fonksiyonların birinci parametreleri tahsisatın hangi dilim önbelleğinden yapılacağını belirtmektedir. Yani bu parametreye ``kmem_cache_create`` fonksiyonundan elde edilen ``kmem_cache`` nesnesinin adresi geçirilmelidir. Fonksiyonların ikinci parametreleri ``alloc_pages`` çağrısı için gereken bayrakları belirtmektedir. Yani bu parametre aslında ``alloc_pages`` fonksiyonuna geçirilen birinci parametreyle aynıdır. Yukarıda da belirttiğimiz gibi aslında dilimli tahsisat sistemi arka planda ikiz blok tahsisat sistemini kullanmaktadır. Yani dilimli tahsisat sistemindeki dilimler ``alloc_pages`` fonksiyonu ile ikiz blok tahsisat sisteminden elde edilmektedir. Biz ``alloc_pages`` fonksiyonunu anlatırken bu bayrakların anlamlarını açıklamıştık. Bu bayraklar hem "tahsisatın hangi bölgeden (zone)" yapılacağını hem de göç türünün fallback mekanizması için nereden başlatılacağını belirtiyordu. En çok kullanılan bayrağın ``GFP_KERNEL`` olduğunu anımsayınız. Bu bayrak ``ZONE_NORMAL`` bölgesinden ``MIGRATE_UNMOVABLE`` göç türünden hareketle sayfa tahsisatını yapmaktadır. Anımsanacağı gibi ``GFP_ATOMIC`` bayrağı da ``ZONE_NORMAL`` bölgesinden tahsisat yapmaya çalışır. Ancak bu bayrak ilgili thread'in uykuya yatırılmasını engellemektedir. Kesme kodlarında ve örneğin ``spinlock`` nesnelerinin kilitlendiği durumlarda ``GFP_KERNEL`` yerine ``GFP_ATOMIC`` bayrağını tercih etmelisiniz. ``kmem_cache_zalloc`` fonksiyonu aynı zamanda tahsis edilen alanı sıfırlamaktadır. Aslında bu fonksiyon ``include/linux/slab.h`` dosyasında aşağıdaki gibi bir makro biçiminde yazılmıştır: .. code-block:: c #define kmem_cache_zalloc(_k, _flags) kmem_cache_alloc(_k, (_flags)|__GFP_ZERO) Anımsayacağınız gibi ``__GFP_ZERO`` bayrağı zaten ikiz blok tahsisat sisteminde tahsis edilen sayfaların sıfırlanması için kullanılmaktadır. Dilimli tahsisat sistemlerindeki tahsisat fonksiyonları başarısızlık durumunda ``NULL`` adresle geri dönmektedir. Dilim önbelleğinde nesne kalmadığı zaman ``alloc_pages`` ile ikiz blok tahsisat sisteminden sayfa tahsis edilmektedir. İkiz blok tahsis sisteminde *fallback* mekanizmasının da devreye girdiğini anımsayınız. Sayfa tahsis edilemediğinde zaten çekirdeğin reclaim yapan thread'leri uyandırıldığı için bu fonksiyonların bellek yetersizliğinde başarısız olmaları düşük bir olasılıktır. Ancak bu fonksiyonların başarı kontrolleri mutlaka yapılmalıdır. Bu fonksiyonların başarısız olma nedenlerini aşağıdaki tabloda açıklıyoruz: .. list-table:: :header-rows: 1 * - Başarısızlık Nedeni - NULL Döner mi? - Açıklama * - Bellek baskısı (``GFP_KERNEL``) - Bazen - Önce reclaim denenir * - Bellek baskısı (``GFP_ATOMIC``) - Evet, hemen - Reclaim yapılmaz * - NUMA + ``__GFP_THISNODE`` - Evet - Fallback yok * - Memcg limit aşımı - Evet - Cgroup zorlaması Eğer bu fonksiyonlar başka fonksiyonların içerisinde çağrılmışsa başarısızlık durumunda çağrımın yapıldığı fonksiyonun ``-ENOMEM`` değeri ile geri döndürülmesi uygun olur. Güncel çekirdeklerde ``kmem_cache_alloc`` fonksiyonunun çağrı grafı kabaca şöyledir: .. code-block:: none kmem_cache_alloc(cache, gfp_flags) │ └── slab_alloc() │ [memcg_slab_pre_alloc hook] │ [trace_kmem_cache_alloc tracepoint] │ └── slab_alloc_node() │ [NUMA node seçimi] │ ├── [per-CPU freelist dolu?] ──Evet──→ OBJ DÖNER ← hızlı yol │ └── Hayır → __slab_alloc() ← yavaş yol │ [preempt disable] │ ├── [partial slab var?] ──Evet──→ get_partial() │ │ [kmem_cache_node] │ └──→ OBJ DÖNER │ └── Hayır → new_slab() │ └── allocate_slab() │ └── alloc_pages(gfp_flags, order) │ ← reclaim buradan tetiklenir │ └── __alloc_pages() │ ├── [fast path: watermark OK?] ──Evet──→ sayfa döner │ └── Hayır → __alloc_pages_slowpath() │ ├── [GFP_ATOMIC?] ──Evet──→ NULL DÖNER │ (reclaim yok) │ └── Hayır (GFP_KERNEL vs.) │ ├── wake_all_kswapd() │ │ [async reclaim başlatılır] │ │ [çağıran thread bloklanmaz] │ │ │ └── hızlı tahsisat tekrar denenir │ │ │ └── [başarılı?] ──Evet──→ sayfa döner │ ├── [hâlâ yetersiz?] │ └── direct reclaim │ (try_to_free_pages) │ [thread bloklanır ← burada] │ [sync, çağıran task temizler] │ ├── [hâlâ yetersiz?] │ └── memory compaction │ (compact_zone) │ ├── [hâlâ yetersiz?] │ └── OOM killer │ (out_of_memory) │ [kurban seç + öldür] │ └── [yine de yok?] └──→ NULL DÖNER ← setup_slab() çağrılmaz Tabii bu çağrı dizgesindeki ayrıntıları bir yana bırakırsak kabaca olanlar şunlardır: - Nesne tahsis edilmeye çalışılır. - Dilimlerde uygun nesne yoksa ikiz blok tahsisat sisteminden dilim tahsis edilmeye çalışılır. - İkiz blok tahsisat sisteminde *fallback* mekanizmaları uygulanır. - Eğer hâlâ sayfa yoksa *kswapd* çekirdek thread'i uyandırılır. - Bir kez daha doğrudan reclaim denenir. Doğrudan reclaim başarısız olursa ve eğer ``GFP_ATOMIC`` kullanılmamışsa thread uykuya yatırılır ve reclaim sonrasında uyandırılır. - Eğer hâlâ sayfa tahsisatı yapılamıyorsa fonksiyon başarısız olur. Tahsis edilmiş olan nesne ``kmem_cache_free`` fonksiyonuyla dilim önbelleğine iade edilmektedir. Fonksiyon prototipi şöyledir: .. code-block:: c void kmem_cache_free(struct kmem_cache *s, void *x); Fonksiyonun birinci parametresi dilim önbelleğine ilişkin nesne adresini, ikinci parametresi serbest bırakılacak nesnenin adresini almaktadır. ``kmem_cache_alloc_node`` fonksiyonu belli bir NUMA düğümünden tahsisat yapmaktadır. Başka bir deyişle tahsisat için ilgili NUMA düğümünün bölgelerini kullanan dilim önbelleklerinden tahsisat yapılmaktadır. Ancak fonksiyonda belirtilen NUMA düğümüne ilişkin bölgelerdeki dilim önbelleklerinde boş yer bulunamazsa düğüm temelinde *fallback* yapılmamaktadır. Fonksiyonun prototipi şöyledir: .. code-block:: c void *kmem_cache_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node); Fonksiyonun birinci parametresi dilim önbelleğine ilişkin ``kmem_cache`` nesnesinin adresini, ikinci parametresi tahsisat bayraklarını ve üçüncü parametresi de NUMA düğümünün numarasını almaktadır. ``kmem_cache_alloc_bulk`` fonksiyonu dilim önbelleğinden birden fazla nesne tahsis etmek için kullanılmaktadır. Fonksiyonun prototipi şöyledir: .. code-block:: c int kmem_cache_alloc_bulk(struct kmem_cache *s, gfp_t flags, size_t nr, void **p); Fonksiyonun son iki parametresine dikkat ediniz. Son parametre (``p``) tahsis edilen nesnelerin adreslerinin yerleştirileceği gösterici dizisinin adresini almaktadır. Sondan bir önceki parametre (``nr``) toplam kaç nesnenin tahsis edileceğini belirtmektedir. Fonksiyon "ya hep ya hiç" biçiminde tahsisat yapmaktadır. Yani burada belirtilen sayıdaki tahsisatın hepsi yapılamazsa hiçbir tahsisat yapılmamış gibi geri dönmektedir. kmalloc Fonksiyonu ve Türevleri ------------------------------- Dilimli tahsisat sisteminin amacının belli uzunluklardaki nesneleri hızlı bir biçimde tahsis etmek olduğunu söylemiştik. Ancak her farklı uzunluktaki nesne için yeni bir dilim önbelleği oluşturmak zahmetlidir. Örneğin biz çekirdek kodlamasında 28 byte'lık bir tahsisat yapmak isteyelim. Bunun için 28 byte'lık nesnelerden oluşan bir dilim önbelleği yaratmak oldukça zahmetlidir. İşte Linux çekirdeğinde bu zahmetten kurtulmak için belli uzunluklarda hazır dilim önbellekleri de oluşturulmuştur. Bu hazır dilim önbelleklerinin nesne uzunlukları şöyledir: .. list-table:: :header-rows: 1 * - # - Nesne Boyutu - ``KMALLOC_NORMAL`` - ``KMALLOC_CGROUP`` - ``KMALLOC_DMA`` * - 1 - 8 B - ``kmalloc-8`` - ``kmalloc-cg-8`` - ``dma-kmalloc-8`` * - 2 - 16 B - ``kmalloc-16`` - ``kmalloc-cg-16`` - ``dma-kmalloc-16`` * - 3 - 32 B - ``kmalloc-32`` - ``kmalloc-cg-32`` - ``dma-kmalloc-32`` * - 4 - 64 B - ``kmalloc-64`` - ``kmalloc-cg-64`` - ``dma-kmalloc-64`` * - 5 - 96 B (*) - ``kmalloc-96`` - ``kmalloc-cg-96`` - ``dma-kmalloc-96`` * - 6 - 128 B - ``kmalloc-128`` - ``kmalloc-cg-128`` - ``dma-kmalloc-128`` * - 7 - 192 B (*) - ``kmalloc-192`` - ``kmalloc-cg-192`` - ``dma-kmalloc-192`` * - 8 - 256 B - ``kmalloc-256`` - ``kmalloc-cg-256`` - ``dma-kmalloc-256`` * - 9 - 512 B - ``kmalloc-512`` - ``kmalloc-cg-512`` - ``dma-kmalloc-512`` * - 10 - 1024 B (1k) - ``kmalloc-1k`` - ``kmalloc-cg-1k`` - ``dma-kmalloc-1k`` * - 11 - 2048 B (2k) - ``kmalloc-2k`` - ``kmalloc-cg-2k`` - ``dma-kmalloc-2k`` * - 12 - 4096 B (4k) - ``kmalloc-4k`` - ``kmalloc-cg-4k`` - ``dma-kmalloc-4k`` * - 13 - 8192 B (8k) - ``kmalloc-8k`` - ``kmalloc-cg-8k`` - ``dma-kmalloc-8k`` Burada nesne uzunlukları 2'nin kuvvetine ilişkin olsa da (*) ile gösterilen iki istisna bulunmaktadır. Bu dilim önbellekleri çekirdek imajı belleğe yüklenip ilklenirken bellek yönetim işlevlerine ilişkin ilkdeğerlerin verildiği ``mm_init`` fonksiyonu içerisinde yaratılmaktadır: .. code-block:: none start_kernel [init/main.c] └─► mm_init └─► kmem_cache_init [mm/slub.c] Tablodaki ``KMALLOC_CGROUP``, *docker* gibi *container* teknolojilerinin kullanımını daha etkin hale getirmek için Linux çekirdekine sokulmuş olan "memcg (memory cgroup)" kavramı ile ilgilidir. ``kmalloc-cg-*`` önbellekleri memcg (memory cgroup) tarafından izlenmesi gereken tahsisatlar için bulundurulmuştur. ``KMALLOC_DMA`` ise ``GFP_DMA`` bayrağıyla tahsisat yapıldığında kullanılan dilim önbellekleridir. Örneğin biz hiç yeni bir dilim önbelleği yaratmadan 12 byte tahsisat yapmak isteyelim. Tabloda 12 byte'lık hazır bir dilim önbelleği olmadığı için tahsisat 16 byte'lık dilim önbelleğinden yapılacaktır. Tabii bu durumda 4 byte boşa harcanmış olacaktır. Yani bu hazır dilim önbelleklerini kullanmanın bir "içsel bölünme (internal fragmentation)" maliyeti vardır. Yukarıdaki genel dilim önbelleklerinden tahsisat yapan genel tahsisat fonksiyonları da bulundurulmuştur. Şimdi onları açıklayacağız. ``kmalloc`` fonksiyonu özellikle çekirdek modülleri ve aygıt sürücüler tarafından sıkça kullanılmaktadır. Bu fonksiyon yukarıdaki hazır dilim önbelleklerinin hangisi talep edilen uzunluk için uygunsa tahsisatı oradan yapmaktadır. Dolayısıyla fonksiyonun kullanımı oldukça kolaydır. ``kmalloc`` fonksiyonunu adeta C'nin ``malloc`` fonksiyonuna benzetebilirsiniz. Fonksiyonun prototipi şöyledir: .. code-block:: c void *kmalloc(size_t size, gfp_t flags); Fonksiyonun birinci parametresi tahsis edilecek nesnenin uzunluğunu, ikinci parametre ise yine tahsisat bayraklarını belirtmektedir. Bu ikinci parametre ilgili dilim önbelleğinde sayfa bulunamadığında ``alloc_pages`` fonksiyonu ile ikiz blok tahsisat sisteminden sayfa istenirken kullanılmaktadır. Bu ikinci parametreye tipik olarak ``GFP_KERNEL`` geçilmektedir. ``kmalloc`` fonksiyonu başarısızlık durumunda ``NULL`` adrese geri dönmektedir. Örneğin: .. code-block:: c if ((obj = kmalloc(12, GFP_KERNEL)) == NULL) return -ENOMEM; ``kmalloc`` fonksiyonun gerçekleştirimi eski çekirdeklerde oldukça basitti. Örneğin çekirdeğin 2.4 versiyonunda ``kmalloc`` şöyle yazılmıştı: .. code-block:: c void *kmalloc(size_t size, int flags) { cache_sizes_t *csizep = cache_sizes; for (; csizep->cs_size; csizep++) { if (size > csizep->cs_size) continue; return __kmem_cache_alloc(flags & GFP_DMA ? csizep->cs_dmacachep : csizep->cs_cachep, flags); } return NULL; } Bu sürümde tüm dilim önbelleklerinin uzunlukları ``cache_sizes`` isimli bir dizide toplanmıştır. Döngü içerisinde talep edilen tahsisata en uygun dilim önbelleğinin tespit edildiğine dikkat ediniz. Sonra da ``__kmem_cache_alloc`` fonksiyonu ile o dilim önbelleğinden tahsisat yapılmıştır. Güncel çekirdeklerde "memcg (memory cgroup)" gibi kavramların çekirdeğe dahil edilmesiyle fonksiyon daha karmaşık bir hale gelmiştir. Güncel çekirdeklerdeki ``kmalloc`` çağrı dizgesi şöyledir: .. code-block:: none kmalloc(size, flags) [slab.h — makro] │ │ alloc_hooks(__kmalloc_noprof(...)) │ ├─[size > KMALLOC_MAX_CACHE_SIZE (~8 KiB)] │ │ │ ▼ │ kmalloc_large_noprof() [slab_common.c] │ │ │ ▼ │ alloc_pages() [Buddy Allocator — SLUB devre dışı] │ └─[size ≤ KMALLOC_MAX_CACHE_SIZE] │ ▼ __do_kmalloc_node() [slab_common.c] │ ▼ kmalloc_slab(size, flags) │ kmalloc_type() → NORMAL / CGROUP / RECLAIM / DMA │ kmalloc_index() → boyuta göre indeks │ ▼ kmalloc_caches[type][idx] ── kmem_cache seçildi │ ▼ slab_alloc_node() [slub.c] │ ├──────────────────────────────────────┐ │ │ ▼ [HIZLI YOL — lockless] [YAVAŞ YOL] │ c->freelist != NULL ___slab_alloc() this_cpu_cmpxchg_double() │ nesne döner ┌─────────────┼──────────────────┐ │ │ │ [1] per-CPU [2] node [3] yeni sayfa partial partial gerekiyor listesi listesi │ (c->partial) (kmem_cache_node │ │ ->partial) │ │ │ new_slab() │ │ │ └──────┬──────┘ alloc_pages() │ [Buddy Allocator] ▼ nesne döner ✓ ``kmalloc`` ailesinden başka çekirdek fonksiyonları da vardır. ``kzalloc`` fonksiyonu tahsis edilen alanı aynı zamanda sıfırlamaktadır. Yani aşağıdaki işlemi yapmaktadır: .. code-block:: c void *kzalloc(size_t size, gfp_t flags) { return kmalloc(size, flags | __GFP_ZERO); } ``kmalloc_array`` fonksiyonu iki parametresinin çarpımı kadar alanı tahsis etmektedir. Fonksiyonun prototipi şöyledir: .. code-block:: c void *kmalloc_array(size_t n, size_t size, gfp_t flags); Tabii bu iki parametreyi programcı kendi çarpıp ``kmalloc`` fonksiyonuna verebilir. Ancak bazen ``n`` ve ``size`` değerleri başka bir yerden, örneğin kullanıcı alanından geliyor olabilir. Bu durumda taşma kontrolünün yapılması gerekebilir. (İki büyük işaretsiz tamsayı çarpıldığında taşmadan dolayı küçük bir işaretsiz tamsayının elde edilebileceğine dikkat ediniz.) ``kcalloc`` fonksiyonu C'deki ``calloc`` fonksiyonuna benzetilebilir. Bu fonksiyon ``kmalloc_array`` gibi iki uzunluk parametresi almaktadır ancak tahsis edilen alanı da sıfırlamaktadır. Fonksiyonun prototipi şöyledir: .. code-block:: c void *kcalloc(size_t n, size_t size, gfp_t flags); ``krealloc`` fonksiyonu C'nin ``realloc`` fonksiyonu gibi düşünülebilir. Daha önce tahsis edilmiş olan bloğu büyütmek ya da küçültmek için kullanılmaktadır. Fonksiyonun prototipi şöyledir: .. code-block:: c void *krealloc(const void *p, size_t new_size, gfp_t flags); Fonksiyonun birinci parametresi daha önce ``kmalloc`` ve türevleri tarafından tahsis edilmiş olan bloğun adresini, ikinci parametresi bu bloğun toplam yeni uzunluğunu ve üçüncü parametresi de tahsisat bayraklarını belirtmektedir. Fonksiyon zaten daha önce tahsis edilmiş olan nesnede talep edilen büyüklüğü içerecek kadar boş yer varsa ya da blok küçültülüyorsa aynı adresle geri döner. Ancak daha önce tahsis edilmiş nesnede talep edilen uzunluk kadar boş yer yoksa bu durumda sanki yeniden ``kmalloc`` çağrılıyormuş gibi tahsisat daha büyük bir dilim önbelleğinden yapılmaktadır. .. list-table:: :header-rows: 1 * - Durum - Davranış * - ``p == NULL`` - ``kmalloc(new_size, flags)`` ile eşdeğer * - ``new_size == 0`` - ``kfree(p)``; ``ZERO_SIZE_PTR`` döner * - Aynı nesneye sığıyor - Yerinde işlem; yeni tahsis yapmaz * - Farklı önbellek gerekiyor - Yeni tahsis + ``memcpy`` + eski bloğu ``kfree`` ``krealloc_array`` fonksiyonu 6.1 çekirdekleriyle eklenmiştir. ``kmalloc_array`` fonksiyonunun *realloc* biçimi gibidir: .. code-block:: c void *krealloc_array(void *p, size_t new_n, size_t new_size, gfp_t flags); ``kmalloc`` türevi fonksiyonlarla tahsis edilen alanlar ``kfree`` fonksiyonu ile serbest bırakılmaktadır. ``kfree`` fonksiyonunun prototipi şöyledir: .. code-block:: c void kfree(const void *objp); Fonksiyonun yalnızca blok adresini parametre olarak aldığına dikkat ediniz. Peki bu fonksiyon ilgili dilim önbelleğini nasıl bulmaktadır? Eskiden eğer bir sayfa dilimli tahsisat sistemi tarafından kullanılıyorsa ona ilişkin ``kmem_cache`` nesnesinin adresi de sayfayı temsil eden ``page`` yapısının içerisinde tutuluyordu. Böylece eskiden herhangi bir nesnenin içinde bulunduğu sayfaya ilişkin ``page`` nesnesinden hareketle geriye doğru ``kmem_cache`` nesnesi elde edilebiliyordu. Çekirdeğin 5.17 versiyonuyla birlikte ayrı bir ``slab`` yapısı oluşturuldu (biz bu yapıyı incelemiştik) ve bu yapı ``page`` yapısının üstüne bindirildi. ``slab`` yapısının içerisinde de ``kmem_cache`` nesnesinin adresinin tutulduğunu belirtmiştik. Yani hem eski çekirdeklerde hem de yeni çekirdeklerde biz nesnenin adresini biliyorsak oradan onun bulunduğu ``page`` nesnesine ya da ``slab`` nesnesine, oradan da ``kmem_cache`` nesnesine erişebiliriz. 5.17 ve sonrasında aslında ``page`` nesnesinin ``slab`` nesnesi ile aynı nesne olduğuna dikkat ediniz. Yani yeni çekirdeklerde erişim şu yoldan geçilerek yapılmaktadır: .. code-block:: none Nesne adresi (void *obj) │ │ virt_to_page(obj) ▼ struct page *page ← mem_map[] girdisi │ │ page_slab(page) │ └─► (struct slab *)page [cast — aynı bellek] ▼ struct slab *slab │ │ slab->slab_cache ▼ struct kmem_cache *s ← cache bulundu Dilim Önbelleklerine İlişkin Bilgilerin proc ve sys Dosya Sistemleri Yoluyla Elde Edilmesi ------------------------------------------------------------------------------------------ Çalışan bir sistemde ``proc`` ve ``sys`` dosya sistemi yoluyla dilim önbellekleri hakkındaki bilgiler aşağıdaki dizin girişlerinden edinilebilmektedir: .. code-block:: none /proc/slabinfo /proc/meminfo (slab alanları) /sys/kernel/slab/<önbellek-adı>/ ``/proc/slabinfo`` dosyası yaratılmış olan tüm dilim önbellekleri (yani ``kmem_cache`` nesneleri) hakkında bilgiler bulundurmaktadır. Normal proseslerin bu dosyaya *r* hakkı olmadığı için dosyayı ancak ``sudo`` ile görüntüleyebilirsiniz. Dosya aşağıdakine benzer bir içeriğe sahiptir: .. code-block:: none slabinfo - version: 2.1 # name : tunables # : slabdata isofs_inode_cache 56 72 664 24 4 : tunables 0 0 0 : slabdata 3 3 0 QIPCRTR 56 78 832 39 8 : tunables 0 0 0 : slabdata 2 2 0 AF_VSOCK 48 50 1280 25 8 : tunables 0 0 0 : slabdata 2 2 0 ext4_groupinfo_4k 960 962 152 26 1 : tunables 0 0 0 : slabdata 37 37 0 btrfs_delayed_node 0 0 312 26 2 : tunables 0 0 0 : slabdata 0 0 0 btrfs_ordered_extent 0 0 416 39 4 : tunables 0 0 0 : slabdata 0 0 0 bio-328 28 42 384 21 2 : tunables 0 0 0 : slabdata 2 2 0 bio-392 28 36 448 36 4 : tunables 0 0 0 : slabdata 1 1 0 btrfs_extent_buffer 0 0 240 34 2 : tunables 0 0 0 : slabdata 0 0 0 bio-408 28 36 448 36 4 : tunables 0 0 0 : slabdata 1 1 0 btrfs_inode 0 0 1008 32 8 : tunables 0 0 0 : slabdata 0 0 0 bio-424 28 36 448 36 4 : tunables 0 0 0 : slabdata 1 1 0 fsverity_info 0 0 272 30 2 : tunables 0 0 0 : slabdata 0 0 0 fscrypt_inode_info 0 0 128 32 1 : tunables 0 0 0 : slabdata 0 0 0 ... Buradaki sütunlarda dilim önbelleğinin ismi, oradaki toplam nesne sayısı, nesnelerin uzunlukları gibi önemli bilgiler bulunmaktadır. Tabii liste çok uzun olabileceği için ``grep`` yardımıyla ilgili satırı görüntüleyebilirsiniz. ``/proc/slabinfo`` dosyasındaki sütunların anlamları şöyledir: .. list-table:: :header-rows: 1 * - Alan - Kolon - Açıklama * - ``name`` - 1 - ``kmem_cache->name`` elemanı. Ör: ``"task_struct"``, ``"kmalloc-64"`` * - ``active_objs`` - 2 - Şu an kullanımda olan nesne sayısı * - ``num_objs`` - 3 - Toplam ayrılmış nesne kapasitesi (kullanımda + boşta) * - ``objsize`` - 4 - Her nesnenin kullanıcıya sunulan boyutu (bayt). Kırmızı bölge dahil edilmez. ``kmem_cache->object_size`` ile eşleşir. * - ``objperslab`` - 5 - Her dilimde kaç nesne bulunduğu. SLUB'da ``objs_per_slab`` sysfs alanıyla örtüşür. * - ``pagesperslab`` - 6 - Her dilimin kaç sayfadan oluştuğu (2^order adet sayfa) * - ``limit`` - 7 - SLUB'da her zaman 0. Eski SLAB'ın per-CPU cache limiti idi. * - ``batchcount`` - 8 - SLUB'da her zaman 0. Eski SLAB'ın per-CPU transfer batch sayısı. * - ``sharedfactor`` - 9 - SLUB'da her zaman 0. Eski SLAB'da shared cache çarpanı. * - ``active_slabs`` - 10 - En az bir nesnesi kullanımda olan dilimlerin sayısı. * - ``num_slabs`` - 11 - Toplam dilimlerin sayısı (boş + kısmi + dolu) * - ``sharedavail`` - 12 - SLUB'da her zaman 0. Eski SLAB'ın shared cache havuzundaki boş nesne sayısı idi. Örneğin belli bir dilim önbelleğine ilişkin bilgileri şöyle elde edebiliriz: .. code-block:: bash $ sudo cat /proc/slabinfo | grep "myobject_cache" myobject_cache 60 64 128 32 1 : tunables 0 0 0 : slabdata 2 2 0 Buradaki kendi yarattığımız dilim önbelleği için değerleri tablo halinde veriyoruz: .. list-table:: :header-rows: 1 * - Alan - Değer - Açıklama * - ``name`` - ``myobject_cache`` - ``kmem_cache->name``. Önbellek bağımsız oluşturulmuş; merge edilmemiş (aksi halde bu isimle görünmezdi). * - ``active_objs`` - 60 - Şu an ``kmem_cache_alloc`` ile tahsis edilmiş, henüz ``kmem_cache_free`` yapılmamış nesne sayısı. * - ``num_objs`` - 64 - Tüm dilimlerdeki toplam nesne kapasitesi. 2 dilim × 32 nesne/slab = 64. ``active_objs`` ≤ ``num_objs``. * - ``objsize`` - 128 - ``kmem_cache->object_size`` = 128 bayt. ``sizeof(struct myobject)`` = 72 bayt olmasına karşın SLUB bunu 128'e yuvarlıyor. * - ``objperslab`` - 32 - Her dilimde 32 nesne barınıyor. 1 page = 4096 bayt → 4096 / 128 = 32. * - ``pagesperslab`` - 1 - Her dilim 1 page'den oluşuyor (order = 0). Slab boyutu = ``PAGE_SIZE`` << 0 = 4096 bayt. * - ``limit`` - 0 - SLUB'da kullanılmaz. SLAB uyumluluğu için 0. * - ``batchcount`` - 0 - SLUB'da kullanılmaz. SLAB uyumluluğu için 0. * - ``sharedfactor`` - 0 - SLUB'da kullanılmaz. SLAB uyumluluğu için 0. * - ``active_slabs`` - 2 - En az bir nesnesi kullanımda olan slab sayısı. Her iki slab da aktif: 60 nesne 2 slab'a dağılmış. * - ``num_slabs`` - 2 - Toplam dilim sayısı. ``active_slabs`` = ``num_slabs`` → boş slab yok. * - ``sharedavail`` - 0 - SLUB'da kullanılmaz. SLAB uyumluluğu için 0. ``/proc/meminfo`` dosyası aslında bellek kullanımı hakkında genel bilgi veren bir dosyadır. Bu dosyanın içeriği aşağıdakine benzer biçimdedir: .. code-block:: bash $ cat /proc/meminfo MemTotal: 8081824 kB MemFree: 2802508 kB MemAvailable: 6072932 kB Buffers: 372700 kB Cached: 3021924 kB SwapCached: 0 kB Active: 3296008 kB Inactive: 1342872 kB Active(anon): 1292996 kB Inactive(anon): 0 kB Active(file): 2003012 kB Inactive(file): 1342872 kB Unevictable: 64 kB Mlocked: 64 kB SwapTotal: 3991548 kB SwapFree: 3991548 kB Zswap: 0 kB Zswapped: 0 kB Dirty: 132 kB Writeback: 0 kB AnonPages: 1244544 kB Mapped: 452140 kB Shmem: 48732 kB KReclaimable: 229804 kB Slab: 398272 kB SReclaimable: 229804 kB SUnreclaim: 168468 kB ... Buradaki ``Slab`` satırı dilimli tahsisat sistemlerinin toplamda fiziksel RAM'de ne kadar yer kapladığını belirtmektedir. ``SReclaimable`` çekirdek tarafından geri alınabilen dilim önbelleklerinin büyüklüğünü, ``SUnreclaim`` ise geri alınamayan dilim önbelleklerinin büyüklüğünü belirtmektedir. ``sys`` dosya sistemindeki ``/sys/kernel/slab//`` dizini belli bir dilim önbelleğine ilişkin bilgileri daha yapısal bir biçimde vermektedir. Bu dizinin içeriği şöyledir: .. code-block:: bash $ ls /sys/kernel/slab/myobject_cache/ aliases ctor object_size poison sheaf_capacity slab_size validate align destroy_by_rcu objects_partial reclaim_account shrink store_user cache_dma hwcache_align objs_per_slab red_zone skip_kfence total_objects cpu_partial min_partial order remote_node_defrag_ratio slabs trace cpu_slabs objects partial sanity_checks slabs_cpu_partial usersize Bu dosyaların içerdiği bilgileri aşağıdaki tabloda veriyoruz: .. list-table:: :header-rows: 1 * - Dosya - Açıklama * - ``object_size`` - Kullanıcıya sunulan nesne boyutu (bayt). ``kmem_cache->object_size`` ile birebir eşleşir. * - ``slab_size`` - Dilimli tahsisat sisteminin dahili olarak her nesne için ayırdığı gerçek alan (hizalama + red zone dahil). ``kmem_cache->size`` ile eşleşir. * - ``align`` - Nesne hizalaması (bayt cinsinden). ``kmem_cache->align`` alanından okunur. * - ``order`` - Bir dilimi oluşturan page bloğunun düzey değeri. Dilim boyutu = ``PAGE_SIZE`` << order. * - ``objs_per_slab`` - Her dilimde kaç nesne barınabileceği. * - ``objects`` - Şu an dilim önbelleğinde yaşayan toplam nesne sayısı (NUMA node'larına göre ``N0=x`` şeklinde de gösterilir). * - ``total_objects`` - Tüm dilimlerdeki toplam nesne kapasitesi. ``objects`` ≤ ``total_objects`` olmalıdır. * - ``objects_partial`` - Kısmi dilimlerde (partial list) bulunan nesne sayısı. * - ``slabs`` - Toplam dilim sayısı. NUMA bilgisi ``N0=x`` formatında eklenir. * - ``partial`` - Partial listesindeki dilim sayısı. ``kmem_cache_node->nr_partial`` ile eşleşir. * - ``cpu_slabs`` - Şu an per-CPU yapısı (``kmem_cache_cpu->page``) tarafından tutturulan aktif dilim sayısı (per-CPU kırılımıyla). * - ``aliases`` - Bu dilim önbelleğinin kaç isimle takma ad (alias) verildiği. ``kmem_cache_create_usercopy()`` ile birleştirme yapıldıysa > 0 olur. * - ``ctor`` - Dilim önbelleği bir constructor ile oluşturulduysa fonksiyon adresi burada görünür; yoksa boştur. * - ``usersize`` - Kullanıcıya "kopyalanabilir" olarak işaretlenen bayt sayısı. ``kmem_cache_create_usercopy()`` ile belirlenir; ``copy_to_user`` güvenlik kontrollerinde kullanılır. * - ``slabs_cpu_partial`` - Per-CPU partial listesindeki dilim ve nesne sayısı. Format: ``"dilim_sayısı(toplam_nesne_sayısı)"`` Fiziksel Bellekte Ardışıl Olmayan Tahsisatlar ============================================= Biz şimdiye kadar fiziksel bellekte ardışıl olan tahsisat üzerinde durduk. Çekirdeğin sayfa düzeyinde tahsisat yapan *ikiz blok tahsisat sistemi (buddy allocator)* fiziksel bellekte ardışıl sayfaları tahsis ediyordu. Dilimli tahsisat sistemi de arka planda ikiz blok tahsisat sistemini kullanıyordu. Ancak fiziksel bellekte ardışıl sayfa tahsisatları fiziksel belleğin bölünmesine (fragmente olmasına) yol açabilmektedir. İşte bazı durumlarda fiziksel bellekte ardışıl olmayabilen ancak sanal adres alanında (yani sayfa tablosunda) ardışıl olan tahsisatların da yapılması gerekebilmektedir. Linux çekirdeğinde fiziksel bellekte ardışıl olmayan ancak sanal adres alanında ardışıl olan tahsisatlar ``vmalloc`` ailesi fonksiyonlarla yapılmaktadır. Biz önce çekirdekteki ``vmalloc`` ailesi fonksiyonları ele alacağız, sonra da bu fonksiyonların tahsisatları nasıl yaptığı üzerinde duracağız. vmalloc Ailesi Fonksiyonlar --------------------------- Güncel çekirdeklerde ``vmalloc`` ailesi fonksiyonlar makrolar yoluyla oluşturulmuştur. Ancak biz burada fonksiyonun anlaşılabilir parametrik yapısı için eski çekirdeklerdeki gibi prototipleri kullanacağız. ``vmalloc`` fonksiyonunun prototipi şöyledir: .. code-block:: c void *vmalloc(unsigned long size); Görüldüğü gibi fonksiyon tıpkı C'nin ``malloc`` fonksiyonunda olduğu gibi byte cinsinden tahsis edilecek bellek miktarını parametre olarak almaktadır. Tahsis edilen alanın sanal bellek adresiyle geri dönmektedir. ``vmalloc`` ile tahsis edilen alan ``vfree`` ve ``vfree_atomic`` fonksiyonlarıyla serbest bırakılmaktadır: .. code-block:: c void vfree(const void *addr); void vfree_atomic(const void *addr); /* Linux 4.3'den itibaren */ ``vfree_atomic`` fonksiyonu kesme kodlarından da çağrılabilmektedir. ``vzalloc`` fonksiyonu tahsis edilen alanı aynı zamanda sıfırlamaktadır. Tabii bu sıfırlama işlemi aslında ikiz blok tahsisat sisteminde ``__GFP_ZERO`` bayrağı kullanılarak yapılmaktadır: .. code-block:: c void *vzalloc(unsigned long size); ``vmalloc_node`` fonksiyonu belli bir NUMA düğümünü hedef alarak tahsisatı yapmaktadır: .. code-block:: c void *vmalloc_node(unsigned long size, int node); Ancak bu tahsisatlara ilişkin sayfaların hepsi *fallback* nedeniyle istenilen NUMA düğümünden karşılanmak zorunda değildir. (Düğümler arasında *fallback* işleminin engellenmesi için ``alloc_pages`` gibi sayfa tahsisat fonksiyonlarında ``__GFP_THISNODE`` özel bayrağı kullanılabilmektedir.) ``vmalloc_user`` fonksiyonu kullanıcı alanına haritalanabilen (map edilebilen) tahsisatlar yapmaktadır. Bu konuyu ileride ele alacağız. Yukarıda da belirttiğimiz gibi ``vmalloc`` ailesi fonksiyonlar arka planda ikiz blok tahsisat sistemini kullanmaktadır. Aşağıdaki tabloda fonksiyonların ikiz blok tahsisat sisteminden sayfa tahsis ederken hangi bayrakları kullandığı belirtilmektedir: .. list-table:: ``vmalloc`` Ailesi Fonksiyonların Kullandığı GFP Bayrakları :header-rows: 1 :widths: 30 70 * - vmalloc Türevi - Kullanılan GFP Bayrakları * - ``vmalloc()`` - ``GFP_KERNEL | __GFP_HIGHMEM`` * - ``vzalloc()`` - ``GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO`` * - ``vmalloc_user()`` - ``GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO`` * - ``vmalloc_node()`` - ``GFP_KERNEL | __GFP_HIGHMEM`` (+ NUMA node kısıtı) ``vmalloc`` ailesi fonksiyonlar tahsisatları arka planda *ikiz blok tahsisat sistemini (buddy allocator)* kullanarak sayfa düzeyinde yapmaktadır. Dolayısıyla bu fonksiyonlar tarafından yapılan tahsisatlar her zaman sayfa katları büyüklüğünde olur. Örneğin biz ``vmalloc`` ile 1000 byte tahsis etmek isteyelim. Aslında fonksiyon arka planda 4096 byte'lık (1 sayfalık) bir tahsisat yapacaktır. Dolayısıyla ``vmalloc`` ailesi fonksiyonlarda sayfa katlarından küçük olan tahsisat yapmanın bir anlamı yoktur. Peki ne zaman dilim önbelleği yolu ile (örneğin ``kmalloc`` ile), ne zaman ``vmalloc`` ile ve ne zaman sayfa düzeyinde (örneğin ``alloc_pages`` ile) tahsisat yapmalıyız? ``vmalloc`` ailesi fonksiyonlar ile fiziksel bellekte çok büyük alanlar tahsis edilebilmektedir. Çünkü ``vmalloc`` ailesi sayfaların fiziksel bellekte ardışıl bulunmasını zorunlu tutmamaktadır. ``alloc_pages`` fonksiyonuyla en fazla 8 MB (order 11) kadar tahsisatın tek hamlede yapılabildiğini anımsayınız. Öte yandan dilimli tahsisat sistemi de temel olarak küçük miktardaki byte tahsisatları için düşünülmüştür. Ancak ``vmalloc`` ailesi fonksiyonlar toplamda daha fazla meta data kullanma eğilimindedir. İzleyen paragraflarda açıklayacağımız gibi her ``vmalloc`` çağrısında çekirdek tahsis edilen alanı yönetmek için ayrı bir nesne oluşturmaktadır. ``vmalloc`` ailesi çağrılar bir döngü içerisinde sürekli olarak 0'ıncı düzeyde (order 0) sayfa tahsisatlarıyla sağlanmaktadır. Bu işlem de göreli bir zaman kaybına yol açmaktadır. Aynı zamanda ``vmalloc`` ailesi fonksiyonlar sayfa tablosunda da güncelleme yapmaktadır. Bu da ek bir maliyete yol açmaktadır. Aşağıdaki tabloda üç tahsisat yöntemini birbirleriyle karşılaştırıyoruz: .. list-table:: :header-rows: 1 :widths: 28 24 24 24 * - Özellik - ``kmalloc`` - ``vmalloc`` - ``alloc_pages`` * - Fiziksel ardışıllık - Garantili - Garantisiz (dağınık olabilir) - Garantili (2^order ardışık sayfa) * - Sanal ardışıllık - Evet (physmap üzerinden) - Evet (vmalloc bölgesi) - Evet (physmap'te fiziksel ardışıklık sanal ardışıklığa da yansır) * - Döndürülen değer - ``void*`` (sanal adres) - ``void*`` (sanal adres) - ``struct page*`` (sayfa tanımlayıcısı) * - Sanal adres edinme - Doğrudan - Doğrudan - ``page_address``: physmap aritmetiği ``PAGE_OFFSET + pfn<