# Linux 核心專題: 重作第一次作業 > 執行人: jerry7961 > [專題解說影片](?) ### Reviewed by 'ken-LuWeiRu' 可以參考 https://hackmd.io/@sysprog/Syc7ROemA#%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90 做效能分析 ## 任務簡介 重作第一次作業,並強化若干子任務。 ## TODO: 重做 lab0,整合 Timsort,比較 Linux 核心的 `list_sort` > 量化並分析程式表現 (設計實驗,考慮到資料排序演算法最差的狀況、cache / locality of reference, 資料亂度/分佈) :::danger HackMD 不是讓你張貼完整程式碼的地方,而 GitHub 才是。 你若要張貼程式碼,就必定是因為你想討論或者提出後續的改進。 務必詳細閱讀第一次作業的規範! ::: ### `q_free` 先檢查佇列是否為空,若為空則 `return` 。 用 `list_for_each_entry_safe` 巨集走訪所有節點,並釋放節點。 最後釋放開頭節點。 ```c void q_free(struct list_head *head) { if (!head) return; element_t *cur, *next; list_for_each_entry_safe (cur, next, head, list) { q_release_element(cur); } free(head); } ``` ### `q_insert_head` 一開始誤用 `strcpy` (如下) 。 ```c bool q_insert_head(struct list_head *head, char *s) { if(!head) return false; element_t *new_element=malloc(sizeof(element_t)); if(!new_element) return false; strcpy(new_element->value,s); if(!new_element->value){ free(new_element); return false; } list_add(&new_element->list,head); return true; } ``` `strcpy(s1,s2)` 的功能是複製字串 `s2` 到字串 `s1` ,不會為 `s1` 分配内存, `s1` 的長度必須大於 `s2` 的長度。 因此在沒有為 `new_element->value` 分配內存的情況下直接用 `strcpy(new_element->value,s)` 會造成錯誤。 事先為 `new_element->value` 分配內存再用 `strcpy` 複製字串即可修正錯誤。 ```c bool q_insert_head(struct list_head *head, char *s) { if(!head) return false; element_t *new_element=malloc(sizeof(element_t)); if(!new_element) return false; new_element->value=malloc(strlen(s) + 1); if(!new_element->value){ free(new_element); return false; } strcpy(new_element->value,s); list_add(&new_element->list,head); return true; } ``` 另一種做法是使用 `strdup` 來代替 `strcpy` ,`strdup` 會為字串分配內存空間,並返回新分配空間的指標。 ```c bool q_insert_head(struct list_head *head, char *s) { if(!head) return false; element_t *new_element=malloc(sizeof(element_t)); if(!new_element) return false; new_element->value=strdup(s); if(!new_element->value){ free(new_element); return false; } list_add(&new_element->list,head); return true; } ``` ### `q_insert_tail` 與 `q_insert_head` 的做法類似,差別是改用 `list_add_tail`。 ```c bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *new_element = malloc(sizeof(element_t)); if (!new_element) return false; new_element->value = strdup(s); if (!new_element->value) { free(new_element); return false; } list_add_tail(&new_element->list, head); return true; } ``` ### `q_remove_head` `q_remove_head` 的作用是從鏈結串列的開頭移除一個元素。 首先檢查 `head` 是否為空指標,以及鏈結串列中是否至少有一個節點,確保鏈結串列存在且不為空。接著用 `list_first_entry` 取得鏈結串列第一個元素的指標,並透過 `list_del` 將這個元素從鏈結串列中移除。 如果 `sp` 不為 `NULL` ,使用 `strncpy` 函式將被移除元素的字串 `(to_delete->value)` 複製到 `sp` 指向的位址中。複製的字元數量最多為 `bufsize - 1` ,這是為了在字串結尾預留一個位置,用來存放 `\0` 。 ```c element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (head && !list_empty(head)) { element_t *to_remove = list_first_entry(head, element_t, list); list_del(&to_remove->list); if (sp) { strncpy(sp, to_remove->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return to_remove; } return NULL; } ``` ### `q_remove_tail` 與 `q_remove_head` 類似,差別在使用 `list_last_entry` 來取得要移除的元素。 ### `q_size` 使用 `list_for_each` 來走訪所有節點,每走訪一個節點 `count` 值就加一。 ```c int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; int count = 0; struct list_head *temp; list_for_each (temp, head) count++; return count; } ``` ### `q_delete_mid` 使用快慢指標,快指標每次前進兩步,慢指標每次前進一步。 * 當鏈結串列包含奇數個節點時,快指標會停在最後一個節點上,此時慢指標正好指向鏈結串列的中間節點。 * 當鏈結串列包含偶數個節點時,快指標會回到鏈結串列的 `head` ,此時慢指標將指向中間兩個節點中的第二個。為了使 `q_delete_mid` 能夠刪除中間兩個節點中的第一個,需要增加一個 `if` 語句來調整慢指標的位置。 ```c if (fast == head) { slow = slow->prev; } ``` ### `q_delete_dup` `q_delete_dup` 是要在鏈結串列已經排序好的狀況下,移走其中具有重複內容的節點。 * 使用 `list_for_each_safe` 來走訪鏈結串列中的每個節點。 * `mark_del` 用來標記是否遇到重複節點。 * 在走訪過程中,根據當前節點與前後節點的關係,需要處理以下幾種情況: 1. 當前節點已經是最後一個節點,此時用 `mark_del` 來判斷當前節點是否與前一個節點重複,如果重複則刪除它。 2. 當前節點與下一個節點內容相同,此時刪除當前節點,並將 `mark_del` 設為 `true` ,表示已遇到重複節點。 3. 當前節點與下一個節點內容不同,但 `mark_del` 為 `true`,這表示當前節點與上一個節點內容相同,因此刪除當前節點,並將 `mark_del` 設為 `false`。 4. 非前三種情況,代表當前節點與下一個節點內容不同,與上一個節點也不同,無需執行任何操作。 ### `q_swap` `q_swap` 的作用是交換鏈結串列中鄰近的節點。 * 如果鏈結串列中的節點數量小於等於 1 ,無需執行任何操作。 * 如果鏈結串列中的節點數量大於 1 ,則 `q_swap` 從頭部節點的下一個節點開始走訪鏈結串列。對於每對相鄰節點,`q_swap` 會先將它們從鏈結串列中移除,然後以交換後的順序將它們重新插入到鏈結串列中。這個過程會一直重複,直到走訪完鏈結串列的所有節點。 ```c void q_swap(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *node = head->next; while (node != head && node->next != head) { struct list_head *nextNode = node->next; list_del(node); list_del(nextNode); list_add(nextNode, node->prev); list_add(node, nextNode); node = node->next; } } ``` ### `q_reverse` 從頭部節點開始走訪鏈結串列,交換每個節點的 `next` 和 `prev` 指標以反轉鏈結串列方向,直到走訪完整個鏈結串列。 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) { return; } struct list_head *current = head; struct list_head *temp = NULL; do { temp = current->next; current->next = current->prev; current->prev = temp; current = current->prev; } while (current != head); } ``` ### `q_sort` 一開始使用 Insertion Sort ,無法通過測資,後來參考同學寫法,改用 Merge Sort 。 ```c void merge(struct list_head *l_head, struct list_head *r_head, struct list_head *head) { struct list_head temp_list; INIT_LIST_HEAD(&temp_list); while (!list_empty(l_head) || !list_empty(r_head)) { struct list_head *chosen; if (list_empty(l_head)) { chosen = r_head; } else if (list_empty(r_head)) { chosen = l_head; } else { element_t *l_entry = list_entry(l_head->next, element_t, list); element_t *r_entry = list_entry(r_head->next, element_t, list); chosen = (strcmp(l_entry->value, r_entry->value) <= 0) ? l_head : r_head; } list_move_tail(chosen->next, &temp_list); } list_splice_tail_init(&temp_list, head); } void merge_sort_recursive(struct list_head *head, int length) { if (length <= 1) return; int mid = length / 2; struct list_head left, right; INIT_LIST_HEAD(&left); INIT_LIST_HEAD(&right); struct list_head *current = head->next; for (int i = 0; i < mid; i++) { struct list_head *next = current->next; list_move_tail(current, &left); current = next; } list_splice_tail_init(head, &right); merge_sort_recursive(&left, mid); merge_sort_recursive(&right, length - mid); INIT_LIST_HEAD(head); merge(&left, &right, head); } void merge_sort(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; int length = 0; struct list_head *pos; list_for_each (pos, head) { length++; } merge_sort_recursive(head, length); } void q_sort(struct list_head *head, bool descend) { merge_sort(head); if (descend) { q_reverse(head); } } ``` ### `q_merge` <s> ![image](https://hackmd.io/_uploads/ByoIcC-wC.png) </s> ``` +++ TESTING trace trace-03-ops: # Test of insert_head, insert_tail, remove_head, reverse and merge ERROR: Freed queue, but 2 blocks are still allocated ==665== 112 bytes in 2 blocks are still reachable in loss record 1 of 1 ==665== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==665== by 0x10F2E7: test_malloc (harness.c:133) ==665== by 0x10F6EC: q_new (queue.c:17) ==665== by 0x10CDA3: do_new (qtest.c:155) ==665== by 0x10DFD1: interpret_cmda (console.c:181) ==665== by 0x10E586: interpret_cmd (console.c:201) ==665== by 0x10E987: cmd_select (console.c:610) ==665== by 0x10F273: run_console (console.c:705) ==665== by 0x10D3C3: main (qtest.c:1258) ==665== trace-03-ops 0/6 ``` :::danger 文字訊息「不要」用圖片展現。 ::: :::info 收到,已更正 ::: 在 trace-03-ops 出現以上錯誤,經過排查,問題出在 `q_merge` 函式。原始版本的 `q_merge` 創建了一個新的 `first_queue` 指標,它用來指向第一個遇到的非空鏈結串列,這個鏈結串列將作為所有鏈結串列合併的目標,所有其他鏈結串列的元素都會被合併到這個 `first_queue` 中,其他鏈結串列在合併到 `first_queue` 後 `q` 指標會被設為 `NULL`,但相關記憶體沒有被釋放,導致錯誤。修改 `q_merge` 函式後, trace-03-ops 可順利通過。 :::danger 注意用語: * memory 是「記憶體」 務必使用本課程教材規範的術語。 ::: :::info 收到,已更正 ::: 原始版本: ```c int q_merge(struct list_head *head, bool descend) { if (!head || list_empty(head)) { return 0; } if (list_is_singular(head)) { return 1; } int total_elements = 0; queue_contex_t *context, *tmp; struct list_head *first_queue = NULL; list_for_each_entry_safe (context, tmp, head, chain) { if (!first_queue) { first_queue = context->q; total_elements += context->size; } else { list_splice_init(context->q, first_queue); context->q = NULL; total_elements += context->size; } } q_sort(first_queue, false); return total_elements; } ``` 修改版本: ```c int q_merge(struct list_head *head, bool descend) { if (!head || list_empty(head)) { return 0; } queue_contex_t *base_queue = list_first_entry(head, queue_contex_t, chain); if (list_is_singular(head)) { return base_queue->size; } queue_contex_t *queue_to_merge; struct list_head *current, *next; list_for_each_safe (current, next, head) { if (current == &base_queue->chain) { continue; } queue_to_merge = list_entry(current, queue_contex_t, chain); list_splice_tail_init(queue_to_merge->q, base_queue->q); base_queue->size += queue_to_merge->size; } q_sort(base_queue->q, descend); return base_queue->size; } ``` ## TODO: 留意 lab0-c 記憶體佔用分析 > 善用 valgrind, massif, perf 等工具 ### 透過 Massif 分析記憶體使用量 - 參考 [alanjiang85](https://hackmd.io/@alanjian85/lab0-2023#%E9%80%8F%E9%81%8E-Massif-%E5%88%86%E6%9E%90%E8%A8%98%E6%86%B6%E9%AB%94%E4%BD%BF%E7%94%A8%E9%87%8F) 的說明,用 `trace-eg.cmd` 進行分析 - 透過 Valgrind 產生 massif 檔案 ``` $ valgrind --tool=massif ./qtest -f traces/trace-eg.cmd ``` - 產生記憶體使用情形的時間分佈圖 ``` $ massif-visualizer ./massif.out.59554 ``` ![image](https://hackmd.io/_uploads/BkS-GBXPA.png) ## TODO: 修正 lab0-c 網頁伺服器的整合問題並整合 coroutine 來改進性能 > 留意 I/O Multiplexing Model,務必詳閱 [Linux 核心設計: 針對事件驅動的 I/O 模型演化](https://hackmd.io/@sysprog/linux-io-model)和[並行程式設計: 排程器原理](https://hackmd.io/@sysprog/concurrency-sched)