--- title: 2023q1 Homework3 (quiz3) tags: Linux核心設計 --- # [2023q1 Homework3 (quiz3)](https://hackmd.io/@sysprog/linux2023-quiz3) ## [測驗一](https://hackmd.io/@sysprog/linux2023-quiz3#%E6%B8%AC%E9%A9%97-1) ### 程式碼原理 在 [include/linux/rbtree_types.h](https://github.com/torvalds/linux/blob/master/include/linux/rbtree_types.h) 中 `tree node` 宣告為 ```c struct rb_node { unsigned long __rb_parent_color; struct rb_node *rb_right; struct rb_node *rb_left; } __attribute__((aligned(sizeof(long)))); ``` 而在 [incomplete treesort.c](https://gist.github.com/jserv/920766d1fd893d0efd66ac7fdaf23401) 中 `tree node` 宣告為 ```c typedef struct __node { uintptr_t color; struct __node *left, *right; struct __node *next; long value; } node_t __attribute__((aligned(sizeof(long)))); ``` 在看完 [Linux 核心的紅黑樹](https://hackmd.io/@sysprog/linux-rbtree#Linux-%E6%A0%B8%E5%BF%83%E7%9A%84%E7%B4%85%E9%BB%91%E6%A8%B9%E5%AF%A6%E4%BD%9C) 得知,透過 `__attribute__((aligned(sizeof(long))))` 讓編譯器知道 `struct rb_node` 會對齊 `sizeof(long)`,這樣就會讓指標最低 2 個位元是沒有使用到的,就可以把顏色的資料放進去其中一個位元中。舉例來說,一個節點指標如果指向 `0xF724315C` ,這個位址轉成二進位會變成 `...1100`,最低 2 個位元會是 0,Linux 核心開發者利用這特性,將其中一個沒用到的位元拿來標注紅和黑這二種顏色。 ```c #define rb_parent(r) ((node_t *) (AAAA)) #define rb_color(r) ((color_t) (r)->color & 1) ``` 可以看到會將 `color` 的最後一個 bit 當作顏色來使用,而 Linux kernel 會將親代節點跟顏色一起宣告,因為指標會對齊 `sizeof(long)` ,也就是 8 個 bytes ,所以指標的地址會是 8 的倍數,所以指標的最後 3 個 bits 不會使用到,故將指標最後 3 個 bits 設成 0 ,就能得到親代節點的地址,也就是將地址與 `11....11000` 作 and ,而 `11....11000` 只要將 `00...00111` ,也就是 `7` 取反位元運算就能得到,所以 `AAAA` 為 `(r)->color & ~7` 。 ```c typedef enum { CMAP_RED = 0, CMAP_BLACK } color_t; ``` 可以看到將節點顏色紅色設成 0,黑色設成 1。 ```c #define rb_set_red(r) \ do { \ BBBB; \ } while (0) ``` 要將節點設成紅色的,只須將最後一個 bit 設成 0 即可,也就是將地址與 `11....11110` 作 and,而 `11....11110` 只要將 `00...00001` ,也就是 `1` 取反位元運算就能得到,所以 `BBBB` 為 `(r)->color &= ~1`。 ```c #define rb_set_black(r) \ do { \ CCCC; \ } while (0) ``` 將節點設成紅色的,只須將最後一個 bit 設成 1 即可,所以只須 `or 1` 即可,故 `CCCC` 為 `(r)->color |= 1` 。 ```c for (node_t *cur = obj->head;;) { int res = obj->comparator(&node->value, &cur->value); if (!res) /* If the key matches something else, don't insert */ assert(0 && "not support repetitive value"); if (res < 0) { if (!cur->left) { cur->left = node; rb_set_parent(node, cur); cmap_fix_colors(obj, node); break; } DDDD; } else { if (!cur->right) { cur->right = node; rb_set_parent(node, cur); cmap_fix_colors(obj, node); break; } EEEE; } } ``` 再來看到函式 `cmap_insert` ,主要是用來加入節點到紅黑樹中,變數 `res` 存放的是插入節點與目前走訪節點的比較值,0 表示相等,-1 表示插入節點小於目前走訪節點,1 表示插入節點大於目前走訪節點。 當 `res = 0` 時,則表示紅黑樹中已經有相同的數存在,所以不須插入。當 `res = -1` 時,會有兩種情形,第一種情形是目前走訪節點沒有左子節點,則將要插入節點設成目前走訪節點的左子節點,再調整顏色跟節點關係,第二種情形是目前走訪節點有左子節點,因為尚未得知後續節點的值大小,所以需要繼續往左子節點走訪,故 `DDDD` 為 `cur = cur->left` ,而 `res = -1` 時,也為同理,繼續往右子點走訪,故 `EEEE` 為 `cur = cur->right` 。 ```c void tree_sort(node_t **list) { node_t **record = list; cmap_t map = cmap_new(sizeof(long), sizeof(NULL), cmap_cmp_int); while (*list) { cmap_insert(map, *list, NULL); list = FFFF; } node_t *node = cmap_first(map), *first = node; for (; node; node = cmap_next(node)) { *list = node; list = GGGG; } HHHH; *record = first; free(map); } ``` 再來是函式 `tree_sort` ,執行 `tree_sort` 須走訪每一個節點,所以一開始將節點一個個插入紅黑樹中,所以 `FFFF` 應為往下一個節點移動,`list` 為指向節點指標的指標,所以應先取出指標的地址 `*list` ,再找出下一個節點 `(*list)->next` ,再將這個指標的位址給 `list` ,所以 `FFFF` 為 `&(*list)->next` 。 ```c static node_t *cmap_first(cmap_t obj) { node_t *n = obj->head; if (!n) return NULL; while (n->left) n = n->left; return n; } ``` 函式 `cmap_first` 為找出最小的節點,而 `cmap_next` 會找出比目前節點還大,但是比其他所有節點還小的點,再將他們串接起來,所以 `GGGG` 跟 `FFFF` 一樣為 `&(*list)->next`,最後當所有節點走訪完,將最後一個節點指向 `NULL`,所以 `HHHH` 為 `*list = NULL` 。 ### 答案 :::success * `AAAA = r->color & ~7` * `BBBB = r->color &= ~1` * `CCCC = r->color |= 1` * `DDDD = cur = cur->left` * `EEEE = cur = cur->right` * `FFFF = &(*list)->next` * `GGGG = &(*list)->next` * `HHHH = *list = NULL` ::: --- ## [測驗二](https://hackmd.io/@sysprog/linux2023-quiz3#%E6%B8%AC%E9%A9%97-2) ### 程式碼原理 ```c struct avl_node { unsigned long parent_balance; struct avl_node *left, *right; } AVL_NODE_ALIGNED; ``` ```c #define AVL_NODE_ALIGNED __attribute__((aligned(sizeof(unsigned long)))) ``` 架構與測驗一紅黑樹類似,指標也是對齊 `sizeof(long)` 。 ```c static inline struct avl_node *avl_parent(struct avl_node *node) { return (struct avl_node *) (IIII); } ``` 故要取得親代節點的位址,只須將最後 3 個 bits 設成 0 即可,所以 `IIII` 為 `node->parent_balance & ~7` 。 ```c enum avl_node_balance { AVL_NEUTRAL = 0, AVL_LEFT, AVL_RIGHT, }; ``` 再來看到 `balance` 的定義,當左右子樹高度相等時為 0 ,當左子樹較右子樹高 1 時為 1 ,當右子樹較左子樹高 1 時為 2 ,所以總共需要 2 個 bits 來表示。 ```c static inline enum avl_node_balance avl_balance(const struct avl_node *node) { return (enum avl_node_balance)(JJJJ); } ``` 所以只須取得最右邊 2 個 bits 即可,故 `JJJJ` 為 `node->parent_balance &= 3` 。 ```c static void avl_set_parent(struct avl_node *node, struct avl_node *parent) { node->parent_balance = (unsigned long) parent | (KKKK); } ``` 再來要設定新的親代節點時,需要保留 `balance` 的資訊,也就是最右邊 3 個 bits,剩下的 bit 為舊的親代節點位址,故只須與舊位址的最右邊 3 個 bits `or` 即可,故 `KKKK` 為 `node->parent_balance & 3` 。 ```c static void avl_set_balance(struct avl_node *node, enum avl_node_balance balance) { node->parent_balance = (LLLL) | balance; } ``` 而要設定新的 `balance` 資訊時,只須留下除了最右邊 3 個 bits 以外的 bits 即可,故 `LLLL` 為 `node->parent_balance & ~7` 。 ```c else { switch (avl_balance(parent)) { default: case AVL_RIGHT: /* new left child + right leaning == balanced * nothing to propagate upwards after that */ avl_set_balance(parent, AVL_NEUTRAL); parent = NULL; break; case AVL_NEUTRAL: /* mark balance as left and continue upwards */ avl_set_balance(parent, AVL_LEFT); break; case AVL_LEFT: /* compensate double left balance by rotation * and stop afterwards */ switch (avl_balance(node)) { default: case AVL_LEFT: case AVL_NEUTRAL: MMMM(node, parent, root); break; case AVL_RIGHT: NNNN(node, parent, root); break; } parent = NULL; break; } } ``` 函式 `avl_insert_balance` 主要是用來平衡 AVL tree,主要會有 4 種情形需要做 rotation,分別是 LL 型、 LR 型、 RR 型、 RL 型,此 `else` 為 L 情形,所以 `case AVL_NEUTRAL:` 為 LL 型,而 `case AVL_RIGHT:` 為 LR 型,故 LL 型須做右旋,所以 `MMMM` 為 `avl_rotate_right` ,而 LR 型須先做左旋,再做右旋,所以 `NNNN` 為 `avl_rotate_leftright` 。 ### 答案 :::success * `IIII = node->parent_balance & ~7` * `JJJJ = node->parent_balance & 3` * `KKKK = node->parent_balance & 3` * `LLLL = node->parent_balance & ~7` * `MMMM = avl_rotate_right` * `NNNN = avl_rotate_leftright` ::: --- ## [測驗三](https://hackmd.io/@sysprog/linux2023-quiz3#%E6%B8%AC%E9%A9%97-3) ### 程式碼原理 ```c static const char log_table_256[256] = { #define _(n) n, n, n, n, n, n, n, n, n, n, n, n, n, n, n, n -1, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, _(4), _(5), _(5), _(6), _(6), _(6), _(6), _(7), _(7), _(7), _(7), _(7), _(7), _(7), _(7), #undef _ }; ``` `log_table_256` 可以處理的以 2 為底的 x 的對數,可以看到只有 $2^8$ ,所以若要處理 64 bit 的數值,只能將數值拆解成每 8 bit 進行處理。 ```c uint64_t log2_64(uint64_t v) { unsigned r; uint64_t t, tt, ttt; ttt = v >> 32; if (ttt) { tt = ttt >> 16; if (tt) { t = tt >> 8; if (t) { r = AAAA + log_table_256[t]; } else { r = BBBB + log_table_256[tt]; } } else { t = ttt >> 8; if (t) { r = CCCC + log_table_256[t]; } else { r = DDDD + log_table_256[ttt]; } } } ... } ``` 函式 `log2_64` 是用來計算以 2 為底的 x 的對數 (logarithm of x to the base b),且下取整函數 (floor),因為 `log_table_256` 只能處理 $2^8$ 的數,所以不難看出須以每 8 個 bit 進行處理。`t` 為將輸入右移 56 個 bit 的數,這裡以 $2^{57}$ 為例,`t` 會等於 1,而 $2^{57}$ 取 log 會等於 57 ,所以我們可以知道 output `r` 為 57 ,所以 `AAAA + log_table_256[1]` = 57 ,而 `log_table_256[1]` 查表為 0 ,所以 `AAAA` 為 56 。 ``` 2^57 >> 56 = 00...001 t = 1 log_table_256[1] = 1 AAAA + log_table_256[1] = 57 AAAA + 1 = 57 AAAA = 56 ``` 而 `BBBB` 為輸入大於 48 個 bit ,小於 56 個 bit 的情形,所以 `BBBB` 可以推得為 48 ,以此類推,以每 8 bit 為範圍去分。 ```c static void lfsr(uint64_t *up) { uint64_t new_bit = ((*up) ^ ((*up) >> 3) ^ ((*up) >> 30) ^ ((*up) >> 55)) & 1u; /* don't have to map '+1' in polynomial */ *up = ((*up) >> 1) | (new_bit << 63); /* shift *up by 1 to RIGHT and insert new_bit at "empty" position */ } ``` 再來看函式 `lfsr` ,[LFSR](https://en.wikipedia.org/wiki/Linear-feedback_shift_register) 的功用為指給定前一狀態,將該輸出的線性函數作為輸入的移位暫存器,這裡用於產生隨機數。 ```c unsigned int bucket_number(uint64_t x) { uint64_t mask111 = (1 << (N_BITS + 1)) - 1; uint64_t mask011 = (1 << (N_BITS)) - 1; /* one 1 less */ unsigned char leq = ((x & mask111) < N_BUCKETS); /* leq (less or equal) is 0 or 1. */ return (leq * (x & IIII)) + ((1 - leq) * ((x >> (N_BITS + 1)) & JJJJ)); /* 'x >> (N_BITS + 1)' : take different set of bits -> better uniformity. * '... & mask011' guarantees that the result is less or equal N_BUCKETS. */ } ``` 先來看函式 `bucket_number` 其作用為在指定區間內,使產生的 LFSR 數值得以均勻分佈,當 `N_BUCKETS` 為 120 時, `mask111` 與 `mask011` 分別為 127 與 63 ,`lep` 用來判定輸入值的最右邊 7 bit 是否會小於等於 `bucket_number` 。 ``` N_BUCKETS = 120 N_BITS = log2_64(120) = 6 mask111 = (1 << (N_BITS + 1)) - 1 = (1 << (6 + 1)) - 1 = 127 mask011 = (1 << (N_BITS)) - 1 = (1 << (6)) - 1 = 63 ``` 因為`lep` 只為 0 或 1 ,所以 `(leq * (x & IIII)) + ((1 - leq) * ((x >> (N_BITS + 1)) & JJJJ));` 會有兩種 case ``` case1: x & IIII case2: ((x >> (N_BITS + 1)) & JJJJ ``` 當`lep = 1`,則表示輸入值的最右邊 7 bit 會小於等於 `bucket_number`,所以輸出 `x & mask111` 即可,故 `IIII` 為 `mask111` ,而若超過 `bucket_number` 時,則用不同組的 bit ,所以會先右移 `N_BITS` (7) ,因為該值可能會超過 `bucket_number` ,所以與 `mask111` `and` 確保不會超過 `bucket_number` 。 ### 答案 :::success * `AAAA = 56` * `BBBB = 48` * `CCCC = 40` * `DDDD = 32` * `EEEE = 24` * `FFFF = 16` * `GGGG = 8` * `HHHH = 0` * `IIII = mask111` * `JJJJ = mask011` ::: --- ## [測驗四](https://hackmd.io/@sysprog/linux2023-quiz3#%E6%B8%AC%E9%A9%97-4) ### 程式碼原理 ```c int ceil_log2(uint32_t x) { uint32_t r, shift; x--; r = (x > 0xFFFF) << 4; x >>= r; shift = (x > 0xFF) << 3; x >>= shift; r |= shift; shift = (x > 0xF) << 2; x >>= shift; r |= shift; shift = (x > 0x3) << 1; x >>= shift; return (KKKK) + 1; } ``` `x` 分別跟四個不同值比較,分別是 $2^{16}-1$, $2^{8}-1$, $2^{4}-1$, $2^{2}-1$,也就是將 x 拆解成四個區間去判斷,並將其對數大小的結果存於變數 `r` 裡面,,當 `x` 大於第一個區間時,會將 16 存在 `r` 中,並將 `x` 右移 16 個 bit ,再去判斷剩下的 bit 在哪個區間裡,而可以看到在 $2^{2}-1$ 時,並無將其對數大小的結果存於變數 `r` 裡,所以最後須作 `r | shift` ,而這些區間加總為 16 + 8 + 4 + 2 = 30 ,所以若 `x` 大於 $2^{30}$ ,則最後的 `x` 會剩 2 個 bit ,於是將其右移1,去判斷其值是否大於 $2^{30}$ 或大於 $2^{31}$ ,所以 `KKKK` 為 `r | shift | x >> 1`,最後加 1 是為了取其對數的上界。 :::success * `KKKK = r | shift | x >> 1` :::