--- tags: Linux Kernel --- # 2023q1 Homework1 (lab0) contributed by < [`chun61205`](https://github.com/chun61205) > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0\ $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz CPU family: 6 Model: 126 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 5 CPU max MHz: 3600.0000 CPU min MHz: 400.0000 BogoMIPS: 2380.80 ``` ## 資料結構 ### `element_t` :::info 圖待補 ::: ### `queue_contex_t` :::info 圖待補 ::: ## 開發過程 :::success 詳閱作業書寫規範。 :notes: jserv 已解決 ::: ### `q_size` ```c /* Create an empty queue */ int q_size(struct list_head *head) { if (!head) return 0; int len = 0; struct list_head *li; list_for_each (li, head) len++; return len; } ``` 走訪所有節點,已找到佇列的 `size` 。 ### `q_new` ```c /* Create an empty queue */ struct list_head *q_new() { struct list_head *head = malloc(sizeof(struct list_head)); if (head == NULL) return NULL; INIT_LIST_HEAD(head); return head; } ``` 利用 `INIT_LIST_HEAD` 來建立一個空的佇列。 ### `q_free` ```c /* Free all storage used by queue */ void q_free(struct list_head *l) { if (!l || list_empty(l)) { free(l); return; } element_t *entry, *safe; list_for_each_entry_safe (entry, safe, l, list) { q_release_element(entry); } free(l); return; } ``` 利用 `list_for_each_entry_safe` 走訪所有 `entry` ,再藉由 `q_release_element` 釋放記憶體。 ### `q_insert_head` 和 `q_insert_tail` :::danger 避免只列出程式碼,你的想法和遇到的各式開發問題,更為關鍵,HackMD 適合討論,而非張貼大量程式碼。 :notes: jserv ::: ```c /* Insert an element at head of queue */ bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *tmp = malloc(sizeof(element_t)); if (!tmp) return false; size_t s_len = strlen(s); tmp->value = malloc(s_len + 1); if (!(tmp->value)) { free(tmp); return false; } strncpy(tmp->value, s, s_len); tmp->value[s_len] = '\0'; list_add(&(tmp->list), head); return true; } /* Insert an element at tail of queue */ bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *tmp = malloc(sizeof(element_t)); if (!tmp) return false; size_t s_len = strlen(s); tmp->value = malloc(s_len + 1); if (!(tmp->value)) { free(tmp); return false; } strncpy(tmp->value, s, s_len); tmp->value[s_len] = '\0'; list_add_tail(&(tmp->list), head); return true; } ``` 生成要插入的 `entry` 後,分別藉由 `list_add` 和 `list_add_tail` 來插入 `entry` 。 :::info 實測後發現無法滿足時間複雜度 $O(1)$ 的要求,推測原因為 strncpy,預計在研究 [dudect](https://github.com/oreparaz/dudect) 工具和論文 [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf) 後改善。 ::: ### `q_remove_head` 和 `q_remove_tail` ```c /* Remove an element from head of queue */ element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *target = list_first_entry(head, element_t, list); list_del(head->next); if (sp) { strncpy(sp, target->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return target; } /* Remove an element from tail of queue */ element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; element_t *target = list_last_entry(head, element_t, list); list_del(head->prev); if (sp) { strncpy(sp, target->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return target; } ``` 兩個函式分別藉由 `list_first_entry` 和 `list_last_entry` 取得要移除的 `entry` 再藉由 `list_del` 將目標移除掉。 因為 `list_del` 會將目標前後的 `entry` 連接,因此不需要額外進行調整。 此外,這兩個函式的回傳值為 `target->value` 的值。 :::info 實測後發現無法滿足時間複雜度 $O(1)$ 的要求,推測原因為 strncpy,預計在研究 [dudect](https://github.com/oreparaz/dudect) 工具和論文 [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf) 後改善。 ::: ### `q_delete_mid` ```c /* Delete the middle node in queue */ bool q_delete_mid(struct list_head *head) { if (!head || list_empty(head)) return false; struct list_head *left = head->prev, *right = head->next; while (!(left == right) && !(right->prev == left)) { right = right->next; left = left->prev; } list_del(right); q_release_element(list_entry(right, element_t, list)); return true; } ``` 我的想法是使用左右指標,分別從左邊和右邊開始尋訪整個佇列。 如此一來,我們便需要分作兩種情況討論: 1. 節點數為單數,則代表左右指標相同時,它們所指向的便是中間的節點。 2. 節點數為複數,則代表右指標會超過一格,因此中間的節點會出現在 `right->prev == left` ### `q_delete_dup` 我的想法為 只要使用兩個連續的指標, `curr_node` 和 `next_node` ,並尋訪整個佇列,便能夠直接透過比較的方式來查看相鄰兩個節點是否有相同的值。 ```c if(strcmp((list_entry(curr_node, element_t, list))->value, (list_entry(next_node, element_t, list))->value) ``` 然而,這樣做會衍生出一個問題,就是當出現相同值得節點,並刪除其中一個的時候,下一步會無法確定是否要移除 `curr_node` 。因此我的作法為,先透過一個變數紀錄上一個節點是否需要移除。 ```c if(strcmp((list_entry(curr_node, element_t, list))->value, (list_entry(next_node, element_t, list))->value)){ //curr and next elements don't have the same value. if(last_cmp){ list_del(curr_node); q_release_element(list_entry(curr_node, element_t, list)); } last_cmp = 0; } else{ //curr and next elements have the same value; last_cmp = 1; list_del(curr_node); q_release_element(list_entry(curr_node, element_t, list)); } ``` ### `q_swap` :::warning > :warning: 不要急著說「完成」,工程是持續反省和改進的過程,目前只是 `qtest` 搭配自動評分沒有遇到 regression,不代表「完成」。再改進! > :notes: jserv ::: ```c /* Swap every two adjacent nodes */ void q_swap(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *curr_node; list_for_each (curr_node, head) { if (curr_node->next == head) break; list_move(curr_node, curr_node->next); } return; } ``` 我的思路為,透過 `curr_node` 和 `curr_node->next` 表示要交換的兩個節點 ,接著尋訪所有節點,再利用 `list_move` 將 `curr_node` 搬移到 `curr_node->next` 前面,以實現兩個節點交換。 其實最一開始的想法是透過 `curr_node` 和 `next_node` 的操作來實現 `q_swap` ,不過在看到 [komark06](https://hackmd.io/@komark06/SyCUIEYpj#q_reverse-%E5%AF%A6%E4%BD%9C) 的 `q_reverse` 後,我又回去重看了 `list_move` ,發現 `list_move` 不只有在移動到 `head` 時可以使用,中途的節點也可以使用,因此才有了現在的版本。 ### `q_reverse` ```c /* Reverse elements in queue */ void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *node = NULL, *safe = NULL; list_for_each_safe (node, safe, head) list_move(node, head); return; } ``` 這裡受到 [komark06](https://hackmd.io/@komark06/SyCUIEYpj#q_reverse-%E5%AF%A6%E4%BD%9C) 的啟發,只要走訪所有節點,再將節點移動到佇列開頭,整個佇列就會被反轉。 ### `q_reverseK` ```c void q_reverseK(struct list_head *head, int k) { if (!head || list_empty(head) || k <= 1) return; struct list_head *node = head->next; while (1) { struct list_head *tmp = node, *curr_head = node->prev; for (int i = 0; i < k; i++, node = node->next) if (node == head) return; node = tmp; tmp = node->next; for (int i = 0; i < k; i++, node = tmp, tmp = tmp->next) list_move(node, curr_head); } } ``` 我的想法為,將每 $k$ 個節點分為一組,在確認接下來的節點不會有 `head` 後,再一個一個用 `list_move` 來將排列順序倒轉。 ### `q_sort` 和 `lib/list_sort.c` 研讀 #### `q_sort` 初步開發 我使用 merge sort 實作,實作分兩個部份,負責合併兩個佇列的 `q_merge_two` ,和負責分割佇列的 `q_sort` : ```c list_for_each_safe(node, safe, head){ INIT_LIST_HEAD(node); stack[i] = node; i++; } INIT_LIST_HEAD(head); for(i = 1; i < queue_size; i *= 2){ for(int j = 0; i + j < queue_size; j += (i * 2)){ stack[j] = merge_two_list(stack[j], stack[i + 1]); } } list_add_tail(head, stack[0]); ``` :::danger 不用列舉完整程式碼,HackMD 是讓你描述想法和過程中遇到的問題的地方,你應該只列出關鍵程式碼片段,並在之前就探討你的構思和對應的考量。 :notes: jserv ::: 這裡的 `q_merge_two` 的輸入參數不需要輸入 `head` 也就是沒有內存資料的部份。 另外,對於 `q_sort` 我的想法是,先透過尋訪整的佇列,並將所有節點堆疊在 `stack` 裡面,如此一來,只要在最後將所有節點兩兩合併,便可以得到結果。 #### `lib/list_sort.c` 研讀 此外,我研讀了 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 以及 [kdnvt](https://hackmd.io/@sysprog/linux2022-sample-lab0) 的整理。 1. `list_sort.c` 在 `merge` 和 `merge_final` 將串列當成單向鏈結串列,並無視 prev 指標的操作,最後再在 `merge_final` 時再串起來。 2. 在合併的過程中 `list_sort.c` 確保合併的兩個子串列的大小不會大於 $2:1$,如此一來便可以降低比較的次數,以增加 performance : ```c /* * The number of pending lists of size 2^k is determined by the * state of bit k of "count" plus two extra pieces of information: * * - The state of bit k-1 (when k == 0, consider bit -1 always set), and * - Whether the higher-order bits are zero or non-zero (i.e. * is count >= 2^(k+1)). * * There are six states we distinguish. "x" represents some arbitrary * bits, and "y" represents some arbitrary non-zero bits: * 0: 00x: 0 pending of size 2^k; x pending of sizes < 2^k * 1: 01x: 0 pending of size 2^k; 2^(k-1) + x pending of sizes < 2^k * 2: x10x: 0 pending of size 2^k; 2^k + x pending of sizes < 2^k * 3: x11x: 1 pending of size 2^k; 2^(k-1) + x pending of sizes < 2^k * 4: y00x: 1 pending of size 2^k; 2^k + x pending of sizes < 2^k * 5: y01x: 2 pending of size 2^k; 2^(k-1) + x pending of sizes < 2^k * (merge and loop back to state 2) */ ``` |count decimal|count binary|merge|before merge|after merge | -------- | -------- | -------- |---|---| 0|0000 |No| NULL| X 1|0001 |No| 1 |X 2|0010 |Yes| 1-1 |2 3|0011 |No| 1-2 |X 4|0100 |Yes| 1-1-2 |2-2 5|0101 |Yes| 1-2-2 |1-4 6|0110 |Yes| 1-1-4 |2-4 7|0111 |No| 1-2-4 |X 8|1000 |Yes| 1-1-2-4 |2-2-4 9|1001 |Yes| 1-2-2-4 |1-4-4 10|1010 |Yes | 1-1-4-4| 2-4-4 11|1011 |Yes | 1-2-4-4| 1-2-8 12|1100 |Yes| 1-1-2-8| 2-2-8 13|1101 |Yes | 1-2-2-8 |1-4-8 14|1110|Yes | 1-1-4-8 |2-4-8 15|1111 |No | 1-2-4-8 |X 16|10000 |Yes| 1-1-2-4-8| 2-2-4-8 #### 回頭檢查 `q_sort` 研讀完 `lib/list_sort.c` 後,我發現原本的程式碼有以下幾點做的不夠好 1. `int queue_size = q_size(head);` 我原本的程式碼使用 `q_size` 來獲得佇列的長度,以此來走訪所有節點,然而,這個做法式建立在「需要利用尋訪整個佇列,並把所有節點兩兩合併」的情況下才需要用到。若是參考 `lib/list_sort.c` 的做法,便不須要這樣做。 2. `stack` 在原本的程式碼中,我使用 `stack[STACKCAPACITY]` 來儲存佇列中所有節點,然而,這樣做會有 `stack` 的記憶體空間浪費或是不足的問題,因此這裡也改成 `lib/list_sort.c` 的 `pending` 的方法。 在考慮到這些因素後,我發現難以已 `lib/list_sort.c` 的作法來修改我的程式碼,並且我發現能夠使用 `lib/list_sort.c` 的想法來實做 `q_merge` 因此我打算先使用 `lib/list_sort.c` 的程式碼,並轉頭修改 `q_merge` 。 ### `q_descend` 我的想法為,利用 `curr_node` 和 `prev_node` 來尋訪整個佇列,如此一來便能夠直接透過比較來判斷是否移除。另外,這個函式必須使用相反的順序來尋訪,以確保迭代的過程中會不會遇到嚴格大於的節點。 ```c /* Remove every node which has a node with a strictly greater value anywhere to * the right side of it */ while (prev_node != head) { if (strcmp(list_entry(curr_node, element_t, list)->value, list_entry(prev_node, element_t, list)->value) < 0) { list_del(prev_node); q_release_element(list_entry(prev_node, element_t, list)); } curr_node = prev_node; prev_node = prev_node->prev; } ``` 不過,在實際測試後發現這個函式會有兩個問題: 1. 在刪除節點後 `curr_node` 應該保持不動。 2. 在 `prev_node` 被刪除後,便無法存取到 `prev_node->prev` 因此應該事先記錄下來 ```c while (prev_node != head) { if (strcmp(container_of(prev_node, element_t, list)->value, container_of(curr_node, element_t, list)->value) < 0) { tmp = prev_node->prev; list_del(prev_node); q_release_element(container_of(prev_node, element_t, list)); prev_node = tmp; } else { curr_node = prev_node; prev_node = prev_node->prev; } } ``` ### q_merge :::info 待補圖 待測試 待優化 ::: ```c= /* Merge all the queues into one sorted queue, which is in ascending order */ int q_merge(struct list_head *head) { if (!head || list_empty(head)) return 0; queue_contex_t *first_queue = container_of(head->next, queue_contex_t, chain); if (list_is_singular(head)) return first_queue->size; for (struct list_head *node = head->next->next; node != head; node = node->next) { queue_contex_t *target = container_of(node, queue_contex_t, chain); merge_two_queue(first_queue->q, target->q); if(first_queue->size + target->size < first_queue->size) return -1; first_queue->size += target->size; target->size = 0; } return first_queue->size; } ``` 這題參考了 [komark06](https://hackmd.io/@komark06/SyCUIEYpj#q_merge-%E5%AF%A6%E4%BD%9C) 的做法,並做出優化。 我在每次尋訪到新的佇列時就進行 `merge_two_list` 並更新 `first_queue->size` 而不是在最後才 `q_sort` 。 但是, `q_merge` 可以使用 divide-and-conquer 方法進行優化。 首先,透過走訪整個佇列,並將目前的節點和下一個節點合併 ```c if(queue != head && queue->next != head){ struct list_head *a_queue = queue, *b_queue = a_queue->next; queue_contex_t *a_queue_container= container_of(a_queue, queue_contex_t, chain), *b_queue_container = container_of(b_queue, queue_contex_t, chain); if(!a_queue_container->size){ list_del(a_queue); } if(!b_queue_container->size){ list_del(b_queue); } if(a_queue_container->size && b_queue_container->size){ if(a_queue_container->q->prev){ a_queue_container->q->prev->next = NULL; a_queue_container->q->prev = NULL; } if(b_queue_container->q->prev){ b_queue_container->q->prev->next = NULL; b_queue_container->q->prev = NULL; } a_queue_container->q->next = merge_two_queue(a_queue_container->q->next, b_queue_container->q->next); a_queue_container->size += b_queue_container->size; b_queue_container->size = 0; list_del(b_queue); } } ``` 結束迴圈的時間點就在只剩下一個佇列時。 不過,在我寫完這個函式後才發現,題目的要求是不能把 `queue_contex_t` 的節點移除掉的。因此須要再調整。 ## References [sysprog21/lab0-c: C Programming Lab - GitHub](https://github.com/sysprog21/lab0-c) [2023q1 Homework1 (lab0) contributed by < komark06 >](https://hackmd.io/@komark06/SyCUIEYpj#2023q1-Homework1-lab0) [linux/list_sort.c at master · torvalds/linux - GitHub](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) [2023q1 Homework1 (lab0) contributed by < kdnvt >](https://hackmd.io/@sysprog/linux2022-sample-lab0)