# 2018q3 Homework3 (list) ###### tags: `C語言` contributed by < `asd757817` > ## 自我評量 **為何 Linux 採用 macro 來實作 linked list?一般的 function call 有何成本?** - 以 macro 實作的目的在於將資料型態抽象化,開發者不用理會 list 內的資料型態,型態的問題會由 macro 展開程式碼的機制而解決。 - 以 function call 的方式實作,要以同一個函式處理多種型態就必須在函式內定義所有型態,如此一來會使程式碼過於繁瑣,且遇到定義外的情況時將無法運作。 **Linux 應用 linked list 在哪些場合?舉三個案例並附上對應程式碼,需要解說,以及揣摩對應的考量。** **GNU extension 的 [typeof](https://gcc.gnu.org/onlinedocs/gcc/Typeof.html) 有何作用?在程式碼中扮演什麼角色?** - typeof 的功用在於取得變數型態或函式回傳的型態,如 typeof(\*x) y,以 x 指向的型態作為 y 的型態進行宣告 - 以 typeof 取得函式回傳型態時函式並不會執行 - 在使用 macro 處理多種形態的問題時,除了傳入的參數外,還會使用新的變數且變數型態需要與傳入的參數相符,此時就需要利用 typeof 取得傳入的參數型態 - 在撰寫 header file 且會被 ISO C 引用時,會以 \_\_typeof\_\_ 取代 type **解釋以下巨集的原理** ```Clike #define container_of(ptr, type, member) \ __extension__({ \ const __typeof__(((type *) 0)->member) *__pmember = (ptr); \ (type *) ((char *) __pmember - offsetof(type, member)); \ }) ``` - (type \*) 0: 把 0 轉換成 pointer to type - ((type \*) 0)->member: 取的 type struct 裡面的 member 變數 - \_\_typeof\_\_(((type \*) 0)->member): 取得 memeber 的型態,並且以這個型態宣告指標\*\_\_pmember,並讓 \*\_\_pmember = (ptr) - offsetof(type, member): member 在 type (通常是 struct) 中的 offset - 以 \_\_pmember 的位置減去 member 在 type 中的 offset 可以獲得 pmember 所在的 type 的地址,其型態為 pointer to type,(char \*) 轉換指標型態,如此一來在運算時才會以一個 byte 為單位進行加減。 - 這個巨集的功能是已知 struct 的 member 找出 struct (傳入 A -> member 找出 A 的 ptr) :::info :question: 既然做運算時會強制轉型成 (char \*),為什麼不在一開始就以 char 宣告? ::: **除了你熟悉的 add 和 delete 操作,`list.h` 還定義一系列操作,為什麼呢?這些有什麼益處?** **`LIST_POISONING` 這樣的設計有何意義?** **linked list 採用環狀是基於哪些考量?** - 在一個特定的排序中不斷循環時就需要使用到 circular linked list,在 CPU 排程就會使用到 - 環狀能夠處理單向的工作,但是以單向來處理環狀的情況會複雜很多,雖然相較之下單向所需要的成本較少,但節省的成本並不至於特地使用單向來實作,反而全部統一以環狀實作在開發上較為方便。 Reference: [1](https://www.quora.com/Why-are-all-the-linked-lists-circular-in-the-Linux-Kernel) **`list_for_each_safe` 和 `list_for_each` 的差異在哪?"safe" 在執行時期的影響為何?** - 走訪結束的條件為:pos = head,而 pos 改變是在當次迴圈動作結束後,因此考慮一個情況:當 pos->next = head,正常情況下這次迴圈結束後 pos = pos->next 會使 pos = head,但若是 pos 或 pos->next 經過某些運算使得迴圈執行結束後,pos = pos->next pos 不等於 head,迴圈將沒辦法正確停止。 - list_for_each_safe 則是改善了這個情形,多了一個 n 變數儲存 pos->next 的內容,在每次迴圈結束將 pos 移動到 n 的位置上,在上述情況時,即使把 pos 或者是 pos->next 經過修改,n 的內容仍然是 head,所以在這次迴圈結束後 pos 仍然可以指向 head 滿足迴圈結束的條件。 **for_each 風格的開發方式對程式開發者的影響為何?** * 提示:對照其他程式語言,如 Perl 和 Python **程式註解裡頭大量存在 `@` 符號,這有何意義?你能否應用在後續的程式開發呢?** * 提示: 對照看 Doxygen **`tests/` 目錄底下的 unit test 的作用為何?就軟體工程來說的精神為何?** 軟體開發的過程以 waterfall model 為例可分為: ```graphviz digraph linked_list{ node[shape=record] rec1 [label="<f0> Requirements"]; rec2 [label="<f0> Design"]; rec3 [label="<f0> Implementation"]; rec4 [label="<f0> Verification"]; rec5 [label="<f0> Maintenance"]; rec1 -> rec2; rec2 -> rec3; rec3 -> rec4; rec4 -> rec5; } ``` - 第三個階段 Implementation 指的是程式開發,包含程式碼撰寫、測試、除錯等,在這個階段必須確定開發的程式能夠運行,各函式功能正常,才能移動到下一個階段,unit test 就是屬於這個階段的工作,程式必須通過 unit test 開發才會繼續至下一個階段。 - 第四階段的 Verification 驗證整體程式運作是否符合 requirement 階段時所提出的要求,會有系統地進行測試,除了驗證程式可以正確輸入、輸出以外還會驗證程式的效能是否符合要求。 - 在軟體工程的精神上,程式開發就像是堆積木一樣,各個部分逐一完成,對應到實際開發就是一個函式一個函式逐步完成,unit test 的作用在於驗證各函式是否運作正常,當這個函式撰寫完畢且驗證正確後再接著開發下一個函式,如此一來開發的程式才更具可靠性。 在 test 資料夾中可以發現程式碼使用了許多 assert,其功能是進行比較,若比較結果不符合預期就回報錯誤訊息。 以 containerof.c 內容為例: ```C #include <assert.h> #include <stddef.h> #include "list.h" struct teststruct { int a; int b; }; int main(void) { struct teststruct item; assert(&item == container_of(&item.b, struct teststruct, b)); return 0; } ``` container_of 的功能找到所屬 struct 的地址,所以這份測試檔案先宣告了一個 struct, 以 & 取得該 struct 的地址與 container_of 取得的地址進行比較,若兩者不相等 assert 就會發出錯誤訊息。 Reference: [waterfall model](https://zh.wikipedia.org/wiki/%E7%80%91%E5%B8%83%E6%A8%A1%E5%9E%8B) **`tests/` 目錄的 unit test 可如何持續精進和改善呢?** ## queue 修改 ### singly linked list 改為 doubly linked list - 基於原本的 queue 結構,新增 prev 指標 - 呼叫 insert、remove 時更新 prev 指標 **queue.h 修正** 更新每個節點的結構: ```C typedef struct ELE { char *value; struct ELE *next; struct ELE *prev; } list_ele_t; ``` **queue.c 修正** q_insert_head 函式加入 prev 更新機制 ```C bool q_insert_head(queue_t *q, char *s) { if (!q || !s) { return false; } else { list_ele_t *newh; newh = malloc(sizeof(list_ele_t)); char *s_cpy = malloc(strlen(s) + 1); if (newh && s_cpy) { strcpy(s_cpy, s); newh->value = s_cpy; newh->next = q->head; /* 因為是加入的位置在 head,prev 為 NULL*/ newh->prev = NULL; /* 如果 head 存在,把 head->prev 指向 newrec */ if (q->head) { q->head->prev = newh; } q->head = newh; if (!q->tail) q->tail = newh; q->size += 1; return true; } else { if (newh) free(newh); if (s_cpy) free(s_cpy); return false; } } } ``` q_insert_tail 函式加入 prev 更新機制 ```C bool q_insert_tail(queue_t *q, char *s) { if (!q || !s) { return false; } else { list_ele_t *newh; newh = malloc(sizeof(list_ele_t)); char *s_cpy = malloc(strlen(s) + 1); if (newh && s_cpy) { s_cpy = strcpy(s_cpy, s); newh->next = NULL; newh->value = s_cpy; /* 因為是加入在 tail,newrec->prev 直接指向原本的 tail */ newh->prev = q->tail; if (q->head == NULL) q->head = newh; if (q->tail != NULL) q->tail->next = newh; q->tail = newh; q->size += 1; return true; } else { if (s_cpy) free(s_cpy); if (newh) free(newh); return false; } } } ``` 參考 qtest.c 的 show_queue,加入 show_queue_reverse,從 tail 開始,透過 prev 指標走訪至開頭,顯示的結果應與 show_queue 結果相反,以此測試是否成功改為 doubly linked list ```C static bool show_queue_reverse(int vlevel) { bool ok = true; if (verblevel < vlevel) return true; int cnt = 0; if (q == NULL) { report(vlevel, "q = NULL"); return true; } report_noreturn(vlevel, "reverse_q = ["); // 從 tail 開始走訪 list_ele_t *e = q->tail; if (exception_setup(true)) { while (ok && e && cnt < qcnt) { // 走訪次數由 queue_size 決定 if (cnt < big_queue_size) report_noreturn(vlevel, cnt == 0 ? "%s" : " %s", // 每次替換為前一個節點 e = e->prev; cnt++; ok = ok && !error_check(); } } exception_cancel(); if (!ok) { report(vlevel, " ... ]"); return false; } /* 走訪完 queue_size 後,e 正常情況為 head,而 head 的 prev 為 NULL, 若 head->prev 不為 NULL 表示: 1. queue_size 有錯 2. queue 為 circular */ if (e == NULL) { if (cnt <= big_queue_size) report(vlevel, "]"); else report(vlevel, " ... ]"); } else { ``` 測試 doubly linked list 是否建立成功 ```shell $ make && ./qtest gcc -O0 -g -Wall -Werror -o qtest qtest.c report.c console.c harness.c queue.o cmd>new cmd>new q = [] reverse_q = [] cmd>ih a cmd>ih a q = [a] reverse_q = [a] cmd>ih b cmd>ih b q = [b a] reverse_q = [a b] cmd>ih c cmd>ih c q = [c b a] reverse_q = [a b c] cmd>it t cmd>it t q = [c b a t] reverse_q = [t a b c] cmd> ``` 輸入結果符合預期,接著以 make test 檢查是否影響原本的程式 ```shell $ make test ...略... --- trace-15-perf 7/7 --- TOTAL 100/100 ``` 沒有影響原先程式,記憶體空間也有正確配置、釋放,截至目前為止修改為 doubly linked list 成功。