# Linux kernel linked list **Reference: [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)** ### Linux中的linked list circular doubly-linked list (雙向環狀鏈結串列,以下簡稱 list) 是Linux核心中相當實用的結構體,在行程 (process) 管理的相關實作中可見到應用。其概念是將結構體 list_head 嵌入到所需的結構體中,再藉由稍後會提及的 container_of 巨集得知 list 個別節點的地址,架構如下。 ![image](https://hackmd.io/_uploads/rk3OjPCyke.png) :::info Head 是個特例,無法藉由 container_of 巨集來找到對應的節點,因為它並未嵌入到任何結構體之中,其存在是為了找到 list 整體 (list header)。 ::: * **list_head結構體** ```c struct list_head { //the struct takes 8*2 bytes struct list_head *prev; struct list_head *next; }; ``` 放進自定義的結構體中就會像 ... ```c typedef struct node { int val; struct list_head list; }node; ``` ### API的實作 當然,在Linux kernel的list.h檔中有許多操作應用,這邊簡單舉幾個例子 ... * **LIST_HEAD** ```c /* this define is used to create the header of list, * and point the next and prev to itself. */ #define LIST_HEAD(head) struct list_head head = {&head, &head}; ``` ![image](https://hackmd.io/_uploads/SkuDxuCkyl.png) * **list_entry()** ```c /* given the member of element of struct (which we want to get address), * member's ptr and struct's type, than we can get the address of element*/ #define container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member))) ``` :::info ```container_of()```是一個以```offsetof()```為基礎,擴充為「給定成員的位址、struct 的型態,及成員的名稱,傳回此 struct 的位址」的巨集。 ![image](https://hackmd.io/_uploads/H1ddVWCJkl.png) ![image](https://hackmd.io/_uploads/HJHREbCkkg.png) ::: * **list_add()** ```c /* add the node to the first element of list (head->next)*/ void list_add(struct list_head *node, struct list_head *head) { struct list_head *next_node = head->next; next_node->prev = node; node->next = next_node; node->prev = head; head->next = node; } ``` 1. 創建一個指標```next_node```指向```head```的下一個節點(也就是list的第一個節點)。並且對三個節點進行連接操作。 2. 這樣可以確保新節點```node```永遠都插入在list的首位(head後)。 ### hash table應用 ``` typedef struct { int bits; struct hlist_head *ht; } map_t; struct hlist_node { struct hlist_node *next, **pprev; }; struct hlist_head { struct hlist_node *first; }; struct hash_key { int key; void *data; struct hlist_node node; }; ``` 以上是 linux 中專門為了 hash table 而使用的結構,hash table 主要是由一個 hlist_head 的動態陣列所構成,每個 entry 指向一個由 struct hlist_node 構成的非環狀 doubly linked list,此外hlist_head 的設計也省去了 list 起始端 pprev 的存放空間、在初始狀態就省去了一半的記憶體容量。 * **hlist結構示意圖** ![image](https://hackmd.io/_uploads/BkEOrW01yl.png) ![image](https://hackmd.io/_uploads/HJBWLWA1ke.png) :::info ==`pprev`為何是指標的指標?== **reference中有詳細提到**:100: ... `hlist_node`有兩個指標,<span style="color:red">**但是需要注意的是`pprev`是指標的指標,它指向的是前一個節點的`next`指標**</span>。 這樣的好處是即使要刪除的節點是"最前頭的節點"時,也可以通過`*pprev = next`直接修改指標的指向。 ::: * 以下是一個刪除節點的範例 ```c // deletes entry from hlist void __hlist_del(struct hlist_node* entry) { struct hlist_node *next = entry->next; // step1 struct hlist_node **pprev = entry->pprev; //step2 *pprev = next; //step3 if (next) next->pprev = pprev; //step4 } ``` 1.`struct hlist_node *next = entry->next; // step1` 記錄目標節點的下一個節點 `next`。 `entry->next` 是目標節點的下一個節點指針,這樣在刪除節點後還能保持對剩餘鏈表的連接。 2. `struct hlist_node **pprev = entry->pprev; // step2` 記錄目標節點的前一個節點的指針,即 `pprev`。這是一個指向指針的指針,因為哈希鏈表的節點可能是表頭(`hlist_head`)的一部分,所以用指針指向它的上一個節點。 3. `*pprev = next; // step3` 更新前一個節點的 `next` 指針,使其指向目標節點的下一個節點 `next`,從而完成對目標節點的跳過,實現刪除。這一步驟較難理解,`pprev`其實就是指向上一個節點的`next`的指標(pointer to pointer),故對`pprev`取值符`*pprev`得到`pre_entry->next`的概念 4. `if (next) next->pprev = pprev; // step4` 如果目標節點的下一個節點 `next` 存在,則更新它的 `pprev`,使其指向目標節點的前一個節點的 `pprev`。 ![image](https://hackmd.io/_uploads/SytXCgTJJx.png) ### 其他hash table API的實作 我們可以藉由更多的實作,來加深對這種特殊的雙向鏈結的概念 ... * **map_init** ```c // initialize the map of hash table map_t *map_init(int bits) { map_t *map = (map_t*)malloc(sizeof(map_t)); if (!map) return NULL; map->bits = bits; map->ht = (struct hlist_head *)malloc(sizeof(struct hlist_head) * MAP_HASH_SIZE(map->bits)); if (map->ht) { for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) (map->ht)[i].first = NULL; } else { free(map); map = NULL; } return map; } ``` 1. 創建一個空間給`map_t`結構體使用,結構體中包含hash table的bucket數`bits`以及一個`hlist_head`指標,指向一陣列分別對應到各個bucket的首位`first`。 2. `first`初始化為空`NULL`。 * **map_add** ```c void map_add(map_t *map, int key, void *data) { struct hash_key *kn = find_key(map, key); if (kn) return; // step 1 kn = (struct hash_key *)malloc(sizeof(struct hash_key)); kn->key = key, kn->data = data; // step 2 struct hlist_head *h = &map->ht[hash(key, map->bits)]; struct hlist_node *n = &kn->node, *first = h->first; n->next = first; //step 3 if (first) first->pprev = &n->next; //step 4 h->first = n; //step 5 n->pprev = &h->first; //step 6 } ``` 1. 創建`hash_key`結構體並檢查鍵是否已存在list中。 2. 計算出哈希值,從hash table中獲取對應的`hlist_head`,並保存當前list第一個節點`first`。 3. 接下來就是操作`hlist_node`(雙向鏈結),將新節點插入list頭部。 4. 值得注意的是連接方式都是把<mark><span style="color:red">後節點的pprev指向前節點的next的地址</span></mark>。 * **map_deinit** ```c void map_deinit(map_t *map) { if (!map) return; for (int i = 0; i < MAP_HASH_SIZE(map->bits); i++) { struct hlist_head *head = &map->ht[i]; for (struct hlist_node *p = head->first; p;) { struct hash_key *kn = container_of(p, struct hash_key, node); struct hlist_node *n = p; p = p->next; // Unlink the node from the list if (n->pprev) { struct hlist_node *next = n->next, **pprev = n->pprev; *pprev = next; if (next) next->pprev = pprev; n->next = NULL; n->pprev = NULL; } // Free associated data and the node itself free(kn->data); free(kn); } } // Free the map free(map); } } ``` 1. 遍歷哈希表所有的桶(bucket)。 2. map->ht 是哈希表(由多個 "桶" 組成的數組)。`MAP_HASH_SIZE(map->bits)`計算哈希表的大小(也就是桶的數量),程式逐一遍歷每一個桶。head 是指向每個桶的指標。 3. 遍歷每個桶中的節點,找到對應的`hash_key`結構。 4. `n`保存當前節點並移到下一個節點`p = p->next`。 5. 檢查當前節點是否鏈接到哈希表,檢查 n->pprev 是否為 NULL。如果是 NULL,代表該節點未鏈接到哈希表,則跳過它。如果有值,表示這個節點是鏈表的一部分,需要進行後續操作將它從鏈表中刪除。 6. 將節點從鏈表中移除並釋放記憶體空間, `next = n->next`:保存當前節點 n 的下一個節點。 `pprev = n->pprev`:<mark>獲取上一個節點的 next 指標</mark>,這是用來移除當前節點的關鍵。 `*pprev = next`:將上一個節點的 next 指標指向當前節點的下一個節點,這樣當前節點就從鏈表中移除了。 如果`next`不為`NULL`,則把`next->pprev`設置為當前節點的前一個指標`pprev`,以保持鏈表完整。 最後將`n->next`和`n->pprev`設置為`NULL`,這樣該節點就完全從鏈表中脫離了。 7. 最後釋放哈希表本身。