---
# System prepended metadata

title: Linux kernel linked list

---

# 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. 最後釋放哈希表本身。
