# 2021q1 Homework2 (quiz2)
contributed by < `D4nnyLee` >
> [題目連結](https://hackmd.io/@sysprog/linux2021-quiz2)
:::warning
不需要多事加上 `[toc]`,HackMD 的瀏覽模式就內建 TOC,謹記 KISS 原則。
:notes: jserv
:::
## 測驗 `1`
### 解釋程式碼運作原理
先視覺化和 doubly linked list 有關的 API 的效果。
#### `list_add_tail()`
* Before
```graphviz
graph {
node [shape = box, label = ""]
head [shape = none, label = "head"]
pnode [shape = none, label = "node"]
prev [shape = none, label = "prev"]
many [shape = none, label = "..."]
nnode [label = ""]
pnode -- nnode [dir = forward]
head -- n0 [dir = forward]
prev -- n2 [dir = forward]
{
rank = same
n0 -- n1 -- many -- n2 -- n0
}
}
```
* After
```graphviz
graph {
node [shape = box, label = ""]
head [shape = none, label = "head"]
pnode [shape = none, label = "node"]
prev [shape = none, label = "prev"]
many [shape = none, label = "..."]
nnode [label = ""]
pnode -- nnode [dir = forward]
head -- n0 [dir = forward]
prev -- n2 [dir = forward]
{
rank = same
n0 -- n1 -- many -- n2 -- nnode -- n0
}
}
```
#### `list_del()`
* Before
```graphviz
graph {
subgraph ptr {
node [shape = none]
p_prev [label = "prev"]
p_next [label = "next"]
p_node [label = "node"]
}
subgraph obj {
node [shape = box, label = ""]
o_1 o_2 o_prev o_next o_node
o_many1, o_many2 [shape = none, label = "..."]
}
p_prev -- o_prev [dir = forward]
p_next -- o_next [dir = forward]
p_node -- o_node [dir = forward]
{
rank = same
o_1 -- o_many1 -- o_prev -- o_node -- o_next -- o_many2 -- o_2
o_2 -- o_1 [constraint = false]
}
}
```
* After
```graphviz
graph {
subgraph ptr {
node [shape = none]
p_prev [label = "prev"]
p_next [label = "next"]
p_node [label = "node"]
}
subgraph obj {
node [shape = box, label = ""]
o_1 o_2 o_prev o_next o_node
o_many1, o_many2 [shape = none, label = "..."]
}
p_prev -- o_prev [dir = forward]
p_next -- o_next [dir = forward]
p_node -- o_node [dir = forward]
o_node -- o_prev [dir = forward]
o_node -- o_next [dir = forward]
{
rank = same
o_1 -- o_many1 -- o_prev -- o_next -- o_many2 -- o_2
o_2 -- o_1 [constraint = false]
}
}
```
#### `list_splice_tail()`
* Before
```graphviz
graph {
subgraph ptr {
node [shape = none]
list list_first list_last
head head_last
}
subgraph obj {
node [shape = box, label = ""]
o_list
o_list_first, o_list_last [style = filled, color = blue]
o_head o_head_last
o_many_list, o_many_head [shape = none, label = "..."]
}
list -- o_list [dir = forward]
list_first -- o_list_first [dir = forward]
list_last -- o_list_last [dir = forward]
head -- o_head [dir = forward]
head_last -- o_head_last [dir = forward]
{
rank = same
o_list -- o_list_first -- o_many_list -- o_list_last -- o_list
o_head -- o_many_head -- o_head_last -- o_head
}
}
```
* After
```graphviz
graph {
subgraph ptr {
node [shape = none]
list list_first list_last
head head_last
}
subgraph obj {
node [shape = box, label = ""]
o_list
o_list_first, o_list_last [style = filled, color = blue]
o_head o_head_last
o_many_list, o_many_head [shape = none, label = "..."]
}
list -- o_list [dir = forward]
list_first -- o_list_first [dir = forward]
list_last -- o_list_last:ne [dir = forward]
head -- o_head [dir = forward]
head_last -- o_head_last [dir = forward]
{
rank = same
o_head -- o_many_head -- o_head_last -- o_list_first -- o_many_list -- o_list_last
o_list_last -- o_head [constraint = false]
}
o_list -- o_list_first [dir = forward]
o_list -- o_list_last:n [dir = forward]
}
```
#### `list_cut_position()`
* Before
```graphviz
graph {
subgraph ptr {
node [shape = none]
p_head_from [label = "head_from"]
p_head_to [label = "head_to"]
p_node [label = "node"]
p_head_from_first [label = "head_from_first"]
}
subgraph obj {
node [shape = box, label = ""]
o_many_cut, o_many_remain [shape = none, label = "..."]
o_first, o_node [style = filled, color = blue]
o_last o_dummy_from o_dummy_to
}
p_head_from -- o_dummy_from [dir = forward]
p_head_from_first -- o_first [dir = forward]
p_head_to -- o_dummy_to [dir = forward]
p_node -- o_node [dir = forward]
{
rank = same
o_dummy_from -- o_first -- o_many_cut -- o_node -- o_many_remain -- o_last -- o_dummy_from
}
}
```
* After
```graphviz
graph {
subgraph ptr {
node [shape = none]
p_head_from [label = "head_from"]
p_head_to [label = "head_to"]
p_node [label = "node"]
p_head_from_first [label = "head_from_first"]
}
subgraph obj {
node [shape = box, label = ""]
o_many_cut, o_many_remain [shape = none, label = "..."]
o_first, o_node [style = filled, color = blue]
o_last o_dummy_from o_dummy_to
}
p_head_from -- o_dummy_from [dir = forward]
p_head_from_first -- o_first [dir = forward]
p_head_to -- o_dummy_to [dir = forward]
p_node -- o_node [dir = forward]
{
rank = same
o_dummy_from -- o_many_remain -- o_last -- o_dummy_from
o_dummy_to -- o_first -- o_many_cut -- o_node -- o_dummy_to
}
}
```
---
接著來看 Merge Sort 的實作。
#### `list_ele_t`, `queue_t`
```cpp
typedef struct __element {
char *value;
struct __element *next;
struct list_head list;
} list_ele_t;
typedef struct {
list_ele_t *head; /* Linked list of elements */
list_ele_t *tail;
size_t size;
struct list_head list;
} queue_t;
```
從上面定義可以看到 `queue_t` 裡面已經有引入 `struct list_head` 了,但是還又自行維護另一個 singly linked list 。
引入 `struct list_head` 的目的在於可以利用 API 來快速實作完 Merge Sort 。
自行維護 singly linked list 的目的在於實作 `q_free()` 的時候比較方便。
#### `list_merge_sort()`
典型的分治法實作,先將待排序的 linked list 從中間切分成兩個小的 linked list 之後利用遞迴呼叫分別排序完後再將兩個已排序的 linked list 合併成一個已排序的 linked list 。
:::warning
這邊的排序只有對 circular doubly linked list 做排序。
:::
### 指出改進空間並實作
#### 改進空間
在 `queue_t` 中總共有兩種 linked list ,雖然這兩個 linked list 都有各自的存在目的,但是這樣就會出現兩個問題:
1. 會有兩個順序一模一樣的 linked list 存在,差別只在於一個為 circular doubly linked list 而另一個為 singly linked list ,且在這個實作中這兩個 linked list 並不是必須同時存在的,可以只保留其中一個就完成需要的功能。
2. 呼叫完 `list_merge_sort()` 後兩個 linked list 的順序並不一致。
singly linked list 的順序為排序前的順序。
上述兩個問題只要把其中一種 linked list 去掉後就可以同時解決。
這邊我決定保留 `struct list_head` ,因為有前面許多 API 的幫助確實讓 Merge Sort 的實作簡單不少,而且去掉 singly linked list 之後並不會讓 `q_free()` 變得太複雜。
#### 實作
##### `queue_t`, `list_ele_t`
```diff
typedef struct __element {
char *value;
- struct __element *next;
struct list_head list;
} list_ele_t;
typedef struct {
- list_ele_t *head; /* Linked list of elements */
- list_ele_t *tail;
size_t size;
- struct list_head list;
+ struct list_head list; /* Linked list of elements */
} queue_t;
```
首先最基本的就是把 singly linked list 的 link 刪掉。
##### `get_middle()`, `list_merge_sort()`
```diff
-static list_ele_t *get_middle(struct list_head *list)
+static struct list_head *get_middle(struct list_head *list)
{
...
- return list_entry(slow, list_ele_t, list);
+ return slow;
}
void list_merge_sort(queue_t *q)
{
...
INIT_LIST_HEAD(&left.list);
- list_cut_position(&left.list, &q->list, &get_middle(&q->list)->list);
+ list_cut_position(&left.list, &q->list, get_middle(&q->list));
list_merge_sort(&left);
list_merge_sort(q);
...
}
```
和之前提到的兩個問題沒什麼關係,但是也是可以簡化的部分。
`get_middle()` 只有在 `list_merge_sort()` 中被呼叫到,且原本的作法為回傳中間點的 `list_ele_t` ,然後 `list_merge_sort()` 中使用的時候還是取 `list_ele_t` 中的 `struct list_head` 成員。
因此改成直接回傳 `struct list_head *` 就可以省下一些不必要的操作且可以讓程式碼更好閱讀。
##### `q_new()`
```diff
static queue_t *q_new()
{
queue_t *q = malloc(sizeof(queue_t));
if (!q)
return NULL;
- q->head = q->tail = NULL;
q->size = 0;
INIT_LIST_HEAD(&q->list);
return q;
}
```
##### `q_free()`
以下為使用 `list_for_each` 的作法:
```cpp
static void q_free(queue_t *q)
{
if (!q)
return;
struct list_head *current;
list_for_each(current, &q->list) {
list_ele_t *tmp = list_entry(current, list_ele_t, list);
free(tmp->value);
free(tmp);
}
free(q);
```
但是因為 `current` 為指向 `list_ele_t` 的成員的指標,然後在迴圈執行完 `free(ele)` 之後 `current` 指向的物件的生命週期已經結束,但是又會因為 `list_for_each` 而執行 `current = current->next` ,因此就造成了 heap-use-after-free 漏洞。
解決的方法為使用 Linux 核心 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 中的 `list_for_each_safe` 。
原理為事先紀錄下一個要走到的節點 `n`,迴圈執行完之後就用 `pos = n` 而不是 `pos = pos->next` 來走到下一個節點,如此一來就不會用到已經被釋放的空間。
:::warning
TODO: 解釋 Linux 核心中以 `_safe` 結尾的 API 背後的設計考量,並舉出 Linux 核心原始程式碼的案例
:notes: jserv
:::
最終修改:
```diff
+/**
+ * list_for_each_safe - iterate over a list safe against removal of list entry
+ * @pos: the &struct list_head to use as a loop cursor.
+ * @n: another &struct list_head to use as temporary storage
+ * @head: the head for your list.
+ */
+#define list_for_each_safe(pos, n, head) \
+ for (pos = (head)->next, n = pos->next; pos != (head); \
+ pos = n, n = pos->next)
static void q_free(queue_t *q)
{
if (!q)
return;
- list_ele_t *current = q->head;
- while (current) {
- list_ele_t *tmp = current;
- current = current->next;
+ struct list_head *current, *next;
+ list_for_each_safe (current, next, &q->list) {
+ list_ele_t *tmp = list_entry(current, list_ele_t, list);
free(tmp->value);
free(tmp);
}
free(q);
}
```
##### `q_insert_head()`
```diff
bool q_insert_head(queue_t *q, char *s)
{
...
newh->value = new_value;
- newh->next = q->head;
- q->head = newh;
- if (q->size == 0)
- q->tail = newh;
q->size++;
list_add_tail(&newh->list, &q->list);
}
```
---
## 測驗 `2`
### 運作原理
函式最後的回傳為 `return (n + 1) >> 1;` ,如果有辦法把 `n` 中最高位的 1 以下的位元也都設為 1 ,則這個 `return` 的寫法就會回傳預期的值。
1. `n |= n >> 1` 會將從最高位的 1 算起的 2 個位元設為 1 。
2. `n |= n >> 2` 又會將從最高位的 1 算起的 4 個位元設為 1 。
> 因為有了 1. 的操作,這行程式碼才有辦法確保將 4 個位元設為 1 的效果
而之後就以此類推。
因為參數為 16 位元的整數,所以最多可能需要將 16 個位元設為 1 ,也因為最多只需要設 16 個位元,最後只有停在 `n |= n >> 8` 而沒有繼續 `n |= n >> 16` 。
且根據 C11 6.7.5.2
> The integer promotions are performed on each of the operands. The type of the result is
> that of the promoted left operand. If the value of the right operand is negative or is
> greater than or equal to the width of the promoted left operand, the behavior is undefined.
當 `>>` 運算子的位移量大於或等於數字的長度,這個操作是 Undefined Behavior ,所以不應該出現 `n |= n >> 16` 。
### `is_power_of_2()`
```cpp
/**
* is_power_of_2() - check if a value is a power of two
* @n: the value to check
*
* Determine whether some value is a power of two, where zero is
* *not* considered a power of two.
* Return: true if @n is a power of 2, otherwise false.
*/
static inline __attribute__((const))
bool is_power_of_2(unsigned long n)
{
return (n != 0 && ((n & (n - 1)) == 0));
}
```
從註解可以看到 0 不算是一個二的冪,因此在 `return` 的時候就先把這個情況特判回傳 `false` ,所以接下來只考慮 `n != 0` 的情況。
因為 `n` 大於 0 ,所以 `n` 可以視為一個以上不同的二的冪的和,也就是說 $n = 2^{x_0} + 2^{x_1} + \dots$ 。
對於一個二的冪,一定會符合 `(n & (n - 1)) == 0` 的情況,以 `32` 來舉例, `32` 是二的冪,以八位元的二進位表示就是 `00100000` ,`32 - 1 = 31` 以二進位表示就是 `00011111` ,可以看到 `00100000 & 00011111` 的結果就是 `0` ,因為沒有任何一個位置的位元同時是 1 。
如果 `n` 不是二的冪,所以就可以將 `n` 看成**兩個**以上不同的二的冪的和,而 `n - 1` 只會對最小的二的冪造成影響,其餘的二的冪還是保持原樣,所以可以確定如果 `n` 不是二的冪的話 `(n & (n - 1)) == 0` 一定不成立。
以下用 `40` 來舉例,`40` 的二進位表示為 `00101000` ,可以看成 `00100000 + 00001000` ,然後將 `40 - 1 = 39` 以二進位表示就是 `00100111` ,這邊可以視為 `00100000 + 00000111` ,可以看到減一的效果只有作用在 `00001000` 上面,並不會影響到 `00100000` ,所以不管有沒有減一 `00100000` 都會存在,並且這會造成做 bitwise and 運算的時候結果不為 0 ,也就讓判斷式回傳 `false` 。
### `__rounddown_pow_of_two()`
```cpp
/**
* __rounddown_pow_of_two() - round down to nearest power of two
* @n: value to round down
*/
static inline __attribute__((const))
unsigned long __rounddown_pow_of_two(unsigned long n)
{
return 1UL << (fls_long(n) - 1);
}
```
首先先看有用到此函式的地方,也就是 `rounddown_pow_of_two()` 這個 macro 的註解,可以看到當 `n` 為 0 的時候結果為未定義。
```cpp
/**
* rounddown_pow_of_two - round the given value down to nearest power of two
* @n: parameter
*
* round the given value down to the nearest power of two
* - the result is undefined when n == 0
* - this can be used to initialise global variables from constant data
*/
#define rounddown_pow_of_two(n) \
( \
__builtin_constant_p(n) ? ( \
(1UL << ilog2(n))) : \
__rounddown_pow_of_two(n) \
)
```
因為實作的想法在數學意義上可以表示成回傳 $2^{\lfloor log_2{n} \rfloor}$ ,而當 `n` 為 0 的時候取 $log$ 則會得到無限大,而 $2^\infty = \infty$ ,因此結果為未定義,使用時也該注意不可傳入 0 。
接著再看 `fls_long()` ,此函式會根據 `unsigned long` 為 32 還是 64 位元來選擇使用 `fls()` 還是 `fls64()` ,這兩個函式的共通點有兩個:
1. 都是回傳最高位的 1 的位置,如果傳入 0 則回傳 0 。
2. 位置的編號從 LSB 到 MSB 都是 `1 ~ 8 * sizeof(unsigned long)`
因為此函式為 round down ,所以不管 `n` 是不是二的冪,回傳的值都會是 `n` 的最高位的 1 ,而最高位的 1 的位置為 `fls_long(n)` ,而 LSB 的位置為 1 ,因此 `fls_long(n) - 1` 的值就是從 1 這個位置 shift 到 `fls_long(n)` 所需要的偏移量,
`1UL << (fls_long(n) - 1)` 則就會是 `n` 的最高位的 1 。
### `__roundup_pow_of_two()`
```cpp
/**
* __roundup_pow_of_two() - round up to nearest power of two
* @n: value to round up
*/
static inline __attribute__((const))
unsigned long __roundup_pow_of_two(unsigned long n)
{
return 1UL << fls_long(n - 1);
}
```
首先當 `n` 為 0 的時候和 [`__rounddown_pow_of_two()`](#__rounddown_pow_of_two) 同理,是未定義。
此函式的原理將分成以下兩種情境來探討:
* `n` 是二的冪
預期回傳 `n` 。
因為 `n` 是二的冪,所以 `fls_long(n - 1)` 就會是 `fls_long(n) - 1` ,所以 `1UL << fls_long(n - 1)` 就會變成 `1UL << (fls_long(n) - 1)` ,也就是 `n` 的最高位的 1 ,然後又因為 `n` 是二的冪,所以 `1UL << (fls_long(n) - 1)` 也就會等於 `n` 。
* `n` 不是二的冪
預期回傳 `1UL << fls_long(n)` 。
和 [`is_power_of_2`](#is_power_of_2) 的解釋中提到的相同,此時 `n` 可以視為兩個以上不同的二的冪的和,且減一的效果只會作用在最小的二的冪,換句話說 `n` 的最高位的 1 不會受到影響,所以 `fls_long(n - 1)` 就等同於 `fls_long(n)` ,也就得到預期的結果了。
---
## 測驗 `3`
## 測驗 `4`