---
tags: linux2025
---
# [2025q1](https://wiki.csie.ncku.edu.tw/linux/schedule) 第 2 週測驗題
:::info
目的: 檢驗學員對[鏈結串列](https://hackmd.io/@sysprog/c-linked-list)、[雜湊表](https://hackmd.io/@sysprog/linux-hashtable),及位元操作的認知
:::
==[作答表單: 測驗 `1` 和測驗 `2`](https://docs.google.com/forms/d/e/1FAIpQLSd9JhDDF42hntIRS9Zk0YFrcvrj9S3tzIZBm1UBsoRrQc9gLQ/viewform?usp=dialog)==
==[作答表單: 測驗 `3`](https://docs.google.com/forms/d/e/1FAIpQLSd9PfCvslu58tV9dS6OEZfteVP3ouwcqGeMxv8cVngZC4kyqQ/viewform?usp=dialog)==
### 測驗 `1`
我們使用 Linux 核心風格 List API 的簡化標頭檔 [list.h](https://github.com/sysprog21/lab0-c/blob/master/list.h) 來開發快速排序程式碼。
首先定義結構體和比較用的函式:
```c
#include <stdint.h>
struct listitem {
uint16_t i;
struct list_head list;
};
static inline int cmpint(const void *p1, const void *p2)
{
const uint16_t *i1 = (const uint16_t *) p1;
const uint16_t *i2 = (const uint16_t *) p2;
return *i1 - *i2;
}
```
預期開發的快速排序函式的原型宣告如下:
```c
static void list_quicksort(struct list_head *head);
```
接著準備測試用的輔助函式:
```c
#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
static uint16_t values[256];
static inline uint8_t getnum(void)
{
static uint16_t s1 = UINT16_C(2);
static uint16_t s2 = UINT16_C(1);
static uint16_t s3 = UINT16_C(1);
s1 *= UINT16_C(171);
s1 %= UINT16_C(30269);
s2 *= UINT16_C(172);
s2 %= UINT16_C(30307);
s3 *= UINT16_C(170);
s3 %= UINT16_C(30323);
return s1 ^ s2 ^ s3;
}
static uint16_t get_unsigned16(void)
{
uint16_t x = 0;
size_t i;
for (i = 0; i < sizeof(x); i++) {
x <<= 8;
x |= getnum();
}
return x;
}
static inline void random_shuffle_array(uint16_t *operations, uint16_t len)
{
for (uint16_t i = 0; i < len; i++) {
/* WARNING biased shuffling */
uint16_t j = get_unsigned16() % (i + 1);
operations[i] = operations[j];
operations[j] = i;
}
}
```
以下是測試程式:
```c
int main(void)
{
struct list_head testlist;
struct listitem *item, *is = NULL;
size_t i;
random_shuffle_array(values, (uint16_t) ARRAY_SIZE(values));
INIT_LIST_HEAD(&testlist);
assert(list_empty(&testlist));
for (i = 0; i < ARRAY_SIZE(values); i++) {
item = (struct listitem *) malloc(sizeof(*item));
assert(item);
item->i = values[i];
list_add_tail(&item->list, &testlist);
}
assert(!list_empty(&testlist));
qsort(values, ARRAY_SIZE(values), sizeof(values[0]), cmpint);
list_quicksort(&testlist);
i = 0;
list_for_each_entry_safe (item, is, &testlist, list) {
assert(item->i == values[i]);
list_del(&item->list);
free(item);
i++;
}
assert(i == ARRAY_SIZE(values));
assert(list_empty(&testlist));
return 0;
}
```
預期執行時期不會遇到 [assert](https://man7.org/linux/man-pages/man3/assert.3.html) 錯誤。
相較於尋常的快速排序程式碼,我們希望排序結果是符合 stable sorting,以下是 `list_quicksort` 的程式碼 (部分遮蔽):
```c
{
struct list_head list_less, list_greater;
struct listitem *pivot;
struct listitem *item = NULL, *is = NULL;
if (list_empty(head) || list_is_singular(head))
return;
INIT_LIST_HEAD(&list_less);
INIT_LIST_HEAD(&list_greater);
pivot = AAAA(head, struct listitem, list);
BBBB(&pivot->list);
list_for_each_entry_safe (item, is, head, list) {
if (cmpint(&item->i, &pivot->i) < 0)
list_move_tail(&item->list, &list_less);
else
CCCC(&item->list, &list_greater);
}
list_quicksort(&list_less);
list_quicksort(&list_greater);
DDDD(&pivot->list, head);
EEEE(&list_less, head);
FFFF(&list_greater, head);
}
```
補完程式碼,使其符合預期。
作答規範:
* AAAA`, `BBBB`, `CCCC`, `DDDD`, `EEEE`, `FFFF` 均為 [list.h](https://github.com/sysprog21/lab0-c/blob/master/list.h) 定義的巨集或行內展開函式 (inline function) 名稱,可能的選項:
* `list_add`
* `list_add_tail`
* `list_del`
* `list_for_each_entry_reverse`
* `list_for_each_entry`
* `list_first_entry`
* `list_move`
* `list_move_tail`
* `list_splice`
* `list_splice_tail`
* `list_cut_position`
* 以最精簡的方式撰寫,不包含空白
:::success
延伸問題:
* 解釋上方程式碼運作原理,改進 `random_shuffle_array` 也探討 `list_for_each_entry_safe` 建構的迴圈中,若將 `list_move_tail` 換為 `list_move`,為何會無法滿足 stable sorting
* 將程式碼整合進 [lab0](https://hackmd.io/@sysprog/linux2025-lab0) 提及的 `qtest`,並探討環狀雙向鏈結串列的排序效能,並善用 [perf](https://hackmd.io/@sysprog/linux-perf) 一類的效能分析工具,探討包含 Linux 核心 [list_sort](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 在內排序實作的效能表現並充分解釋
:::
---
### 測驗 `2`
以下針對 [從 √2 的存在談開平方根的快速運算](https://hackmd.io/@sysprog/sqrt),嘗試開發整數的開平方根運算。
首先是 `clz2` 函式,其作用是藉由遞迴呼叫以計算 [count leading zero](https://en.wikipedia.org/wiki/Find_first_set) (clz)。每次呼叫 `clz2()` 函式時,代表將目前關注的位元(bits)「切成一半」,以檢查 leading zero 的數量。以下藉由一個數值(`0x0001F000`)來說明其步驟:
```plaintext
0x0001F000 = 0000 0000 0000 0001 1111 0000 0000 0000 ~2~
```
- [ ] Step 1
將此數值等分為兩部分:較高位(upper)與較低位(lower)。
| Upper | Lower |
|:--:|:--:|
| 0000 0000 0000 0001 | 1111 0000 0000 0000 |
- [ ] Step 2
此時,依據 `upper` 的數值判斷下一次遞迴呼叫應該處理哪一部分,以累計 leading zero 的數量。
- **若 `upper` 等於 `0`**,則下一次遞迴呼叫應檢查 `lower` 部分,並用計數器(counter)記錄目前已累計的 leading zero(等於 `upper` 位元數)。
- **若 `upper` 不等於 `0`**,則下一次遞迴呼叫應繼續檢查 `upper` 部分。
```plaintext
upper = 0000 0000 0000 0001
```
由於 `upper` 不為 `0`,下一次遞迴呼叫將繼續檢查 `upper` 部分的 leading zero。
- [ ] Step 3
遞迴呼叫 `clz2()` 函式,直到僅剩 2 位元(bits)需要檢查 leading zero,然後回傳結果。
`clz2` 函式的實作如下: (部分遮蔽)
```c
static const int mask[] = {0, 8, 12, GGGG};
static const int magic[] = {HHHH, 1, 0, IIII};
unsigned clz2(uint32_t x, int c)
{
if (!x && !c)
return 32;
uint32_t upper = (x >> (16 >> c));
uint32_t lower = (x & (0xFFFF >> mask[c]));
if (c == JJJJ)
return upper ? magic[upper] : KKKK + magic[lower];
return upper ? clz2(upper, c + 1) : (16 >> (c)) + clz2(lower, c + LLLL);
}
```
及其對應的操作巨集:
```c
#define clz32(x) clz2(x, 0)
```
參考執行輸出:
* 輸入: 0x00000F00,其 clz32 為 20
* 輸入: 0x00000001,其 clz32 為 31
我們可運用 `clz32` 建構 `clz64`:
```c
static inline int clz64(uint64_t x)
{
/* If the high 32 bits are nonzero, count within them.
* Otherwise, count in the low 32 bits and add 32.
*/
return (x >> 32) ? clz32((uint32_t) (x >> 32)) : clz32((uint32_t) x) + 32;
}
```
最後實作整數開平方根: (部分遮蔽)
```c
uint64_t sqrti(uint64_t x)
{
uint64_t m, y = 0;
if (x <= 1)
return x;
int total_bits = 64;
/* clz64(x) returns the count of leading zeros in x.
* (total_bits - 1 - clz64(x)) gives the index of the highest set bit.
* Rounding that index down to an even number ensures our starting m is a
* power of 4.
*/
int shift = (total_bits - 1 - clz64(x)) & MMMM;
m = 1ULL << shift;
while (m) {
uint64_t b = y + m;
y >>= NNNN;
if (x >= b) {
x -= b;
y += m;
}
m >>= PPPP;
}
return y;
}
```
參考執行結果:
* 輸入: 63,得到 7
* 輸入: $2^{20}$,得到 1024
補完程式碼,使其符合預期。
作答規範:
* `GGGG`, `HHHH`, ..., `LLLL` 均為整數
* `MMMM`, `NNNN` 和 `PPPP` 為有效的 C 語言表示式,以最精簡的形式書寫
:::success
延伸問題:
1. 解釋上述程式碼,並探討擴充為 $\lceil \sqrt{x}) \rceil$ (向上取整數) 實作,該如何調整並保持只用整數加減及位元操作
2. 參照[計算機結構測驗題 C](https://hackmd.io/@sysprog/arch2024-quiz3-sol#Problem-C) 及其[注釋](https://hackmd.io/@sysprog/HJKRbSAVJl#Problem-C30),以 C 語言 (搭配 GNU extension) 撰寫不依賴除法指令的 `sqrtf`,確保符合 IEEE 754 規格。對照 [sqrtf, rsqrt, reciprocal implementations in Arm assembly](https://github.com/picolibc/picolibc/issues/956)
3. 參照[從 √2 的存在談開平方根的快速運算](https://hackmd.io/@sysprog/sqrt)提到的「藉由查表的平方根運算」,以 C 語言實作,並比較不同手法的整數開平方根效能
* 一旦發現效能改進的機會,可準備提交改進 `int_sqrt` 程式碼到 Linux 核心
:::
---
### 測驗 `3`
檢測學員對於 [Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable)的認知。
[LeetCode](https://leetcode.com/) 編號 1 的題目 [Two Sum](https://leetcode.com/problems/two-sum/),貌似簡單,作為 LeetCode 的開篇之題,乃是經典中的經典,正所謂「平生不識 [Two Sum](https://leetcode.com/problems/two-sum/),刷盡 [LeetCode](https://leetcode.com/) 也枉然」,就像英語單詞書的第一個單詞總是 [Abandon](https://www.dictionary.com/browse/abandon) 一樣,很多沒有毅力堅持的人就只能記住這一個單詞,所以通常情況下單詞書就前幾頁有翻動的痕跡,後面都是[嶄新如初](https://en.wikipedia.org/wiki/Mint_condition),道理不需多講,雞湯不必多灌,明白的人自然明白。
> 以上說法取自 [Two Sum 兩數之和](https://www.cnblogs.com/grandyang/p/4130379.html)
> [mint condition](https://en.wikipedia.org/wiki/Mint_condition): "mint" 除了薄荷的意思,還可指鑄幣廠,"mint condition" 裡的 “mint” 就與鑄幣廠有關。有些人收集錢幣會在錢幣剛開始發行時收集,因爲這樣的錢幣看起來很新,他們會用 "mint condition" 來形容這種錢幣的狀況,強調「像剛從鑄幣廠出來」,後來衍伸出「有如新一樣的二手商品」的意涵。
題意是給定一個陣列 `nums` 和一個目標值 `target`,求找到 `nums` 的 2 個元素相加會等於 target 的索引值。題目確保必為單一解,且回傳索引的順序沒差異。例如給定輸入 `nums = [2, 7, 11, 15]`, `target = 9`,相加變成 `9` 的元素僅有 `2` 及 `7`,因此回傳這二個元素的索引值 `[0, 1]`
考慮以下 C 語言實作:
```cpp
#include <stdlib.h>
static int cmp(const void *lhs, const void *rhs) {
if (*(int *) lhs == *(int *) rhs)
return 0;
return *(int *) lhs < *(int *) rhs ? -1 : 1;
}
static int *alloc_wrapper(int a, int b, int *returnSize) {
*returnSize = 2;
int *res = (int *) malloc(sizeof(int) * 2);
res[0] = a, res[1] = b;
return res;
}
int *twoSum(int *nums, int numsSize, int target, int *returnSize)
{
*returnSize = 2;
int arr[numsSize][2]; /* {value, index} pair */
for (int i = 0; i < numsSize; ++i) {
arr[i][0] = nums[i];
arr[i][1] = i;
}
qsort(arr, numsSize, sizeof(arr[0]), cmp);
for (int i = 0, j = numsSize - 1; i < j; ) {
if (arr[i][0] + arr[j][0] == target)
return alloc_wrapper(arr[i][1], arr[j][1], returnSize);
if (arr[i][0] + arr[j][0] < target)
++i;
else
--j;
}
*returnSize = 0;
return NULL;
}
```
若用暴力法,時間複雜度為 $O(n^2)$,顯然不符合期待。我們可改用 [hash table](https://en.wikipedia.org/wiki/Hash_table) (以下簡稱 `HT`) 記錄缺少的那一個值 (即 `target - nums[i]`) 和對應的索引。考慮以下案例:
> nums = `[2, 11, 7, 15]`:
對應的步驟:
1. `nums[0]` 是 `2`,`HT[2]` 不存在,於是建立 `HT[9 - 2] = 0`
2. `nums[1]`是 `11`, `HT[11]` 不存在,於是建立 `HT[9 - 11] = 1`
3. `nums[2]` 是 `7`,`HT[7]` 存在 (設定於步驟 `1`),於是回傳 `[2, HT[7]] = [2, 0]`

`hlist` 用於 hash table 的實作,它的資料結構定義在 [include/linux/types.h](https://github.com/torvalds/linux/blob/master/include/linux/types.h) 中:
```cpp
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
```
示意如下:

`hlist` 的操作與 `list` 一樣定義於 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h),以 `hlist_` 開頭。`hlist_head` 和 `hlist_node` 用於 hash table 中 bucket 的實作,具有相同 hash value 的節點會放在同一條 `hlist` 中。 為了節省空間,`hlist_head` 只使用一個 `first` 指標指向 `hlist_node`,沒有指向串列尾節點的指標。
`hash table` 主要是由一個 `hlist_head` 的動態陣列所構成,每個 entry 指向一個由 `struct hlist_node` 構成的非環狀 doubly linked list ,hash table 長度依照 `bits` 給定,可知大小為 2 的冪。
而可以發現 `struct hlist_head` 只有一個 `struct hlist_node *` 的成員; 而 `struct hlist_node` 型態卻包含一個 `struct hlist_node *` 及 `struct hlist_node **` ,其中一個原因為 hash table 指向的為非環狀的 linked list ,因此只需指向 list 的一端點,若單純使用 `struct hlist_node` 當作 head ,無用的 "pprev" 會造成大量的記憶體空間浪費,因此將 head 與 node 分開來實作。
而 `struct hlist_node` 中的 pprev 為何使用「指標的指標」而非「指標」? 回答這個問題前可以先參考 Linux 原始碼中 [type.h](https://elixir.bootlin.com/linux/v6.13.4/source/include/linux/types.h#L194-L204) :
```c
struct list_head {
struct list_head *next, *prev;
};
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
```
可知在 type.h 中有兩種 list 的結構:
`struct list_head` 在 Linux 中實作為環狀 doubly-linked list,且可以在行程管理 (process scheduling) 的相關實作上看到,如 [sched.h](https://github.com/torvalds/linux/blob/master/include/linux/sched.h) 中有約 20 處使用到此結構,因可快速存取到頭以及尾的節點(時間複雜度 $O(1)$) 故有較好的效能,適用於行程管理這種對時間要求嚴謹的部分做使用。
引用自 linux [sched.h](https://github.com/torvalds/linux/blob/master/include/linux/sched.h)
```c=547
struct sched_entity {
/* For load-balancing: */
struct load_weight load;
struct rb_node run_node;
struct list_head group_node;
unsigned int on_rq;
...
}
```
struct `hlist_head` 搭配 `hlist_node`。在 Linux 核心中專門為了 hash table 而使用,`hlist_head` 的設計也省去了 list 起始端 `pprev` 的存放空間、在初始狀態就省去了一半的記憶體容量。而且同時 hash table 不會特別需要存取到 list 的尾端,並且走訪 list 相對沒那麼講求效率(因為 hash 的設計理念就是講求 hash collision rate 要低、因此一個 list 若太長比較需要改進的為 hash function 的設計而非改進整個資料結構)。綜合上述所說單向 list 已滿足 hash table 的需求。
**`pprev` 為何是「指標的指標」**?若和 `list_head` 一樣使用單純的指標( `hlist_node *`),則考慮到 list 有方向性,delete node 時需要額外檢查其是否為 list 的 head 或是 NULL 等等,有較冗餘的程式碼必須實作,因此使用 `hlist_node **pprev` 直接存取上一個 node 所在的位址。Linux 為求程式碼簡潔故以 pointer to pointer 的方式用 `pprev` 直接指向前一個元素的記憶體位址本身。
以下是引入 [hash table](https://en.wikipedia.org/wiki/Hash_table) 的實作,學習 Linux 核心程式碼風格: (省略 hlist_{node,head} 及 `container_of` 的定義)
首先是結構體:
```c
typedef struct {
int bits;
struct hlist_head *ht;
} map_t;
struct hash_key {
int key;
void *data;
struct hlist_node node;
};
```
初始化操作:
```c
map_t *map_init(int bits)
{
map_t *map = malloc(sizeof(map_t));
if (!map)
return NULL;
map->bits = bits;
map->ht = 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;
}
```
雜湊函數:
```c
#define GOLDEN_RATIO_32 0x61C88647
static inline unsigned int hash(unsigned int val, unsigned int bits)
{
/* High bits are more random, so use them. */
return (val * GOLDEN_RATIO_32) >> (32 - bits);
}
```
對應的檢索程式碼: (部分遮蔽)
```c
static struct hash_key *find_key(map_t *map, int key)
{
struct hlist_head *head = &(map->ht)[hash(key, AAAA)];
for (struct hlist_node *p = head->first; p; p = p->next) {
struct hash_key *kn = container_of(p, struct hash_key, node);
if (kn->key == key)
return kn;
}
return NULL;
}
void *map_get(map_t *map, int key)
{
struct hash_key *kn = find_key(map, key);
return kn ? kn->data : NULL;
}
```
新增操作: (部分遮蔽)
```c
void map_add(map_t *map, int key, void *data)
{
struct hash_key *kn = find_key(map, key);
if (kn)
return;
kn = malloc(sizeof(struct hash_key));
kn->key = key, kn->data = data;
struct hlist_head *h = &map->ht[hash(key, BBBB)];
struct hlist_node *n = &kn->node, *first = h->first;
n->next = first;
if (first)
CCCC = &n->next;
h->first = n;
DDDD = &h->first;
}
```
釋放記憶體的操作:
```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;
if (!n->pprev) /* unhashed */
goto bail;
struct hlist_node *next = n->next, **pprev = EEEE;
*pprev = next;
if (next)
next->pprev = pprev;
n->next = NULL, n->pprev = NULL;
bail:
free(kn->data);
free(kn);
}
}
free(map);
}
```
主體程式碼:
```c
int *twoSum(int *nums, int numsSize, int target, int *returnSize)
{
map_t *map = map_init(10);
*returnSize = 0;
int *ret = malloc(sizeof(int) * 2);
if (!ret)
goto bail;
for (int i = 0; i < numsSize; i++) {
int *p = map_get(map, target - nums[i]);
if (p) { /* found */
ret[0] = i, ret[1] = *p;
*returnSize = 2;
break;
}
p = malloc(sizeof(int));
*p = i;
map_add(map, nums[i], p);
}
bail:
map_deinit(map);
return ret;
}
```
補完程式碼,使其運作符合預期。作答規範:
* `AAAA`, `BBBB`, `CCCC`, `DDDD`, `EEEE` 皆為有效的 C 語言表示式
* 以最精簡的形式書寫,不包含空白
:::success
延伸問題:
* 解釋上述程式碼運作原理,應提供測試程式碼
> 針對 [Linux 核心的 hash table 實作](https://hackmd.io/@sysprog/linux-hashtable),提供更多圖例並揣摩 Linux 核心開發者
* 進行《[The Art of Computer Programming](https://www-cs-faculty.stanford.edu/~knuth/taocp.html)》(vol 3) 一書section 6.4 的 exercise 8,也就是證明 Theorem S
> 注意: Linux 核心內部大量使用 hash table,但並非每處的雜湊函數都滿足減少碰撞及避免 clustering 現象的原則,也就是存在若干改進空間
* 探討 Linux 核心中使用 hash table 的應用案例,至少二項,應當列出原始程式碼並分析,斟酌搭配 git log 以得知 Linux 核心開發者變更程式碼的考量因素
:::