Try   HackMD

2024q1 Homework2 (quiz1+2)

contributed by < 56han >

第 1 週測驗題

2024q1 第 1 週測驗題

測驗 1

1. 補完程式碼,使其得以符合預期運作

取得 list 尾端的 node_t,先確認是否 leftleft->next 不為 NULL,若不為 NULL 則指標往下一個 node_t 指,即 left->next,最後跳出 while 迴圈時剛好會指到 tail。

node_t *list_tail(node_t **left)
{
    while ((*left) && (*left)->next)
-       left = &(AAAA);
+       left = &((*left)->next);
    return *left;
}

left 不為 NULL 時,持續往下一個 node_t 指,走完整個鏈結串列時,剛好可得鏈結串列長度。

int list_length(node_t **left)
{
    int n = 0;
    while (*left) {
        ++n;
-       left = &(BBBB);
+       left = &((*left)->next);
    }
    return n;
}

while 迴圈持續往下一個指,將原本的鏈結串列分成 > pivot< pivot,因此 CCCC 可填入 n->nextp->next
接著判斷是否比 pivot 值還大,若有加入 right 鏈結串列,反之加入 left 鏈結串列。

while (p) {
    node_t *n = p;
-   p = CCCC;
+   p = p->next;
    list_add(n->value > value ? &right : &left, n);
}

使用 begin[i]end[i] 紀錄 left 串列的頭、尾;
begin[i+1]end[i+1] 紀錄 pivot
begin[i+2]end[i+2] 紀錄 right 串列的頭、尾。
這裡可以使用 list_tail 子函式,取得尾端的節點

    begin[i] = left;
-   end[i] = DDDD;
+   end[i] = list_tail(&left);
    begin[i + 1] = pivot;
    end[i + 1] = pivot;
    begin[i + 2] = right;
-   end[i + 2] = EEEE;
+   end[i + 2] = list_tail(&right);

2. 運作原理

使用鏈結串列實作 quick sort,實際上的鏈結串列如下,引用 YangYeh-PD 的畫法。

typedef struct __node {
    struct __node *left, *right;
    struct __node *next;
    long value;
} node_t;






queue


cluster node1



cluster node2



cluster node3




node1_1

next



node2_1

next



node1_1->node2_1





node1_2

value



node1_3

left

right



node3_1

next



node2_1->node3_1





node2_2

value



node2_3

left

right



node3_2

value



node3_3

left

right



觀察程式碼可知,並沒有使用到 leftright ,因此可設計鍵結串列如下。







structs



pivot
pivot



struct0

4



pivot->struct0





L
L



L->struct0





R
R



struct4

1



R->struct4





p
p



struct1

5



p->struct1





struct0->struct1





struct2

3



struct1->struct2





struct3

2



struct2->struct3





struct3->struct4





在上面的例子可以發現,當 4 是 pivot 時,
大於 pivot : 5
小於 pivot : 3, 2, 1

while (p) {
    node_t *n = p;
    p = p->next;
    list_add(n->value > value ? &right : &left, n);
}

經過以上程式碼後,就會變成下圖。







structs



left
left



struct2

3



left->struct2





pivot
pivot



struct0

4



pivot->struct0





right
right



struct1

5



right->struct1





struct3

2



struct2->struct3





struct4

1



struct3->struct4





觀察整個 quick_sort() 函式,發現:

  1. 都是先檢查 right 串列是否還有需要排序(因為i += 2;),再去檢查 left

以上面例子來說, right 串列只有一個節點。因此將結果放入 result 串列中, i-- 再去檢查 pivotleft 串列。

  1. beginend 使用了 2 倍的 list_length 空間,去記錄 leftright 串列中的頭尾。
  2. pivot 都是取鏈結串列的第一個節點。

3. 改進方案

  • beginend 是否真的需要 2 倍的 list_length 空間?
    實際執行 count = 10 ~ 100000 ,觀察 beginend 需要使用多大的空間。
int main(int argc, char **argv)
{
    node_t *list = NULL;

    for (int i = 10; i < 1000000; i *= 10) {
        printf("size : %d", i);
        size_t count = i;

        int *test_arr = malloc(sizeof(int) * count);

        for (int i = 0; i < count; i++)
            test_arr[i] = i;
        shuffle(test_arr, count);
        while (count--)
            list = list_construct(list, test_arr[count]);

        quick_sort(&list);
        assert(list_is_ordered(list));
        list_free(&list);

        free(test_arr);
    }

    return 0;
}

begin[i+2]end[i+2] 後面加入

if(i+2 > max_deep){
    max_deep = i+2;
}

最後發現 beginend 只需要 10x( 計算 size 是10的幾次方 ) 的空間,就足夠了。

size : 10, deepest level : 4
size : 100, deepest level : 14
size : 1000, deepest level : 26
size : 10000, deepest level : 38
size : 100000, deepest level : 48
  • 每次都取第一個節點為 pivot ,當遇到已排序成 ascending /descending order 的鏈結串列時,就會成為 worst case,如何以別的方式挑選 pivot 以避免 worst case 的發生?
  1. rand() 取一數,並和 head 做交換,取新的 head 當作 pivot
  2. Median-of-three

使用 Linux 核心風格的 List API 改寫上述程式碼,並針對鏈結串列

commit 9fdec7b

TODO:提出可避免最差狀況的快速排序實作,應設計效能評比的測試程式。

測驗 2

1. 補完程式碼,使其得以符合預期運作

static struct list_head *merge(void *priv,
                               list_cmp_func_t cmp,
                               struct list_head *a,
                               struct list_head *b)
{
    struct list_head *head;
-   struct list_head **tail = AAAA;
+   struct list_head **tail = &head;

    for (;;) {
        /* if equal, take 'a' -- important for sort stability */
        if (cmp(priv, a, b) <= 0) {
            *tail = a;
-           tail = BBBB;
+           tail = &a->next;
            a = a->next;
            if (!a) {
                *tail = b;
                break;
            }
        } else {
            *tail = b;
-           tail = CCCC;
+           tail = &b->next;
            b = b->next;
            if (!b) {
                *tail = a;
                break;
            }
        }
    }
    return head;
}
static void build_prev_link(struct list_head *head,
                            struct list_head *tail,
                            struct list_head *list)
{
    tail->next = list;
    do {
        list->prev = tail;
        tail = list;
        list = list->next;
    } while (list);

    /* The final links to make a circular doubly-linked list */
-   DDDD = head;
+   tail->next = head;
-   EEEE = tail;
+   head->prev = tail;
}
void timsort(void *priv, struct list_head *head, list_cmp_func_t cmp)
{
    stk_size = 0;

    struct list_head *list = head->next, *tp = NULL;
    if (head == head->prev)
        return;

    /* Convert to a null-terminated singly-linked list. */
    head->prev->next = NULL;

    do {
        /* Find next run */
        struct pair result = find_run(priv, list, cmp);
        result.head->prev = tp;
        tp = result.head;
        list = result.next;
        stk_size++;
        tp = merge_collapse(priv, cmp, tp);
    } while (list);

    /* End of input; merge together all the runs. */
    tp = merge_force_collapse(priv, cmp, tp);

    /* The final merge; rebuild prev links */
    struct list_head *stk0 = tp, *stk1 = stk0->prev;
    while (stk1 && stk1->prev)
        stk0 = stk0->prev, stk1 = stk1->prev;
-   if (stk_size <= FFFF) {
+   if (stk_size <= 1) {
        build_prev_link(head, head, stk0);
        return;
    }
    merge_final(priv, cmp, head, stk1, stk0);
}

2. 運作原理

timsort 函式:

  • 一開始會先將雙向鏈結串列轉為單向串列。接著進入主要的迴圈,不斷呼叫 find_run() 找出下一個 run,並根據 run 的長度和目前的堆疊狀態,呼叫 merge_collapse() 觸發必要的合併。
  • 當輸入串列掃描完畢後,函式會呼叫 merge_force_collapse() 強制合併所有剩餘的 run。
  • 最後,根據堆疊的大小決定要呼叫 build_prev_link() 還是 merge_final() 來建立最終排序完成的雙向鏈結串列。

merge 函式:合併兩個已經排序好的鏈結串列 ab

它會比較兩個串列的元素,較小的元素會被放在合併後的串列前面。如果兩個元素相等,為了保持穩定性(stability),會優先選擇串列 a 的元素。

build_prev_link 函式:重建雙向鏈結串列的 prev 指標。

tail 開始,調整每個節點的 prev,讓它指向前一個節點。最後再將頭尾兩端的 prevnext 指標連接起來,構成一個環狀的雙向鏈結串列。

merge_final 函式:執行最後一次的合併,並建立完整的雙向鏈結串列。

函式會不斷比較 ab 的元素,較小的會被連接到 tail 之後,調整對應的 prevnext 指標。一旦 ab 的元素取完, 迴圈就會結束。最後,函式會呼叫 build_prev_link(),將 b 剩餘的元素全部接到 tail 之後,完成雙向鏈結串列的建立。

find_run 函式:在未排序的鏈結串列中找出一段遞增或遞減的子串列(run)。

它會從 list 開始,比較相鄰元素的大小,決定目前的 run 是遞增還是遞減。對於遞減的 run, 函式會同時進行反轉,讓 run 變成遞增。函式會持續比較元素,直到不再滿足遞增/遞減的條件為止。
最後回傳一個 pair,其中 head 指向 run 的起點, next 指向 run 結束後的下一個節點。函式還會在 run 的第二個節點的 prev 欄位中暫存 run 的長度。

merge_at 函式:合併指定位置 at 後面的兩個鏈結串列。

首先計算要合併的兩個段的長度總和,然後呼叫 merge() 將它們合併。最後,它更新合併後的鏈結串列的長度訊息,將前一個節點的 next 指針指向合併後的鏈結串列,並減少堆疊的大小。

merge_force_collapse 函式:在堆疊的 size >= 3 時,持續合併鏈結串列。

它比較 tp 前面兩個鏈結串列的長度,合併較小的那一個。這個過程會一直持續,直到堆疊的 size < 3。

merge_collapse 函式:盡可能地將相鄰的較短鏈結串列合併,從而減少鏈結串列中節點的數量,提高逐一走訪的效率。
確保堆疊上的 run 長度保持平衡。該函式主要檢查堆疊頂端的 3 個 run 是否滿足以下原則:

  1. A 的長度 > B 的長度 + C 的長度。
  2. B 的長度 > C 的長度。

主要是根據堆疊中當前的鏈結串列的長度情況,來決定如何進行合併操作。它會根據以下條件來判斷:
條件一:
stack 的 size >= 3
tp->prev->prev 的長度 <= tp->prev 的長度 + tp 的長度。
條件二:
stack 的 size >= 4
tp->prev->prev->prev 的長度 <= tp->prev->prev 的長度 + tp->prev 的長度。
如果滿足條件一或條件二:
如果 tp->prev->prev 較短,則呼叫 merge_at(priv, cmp, tp->prev) 合併 tp->prevtp->prev->prev
如果 tp 較短,則呼叫 merge_at(priv, cmp, tp) 合併 tptp->prev
如果上述兩個條件都不滿足:
如果 tp->prev 較短,則呼叫 merge_at(priv, cmp, tp) 合併 tptp->prev
如果 tp 較短,終止合併過程,返回 tp

3. 改進方法

  • 原程式碼並無實作 Galloping mode

TODO:實作 Galloping mode

TODO:將改進過的 Timsort 實作整合進 sysprog21/lab0-c,作為另一個有效的排序命令,應設計效能評比的測試程式,確保測試案例符合 Timsort 設想的資料分布樣式。

第 2 週測驗題

2024q1 第 2 週測驗題

測驗 1

1. 補完程式碼,使其得以符合預期運作

將新節點插入到雙向鏈結串列的 head 時,新節點的 next 指針應該指向的位置。因為我們是將新節點插入到 head,所以它的 next 應該指向原來的 head 節點。

static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
    if (h->first)
        h->first->pprev = &n->next;
-   n->next = AAAA;
+   n->next = h->first;
    n->pprev = &h->first;
    h->first = n;
}

hlist_for_each 用於逐一走訪 hash table 中特定鏈結串列的 head 指標。heads 是一個 hash table 的 heads array,每個元素對應一個鏈結串列,而 hash 是根據給定值計算出的 hash values,用於確定逐一走訪哪個鏈結串列。

static int find(int num, int size, const struct hlist_head *heads)
{
    struct hlist_node *p;
    int hash = (num < 0 ? -num : num) % size;
-   hlist_for_each (p, BBBB) {
-       struct order_node *on = CCCC(p, struct order_node, node);
+   hlist_for_each (p, &heads[hash]) {
+       struct order_node *on = list_entry(p, struct order_node, node);
        if (num == on->val)
            return on->idx;
    }
    return -1;
}

獲取 hash table 中特定鏈結串列的 head 指標,用於將新節點插入到對應鏈結串列的 head。

static inline void node_add(int val,
                            int idx,
                            int size,
                            struct hlist_head *heads)
{
    struct order_node *on = malloc(sizeof(*on));
    on->val = val;
    on->idx = idx;
    int hash = (val < 0 ? -val : val) % size;
-   hlist_add_head(&on->node, DDDD);
+   hlist_add_head(&on->node, &heads[hash]);
}

2. 運作原理

此程式碼的用意是透過構建一個 hash table,有效地儲存和查詢 inorder 序列中的節點,從而加速以 preorder 和 inorder 序列重建二元樹的過程。

struct hlist_node {
    struct hlist_node *next, **pprev;
};

hlist_node 以圖像表示。







G



node_1

hlist_node

pprev

next



很多個 hlist_node 連接,則會形成下圖。







G



list_head

list_head

first



node_1

hlist_node_1

pprev

next




list_head:n->node_1:m





node_1:p->list_head:n





node_2

hlist_node_2

pprev

next




node_1:n->node_2:m





node_2:p->node_1:n





node_3

hlist_node_3

pprev

next




node_2:n->node_3:m





node_3:p->node_2:n





NULL
NULL



node_3:n->NULL





struct hlist_head {
    struct hlist_node *first;
};

hlist_head 以圖像表示。







G



node_1

hlist_head

first



透過 node_add 函式:將 node 一個一個加入 hash table 中。
示意圖如下:引用 ShchangAnderson 同學的圖。







so



Array

0

1

2

3

4



in_heads
in_heads




inorder
Inorder:



9
9




3
3




15
15




20
20




7
7











so



inh4
in_heads[4]



9

val : 9

Idx : 0



inh4->9





inh3
in_heads[3]



3

val : 3

Idx : 1



inh3->3





inh2
in_heads[2]



7

val : 7

Idx : 4



inh2->7





inh1
in_heads[1]



inh
in_heads[0]



20

val : 20

Idx : 3



inh->20





15

val : 15

Idx : 2



20->15





建立完成 hash table 之後,呼叫 dfs 函式以深度優先搜尋往下尋找 root ,利用 find 函式找到 root 並且回傳 idx 值,即為 root 的位置。以 root 又可以切成左、右子樹,遞迴呼叫 dfs 函式。
引用 ShchangAnderson 同學的圖。







so



inorder
Inorder:



9
9




3

3




15
15




20
20




7
7




91
9



201
20




31

3





151
15



71
7





preorder
Preorder:











so



inorder
Inorder:



9

9



3

15    20    7



31

3



31->9





31->3






TODO:

  1. 指出其中可改進之處並實作
  2. 在 Linux 核心的 cgroups 相關原始程式碼,找出 pre-order walk 程式碼並探討

測驗 2

1. 補完程式碼,使其得以符合預期運作

目的是在從 hash table 中刪除一個節點時,更新下一個節點的 pprev 指標,使其指向上一個節點的地址。
閱讀 Linux 核心的 hash table 實作 學到為何是 *next**pprev

void hlist_del(struct hlist_node *n)
{
    struct hlist_node *next = n->next, **pprev = n->pprev;
    *pprev = next;
    if (next)
-       EEEE = pprev;
+       next->pprev = pprev;
}

釋放 LRU 快取實例所占用的記憶體。

void lRUCacheFree(LRUCache *obj)
{
    struct list_head *pos, *n;
    list_for_each_safe (pos, n, &obj->dhead) {
-       LRUNode *cache = list_entry(pos, LRUNode, FFFF);
-       list_del(GGGG);
+       LRUNode *cache = list_entry(pos, LRUNode, link);
+       list_del(&cache->link);
        free(cache);
    }
    free(obj);
}

從快取中獲取給定 key 對應的值,如果存在則返回該值,並將該節點移動到鏈結串列的頭部 (表示最近使用);如果不存在則返回 -1。

int lRUCacheGet(LRUCache *obj, int key)
{
    int hash = key % obj->capacity;
    struct hlist_node *pos;
    hlist_for_each (pos, &obj->hhead[hash]) {
-       LRUNode *cache = list_entry(pos, LRUNode, HHHH);
+       LRUNode *cache = list_entry(pos, LRUNode, node);
        if (cache->key == key) {
-           list_move(IIII, &obj->dhead);
+           list_move(&cache->link, &obj->dhead);
            return cache->value;
        }
    }
    return -1;
}

將給定的 鍵-值對 (key–value pair) 插入快取中。
Case 1:如果 key 已存在,則更新對應的值
Case 2:如果 key 不存在且容量已滿,則刪除鏈結串列尾部的節點(最久未使用),然後插入新的鍵-值對。
Case 3:如果 key 不存在且容量未滿,則直接插入新的鍵-值對在鏈結串列的頭部。
不管 key 是新的還是已存在,對應的節點都會被移動到鏈結串列的頭部。

void lRUCachePut(LRUCache *obj, int key, int value) { LRUNode *cache = NULL; int hash = key % obj->capacity; struct hlist_node *pos; hlist_for_each (pos, &obj->hhead[hash]) { - LRUNode *c = list_entry(pos, LRUNode, JJJJ); + LRUNode *c = list_entry(pos, LRUNode, node); if (c->key == key) { - list_move(KKKK, &obj->dhead); + list_move(&c->link, &obj->dhead); cache = c; } } if (!cache) { if (obj->count == obj->capacity) { cache = list_last_entry(&obj->dhead, LRUNode, link); list_move(&cache->link, &obj->dhead); hlist_del(&cache->node); hlist_add_head(&cache->node, &obj->hhead[hash]); } else { cache = malloc(sizeof(LRUNode)); hlist_add_head(&cache->node, &obj->hhead[hash]); list_add(&cache->link, &obj->dhead); obj->count++; } cache->key = key; } cache->value = value; }

不理解第 20、21 行,實際上的意思是刪除第一個節點和加入新節點,但為甚麼 hlist_delhlist_add_head 輸入參數都是 &cache->node

2. 運作原理

利用 lRUCachePutlRUCacheGet 等函式實作 LRU (Least Recently Used) 快取的資料結構。LRU 快取是一種常用的快取演算法,它會根據資料的最近使用情況,自動釋放最久未使用的資料,以確保快取空間的有效利用。

LRUCache 結構體
引用 vax-r 同學的圖

  • capacity :紀錄此 cache 最多能記錄幾筆資料
  • count :cache 當前紀錄的資料筆數
  • dhead :用來串接所有資料的雙向鍵結串列
  • hhead :處理碰撞使用的 hlist 結構體陣列
typedef struct {
    int capacity;
    int count;
    struct list_head dhead;
    struct hlist_head hhead[];
} LRUCache;






structs


cluster_a

LRUCache



struct1

capacity



struct2

count



struct3

dhead



struct3->struct3


next



struct3->struct3


prev



struct4

hhead[0]

hhead[1]

...

hhead[capacity - 1]



struct hlist_node {
    struct hlist_node *next, **pprev;
};

hlist_node 以圖像表示。







G



node_1

hlist_node

prev

next



很多個 hlist_node 連接,則會形成下圖。







G



list_head

list_head

first



node_1

hlist_node_1

pprev

next




list_head:n->node_1:m





node_1:p->list_head:n





node_2

hlist_node_2

pprev

next




node_1:n->node_2:m





node_2:p->node_1:n





node_3

hlist_node_3

pprev

next




node_2:n->node_3:m





node_3:p->node_2:n





NULL
NULL



node_3:n->NULL





3.在 Linux 核心找出 LRU 相關程式碼

Linus Torvalds 的 GitHub 中可見,例如:linux/mm/list_lru.c
以下程式碼主要是在處理 LRU list 時,將一個項目添加到特定節點的 LRU list中。

bool list_lru_add(struct list_lru *lru, struct list_head *item, int nid,
		    struct mem_cgroup *memcg)
{
	struct list_lru_node *nlru = &lru->node[nid];
	struct list_lru_one *l;

	spin_lock(&nlru->lock);
	if (list_empty(item)) {
		l = list_lru_from_memcg_idx(lru, nid, memcg_kmem_id(memcg));
		list_add_tail(item, &l->list);
		/* Set shrinker bit if the first element was added */
		if (!l->nr_items++)
			set_shrinker_bit(memcg, nid, lru_shrinker_id(lru));
		nlru->nr_items++;
		spin_unlock(&nlru->lock);
		return true;
	}
	spin_unlock(&nlru->lock);
	return false;
}
EXPORT_SYMBOL_GPL(list_lru_add);

TODO:指出其中可改進之處並實作

測驗 3

1. 補完程式碼,使其得以符合預期運作

目的是找出 word 參數中最低位的1所在的位置(從0開始計算)。

static inline unsigned long __ffs(unsigned long word)
{
    int num = 0;

#if BITS_PER_LONG == 64
-   if ((word & AAAA) == 0) {
+   if ((word & 0xffffffff) == 0) {
        num += 32;
        word >>= 32;
    }
#endif
    if ((word & 0xffff) == 0) {
        num += 16;
        word >>= 16;
    }
    if ((word & 0xff) == 0) {
        num += 8;
        word >>= 8;
    }
    if ((word & 0xf) == 0) {
        num += 4;
        word >>= 4;
    }
    if ((word & 0x3) == 0) {
        num += 2;
        word >>= 2;
    }
    if ((word & 0x1) == 0)
        num += 1;
    return num;
}

計算出 bitmask,然後找到該位所在的字,最後使用 *p &= ~mask; 來清除該位。

static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr)
{
    unsigned long mask = BIT_MASK(nr);
    unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr);

-   *p &= BBBB;
+   *p &= ~mask;
}

用於尋找第 n 個 bit 為1的索引。

#define FIND_NTH_BIT(FETCH, size, num)                          \
    ({                                                          \
        unsigned long sz = (size), nr = (num), idx, w, tmp;     \
                                                                \
        for (idx = 0; (idx + 1) * BITS_PER_LONG <= sz; idx++) { \
            if (idx * BITS_PER_LONG + nr >= sz)                 \
                goto out;                                       \
                                                                \
            tmp = (FETCH);                                      \
            w = hweight_long(tmp);                              \
            if (w > nr)                                         \
                goto found;                                     \
                                                                \
            nr -= w;                                            \
        }                                                       \
                                                                \
-       if (sz CCCC BITS_PER_LONG)                              \
+       if (sz % BITS_PER_LONG)                                 \
            tmp = (FETCH) & BITMAP_LAST_WORD_MASK(sz);          \
    found:                                                      \
        sz = min(idx * BITS_PER_LONG + fns(tmp, nr), sz);       \
    out:                                                        \
        sz;                                                     \
    })

2. 運作原理

此篇程式碼目的是在指定的記憶體空間找出第 N 個設定的位元。

static inline unsigned long find_nth_bit(const unsigned long *addr,
                                           unsigned long size,
                                           unsigned long n)
{
    if (n >= size)
        return size;

    if (small_const_nbits(size)) {
        unsigned long val = *addr & GENMASK(size - 1, 0);

        return val ? fns(val, n) : size;
    }

    return FIND_NTH_BIT(addr[idx], size, n);
}

不理解執行 find_nth_bit() 時,甚麼時候會需要 return FIND_NTH_BIT(addr[idx], size, n);

用來檢查一個數字 nbits 是否 <= BITS_PER_LONG ,並且> 0 的常數。

__builtin_constant_p(nbits) 是 GCC 提供的一個內建函數,用於在編譯時檢查 nbits 是否是一個已知的常數。

#define small_const_nbits(nbits) \
    (__builtin_constant_p(nbits) && (nbits) <= BITS_PER_LONG && (nbits) > 0)

用於生成一個 bitmask ,其中從第 l 位到第 h 位(含)的所有位都是1,其餘的位都是0。

(~0UL): 這表示一個 unsigned long 的值,其中所有位都是1。~ 是反運算符,所以取反就是全部 bit 都為1。
(1UL << (l)): 這將數值1(也是一個 unsigned long) 左移 l 位。左移操作會在右側補0,因此這個表達式生成了一個數,其中只有第 l 位是1,其餘位都是0。

#define GENMASK(h, l) \
    (((~0UL) - (1UL << (l)) + 1) & (~0UL >> (BITS_PER_LONG - 1 - (h))))

fns 函數的目的是找出 word 中第 n 個設定的位元。

不斷呼叫 __ffs 來找到最低位元的1,如果那不是我們要找的位元,它就使用 __clear_bit 來清除這個位,然後繼續搜索下一個設置位。如果 n 等於0,意思是我們找到了想要的位元,所以它返回當前的 bit
如果 word 為0(即沒有更多設置位),則返回 BITS_PER_LONG,表示沒有找到。

static inline unsigned long fns(unsigned long word, unsigned int n)
{
    while (word) {
        unsigned int bit = __ffs(word);
        if (n-- == 0)
            return bit;
        __clear_bit(bit, &word);
    }

    return BITS_PER_LONG;
}

計算出 bitmask,然後找到該位所在的字,最後使用 *p &= ~mask; 來清除該位。

static inline void __clear_bit(unsigned long nr, volatile unsigned long *addr)
{
    unsigned long mask = BIT_MASK(nr);    //只有第 nr 位是1,其他位都是0
    unsigned long *p = ((unsigned long *) addr) + BIT_WORD(nr);    //計算某個位 nr 在一個 unsigned long 陣列中的哪個字

    *p &= ~mask;
}

3. 在 Linux 核心原始程式碼找出 find_nth_bit 的應用案例

位於 include/linux/cpumask.hcpumask_nth 可以用來取得 cpumask 當中第 n 個 CPU。

使用 cpumask_bits(srcp) 獲取 cpumask 以 bit 表示,然後使用 small_cpumask_bits 作為搜尋範圍的大小,最後通過 cpumask_check(cpu) 確保傳入的 CPU 編號是有效的。

/**
 * cpumask_nth - get the Nth cpu in a cpumask
 * @srcp: the cpumask pointer
 * @cpu: the Nth cpu to find, starting from 0
 *
 * Return: >= nr_cpu_ids if such cpu doesn't exist.
 */
static inline unsigned int cpumask_nth(unsigned int cpu, const struct cpumask *srcp)
{
	return find_nth_bit(cpumask_bits(srcp), small_cpumask_bits, cpumask_check(cpu));
}