=================================================================== Bağlı Listelerin ve Hash Tablolarının Çekirdek Gerçekleştirimleri =================================================================== Bu bölümde Linux çekirdeğindeki bağlı listelerin ve hash tablolarının gerçekleştirimleri üzerinde duracağız. Bağlı Listeler ============== Çekirdeğin en önemli veri yapılarından biri bağlı listelerdir. Genel olarak çekirdekteki neredeyse tüm bağlı listeler *çift bağlı (doubly linked)* biçimde kullanılmaktadır. Önce bağlı listelerin Linux çekirdeğindeki gerçekleştirimleri üzerinde duracağız. Aralarında öncelik-sonralık ilişkisi olan veri yapılarına *liste (list)* tarzı veri yapıları denilmektedir. Örneğin bu tanıma göre diziler de "liste tarzı" veri yapılarıdır. Liste tarzı veri yapılarının en yaygın kullanılanlarından biri *bağlı liste (linked list)* denilen veri yapısıdır. Önceki elemanın sonraki elemanın yerini gösterdiği dolayısıyla elemanların ardışıl olma zorunluluğunun ortadan kaldırıldığı listelere "bağlı liste" denilmektedir. Dizi elemanlarının bellekte fiziksel olarak ardışıl biçimde bulunduğunu anımsayınız. Bağlı listeler adeta "elemanları bellekte ardışıl olmak zorunda olmayan diziler" gibidir. Bağlı listelerin her elemanına *düğüm (node)* denilmektedir. Bağlı listelerde her düğüm sonraki düğümün yerini tuttuğuna göre ilk elemanın yeri biliniyorsa liste elemanlarının hepsine erişilebilmektedir: .. graphviz:: digraph single_linked { rankdir=LR; graph [bgcolor="transparent", pad="0.2"]; node [shape=box, style="rounded,filled", fillcolor="#DDEEFF", fontname="monospace", fontsize=12, width=1.0, height=0.5]; edge [color="#4477AA", arrowsize=0.9, penwidth=1.5]; head [label="head", fillcolor="#AACCFF", fontname="monospace bold"]; n1 [label="node"]; n2 [label="node"]; n3 [label="node"]; n4 [label="node"]; nil [label="NULL", shape=plaintext, fontname="monospace", fontcolor="#888888"]; head -> n1 -> n2 -> n3 -> n4 -> nil; } Her düğümün yalnızca sonraki düğümün değil aynı zamanda önceki düğümün yerini de tuttuğu bağlı listelere *çift bağlı listeler (doubly linked lists)* denilmektedir. Çift bağlı listelerde belli bir düğümün adresini biliyorsak yalnızca ileriye doğru değil, geriye doğru da gidebiliriz: .. graphviz:: digraph double_linked { rankdir=LR; graph [bgcolor="transparent", pad="0.2"]; node [shape=box, style="rounded,filled", fillcolor="#DDEEFF", fontname="monospace", fontsize=12, width=1.0, height=0.5]; edge [color="#4477AA", arrowsize=0.9, penwidth=1.5]; head [label="head", fillcolor="#AACCFF", fontname="monospace bold"]; n1 [label="node"]; n2 [label="node"]; n3 [label="node"]; n4 [label="node"]; nil [label="NULL", shape=plaintext, fontname="monospace", fontcolor="#888888"]; head -> n1 [dir=both]; n1 -> n2 [dir=both]; n2 -> n3 [dir=both]; n3 -> n4 [dir=both]; n4 -> nil [dir=forward]; } Çift bağlı listelere ilişkin bir düğümün bellekte daha fazla yer kaplayacağına dikkat ediniz. Çift bağlı listelerin tek bağlı listelere göre en önemli özelliği "adresi bilinen bir düğümün" silinebilmesidir. Tek bağlı listelerde bu durum mümkün değildir. Uygulamalarda ve özellikle çekirdek kodlarında buna çok sık gereksinim duyulmaktadır. Eğer bir bağlı listede son eleman da ilk elemanı gösteriyorsa bu tür bağlı listelere *döngüsel bağlı listeler (circular linked lists)* denilmektedir. Bağlı Listelere Neden Gereksinim Duyulmaktadır? =============================================== Peki bağlı listelere neden gereksinim duyulmaktadır? Diziler varken bağlı listelere gerek var mıdır? Dizilerle bağlı listeler arasındaki farklılıkları, benzerlikleri ve bağlı listelere neden gereksinim duyulduğunu birkaç maddede açıklayabiliriz: 1. **Bellek parçalanması sorunu:** Diziler ardışıl alana gereksinim duymaktadır. Ancak belleğin bölündüğü (fragmente olduğu) durumlarda bellekte yeteri kadar küçük boş alanlar olduğu halde bunlar ardışıl olmadığı için dizi tahsisatı mümkün olamamaktadır. Bu tür durumlarda ardışıllık gereksinimi olmayan bağlı listeler kullanılabilir. Özellikle heap gibi bir alanda çok sayıda dinamik dizi bellek kullanımı bakımından verimsizliğe yol açabilmektedir. Bu dinamik diziler zamanla büyüdükçe birbirini engeller hale gelebilmektedir. İşte uzunluğu baştan belli olmayan çok sayıda dizinin oluşturulacağı durumlarda dinamik diziler yerine bağlı listeler bellek kullanımı bakımından toplamda daha iyi performans gösterebilmektedir. Dinamik dizilerde büyütme sırasında bloklar yer değiştirebileceği için bu işlem yavaştır. 2. **Araya ekleme ve silme verimliliği:** Dizilerde araya eleman ekleme (insert etme) ve aradaki bir elemanı silme dizinin kaydırılmasına yol açacağından yavaş bir işlemdir. Teknik olarak dizilerde eleman insert etme ve eleman silme O(N) karmaşıklıkta bir işlemdir. Halbuki bağlı listelerde eğer düğümün yeri biliniyorsa bu işlem O(1) karmaşıklıkta (yani döngü olmadan tekil işlemlerle) yapılabilmektedir. O halde araya eleman eklemenin ve aradan eleman silmenin çok yapıldığı sistemlerde diziler yerine bağlı listeler tercih edilebilmektedir. Çekirdek veri yapılarında araya eleman ekleme ve aradan eleman silme gibi işlemler çok yoğun yapılmaktadır. 3. **İndeksle erişim yavaşlığı:** Bağlı listelerde belli bir indeksteki elemana erişmek O(N) karmaşıklıkta bir işlemdir. Halbuki dizilerde elemana erişim O(1) karmaşıklıkta yani çok hızlıdır. O halde belli bir indeks değeri ile elemana erişimin yoğun yapıldığı durumlarda bağlı listeler yerine diziler tercih edilmelidir. Bağlı listeler toplamda bellekte daha fazla yer kaplama eğilimindedir. Çünkü bağlı listenin her düğümü sonraki (ve duruma göre önceki) elemanın yerini de tutmaktadır. O halde bağlı listeler tipik olarak şu durumlarda dizilere tercih edilmelidir: - Eleman insert etmenin ve eleman silmenin sık yapıldığı durumlarda. - Uzunluğu baştan belli olmayan çok sayıda dizinin kullanıldığı durumlarda. - İndeks yoluyla erişimin az yapıldığı durumlarda. - Toplam bellek miktarının yeteri kadar fazla olduğu sistemlerde. İşte tüm yukarıdaki nedenlerden dolayı bağlı listeler çekirdek için en önemli veri yapılarından biridir. Linux Çekirdeğindeki Bağlı Liste Gerçekleştirimi ================================================== Linux çekirdeğinde bağlı liste gerçekleştirimi ``include/linux/list.h`` dosyasında yapılmıştır. Bu dosya içerisindeki fonksiyonlar ``static inline`` biçiminde tanımlanmıştır. Çekirdek derlemesi sırasında derleyicinin optimizasyon seçeneği ayarlandığı için derleyici buradaki *inline* fonksiyonları sanki bir makro gibi koda açmaktadır. Linux çekirdeğindeki bağlı listelerde düğümler ``list_head`` isimli bir yapıyla temsil edilmektedir: .. code-block:: c struct list_head { struct list_head *next; struct list_head *prev; }; Aslında belli yapılar değil, bu ``list_head`` yapıları bağlı liste içerisinde birbirine bağlanmaktadır: .. graphviz:: digraph kernel_list { rankdir=LR; graph [bgcolor="transparent", pad="0.3"]; node [shape=record, style="rounded,filled", fontname="monospace", fontsize=11, height=0.6]; edge [color="#336699", arrowsize=0.85, penwidth=1.5]; root [label="{list_head\n(kök) | {

prev | next}}", fillcolor="#AACCFF"]; n1 [label="{list_head | {

prev | next}}", fillcolor="#DDEEFF"]; n2 [label="{list_head | {

prev | next}}", fillcolor="#DDEEFF"]; n3 [label="{list_head | {

prev | next}}", fillcolor="#DDEEFF"]; n4 [label="{list_head | {

prev | next}}", fillcolor="#DDEEFF"]; /* next bağlantıları: soldan sağa */ root:n -> n1:n [dir=forward, color="#336699"]; n1:n -> n2:n [dir=forward, color="#336699"]; n2:n -> n3:n [dir=forward, color="#336699"]; n3:n -> n4:n [dir=forward, color="#336699"]; n4:n -> root:n [dir=forward, color="#336699", constraint=false, style=dashed]; /* prev bağlantıları: sağdan sola */ n1:p -> root:p [dir=forward, color="#AA4444"]; n2:p -> n1:p [dir=forward, color="#AA4444"]; n3:p -> n2:p [dir=forward, color="#AA4444"]; n4:p -> n3:p [dir=forward, color="#AA4444"]; root:p -> n4:p [dir=forward, color="#AA4444", constraint=false, style=dashed]; } Tabii eğer bu ``list_head`` yapıları başka bir yapının (buna asıl yapı diyelim) içerisindeyse bu durumda aslında bir ``list_head`` yapısının adresi asıl yapının bir elemanının adresi haline gelmektedir. Biz C'de bir yapının bir elemanının adresini biliyorsak kolaylıkla o yapının başlangıç adresini elde edebiliriz. Çünkü yapı elemanları ardışıldır ve standart ``offsetof`` makrosuyla belli bir yapı elemanının yapının başlangıcından itibaren hangi offset'te bulunduğu bilgisi elde edilebilmektedir. ``offsetof`` makrosu ```` içerisinde aşağıdakine benzer biçimde tanımlanmıştır: .. code-block:: c #define offsetof(type, member) ((size_t)&(((type *)0)->member)) ``offsetof`` makrosunun birinci parametresi yapının tür ismini, ikinci parametresi ilgili yapı elemanının ismini almaktadır. Tabii ``offsetof`` makrosu yalnızca ``size_t`` türünden bir byte offset'i vermektedir. Elemanın adresinin makroyla elde edilen bu değerden çıkartılarak tür dönüştürmesinin yapılması gerekir. İşte bunun için Linux çekirdeğinde ``container_of`` isimli bir makro bulundurulmuştur. Bu makronun basit yazımı şöyle yapılabilir: .. code-block:: c #define container_of(ptr, type, member) ((type *)((char *)ptr - myoffsetof(type, member))) ``container_of`` makrosunun birinci parametresi yapı elemanının adresini, ikinci parametresi yapının tür ismini ve üçüncü parametresi de ilgili yapı elemanının ismini almaktadır. Bu makro ikinci parametresine girilmiş olan yapı türünden bir adres vermektedir. ``container_of`` makrosunu ``list_entry`` ismiyle de kullanabilirsiniz: .. code-block:: c #define list_entry(ptr, type, member) container_of(ptr, type, member) Bağlı Listenin Başlatılması --------------------------- Linux çekirdeğindeki bağlı listeler kullanılırken önce bir başlangıç düğümünün oluşturulması gerekir. Başlangıç düğümünde ``next`` ve ``prev`` göstericilerinin başlangıç düğümünün kendisini göstermesi gerekir. Örneğin: .. code-block:: c struct list_head head = {&head, &head}; Bu ilkdeğer verme C'de geçerlidir. Çünkü C'de bir değişken faaliyet alanına ``=`` atomu ile ilkdeğer verme kısmından önce sokulmaktadır. ``list.h`` dosyasında bu işlemi yapan aşağıdaki gibi bir makro da bulundurulmuştur: .. code-block:: c #define LIST_HEAD_INIT(name) {&(name), &(name)} Bu durumda başlangıç düğümü bu makroyla şöyle de oluşturulabilir: .. code-block:: c struct list_head head = LIST_HEAD_INIT(head); Aslında bu tanımlamanın tamamını yapan aşağıdaki gibi bir makro da bulundurulmuştur: .. code-block:: c #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) O halde biz başlangıç düğümünü basit bir biçimde şöyle oluşturabiliriz: .. code-block:: c LIST_HEAD(head); Linux'un bağlı liste gerçekleştiriminde ``next`` ve ``prev`` göstericilerinde ``NULL`` adres kullanılmamıştır. Son düğümün ``next`` göstericisi ``NULL`` yerine başlangıç düğümünü, ilk düğümün ``prev`` göstericisi de ``NULL`` yerine son düğümü göstermektedir. Yani aslında bağlı liste gerçekleştirimi *döngüsel (circular)* gibidir. Herhangi bir düğümden başlanarak başlangıç düğümü de atlanırsa tam dolaşım sağlanabilir. Bağlı listenin başlangıç düğümünün ``prev`` göstericisi de son düğümü göstermektedir. Düğüm Ekleme ------------- Bağlı listenin başına ``list_head`` düğümünü eklemek için ``list_add`` fonksiyonu kullanılmaktadır: .. code-block:: c static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } Fonksiyonun birinci parametresi eklenecek düğümün adresini, ikinci parametresi başlangıç düğümünün adresini almaktadır. Çekirdek kodlarında iki alt tire ile başlayan fonksiyonlar aşağı seviyeli fonksiyonlardır. Yani doğrudan çağrılmayan ancak başka fonksiyonların içerisinden dolaylı biçimde çağrılan fonksiyonlardır. Buradaki ``__list_add`` fonksiyonu şöyle yazılmıştır: .. code-block:: c static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { if (!__list_add_valid(new, prev, next)) return; next->prev = new; new->next = next; new->prev = prev; WRITE_ONCE(prev->next, new); } Buradaki ``WRITE_ONCE`` makrosu atama işlemini *volatile* erişimle yapmaktadır. ``WRITE_ONCE(a, b)`` çağrısını şimdilik ``a = b`` gibi düşünebilirsiniz. Bağlı listenin sonuna düğüm eklemek için ``list_add_tail`` fonksiyonu kullanılmaktadır: .. code-block:: c static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } Bağlı Listenin Dolaşılması ---------------------------- Peki biz bağlı listeyi nasıl dolaşabiliriz? Başlangıç düğümünü geçerek sürekli ileriye gidersek son düğüm de başlangıç düğümünü gösterdiğine göre dolaşımı aşağıdaki gibi bir for döngüsüyle yapabiliriz: .. code-block:: c struct list_head *lh; for (lh = head.next; lh != &head; lh = lh->next) { /* ... */ } Tabii böyle bir döngüde dolaşım yapılırken aslında biz her yinelemede ilgili yapının başlangıç adresini değil ``list_head`` yapısının başlangıç adresini elde ederiz. ``container_of`` makrosu ile bu adresin yapı adresine dönüştürülmesi gerekir. Örneğin biz ``struct SAMPLE`` türünden yapı nesnelerini birbirine bağlamış olalım: .. code-block:: c LIST_HEAD(head); struct SAMPLE { int a; struct list_head link; }; Eklemeleri şöyle yapmış olalım: .. code-block:: c struct SAMPLE *ps; for (int i = 0; i < 10; ++i) { if ((ps = (struct SAMPLE *)malloc(sizeof(struct SAMPLE))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ps->a = i; list_add_tail(&ps->link, &head); } .. note:: Çekirdek kodlarında *malloc* gibi kullanıcı modu fonksiyonları kullanılamaz. Ancak burada yalnızca anlaşılır bir test örneği verilmektedir. ``struct SAMPLE`` yapımızdaki ``link`` elemanı ``list_head`` yapılarını birbirine bağlamak için bulundurulmuştur. Örneğimizdeki ``list_add_tail`` fonksiyonunda ekleme yapılacak düğümün ``SAMPLE`` yapısının içerisindeki ``link`` elemanının adresi olduğuna dikkat ediniz. Şimdi dolaşımı şöyle yapabiliriz: .. code-block:: c struct SAMPLE *ps; struct list_head *lh; for (lh = head.next; lh != &head; lh = lh->next) { ps = container_of(lh, struct SAMPLE, link); /* ... */ } Burada artık ``ps`` göstericisi ``list_head`` nesnesini değil ``struct SAMPLE`` nesnesini göstermektedir. Aslında ``list.h`` dosyası içerisinde buradaki döngüyü oluşturan ``list_for_each`` isimli bir makro da bulundurulmuştur: .. code-block:: c static inline int list_is_head(const struct list_head *list, const struct list_head *head) { return list == head; } #define list_for_each(pos, head) \ for (pos = (head)->next; !list_is_head(pos, (head)); pos = pos->next) Makronun birinci parametresi ``list_head`` türünden bir göstericiyi, ikinci parametresi başlangıç düğümünün adresini almaktadır. Döngünün her yinelenmesinde bu göstericiye sonraki düğümün ``list_head`` adresi atanmaktadır. Örneğin: .. code-block:: c struct list_head *lh; list_for_each(lh, &head) { ps = container_of(lh, struct SAMPLE, link); printf("%d\n", ps->a); } Aslında çekirdekteki ``list.h`` dosyası içerisinde yukarıdaki dolaşımı tek hamlede yapan ``list_for_each_entry`` isimli bir makro da bulunmaktadır: .. code-block:: c #define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ !list_entry_is_head(pos, head, member); \ pos = list_next_entry(pos, member)) Makronun birinci parametresi asıl yapı türünden bir göstericidir; makro her yinelemede bu göstericiye sonraki düğümün adresini yerleştirmektedir. Makronun ikinci parametresi başlangıç düğümünün adresini, üçüncü parametresi ise yapı içerisindeki ``list_head`` elemanın ismini belirtmektedir. Bu makronun eşdeğeri bazı ayrıntılar ihmal edilerek şöyle de yazılabilir: .. code-block:: c #define my_list_for_each_entry(pos, head, member) \ for (pos = container_of((head)->next, typeof(*pos), member); &(pos)->member != head; \ pos = container_of((pos)->member.next, typeof(*pos), member)) .. note:: C standartlarında bir ifadenin türünü veren bir tür belirleyicisi yoktu. C++'a C++11 ile birlikte ``decltype`` ismi ile böyle bir belirleyici eklendi. gcc derleyicileri uzun süredir bu işi yapan ``typeof`` isimli belirleyiciyi desteklemektedir. Nihayet C'ye de C23 ile birlikte resmi olarak ``typeof`` belirleyicisi eklenmiştir. Makroda türün ``typeof(*pos)`` ifadesiyle elde edildiğine dikkat ediniz. Bu makro ile dolaşım şöyle sağlanabilir: .. code-block:: c struct SAMPLE *ps; list_for_each_entry(ps, &head, link) { /* ... */ } .. tip:: Bağlı liste makrolarının ve fonksiyonlarının isimlerinin sonunda *entry* sözcüğü varsa bu makro ya da fonksiyon asıl yapıya ilişkin adres, yoksa ``list_head`` türünden adres verip almaktadır. Ayrıca ``list.h`` içerisinde ``list_for_each_entry_safe`` isimli bir döngü makrosu da bulundurulmuştur. Bu makro eğer liste boşsa hiç dolaşım yapmamaktadır. Makro aşağıdaki gibi yazılmıştır: .. code-block:: c #define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member), \ n = list_next_entry(pos, member); \ !list_entry_is_head(pos, head, member); \ pos = n, n = list_next_entry(n, member)) Makronun birinci elemanı dolaşımda kullanılacak asıl yapı türünden göstericiyi almaktadır. Makronun ikinci parametresi de asıl yapı göstericidir. Üçüncü ve dördüncü parametreler sırasıyla kök düğümün adresi ve bağ düğümünün ismini almaktadır. Bağlı Listeden Düğüm Silme -------------------------- Bağlı listeden bir düğümü silmek için ``list_del`` fonksiyonu kullanılmaktadır. Fonksiyon şöyle tanımlanmıştır: .. code-block:: c static inline void __list_del(struct list_head *prev, struct list_head *next) { next->prev = prev; WRITE_ONCE(prev->next, next); } static inline void __list_del_entry(struct list_head *entry) { if (!__list_del_entry_valid(entry)) return; __list_del(entry->prev, entry->next); } static inline void list_del(struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; entry->prev = LIST_POISON2; } Fonksiyonun parametresi silinecek düğüme ilişkin ``list_head`` nesnesinin adresini almaktadır. Burada silinen düğümdeki ``next`` ve ``prev`` göstericilerine özel bazı değerlerin atandığını görüyorsunuz. Bu değerler silinmiş düğümün kullanılması durumunda *page fault* oluşmasına yol açmaktadır; dolayısıyla bir çeşit debug mekanizması oluşturmak amacıyla atandıklarını söyleyebiliriz. Bağli Listede Araya Düğüm Ekleme -------------------------------- Bağlı listenin arasına düğüm ekleyen ayrı bir insert fonksiyonu yoktur. Zaten ``list_add`` fonksiyonu araya ekleme işlemini de yapmaktadır. Örneğin biz araya şöyle ekleme yapabiliriz: .. code-block:: c list_add(&ps->link, &ps_insert->link); Burada ``ps`` eklenecek düğümün asıl yapı adresini, ``ps_insert`` ise önüne eklemenin yapılacağı asıl yapı adresini belirtmektedir. Bağli Listelerin Yok Edilmesi ----------------------------- Bir bağlı listeyi tümden serbest bırakmak için düğümlerin tek tek serbest bırakılması gerekmektedir. Buradaki düğümler aslında başka yapıların içerisinde olduğuna göre liste içerisindeki o yapı nesnelerinin serbest bırakılması gerekir. Bağlı Listelere RCU Desteği --------------------------- Belli bir süreden sonra ``list.h`` dosyasına RCU (Read-Copy-Update) mekanizmasını destekleyecek biçimde dolaşım yapan ``list_for_each_rcu`` isimli makro da eklenmiştir. Bu makro bir yazıcı varsa okuyucuları bekletmeden işlem yapabilmeyi sağlamaktadır. *Read-Copy-Update* mekanizması ileride ele alınacaktır. Çekirdek Bağlı Listelerinin Örnek Gerçekleştirimi ================================================== Aşağıda kullanıcı alanında çekirdekteki bağlı liste kullanımına bir örnek verilmiştir. .. code-block:: c #include #include #include struct list_head { struct list_head *next, *prev; }; #define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) #define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ ((type *)(__mptr - offsetof(type, member))); }) #define list_entry(ptr, type, member) \ container_of(ptr, type, member) #define list_entry_is_head(pos, head, member) \ (&pos->member == (head)) #define list_first_entry(ptr, type, member) \ list_entry((ptr)->next, type, member) #define list_next_entry(pos, member) \ list_entry((pos)->member.next, typeof(*(pos)), member) #define list_for_each(pos, head) \ for (pos = (head)->next; !list_is_head(pos, (head)); pos = pos->next) #define list_for_each_entry(pos, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member); \ !list_entry_is_head(pos, head, member); \ pos = list_next_entry(pos, member)) #define list_for_each_entry_safe(pos, n, head, member) \ for (pos = list_first_entry(head, typeof(*pos), member), \ n = list_next_entry(pos, member); \ !list_entry_is_head(pos, head, member); \ pos = n, n = list_next_entry(n, member)) #define my_list_for_each_entry(pos, head, member) \ for (pos = container_of((head)->next, typeof(*pos), member); &(pos)->member != head; \ pos = container_of((pos)->member.next, typeof(*pos), member)) static inline int list_is_head(const struct list_head *list, const struct list_head *head) { return list == head; } static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { next->prev = new; new->next = next; new->prev = prev; prev->next = new; } static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } static inline void __list_del(struct list_head *prev, struct list_head *next) { next->prev = prev; prev->next = next; } static inline void __list_del_entry(struct list_head *entry) { __list_del(entry->prev, entry->next); } static inline void list_del(struct list_head *entry) { __list_del_entry(entry); } LIST_HEAD(head); struct SAMPLE { int a; struct list_head link; }; int main(void) { struct SAMPLE *ps, *ps_node, *ps_temp, *ps_del, *ps_insert; struct list_head *lh; for (int i = 0; i < 10; ++i) { if ((ps = (struct SAMPLE *)malloc(sizeof(struct SAMPLE))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ps->a = i; list_add_tail(&ps->link, &head); if (i == 5) ps_del = ps; if (i == 7) ps_insert = ps; } list_for_each(lh, &head) { ps = container_of(lh, struct SAMPLE, link); printf("%d ", ps->a); } printf("\n"); list_del(&ps_del->link); list_for_each(lh, &head) { ps = container_of(lh, struct SAMPLE, link); printf("%d ", ps->a); } printf("\n"); if ((ps = (struct SAMPLE *)malloc(sizeof(struct SAMPLE))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } ps->a = 100; list_add(&ps->link, &ps_insert->link); list_for_each(lh, &head) { ps = container_of(lh, struct SAMPLE, link); printf("%d ", ps->a); } printf("\n"); ps_temp = NULL; list_for_each_entry_safe(ps, ps_node, &head, link) { free(ps_temp); ps_temp = ps; } return 0; } Eski Çekirdek Sürümlerindeki list.h Dosyası =========================================== Çekirdeğin eski sürümlerinde ``list.h`` dosyası oldukça sade idi. Sonra bu dosyanın içeriğinde de bazı faydalı değişiklikler yapıldı. Ancak bu değişiklikler veri yapısının anlaşılmasını da biraz zorlaştırmıştır. Örneğin 2.2.26 çekirdeğindeki ``list.h`` dosyası oldukça sade bir biçimde aşağıdaki gibi oluşturulmuştur: .. code-block:: c /* 2.2.26 dosyasının içeriği */ #ifndef _LINUX_LIST_H #define _LINUX_LIST_H #ifdef __KERNEL__ /* * Simple doubly linked list implementation. * * Some of the internal functions ("__xxx") are useful when * manipulating whole lists rather than single entries, as * sometimes we already know the next/prev entries and we can * generate better code by using them directly rather than * using the generic single-entry routines. */ struct list_head { struct list_head *next, *prev; }; #define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = { &name, &name } #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) static __inline__ void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { next->prev = new; new->next = next; new->prev = prev; prev->next = new; } static __inline__ void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } static __inline__ void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); } static __inline__ void __list_del(struct list_head *prev, struct list_head *next) { next->prev = prev; prev->next = next; } static __inline__ void list_del(struct list_head *entry) { __list_del(entry->prev, entry->next); } static __inline__ int list_empty(struct list_head *head) { return head->next == head; } static __inline__ void list_splice(struct list_head *list, struct list_head *head) { struct list_head *first = list->next; if (first != list) { struct list_head *last = list->prev; struct list_head *at = head->next; first->prev = head; head->next = first; last->next = at; at->prev = last; } } #define list_entry(ptr, type, member) \ ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); pos = pos->next) #endif /* __KERNEL__ */ #endif Arama Algoritmaları ve Sözlük Tarzı Veri Yapıları =================================================== Arama işlemleri bir anahtar (key) eşliğinde yapılmaktadır. Anahtar bulunması istenen varlığı temsil etmektedir. Örneğin pek çok kişinin bilgileri bir veri yapısında tutuluyor olabilir. Biz de ismini bildiğimiz bir kişiyi bu veri yapısında arayabiliriz. Burada anahtar isimdir. Veri yapıları dünyasında anahtar-değer çiftlerini tutan ve anahtar verildiğinde ona karşı gelen değerin elde edilmesini sağlayan veri yapılarına genel olarak *sözlük (dictionary)* tarzı veri yapıları denilmektedir. Elemanların sıralı olmadığı liste tarzı veri yapılarında elemanların tek tek gözden geçirilmesi yoluyla yapılan aramalara *sıralı arama (sequential search)* denilmektedir. Sıralı arama oldukça yavaş bir yöntemdir. Sıralı aramada listedeki eleman sayısı N olmak üzere anahtarın bulunması için ortalama N/2 kez karşılaştırmanın yapılması gerekmektedir. Bu tür algoritmaların karmaşıklığına *Big O* notasyonunda *O(N) karmaşıklık* denildiğini anımsayınız. Ancak ne olursa olsun eleman sayısının makul olduğu bir durumda sıralı arama en iyi yöntem haline de gelebilmektedir. Örneğin en fazla 20 civarında elemanın bulunduğu bir durumda bu elemanları diziye yerleştirip sıralı bir biçimde aramak en etkin yöntem haline gelebilmektedir. Eğer elemanlar sıralıysa ve herhangi bir elemana erişim çok hızlı (buna *rastgele erişim* de denilmektedir) yapılabiliyorsa *ikili arama (binary search)* en iyi yöntemdir. İkili aramanın algoritmik karmaşıklığı O(log N) biçimindedir. Aslında ikili aramanın daha genel bir biçimine *enterpolasyon araması (interpolation search)* denilmektedir. Enterpolasyon aramasında bölme ortadan değil daha uygun yerlerden yapılmaktadır. Ancak enterpolasyon araması dizi dağılımının bilindiği ve özellikle de dizinin düzgün dağıldığı durumlarda faydalı bir etki oluşturmaktadır. Diziyi önce sıraya dizip sonra ikili arama uygulamak ise genellikle iyi bir fikir değildir. Çünkü sırayı korumak için araya eleman ekleme ve aradan eleman silme gibi işlemlerde O(N) karmaşıklıkta kaydırmaların yapılması gerekmektedir. İndeksli Arama -------------- Peki ideal bir anahtar-değer araması nasıl olabilir? Şüphesiz ideal durumda aramanın O(1) karmaşıklıkta yapılması istenir. Anahtarın bir ``int`` değer olduğunu ve kişinin numarasını belirttiğini düşünelim. Biz de numarasını bildiğimiz kişinin bilgilerini elde etmek isteyelim. Örneğimizdeki kişilerin bilgileri ``PERSON`` isimli bir yapıyla temsil edilmiş olsun: .. code-block:: c struct PERSON { ... }; ``struct PERSON`` türünden büyük bir dizi açabiliriz: .. code-block:: c struct PERSON people[MAX_SIZE]; Sonra da kişilerin numaralarını indeks yaparak bu diziye yerleştirebiliriz. Örneğin numarası 123 olan kişinin bilgilerini diziye şöyle yerleştirebiliriz: .. code-block:: c people[123] = person_info; Artık numarası 123 olan bu kişinin bilgilerini O(1) karmaşıklıkta aşağıdaki gibi elde edebiliriz: .. code-block:: c person_info = people[123]; Bu yöntem ilk bakışta çok iyi bir yöntem gibi gözükse de genellikle kullanılabilir bir yöntem değildir. Çünkü burada anahtar ``int`` türdendir. Ancak uygulamalarda anahtarlar farklı türlerden olabilmektedir. Örneğin anahtar kişinin adı soyadı olabilir. Ad ve soyad gibi yazısal bilgiler indeks belirtmemektedir. Bu yöntemin diğer bir sakıncası da anahtarların yüksek basamaklı sayılardan oluşabildiği durumlarda dizilerin çok fazla yer kaplamasıdır. Örneğin TC kimlik numarasının anahtar yapılarak kişilerin bilgilerinin elde edilmesinin istendiği bir durumu düşünelim. TC kimlik numarası 11 basamaklı bir sayıdır. Yani skalası 100 milyarlık sınırdadır. 100 milyarlık bir yapı dizisini bu amaçla oluşturmak mümkün olmayabilir, mümkün olsa da etkin olmayabilir. Bu yönteme *indeksli arama (index search)* denilmektedir. Ancak çok özel durumlarda bu yöntem uygun bir yöntem olarak kullanılabilmektedir. Hash Tabloları =============== Algoritmik aramalarda en çok kullanılan yöntemlerden biri *hash tabloları (hash tables)* denilen yöntemdir. Hash tabloları aslında yukarıda belirttiğimiz indeksli arama ile sıralı aramanın hibrit bir biçimi gibidir. Yöntemde ismine *hash tablosu (hash table)* denilen makul uzunlukta bir dizi oluşturulur. Sonra anahtarlar ismine *hash fonksiyonu (hash function)* denilen bir fonksiyona sokularak dizi indeksine dönüştürülür. Sonra da dizinin o indeksteki elemanına başvurulur. Örneğin kişilerin bilgilerini TC kimlik numaralarına göre saklayıp geri almak isteyelim. Hash tablomuzun uzunluğu da 1000 olsun. Hash fonksiyonumuzun da "1000'e bölümden elde edilen kalan" değerini veren fonksiyon olduğunu varsayalım. Bu durumda örneğin 2566198712 TC kimlik numarasına sahip kişinin bilgileri hash tablosunun 712'nci indeksteki elemanında saklanacaktır. 72484926820 TC kimlik numarasına sahip kişinin bilgileri de dizinin 820'nci indeksteki elemanında saklanacaktır. Ancak farklı kişilerin TC kimlik numaraları hash fonksiyonuna sokulduğunda aynı indeks değerleri de elde edilebilecektir. Örneğin 6238517712 TC kimli numarasına sahip kişi de dizinin 712'nci indeksteki elemanına yerleşmek isteyecektir. İşte hash tablosu yönteminde bu duruma *çakışma (collision)* denilmektedir. Hash tablosu yöntemi çakışma durumunda izlenecek stratejiye göre çeşitli alt kollara ayrılmaktadır. Çakışma Çözümleme Stratejileri -------------------------------- Hash tabloları yönteminde çakışma durumunda bu sorunu çözmek için temel olarak iki alt yöntem grubu kullanılmaktadır: *ayrı zincir oluşturma (separate chaining)* yöntemi ve *açık adresleme (open addressing)* yöntemi. Açık adresleme yöntemi de kendi aralarında *doğrusal yoklama (linear probing)*, *karesel yoklama (quadratic probing)*, *çift hash'leme (double hashing)* gibi alt yöntemlere ayrılmaktadır. Ayrı zincir oluşturma ve açık adresleme yöntemlerinin dışında başka çakışma çözümleme stratejileri de vardır. Ancak ağırlıklı olarak bu stratejiler tercih edilmektedir. .. note:: Linux çekirdeklerindeki tüm hash tablolarında *ayrı zincir oluşturma (separate chaining)* alt yöntemi tercih edilmiştir. Ayrı Zincir Oluşturma (Separate Chaining) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ayrı zincir oluşturma (separate chaining) yönteminde hash tablosu aslında bir bağlı liste dizisi gibi oluşturulur. Yani hash tablosunun her elemanı bağlı listenin ilk elemanını (head pointer) gösteren bir gösterici durumundadır. Anahtar hash fonksiyonuna sokulur, bir indeks elde edilir ve o indeksteki bağlı listenin hemen önüne (ya da duruma göre arkasına) eklenir. Eleman aranırken anahtar yine aynı hash fonksiyonuna sokulur ve dizinin ilgili indeksindeki bağlı listede sıralı arama yapılır. Hash tablolarına eleman insert etmek O(1) karmaşıklıktadır. Tabii burada kullanılacak hash fonksiyonu da önemlidir. Küçük döngüler içeren hash fonksiyonları O(1) karmaşıklığı yükseltmemektedir. Elemanın silinmesi de O(1) karmaşıklıkta yapılabilmektedir. Eleman aramanın O(1) karmaşıklıkta yapılabilmesi için bağlı listelerdeki zincir uzunluklarının uzun olmaması gerekir. Örneğin yukarıda 10 kadar eleman için en hızlı arama yönteminin sıralı arama olduğunu söylemiştik. Bu koşul sağlandığında sıralı aramanın O(1) karmaşıklıkta olduğu söylenebilir. O halde eğer zincirlerdeki ortalama eleman makul bir düzeyde tutulursa arama işlemi de O(1) karmaşıklıkta yapılabilecektir. Ancak hash tablosu küçük fakat tabloya eklenecek eleman fazla ise bu durumda hash tablosu yöntemi artık sıralı arama yöntemine benzer hale gelir. Yani bu yöntemin *en kötü durumdaki (worst case)* karmaşıklığının O(N) olduğu söylenebilir. Tabii hash tablosu yöntemini kullanan kişiler sistem hakkında bazı ön bilgilere sahip olursa tabloyu uygun bir büyüklükte oluşturabilirler. İşletim sistemi gibi yüksek performans isteyen sistemlerde hash tablolarının zincirlerinin ortalama 1 civarında tutulması uygun olabilmektedir. Hash tabloları da duruma göre büyütülebilmektedir. Ancak büyütmenin önemli bir zaman maliyeti vardır. Yeni bir hash tablosunun tahsis edilmesi; eski tablodaki elemanların yeniden hash'lenerek yeni tabloya yerleştirilmesi uzun zaman alan bir işlemdir. İşletim sistemlerinin çekirdeklerinde bu biçimde uzun zaman alacak işlemler tercih edilmez. Bu nedenle Linux çekirdeğindeki hash tabloları büyütülmemektedir. Hash tablolarında tablo elemanlarına (zincirlere değil) İngilizce *bucket (kova)*, tablodaki toplam eleman sayısının bucket sayısına bölümüne de *yükleme faktörü (load factor)* denilmektedir. İdeal yükleme faktörünün <= 1 olduğunu söyleyebiliriz. Sözlük tarzı veri yapılarında genel olarak aynı anahtara ilişkin birden fazla anahtar-değer çifti veri yapısına yerleştirilememektedir. (Bazı kütüphanelerde buna izin verilebilmektedir.) Eğer aynı anahtara ilişkin yeni bir değer insert edilmeye çalışılırsa eski değer yeni değerle yer değiştirilmektedir. Bazı tasarımlar ise aynı anahtara ilişkin insert yapmayı engellemektedir. Yani bu tasarımlarda yalnızca olmayan elemanlar tabloya insert edilebilmektedir. Linux çekirdeğindeki hash tablolarında anahtarlar birbirinden farklı olmak zorundadır. Açık Adresleme ve Doğrusal Yoklama ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Her ne kadar Linux çekirdeğindeki hash tablolarında *ayrı zincir oluşturma (separate chaining)* stratejisi kullanılıyorsa da burada *açık adresleme (open addressing)* stratejisi hakkında da küçük bir açıklama yapmak istiyoruz. Açık adresleme yöntemi de kendi arasında *yoklama (probing)* biçimine göre çeşitli alt yöntemlere ayrılmaktadır. Açık adreslemenin en yaygın ve basit biçimi *doğrusal yoklama (linear probing)* denilen biçimidir. Doğrusal yoklama oldukça basit bir fikre dayanmaktadır. Bu yöntemde yine bir hash tablosu oluşturulur. Ancak hash tablosunda bağlı listelerin adresleri tutulmaz; bizzat değerlerin kendisi tutulur. Tabloya eleman ekleneceği zaman yine anahtardan bir hash değeri elde edilir. Doğrudan değer tablonun hash ile elde edilen indeksine yerleştirilir. Başka bir anahtar aynı hash değerini verdiğinde (yani çakışma durumu oluştuğunda) o indeksten itibaren boş yer bulunana kadar yan yana indekslere sırasıyla bakılır. Örneğin hash olarak 123 değerini elde etmiş olalım. Tablonun 123'üncü elemanının dolu olduğunu düşünelim. Bu durumda 124'üncü elemanına bakarız. O da doluysa 125'inci elemanına bakarız. Ta ki boş bir indeks bulunana kadar. Değeri ilk boş indekse yerleştiririz. Bu yöntemde arama işlemi de benzer biçimde yapılmaktadır. Yani aranacak elemanın hash değeri elde edilir. O indekse başvurulur. Anahtar o indekste değilse anahtar bulunana kadar ya da boş bir kova (bucket) görülene kadar yan yana diğer indekslere bakılır. Anahtara dayalı eleman silme de benzer biçimde yapılmaktadır. Ancak eleman silindiğinde ilgili kovanın (bucket) boşaltılması arama işlemlerinde sorunlara yol açabilecektir. Burada yöntemlerden biri silinen elemana ilişkin kovanın boş yapılmayıp silinmenin özel bir değerle belirtilmesidir. Örneğin her kova için bir durum bayrağı tutulabilir. Bu durum bayrağı ilgili kovanın *dolu* olduğunu, *boş* olduğunu ya da *silinmiş* olduğunu belirtebilir. Böylece arama sırasında *silinmiş* kovalar görüldüğünde durulmaz. İlk boş kova görüldüğünde durulur. Tabii silinmiş kovalara yeni elemanlar eklenebilir. Hash Fonksiyonlarının Tasarımı ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Peki hash tablolarında kullanılacak iyi bir hash fonksiyonu nasıl olmalıdır? İyi bir hash fonksiyonunun *hızlı* olması gerekir. Çünkü insert gibi arama gibi işlemlerde hash fonksiyonu kullanılacaktır. İyi bir hash fonksiyonunun *anahtarlar yanlı bile olsa* tabloya onları iyi bir biçimde yaydırabilmesi gerekir. Örneğin aslında sayısal anahtarlar için "bölümden elde edilen kalan" iyi bir hash fonksiyonu değildir. Hash tablolarında tablonun asal sayı uzunluğunda olması hash fonksiyonlarının daha iyi yaydırmasına yardımcı olmaktadır. (Örneğin tablo uzunluğu için 100 yerine 101 değeri tercih edilebilmektedir.) Hash fonksiyonları *sayıyı indekse dönüştüren* ve *yazıyı indekse dönüştüren* fonksiyonlar biçiminde oluşturulabilir. Hash tablolarının 2'nin kuvveti uzunluğunda alınması hash fonksiyonlarının daha hızlı çalışmasına da yol açabilmektedir. (Örneğin bölümden elde edilen kalan yerine bit düzeyinde öteleme işlemleri hız kazancı sağlayabilmektedir.) Linux çekirdeklerinde hash tabloları için genellikle sayfa katlarında (yani 4096'nın katlarında) alanlar tahsis edilmektedir. Linux Çekirdeğinde Hash Tablosu Gerçekleştirimi ================================================= Şimdi de Linux çekirdeğindeki hash tablosu gerçekleştirimi üzerinde duralım. Linux kaynak kodlarında hash tablosuna ilişkin yapılar ve fonksiyonlar bağlı listelere ilişkin yapıların ve fonksiyonların bulunduğu başlık dosyasında tanımlanmıştır. Yani hash tabloları için ayrı bir başlık dosyası oluşturulmamıştır. RCU'suz hash tablolarının gerçekleştirimi ``include/linux/list.h`` dosyası içerisinde, RCU'lu hash tablolarının gerçekleştirimi ise ``include/linux/rculist.h`` dosyası içerisinde bulunmaktadır. Ancak her iki gerçekleştirim de aynı yapıları kullanmaktadır. hlist_head ve hlist_node Yapıları ----------------------------------- Ayrı zincir oluşturma yönteminde hash tablosundaki kovaların (buckets) aslında bağlı listelerin ilk düğümünün yerini tutan göstericilerden oluştuğunu belirtmiştik. Linux çekirdeklerinde bağlı listenin kök düğümü de eleman düğümleri de ``list_head`` yapısıyla temsil ediliyordu. Ancak ayrı zincir oluşturmalı hash tablolarındaki kovalarda (buckets) bağlı listenin son düğümünün yerinin tutulmasına gerek yoktur. Elemanlar hemen listenin başına eklenebilir. Arama da listenin başından itibaren yapılabilir. Tabii performans bakımından bağlı liste düğümlerinin yine çift bağlı (doubly linked) olması gerekir. Çünkü adresini bildiğimiz bir düğümü hash tablosundan kolaylıkla çıkartabiliriz. O halde çekirdek tablolarında *kök düğümde tek gösterici, eleman düğümlerinde ise çift gösterici* bulunmalıdır. Çekirdekte kullanılan hash tablosu gerçekleştiriminde kök düğüm ``hlist_head`` yapısıyla temsil edilmiştir: .. code-block:: c struct hlist_head { struct hlist_node *first; }; Görüldüğü gibi yapıda tek bir gösterici vardır. O da zincirdeki ilk elemanı göstermektedir. Zincirdeki bağlı listenin düğümleri de ``hlist_node`` yapısıyla temsil edilmiştir: .. code-block:: c struct hlist_node { struct hlist_node *next, **pprev; }; Buradaki düğüm yapısını listelerdeki düğüm yapısı ile karşılaştırınız: .. code-block:: c struct list_head { struct list_head *next, *prev; }; Hash tablosundaki düğümlerin ``pprev`` elemanı önceki düğümün adresini değil önceki düğümdeki ``next`` elemanının adresini göstermektedir. Bu nedenle ``pprev`` göstericiyi gösteren göstericidir. Hash tablolarındaki düğümlerde geri gitmek için bir neden yoktur. Ancak eleman silme durumunda bu tasarım önceki elemanın ``next`` göstericisinin daha kolay güncellenmesine yol açmaktadır. Örneğin ``p`` göstericisi bir ``hlist_node`` düğümünü gösteriyor olsun. Biz de bu düğümü silecek olalım. Burada önceki düğümün ``next`` elemanının sileceğimiz düğümün ``next`` elemanındaki düğümü göstermesi sağlanmalıdır. Bu işlem pratik olarak şöyle yapılabilmektedir: .. code-block:: c *p->pprev = p->next; Halbuki düğümler ``list_head`` yapısıyla temsil ediliyor olsaydı bu işlem ancak şöyle yapılabilirdi: .. code-block:: c p->prev->next = p->next; Görüldüğü gibi bu güncellemede fazladan bir işlem yapılmaktadır. ``hlist_node`` tasarımının diğer önemli bir faydası da bu tasarımda kök düğüm için özel bir işlemin yapılmasına gerek kalmamasıdır. Yani listeye ilk kez eleman eklerken ``*p->pprev`` kök düğümün ``next`` göstericisi haline gelecektir. Heterojen yapılarda bu tür güncellemelerin yapılması daha fazla çabayı gerektirmektedir. Peki neden bütün çift bağlı listelerde bu teknik kullanılmıyor? ``hlist_node`` yapısında olduğu gibi ``prev`` göstericisi önceki düğümün başlangıç adresini göstermek yerine neden önceki düğümün ``next`` göstericisini göstermiyor? İşte geriye doğru ilerlemenin gerekli olabildiği durumlarda bu tasarımda geriye gidiş zorlaşmaktadır. Ancak yukarıda da belirttiğimiz gibi hash tablolarındaki zincirlerde zaten geriye gitmenin bir anlamı yoktur. Hash Tablosunun Oluşturulması ------------------------------- Peki biz yukarıdaki ``hlist_head`` ve ``hlist_node`` yapılarını kullanarak hash tablosunu nasıl oluşturabiliriz? İşte yapılacak ilk şey ``hlist_head`` yapısı türünden bir dizi tahsis etmektir. Örneğin biz bu işlemi kullanıcı modunda aşağıdaki gibi simüle edebiliriz: .. code-block:: c #include #include #define TABLE_SIZE 4096 struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; int main(void) { struct hlist_head *hash_table; if ((hash_table = (struct hlist_head *)malloc( sizeof(struct hlist_head) * TABLE_SIZE)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } /* ... */ free(hash_table); return 0; } Çekirdekteki ``list.h`` dosyası içerisinde hash tablosuna eleman eklemek için çeşitli basit fonksiyonlar bulundurulmuştur. Tabii ``hlist_node`` düğümleri de aslında başka yapıların elemanı durumunda olacaktır. Yine asıl yapı nesnesinin adresi ``container_of`` makrosuyla elde edilecektir. Başlangıçta ``hlist_head`` yapısındaki ``first`` elemanı ``NULL`` değerinde olmalıdır. Bunun için ``list.h`` içerisinde makrolar bulundurulmuştur: .. code-block:: c #define HLIST_HEAD_INIT { .first = NULL } #define HLIST_HEAD(name) struct hlist_head name = { .first = NULL } #define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) Örneğin: .. code-block:: c for (int i = 0; i < TABLE_SIZE; ++i) INIT_HLIST_HEAD(&hash_table[i]); Zincirlerdeki son düğümün ``next`` elemanı da ``NULL`` değerindedir. Böylece arama ``NULL`` görmeyene kadar ilerlenerek yapılabilmektedir. Tablodaki zincirlerin önüne eleman eklemek için ``hlist_add_head`` fonksiyonu bulundurulmuştur: .. code-block:: c static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { struct hlist_node *first = h->first; WRITE_ONCE(n->next, first); if (first) WRITE_ONCE(first->pprev, &n->next); WRITE_ONCE(h->first, n); WRITE_ONCE(n->pprev, &h->first); } Fonksiyonun birinci parametresi eklenecek yeni düğümün adresini, ikinci parametresi ise kök düğüm nesnesini belirtmektedir. Buradaki ``WRITE_ONCE`` atamayı volatile erişimle yapmaktadır. ``WRITE_ONCE(a, b)`` çağrısını ``a = b`` gibi düşünebilirsiniz. Hash Tablosundan Düğüm Silme ---------------------------- Hash tablosundan bir düğüm silmek için ``hlist_del`` fonksiyonu kullanılmaktadır. Fonksiyon ``list.h`` içerisinde şöyle tanımlanmıştır: .. code-block:: c static inline void hlist_del(struct hlist_node *n) { __hlist_del(n); n->next = LIST_POISON1; n->pprev = LIST_POISON2; } Burada asıl silme işlemini yapan fonksiyon ``__hlist_del`` fonksiyonudur. Düğüm silindikten sonra silinen düğümün ``next`` ve ``pprev`` göstericilerine güvenlik amacıyla özel değerler atandığını görüyorsunuz. Bu özel değerler geçerli adresler belirtmemektedir. Eğer silinen düğüm yanlışlıkla kullanılırsa *page fault* oluşmasına yol açacaktır. ``__hlist_del`` fonksiyonu da şöyle tanımlanmıştır: .. code-block:: c static inline void __hlist_del(struct hlist_node *n) { struct hlist_node *next = n->next; struct hlist_node **pprev = n->pprev; WRITE_ONCE(*pprev, next); if (next) WRITE_ONCE(next->pprev, pprev); } Zincirdeki son düğümün ``next`` göstericisinde ``NULL`` adres bulunmalıdır. Fonksiyonun içerisinde bu durumun kontrol edildiğine ve ilk düğümün silinmesinin özel bir durum oluşturmadığına dikkat ediniz. Insert Fonksiyonları -------------------- Çok fazla gereksinim olmasa da zincirdeki belli bir düğümün önüne ve arkasına düğüm insert eden ``hlist_add_before`` ve ``hlist_add_behind`` fonksiyonları da bulundurulmuştur: .. code-block:: c static inline void hlist_add_before(struct hlist_node *n, struct hlist_node *next) { WRITE_ONCE(n->pprev, next->pprev); WRITE_ONCE(n->next, next); WRITE_ONCE(next->pprev, &n->next); WRITE_ONCE(*(n->pprev), n); } static inline void hlist_add_behind(struct hlist_node *n, struct hlist_node *prev) { WRITE_ONCE(n->next, prev->next); WRITE_ONCE(prev->next, n); WRITE_ONCE(n->pprev, &prev->next); if (n->next) WRITE_ONCE(n->next->pprev, &n->next); } Hash Tabloları için Döngü Makroları ----------------------------------- Hash tablosundaki bir ``hlist_node`` adresi bilindiğinde onun içinde bulunduğu asıl yapının adresinin ``container_of`` makrosu ile elde edilebildiğini biliyorsunuz. Ancak tıpkı bağlı listelerde olduğu gibi hash tablolarında da bunun için ``container_of`` makrosu ile aynı işlemi yapan bir ``entry`` makrosu bulundurulmuştur: .. code-block:: c #define hlist_entry(ptr, type, member) container_of(ptr, type, member) Hash tablosundaki bir zinciri dolaşmak için ``hlist_for_each`` döngü makrosu kullanılmaktadır: .. code-block:: c #define hlist_for_each(pos, head) \ for (pos = (head)->first; pos ; pos = pos->next) Makronun ilk parametresi ``hlist_node`` türünden bir göstericiyi, ikinci parametresi ise bağlı liste zincirinin başlangıç düğümüne ilişkin ``hlist_head`` nesnesinin adresini almaktadır. Bu döngü makrosunun ``next`` elemanı ``NULL`` olmayana kadar ilerleme sağladığına dikkat ediniz. Döngü her yinelendikçe birinci parametreye girilen göstericinin içerisine zincirdeki sonraki düğümün adresi yerleştirilmektedir. Bu döngü makrosuyla zincir dolaşılırken döngünün her yinelenmesinde asıl yapı nesnesinin değil onun içerisindeki ``hlist_node`` nesnesinin adresinin elde edildiğine da dikkat ediniz. Bu adresten hareketle ``container_of`` ya da ``hlist_entry`` makrolarıyla asıl nesnenin başlangıç adresinin elde edilmesi gerekir. İşte bu iki işlemi aynı anda yapan ``hlist_for_each_entry`` isimli bir döngü makrosu da bulundurulmuştur: .. code-block:: c #define hlist_for_each_entry(pos, head, member) \ for (pos = hlist_entry((head)->first, typeof(*(pos)), member); \ pos; \ pos = hlist_entry((pos)->member.next, typeof(*(pos)), member)) Bu makronun artık birinci parametresi doğrudan asıl yapı türünden bir göstericiyi, ikinci ve üçüncü parametreler sırasıyla bağlı listenin ``hlist_head`` adresini ve asıl yapıdaki bağ elemanının ismini almaktadır. Yukarıdaki makroda bir noktaya dikkatinizi çekmek istiyoruz. Hash tablosundaki ``hlist_head`` kök düğümünün ``first`` elemanında ``NULL`` adres varsa yukarıdaki ``hlist_for_each_entry`` makrosu tanımsız davranışa yol açar. İşte eğer ``first`` elemanı ``NULL`` ise bu durumu ele alıp dolaşımı sonlandıran ``hlist_for_each_entry_safe`` döngü makrosu da bulundurulmuştur: .. code-block:: c #define hlist_for_each_entry_safe(pos, n, head, member) \ for (pos = hlist_entry_safe((head)->first, typeof(*pos), member); \ pos && ({ n = pos->member.next; 1; }); \ pos = hlist_entry_safe(n, typeof(*pos), member)) Makronun birinci parametresi asıl yapı nesnesi türünden göstericiyi, ikinci parametresi ``hlist_node`` türünden göstericiyi, üçüncü parametresi ``hlist_head`` türünden kök düğümün adresini, dördüncü parametresi de asıl yapıdaki bağ düğümünün ismini almaktadır. Bu makronun ``hlist_for_each_entry`` makrosundan tek farkı zincir boşsa bir soruna yol açmadan döngünün sonlanmasını sağlamasıdır. Döngü makrosunun içerisinde ``hlist_entry_safe`` makrosunun kullanıldığına dikkat ediniz. Bu makro zincirin sonundaki ``NULL`` adresi de dikkate almaktadır: .. code-block:: c #define hlist_entry_safe(ptr, type, member) \ ({ typeof(ptr) ____ptr = (ptr); \ ____ptr ? hlist_entry(____ptr, type, member) : NULL; \ }) ``list.h`` dosyası içerisinde hash tablolarına ilişkin başka yararlı fonksiyonlar da vardır. Bu fonksiyonları ileride gerektiğinde açıklayacağız. Bunları siz de inceleyebilirsiniz. Bir hash tablosunun tamamen yok edilmesi için yalnızca hash tablosunun değil onun tüm zincirlerdeki elemanlarının da serbest bırakılması gerekir. Çekirdekte genel olarak böyle bir gereksinim yoktur. Hash Tablosu Gerçekleştirimine Bir Örnek ---------------------------------------- Aşağıda *list.h* içerisindeki hash tablosu işlemleri için kullanıcı modunda çalıştırılabilecek bir örnek verilmiştir. Örnekte ``TABLE_SIZE`` uzunluğunda her bir elemanı ``hlist_head`` türünden olan bir dizi oluşturulmuştur: .. code-block:: c if ((hash_table = (struct hlist_head *)malloc(sizeof(struct hlist_head) * TABLE_SIZE)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } Sonra bu ``hlist_head`` elemanlarına ilkdeğer verilmiştir. (Yani onların ``first`` göstericilerine ``NULL`` adres yerleştirilmiştir): .. code-block:: c for (int i = 0; i < TABLE_SIZE; ++i) INIT_HLIST_HEAD(&hash_table[i]); Ondan sonra hash tablosuna rastgele 100 eleman eklenmiştir. Ancak bunun yanı sıra belli bir eleman da listeye ayrıca eklenmiştir: .. code-block:: c struct PERSON { char name[32]; int no; struct hlist_node hlink; }; /* ... */ for (int i = 0; i < 100; ++i) { if ((per = (struct PERSON *)malloc(sizeof(struct PERSON))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if (i == 50) { strcpy(per->name, "ALI SERCE"); per->no = 12345678; } else set_random_person(per); hash = hash_func(per->no); hlist_add_head(&per->hlink, &hash_table[hash]); } Sonra liste dolaşılmıştır. Program biterken de tüm hash tablosu zincirleriyle birlikte serbest bırakılmıştır: .. code-block:: c { struct PERSON *per, *temp; struct hlist_node *node; for (int i = 0; i < TABLE_SIZE; ++i) { temp = NULL; hlist_for_each_entry_safe(per, node, &hash_table[i], hlink) { free(temp); temp = per; } free(temp); } free(hash_table); } Bağlı liste düğümleri serbest bırakılırken sonraki düğümün adresini saklamak gerekir. ``NULL`` adrese ``free`` uygulamanın bir soruna yol açmayacağını anımsayınız. Aşağıda örneği bir bütün olarak veriyoruz: .. code-block:: c #include #include #include #include #include #define TABLE_SIZE 4096 struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; }; #define HLIST_HEAD_INIT { .first = NULL } #define HLIST_HEAD(name) struct hlist_head name = { .first = NULL } #define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) #define WRITE_ONCE(a, b) ((a) = (b)) /* bize özgü */ #define LIST_POISON1 (struct hlist_node *)0x00100100 #define LIST_POISON2 (struct hlist_node **)0x00200200 static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) { struct hlist_node *first = h->first; WRITE_ONCE(n->next, first); if (first) WRITE_ONCE(first->pprev, &n->next); WRITE_ONCE(h->first, n); WRITE_ONCE(n->pprev, &h->first); } static inline void __hlist_del(struct hlist_node *n) { struct hlist_node *next = n->next; struct hlist_node **pprev = n->pprev; WRITE_ONCE(*pprev, next); if (next) WRITE_ONCE(next->pprev, pprev); } static inline void hlist_del(struct hlist_node *n) { __hlist_del(n); n->next = LIST_POISON1; n->pprev = LIST_POISON2; } #define container_of(ptr, type, member) ({ \ void *__mptr = (void *)(ptr); \ ((type *)(__mptr - offsetof(type, member))); }) #define hlist_entry(ptr, type, member) \ container_of(ptr, type, member) #define hlist_entry_safe(ptr, type, member) \ ({ typeof(ptr) ____ptr = (ptr); \ ____ptr ? hlist_entry(____ptr, type, member) : NULL; \ }) #define hlist_for_each(pos, head) \ for (pos = (head)->first; pos ; pos = pos->next) #define hlist_for_each_safe(pos, n, head) \ for (pos = (head)->first; pos && ({ n = pos->next; 1; }); \ pos = n) #define hlist_for_each_entry(pos, head, member) \ for (pos = hlist_entry((head)->first, typeof(*(pos)), member); \ pos; \ pos = hlist_entry((pos)->member.next, typeof(*(pos)), member)) #define hlist_for_each_entry_safe(pos, n, head, member) \ for (pos = hlist_entry_safe((head)->first, typeof(*pos), member); \ pos && ({ n = pos->member.next; 1; }); \ pos = hlist_entry_safe(n, typeof(*pos), member)) /* Test code */ struct PERSON { char name[32]; int no; struct hlist_node hlink; }; void set_random_person(struct PERSON *per); unsigned int hash_func(unsigned int key); int main(void) { struct hlist_head *hash_table; struct PERSON *per; unsigned int hash; int no; srand(time(NULL)); if ((hash_table = (struct hlist_head *)malloc( sizeof(struct hlist_head) * TABLE_SIZE)) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } for (int i = 0; i < TABLE_SIZE; ++i) INIT_HLIST_HEAD(&hash_table[i]); for (int i = 0; i < 100; ++i) { if ((per = (struct PERSON *)malloc(sizeof(struct PERSON))) == NULL) { fprintf(stderr, "cannot allocate memory!...\n"); exit(EXIT_FAILURE); } if (i == 50) { strcpy(per->name, "ALI SERCE"); per->no = 12345678; } else set_random_person(per); hash = hash_func(per->no); hlist_add_head(&per->hlink, &hash_table[hash]); } /* hlist_for_each ile arama */ { struct hlist_node *node; struct PERSON *per_find; printf("Person no:"); scanf("%d", &no); hash = hash_func(no); hlist_for_each(node, &hash_table[hash]) { per_find = hlist_entry(node, struct PERSON, hlink); if (per_find->no == no) { printf("Found: %s, %d\n", per_find->name, per_find->no); break; } } if (node == NULL) printf("cannot find...\n"); } /* hlist_for_each_entry_safe ile arama */ { struct PERSON *per_find; struct hlist_node *node; hash = hash_func(no); hlist_for_each_entry_safe(per_find, node, &hash_table[hash], hlink) { if (per_find->no == no) { printf("Found: %s, %d\n", per_find->name, per_find->no); break; } } if (per_find == NULL) printf("cannot find...\n"); } /* Tüm tabloyu serbest bırakma */ { struct PERSON *per, *temp; struct hlist_node *node; for (int i = 0; i < TABLE_SIZE; ++i) { temp = NULL; hlist_for_each_entry_safe(per, node, &hash_table[i], hlink) { free(temp); temp = per; } free(temp); } free(hash_table); } return 0; } void set_random_person(struct PERSON *per) { int i; for (i = 0; i < 31; ++i) per->name[i] = rand() % 26 + 'A'; per->name[i] = '\0'; per->no = rand() % 1000000; } unsigned int hash_func(unsigned int key) { key = (key ^ 61) ^ (key >> 16); key = key + (key << 3); key = key ^ (key >> 4); key = key * 0x27d4eb2d; key = key ^ (key >> 15); return key % TABLE_SIZE; } Örnekte ``TABLE_SIZE`` uzunluğunda, her bir elemanı ``hlist_head`` türünden olan bir dizi oluşturulmuştur. Sonra bu ``hlist_head`` elemanlarına ilk değer verilmiştir (yani onların ``first`` göstericilerine ``NULL`` adres yerleştirilmiştir). Ondan sonra hash tablosuna rastgele 100 eleman eklenmiştir; ancak bunun yanı sıra belli bir eleman (numarası 12345678 olan "ALI SERCE") da listeye ayrıca eklenmiştir. Program biterken tüm hash tablosu zincirleriyle birlikte serbest bırakılmıştır. .. note:: Bağlı liste düğümleri serbest bırakılırken sonraki düğümün adresini saklamak gerekir. ``NULL`` adrese ``free`` uygulamanın bir soruna yol açmayacağını anımsayınız. RCU'lu Hash Tablosu Fonksiyonları =================================== Linux çekirdeğine kilitsiz (lock-free) RCU mekanizması eklendiğinde tıpkı bağlı listelerde olduğu gibi hash tablolarına da yukarıdaki fonksiyonların sonu ``_rcu`` ile biten RCU uyumlu versiyonları eklenmiştir. Bu fonksiyonlar ``include/linux/rculist.h`` dosyası içerisindedir. Bu dosyada yukarıda gördüğümüz hash tablosu fonksiyonlarının RCU mekanizmalı versiyonları bulunmaktadır. Biz buradaki fonksiyonların yalnızca isimlerini vereceğiz. RCU mekanizması başka bir başlık altında ele alınacaktır: .. code-block:: c hlist_add_head_rcu hlist_del_rcu hlist_add_before_rcu hlist_add_behind_rcu hlist_for_each_entry_rcu hlist_for_each_entry_srcu /* safe version */ hlist_first_rcu(head) hlist_next_rcu(node) hlist_pprev_rcu(node) Bu RCU'lu fonksiyonlar tıpkı bağlı listelerde olduğu gibi birden fazla okuyan ancak tek bir yazan taraf varsa beklemeye yol açmamaktadır. Tabii birden fazla yazan tarafın ayrıca bir senkronizasyon nesnesiyle korunması gerekir. Bu makrolarla dolaşım yapılırken yine bağlı listelerde olduğu gibi ilgili kod bloğunun başına ve sonuna ``rcu_read_lock`` ve ``rcu_read_unlock`` çağrılarının yerleştirilmesi gerekmektedir. Çekirdek tıpkı listelerde olduğu gibi pek çok yerde (ancak her yerde değil) hash tablolarıyla RCU mekanizması eşliğinde işlem yapmaktadır. Çekirdeğin RCU mekanizması eşliğinde işlem yaptığı yerlerde sizin de bu RCU'lu fonksiyonları kullanmanız gerekir.