# 2021q1 Homework2 (quiz2) contributed by < `gyes00205` > ###### tags: `linux2021` > [作業要求](https://hackmd.io/@sysprog/ByHRgtofu) ## 測驗一 ### 仿效 Linux 核心 [include/linux/list.h](https://github.com/torvalds/linux/blob/master/include/linux/list.h) 的精簡實作 #### 結構定義 一個環狀雙向 linked list 其結構定義為: ```cpp struct list_head { struct list_head *prev, *next; }; 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; ``` #### list_add_tail 將節點加到 list 尾端 ```cpp static inline void list_add_tail(struct list_head *node, struct list_head *head) { struct list_head *prev = head->prev; prev->next = node; node->next = head; node->prev = prev; head->prev = node; } ``` I. `*prev = head->prev;` ```graphviz digraph linkedlist { layout="circo" node [shape=box]; "prev(f)" [style="filled", fillcolor=lightblue] "head(a)" [style="filled", fillcolor="red"] rankdir=LR { rankdir=LR "head(a)"->b [label="next"] b->c->d->e->"prev(f)"->"head(a)" "head(a)"->"prev(f)" [label="prev"] "prev(f)"->e->d->c->b->"head(a)" } } ``` II. ```cpp prev->next = node; node->next = head; node->prev = prev; head->prev = node; ``` ```graphviz digraph linkedlist { layout="circo" node [shape=box]; "prev(f)" [style="filled", fillcolor=lightblue] "head(a)" [style="filled", fillcolor="red"] "node" [style="filled", fillcolor=green] rankdir=LR { rankdir=LR "head(a)"->b [label="next"] b->c->d->e->"prev(f)"->"node"->"head(a)" "head(a)"->"node" [label="prev"] "node"->"prev(f)"->e->d->c->b->"head(a)" } } ``` #### list_del 並沒有真的釋放節點的記憶體空間,而是把它從原本的串列移除而已。 ```cpp static inline void list_del(struct list_head *node) { struct list_head *next = node->next, *prev = node->prev; next->prev = prev; prev->next = next; } ``` I. `*next = node->next, *prev = node->prev;` ```graphviz digraph linkedlist { layout="circo" node [shape=box]; "prev(a)" [style="filled", fillcolor=lightblue] "next(b)" [style="filled", fillcolor="red"] "node" [style="filled", fillcolor=green] rankdir=LR { rankdir=LR "node"->"next(b)"[label="next"] "next(b)"->c->d->e->f->"prev(a)"->"node" "prev(a)"->f->e->d->c->"next(b)"->"node" "node"->"prev(a)" [label="prev"] } } ``` II. `next->prev = prev; prev->next = next;` ```graphviz digraph linkedlist { layout="circo" node [shape=box]; "prev(a)" [style="filled", fillcolor=lightblue] "next(b)" [style="filled", fillcolor="red"] rankdir=LR { rankdir=LR "next(b)"->c->d->e->f->"prev(a)"->"next(b)" "prev(a)"->f->e->d->c->"next(b)"->"prev(a)" } } ``` #### list_is_singular 判斷是否只有一個節點,如果只有一個節點,就不需要排序 ```cpp static inline int list_is_singular(const struct list_head *head) { return (!list_empty(head) && head->prev == head->next); } ``` #### INIT_LIST_HEAD 初始化空的 list ,next 和 prev 皆指向 head 本身 ```cpp static inline void INIT_LIST_HEAD(struct list_head *head) { head->next = head; head->prev = head; } ``` ```graphviz digraph linkedlist { node [shape=box]; rankdir=LR { rankdir=LR head->head [dir=both] } } ``` ### 解釋 doubly-linked list 的 merge sort #### 1. get_middle ```cpp static list_ele_t *get_middle(struct list_head *list) { struct list_head *fast = list->next, *slow; list_for_each (slow, list) { if (fast->next || fast->next->next) break; fast = fast->next->next; } return list_entry(slow, list_ele_t, list); } ``` 其中 `list_for_each` 的定義如下 ```cpp #define list_for_each(node, head) \ for (node = (head)->next; node != (head); node = node->next) ``` 這與上個作業 [lab0](https://hackmd.io/ftRycx6nQWqzuYl4f14cVA?view#q_sort) 相似,就是透過 fast 和 slow 的前進速度不同,當 fast 指向 list 尾端,slow 則會指向大約中間的位置。 * 第一次 for 迴圈 ```graphviz digraph linkedlist { layout="circo" node [shape=box]; "list" [style=filled, fillcolor=green] "slow:b" [fontcolor=red]; "fast:d" [fontcolor=blue]; rankdir=LR { rankdir=LR list->"slow:b"->c->"fast:d"->e->f->list } } ``` * 第二次 for 迴圈 ```graphviz digraph linkedlist { layout="circo" node [shape=box]; "list" [style=filled, fillcolor=green] "slow:c" [fontcolor=red]; "fast:f" [fontcolor=blue]; rankdir=LR { rankdir=LR list->b->"slow:c"->d->e->"fast:f"->list } } ``` * 第三次 for 迴圈,此時因為 `fast->next == list`,所以離開迴圈,這時候 slow 指向 d ```graphviz digraph linkedlist { layout="circo" node [shape=box]; "list" [style=filled, fillcolor=green] "slow:d" [fontcolor=red]; "fast:f" [fontcolor=blue]; rankdir=LR { rankdir=LR list->b->c->"slow:d"->e->"fast:f"->list } } ``` ```cpp typedef struct __element { char *value; struct __element *next; struct list_head list; } list_ele_t; ``` 得到 slow 的位置後,再透過 `list_entry(slow, list_ele_t, list)` 得到 `struct list_ele_t` 的位址。 :::info 這裡的 list 是 `struct list_ele_t` 中的 member 名稱 ::: #### 2. list_merge 一開始先判斷 `lhs` 和 `rhs` 是否為空,如果為空則不需要排序,直接回傳就好。 ```cpp static void list_merge(struct list_head *lhs, struct list_head *rhs, struct list_head *head) { INIT_LIST_HEAD(head); if (list_empty(lhs)) { list_splice_tail(lhs, head); return; } if (list_empty(rhs)) { list_splice_tail(rhs, head); return; } while (!list_empty(lhs) && !list_empty(rhs)) { char *lv = list_entry(lhs->next, list_ele_t, list)->value; char *rv = list_entry(rhs->next, list_ele_t, list)->value; struct list_head *tmp = strcmp(lv, rv) <= 0 ? lhs->next : rhs->next; list_del(tmp); list_add_tail(tmp, head); } list_splice_tail(list_empty(lhs) ? rhs : lhs, head); } ``` 這裡的 `list_splice_tail(lhs, head);` 和 `list_splice_tail(lhs, head);` 有多此一舉的效果,因為在 `list_splice_tail` 中會再判斷一次 `lhs` 和 `rhs` 是否為空,所以可以直接移除。 ```diff if (list_empty(lhs)) { - list_splice_tail(lhs, head); return; } if (list_empty(rhs)) { - list_splice_tail(rhs, head); return; } ``` :::warning 可簡化為 `if (list_empty(lhs) || list_empty(rhs)) return;` :notes: jserv ::: I. 初始狀態 ```graphviz digraph linkedlist { node [shape=box]; "lhs" [style="filled", fillcolor=green] "rhs" [style="filled", fillcolor=green] "head" [style="filled", fillcolor=lightblue] rankdir=LR { rankdir=LR "lhs"->a [label="next"] a->d->"lhs" } { rankdir=LR "rhs"->b [label=next] b->c->"rhs" } { head->head [dir=both] } } ``` II. 第一次 while 迴圈 ```graphviz digraph linkedlist { node [shape=box]; "lhs" [style="filled", fillcolor=green] "rhs" [style="filled", fillcolor=green] "head" [style="filled", fillcolor=lightblue] rankdir=LR { rankdir=LR "lhs"->d [label="next"] d->"lhs" } { rankdir=LR "rhs"->b [label=next] b->c->"rhs" } { head->a [label=next] a->head } } ``` III. 第二次 while 迴圈 ```graphviz digraph linkedlist { node [shape=box]; "lhs" [style="filled", fillcolor=green] "rhs" [style="filled", fillcolor=green] "head" [style="filled", fillcolor=lightblue] rankdir=LR { rankdir=LR "lhs"->d [label="next"] d->"lhs" } { rankdir=LR "rhs"->c [label=next] c->"rhs" } { head->a [label=next] a->b->head } } ``` IIII. 第三次 while 迴圈 ```graphviz digraph linkedlist { node [shape=box]; "lhs" [style="filled", fillcolor=green] "rhs" [style="filled", fillcolor=green] "head" [style="filled", fillcolor=lightblue] rankdir=LR { rankdir=LR "lhs"->d [label="next"] d->"lhs" } { rankdir=LR "rhs"->"rhs" [dir=both] } { head->a [label=next] a->b->c->head } } ``` V. 因為 `rhs` 為空,所以離開 while 迴圈,並將 `lhs` 接在 `head` 尾端。 ```graphviz digraph linkedlist { node [shape=box]; "head" [style="filled", fillcolor=lightblue] rankdir=LR { head->a [label=next] a->b->c->d->head } } ``` #### 3. list_merge_sort `q->list` 是要做 merge sort 的串列,而 `left.list` 剛做完初始化所以還是空的串列。 ```cpp void list_merge_sort(queue_t *q) { if (list_is_singular(&q->list)) return; queue_t left; struct list_head sorted; INIT_LIST_HEAD(&left.list); list_cut_position(&left.list, &q->list, &get_middle(&q->list)->list); list_merge_sort(&left); list_merge_sort(q); list_merge(&left.list, &q->list, &sorted); INIT_LIST_HEAD(&q->list); list_splice_tail(&sorted, &q->list); } ``` * `list_cut_position(&left.list, &q->list, &get_middle(&q->list)->list);` 將 `q` 分成兩半,一邊是 `left` 一邊是 `q` ```cpp static inline void list_cut_position(struct list_head *head_to, struct list_head *head_from, struct list_head *node) { struct list_head *head_from_first = head_from->next; if (list_empty(head_from)) return; if (head_from == node) { INIT_LIST_HEAD(head_to); return; } head_from->next = node->next; head_from->next->prev = head_from; head_to->prev = node; node->next = head_to; head_to->next = head_from_first; head_to->next->prev = head_to; } ``` I. 起始狀態 ```graphviz digraph linkedlist { node [shape=box]; rankdir=LR { rankdir=LR head_to->head_to [dir=both] } { rankdir=LR head_from->1->2->3->head_from } { rank=same "node"->2 } { rank=same "head_from_first"->1 } "node" [shape=plaintext, fontcolor=blue] "head_from_first" [shape=plaintext, fontcolor=blue] } ``` II. 做完 `list_cut_position` 後 ```graphviz digraph linkedlist { node [shape=box]; rankdir=LR { rankdir=LR head_to->1->2->head_to } { rankdir=LR head_from->3->head_from } { rank=same "node"->2 } { rank=same "head_from_first"->1 } "node" [shape=plaintext, fontcolor=blue] "head_from_first" [shape=plaintext, fontcolor=blue] } ``` ```cpp list_merge_sort(&left); list_merge_sort(q); list_merge(&left.list, &q->list, &sorted); ``` 透過遞迴將 `left` 和 `q` 繼續分成兩半,直到剩下一個節點或串列為空。接著將排序好的串列合併。 ```cpp INIT_LIST_HEAD(&q->list); list_splice_tail(&sorted, &q->list); ``` `sorted` 是排序好的串列,不過他的資料結構為 `struct list_head` ,所以重新初始化 `queue_t q` ,並將 `sorted` 接在 `queue_t q` 上 --- ## 測驗二 考慮函式 `func` 接受一個 16 位元無號整數 $N$ ,並回傳小於或等於 $N$ 的 power-of-2 ```cpp uint16_t func(uint16_t N) { /* change all right side bits to 1 */ N |= N >> 1; N |= N >> 2; N |= N >> 4; N |= N >> 8; return (N + 1) >> 1; } ``` 首先將遇到的第一個值為 1 的 bit 的右邊所有 bit 都設為 1,最後 +1 並往右移一個 bit 以 N = $32768_{10}$ = $1000000000000000_2$ 為例 * `N |= N >> 1;` $N$ = $1100000000000000_2$ * `N |= N >> 2;` $N$ = $1111000000000000_2$ * `N |= N >> 4;` $N$ = $1111111100000000_2$ * `N |= N >> 8;` $N$ = $1111111111111111_2$ * `(N + 1) >> 1;` $N$ = $1000000000000000_2$ :::warning N + 1 已經 overflow 了,那為甚麼 (N + 1) >> 1 不會出錯呢? 改進方法: 參考[你所不知道的 C 語言:數值系統](/@sysprog/c-numerics) >* 比方說 `(x + y) / 2` 這樣的運算,有個致命問題在於 (x + y) 可能會導致 overflow (考慮到 x 和 y 都接近 [UINT32_MAX](https://msdn.microsoft.com/en-us/library/mt764276.aspx),亦即 32-bit 表示範圍的上限之際) * [Binary search 的演算法提出之後十年才被驗證](https://www.comp.nus.edu.sg/~sma5503/recitations/01-crct.pdf) * 於是我們可以改寫為以下: ``` (x & y) + ((x ^ y) >> 1) ``` >* 用加法器來思考: x & y 是進位, x ^ y 是位元和, / 2 是向右移一位 >* 位元相加不進位的總和: x ^ y; 位元相加產生的進位值: (x & y) << 1 >* x + y = x ^ y + ( x & y ) << 1 >* 所以 (x + y) / 2 = (x + y) >> 1 = (x ^ y + (x & y) << 1) >> 1 = (x & y) + ((x ^ y) >> 1) ```diff= - (N + 1) >> 1 + (N & 1) + ((N ^ 1) >> 1) ``` ::: --- ## 測驗三 ### bitcpy #### 初始化變數 `read_lhs` 為位元組中,不需要複製的部分,真正需要複製的地方為 `read_rhs` ;同理 `write_lhs` 為不需要寫入的部分, `write_rhs` 才是需要寫入的部分。 `source` 為開始複製的位元組, `dest` 為開始寫入的位元組。 :::info `source` 和 `dest` 的單位為 bytes 的位址 。 `read_lhs` , `read_rhs` , `read_lhs` , `read_rhs` 的單位是 bits ::: 第 3 行 `(_read / 8);` 可以改寫為 `(_read >> 3);` 平移花的成本較除法低;同理 `(_write / 8)` 也可以改寫為 `(_write >> 3)`。 ```diff= size_t read_lhs = _read & 7; size_t read_rhs = 8 - read_lhs; - const uint8_t *source = (const uint8_t *) _src + (_read / 8); + const uint8_t *source = (const uint8_t *) _src + (_read >> 3); size_t write_lhs = _write & 7; size_t write_rhs = 8 - write_lhs; - uint8_t *dest = (uint8_t *) _dest + (_write / 8); + uint8_t *dest = (uint8_t *) _dest + (_write >> 3); ``` ```cpp= while (count > 0) { uint8_t data = *source++; size_t bitsize = (count > 8) ? 8 : count; if (read_lhs > 0) { data <<= read_lhs; if (bitsize > read_rhs) data |= (*source >> read_rhs); } if (bitsize < 8) data &= read_mask[bitsize]; uint8_t original = *dest; uint8_t mask = read_mask[write_lhs]; if (bitsize > write_rhs) { /* Cross multiple bytes */ *dest++ = (original & mask) | (data >> write_lhs); original = *dest & write_mask[bitsize - write_rhs]; *dest = original | (data << write_rhs); } else { // Since write_lhs + bitsize is never >= 8, no out-of-bound access. mask |= write_mask[write_lhs + bitsize]; *dest++ = (original & mask) | (data >> write_lhs); } count -= bitsize; } ``` * 複製資料 `data` 複製 `source` 所指向的位元組內容。 `bitsize` 為複製的 bits 數,每次最多複製 8-bit 。第 4 行判斷複製的 bit 有沒有對齊 MSB ,沒有的話就將 data 左移。第 6 行判斷有沒有跨位元組,有的話就將 `source` 右移(此時的 `source` 已經是下一個位元組了),並和 `data` 做 bitwise or operation,也就是將資料複製到 `data` 的 right hand side 。第 10 行把超過 `bitsize` 的部分改為 0 避免寫入。 * 寫入資料 第 15 行 `if (bitsize > write_rhs)` 判斷是否需要跨位元組。 如果需要的話,`*dest++ = (original & mask) | (data >> write_lhs);` 先將 `data` 的一部分寫入 `dest` 右半邊。 `original = *dest & write_mask[bitsize - write_rhs];` 保留 `dest` 的右半部(此時的 `dest` 已經到下一個位元組),`*dest = original | (data << write_rhs);` 將 `data` 剩下的部分複製到 `dest`。 如果不需要的話,代表 `bitsize + write_lhs <= 8` , `*dest++ = (original & mask) | (data >> write_lhs);` 將 `data` 寫入 `dest`。 第 4~11 行可以簡化如下,省去判斷的成本。 ```diff - if (read_lhs > 0) { - data <<= read_lhs; - if (bitsize > read_rhs) - data |= (*source >> read_rhs); - } - if (bitsize < 8) - data &= read_mask[bitsize]; + data <<= read_lhs; + data |= (*source >> read_rhs); + data &= read_mask[bitsize]; ``` --- ## 測驗四 #### `cstr.h` ```cpp enum { CSTR_PERMANENT = 1, CSTR_INTERNING = 2, CSTR_ONSTACK = 4, }; ``` * `CSTR_PERMANENT` : TODO * `CSTR_INTERNING` : 常數字串 * `CSTR_ONSTACK` : 字串變數 ```cpp typedef struct __cstr_data { char *cstr; uint32_t hash_size; uint16_t type; uint16_t ref; } * cstring; ``` * cstr : 字串內容 * hash_size : 字串長度 * type : 字串類型(`CSTR_PERMANENT`, `CSTR_INTERNING`, `CSTR_ONSTACK`) * ref : 和多少字串共用記憶體空間 ```cpp typedef struct __cstr_buffer { cstring str; } cstr_buffer[1]; ``` ```cpp #define CSTR_BUFFER(var) \ char var##_cstring[CSTR_STACK_SIZE] = {0}; \ struct __cstr_data var##_cstr_data = {var##_cstring, 0, CSTR_ONSTACK, 0}; \ cstr_buffer var; \ var->str = &var##_cstr_data; ``` 初始化變數類型的 `var` 以及相關變數, [the ## operator](https://www.ibm.com/docs/en/zos/2.3.0?topic=mdd-operator) 可以用來連接變數名稱。宣告一個 char array , size 為 `CSTR_STACK_SIZE (128)` 並且初始化為 0 並且 assign 給 `__cstr_data` 的 `cstr` 。最後 assign 給 `cstr_buffer` 的 `str` 。 ```cpp #define CSTR_LITERAL(var, cstr) \ static cstring var = NULL; \ if (!var) { \ cstring tmp = cstr_clone("" cstr, (sizeof(cstr) / sizeof(char)) - 1); \ if (tmp->type == 0) { \ tmp->type = CSTR_PERMANENT; \ tmp->ref = 0; \ } \ if (!__sync_bool_compare_and_swap(&var, NULL, tmp)) { \ if (tmp->type == CSTR_PERMANENT) \ free(tmp); \ } \ } ``` 初始化常數類型的字串 ### cstr_cat ```cpp= cstring cstr_cat(cstr_buffer sb, const char *str) { cstring s = sb->str; if (s->type == CSTR_ONSTACK) { int i = s->hash_size; while (i < CSTR_STACK_SIZE - 1) { s->cstr[i] = *str; if (*str == 0) return s; ++s->hash_size; ++str; ++i; } s->cstr[i] = 0; } cstring tmp = s; sb->str = cstr_cat2(tmp->cstr, str); cstr_release(tmp); return sb->str; } ``` 第 4~15 行 * 必須是 `CSTR_ONSTACK` * str 已經全部接在 s 後面,且 s 的長度小於 `CSTR_STACK_SIZE - 1` 則直接回傳 s 。 * 如果 s 的長度大於等於 `CSTR_STACK_SIZE - 1` 則要透過 `cstr_cat2` 連接字串。 ### cstr_cat2 ```cpp= static cstring cstr_cat2(const char *a, const char *b) { size_t sa = strlen(a), sb = strlen(b); if (sa + sb < CSTR_INTERNING_SIZE) { char tmp[CSTR_INTERNING_SIZE]; memcpy(tmp, a, sa); memcpy(tmp + sa, b, sb); tmp[sa + sb] = 0; return cstr_interning(tmp, sa + sb, hash_blob(tmp, sa + sb)); } cstring p = xalloc(sizeof(struct __cstr_data) + sa + sb + 1); if (!p) return NULL; char *ptr = (char *) (p + 1); p->cstr = ptr; p->type = 0; p->ref = 1; memcpy(ptr, a, sa); memcpy(ptr + sa, b, sb); ptr[sa + sb] = 0; p->hash_size = 0; return p; } ``` 第 4~10 行,如果長度小於 `CSTR_INTERNING_SIZE(32)` 會進入 `cstr_interning` ,利用 `hash_blob` 產生 hash value 。 第 11 行,如果長度大於等於 `CSTR_INTERNING_SIZE(32)` ,則分配記憶體空間給 `p` , 其中 `ptr` 的記憶體位置在 `p` 的尾端。 :::warning 因為 `CSTR_INTERNING_SIZE(32)` `CSTR_STACK_SIZE(128)` ,所以第 4 行的 if 永遠進不去 ::: ### cstring_interning ```cpp= static cstring cstr_interning(const char *cstr, size_t sz, uint32_t hash) { cstring ret; CSTR_LOCK(); ret = interning(&__cstr_ctx, cstr, sz, hash); if (!ret) { expand(&__cstr_ctx); ret = interning(&__cstr_ctx, cstr, sz, hash); } ++__cstr_ctx.total; CSTR_UNLOCK(); return ret; } ```