# 2022q1 Homework1 (lab0) contributed by < [tommy2234](https://github.com/tommy2234/lab0-c) > [作業要求](https://hackmd.io/@sysprog/linux2022-lab0) ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Byte Order: Little Endian Address sizes: 39 bits physical, 48 bits virtual CPU(s): 8 On-line CPU(s) list: 0-7 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 NUMA node(s): 1 Vendor ID: GenuineIntel CPU family: 6 Model: 140 Model name: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz Stepping: 1 CPU MHz: 2400.000 CPU max MHz: 4200.0000 CPU min MHz: 400.0000 BogoMIPS: 4838.40 Virtualization: VT-x L1d cache: 192 KiB L1i cache: 128 KiB L2 cache: 5 MiB L3 cache: 8 MiB NUMA node0 CPU(s): 0-7 ``` :::danger 更新作業要求! :notes: jserv ::: ## 開發過程 ### q_new 將 queue 的頭部用 malloc 創造之後套用 INIT_LIST_HEAD 將其初始化。 :::danger 用通順的漢語書寫,考慮到日後會去科技公司面試,你現在所準備的,其實就是日後面試過程的「逐字稿」 :notes: jserv ::: ```cpp struct list_head *q_new() { struct list_head *queue = malloc(sizeof(struct list_head)); if (!queue) return NULL; INIT_LIST_HEAD(queue); return queue; } ``` ### q_free 先 free element 裡面的 value,再 free 外層element,最後再把 head free掉 ```cpp void q_free(struct list_head *l) { if (!l) return; struct list_head *tmp = l->next; while (!list_empty(l)) { list_del(tmp); element_t *ele = container_of(tmp, element_t, list); free(ele->value); free(ele); tmp = l->next; } free(l); } ``` ### q_insert_head malloc 一個新的 element,value 所需的大小可用 strlen(s)+1 取得。 另外 strcpy 不安全故使用 strncpy。 ```cpp bool q_insert_head(struct list_head *head, char *s) { if (!head) return false; element_t *ele = malloc(sizeof(element_t)); if (!ele) return false; ele->value = malloc(strlen(s) + 1); if (!ele->value) { free(ele); return false; } strncpy(ele->value, s, strlen(s) + 1); list_add(&ele->list, head); return true; } ``` ### q_insert_tail 和 q_insert_head 概念相同,只是改成修改 list 尾端 ```cpp bool q_insert_tail(struct list_head *head, char *s) { if (!head) return false; element_t *ele = malloc(sizeof(element_t)); if (!ele) return false; ele->value = malloc(strlen(s) + 1); if (!ele->value) { free(ele); return false; } strncpy(ele->value, s, strlen(s) + 1); list_add_tail(&ele->list, head); return true; } ``` ### q_remove_head 移除頭部之後將 value copy 到 sp,重要的是 strncpy 可能不會 copy 到最後的 '\0',因此要手動加上。 根據 man page: > The strncpy() function is similar, except that at most n bytes of src > are copied. Warning: If there is no null byte among the first n bytes > of src, the string placed in dest will not be null-terminated. ```cpp element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize) { if (!head || list_empty(head)) return NULL; struct list_head *target = head->next; element_t *tmp = container_of(target, element_t, list); list_del_init(target); if (sp) { strncpy(sp, tmp->value, bufsize - 1); sp[bufsize - 1] = '\0'; } return tmp; } ``` ### q_size 把 list 走一遍,數數看有幾個 ```cpp int q_size(struct list_head *head) { if (!head || list_empty(head)) return 0; struct list_head *node; int count = 0; list_for_each (node, head) count++; return count; } ``` ### q_reverse 從頭開始,利用 ptr1(current node) 和 ptr2(next node) 兩個指標將每個 node 的 next 和 prev 交換 ```cpp void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *ptr1 = head, *ptr2 = ptr1->next; do { ptr2->prev = ptr2->next; ptr2->next = ptr1; ptr1 = ptr2; ptr2 = ptr2->prev; } while (ptr1 != head); } ``` ### q_delete_mid 先算出 mid 是誰,走訪到他的時候把它刪除並 free 掉 ```cpp if (!head || list_empty(head)) return NULL; struct list_head *node; int count = 0, mid = 0; list_for_each (node, head) count++; mid = (float) count / 2 > count / 2 ? count / 2 + 1 : count / 2; count = 0; element_t *ele; list_for_each_entry (ele, head, list) { if (++count == mid) { list_del(&ele->list); free(ele->value); free(ele); break; } } return true; } ``` ### q_delete_dup start 指向目前的 node ,end 指向和目前 value 開始不同的第一個 node ,並且判斷是否有多個 node 包含目前的 value ,是的話就可以一路從 start 刪到 end 前一個。 ```cpp bool q_delete_dup(struct list_head *head) { if (!head) return false; if (list_empty(head) || list_is_singular(head)) return true; struct list_head *start = head->next, *end = start->next; while (start != head) { element_t *ele1 = container_of(start, element_t, list); element_t *ele2 = container_of(end, element_t, list); bool found = false; while (strcmp(ele1->value, ele2->value) == 0) { found = true; end = end->next; if (end == head) break; ele2 = container_of(end, element_t, list); } while (start != end) { struct list_head *next = start->next; if (found) { list_del(start); element_t *tmp = container_of(start, element_t, list); free(tmp->value); free(tmp); } start = next; } end = start->next; } return true; } ``` ### q_swap 只要將相鄰兩個人的 value 指到彼此的就好了 ```cpp void q_swap(struct list_head *head) { if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *first = head->next, *second = first->next; element_t *ele1, *ele2; while (first != head && second != head) { ele1 = container_of(first, element_t, list); ele2 = container_of(second, element_t, list); char *tmp = ele1->value; ele1->value = ele2->value; ele2->value = tmp; first = first->next->next; second = second->next->next; } } ``` ### q_sort 為了方便判定 sort 時終結走訪的時機,我先將尾端指到NULL,等 sort 完再接回來。 另外 sort 時我只針對 next 串接,因此最後也要接回 prev ```cpp void q_sort(struct list_head *head) { if (!head || list_empty(head)) return; // set tail to NULL for the convenience of terminating the iteration head->prev->next = NULL; // merge sort head->next = merge_sort(head->next); // set prev of each node, and connect head and tail struct list_head *tmp; for (tmp = head; tmp->next; tmp = tmp->next) tmp->next->prev = tmp; tmp->next = head; head->prev = tmp; } ``` merge_sort 參考下圖 ![](https://i.imgur.com/AxSvbFb.png) ```cpp struct list_head *merge_sort(struct list_head *head) { if (!head || !head->next) return head; struct list_head *right = head->next; struct list_head *left = head; // split list while (right && right->next) { left = left->next; right = right->next->next; } right = left->next; left->next = NULL; // sort each list struct list_head *l1 = merge_sort(head); struct list_head *l2 = merge_sort(right); // merge sorted l1 and sorted l2 return merge(l1, l2); } ``` merge 階段練習採用〈[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)〉中的"有品味"版本 ```cpp struct list_head *merge(struct list_head *L1, struct list_head *L2) { struct list_head *head = NULL, **ptr = &head, **node = NULL; for (element_t *ele1, *ele2; L1 && L2; *node = (*node)->next) { ele1 = container_of(L1, element_t, list); ele2 = container_of(L2, element_t, list); node = strcmp(ele1->value, ele2->value) < 0 ? &L1 : &L2; *ptr = *node; ptr = &(*node)->next; } *ptr = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2); return head; } ``` ## 我的 merge sort 和 linux kernel 中的lib/list_sort.c 效能比較 - 一開始整份複製過來,慘了!type 有缺,於是在以下三個地方找到缺漏的 type。(bootlin真好用!) [list_cmp_func_t](https://elixir.bootlin.com/linux/latest/source/include/linux/list_sort.h#L9) [likely , unlikely](https://elixir.bootlin.com/linux/latest/source/tools/include/linux/compiler.h#L93) [u8](https://elixir.bootlin.com/linux/latest/source/arch/powerpc/boot/types.h#L9) - 可以使用了!,接著修改 qtest.c , 將 sort 新增一個 argument "l" , sort l 代表要使用 list_sort.c 中的版本。 ```cpp ADD_COMMAND(sort, " [option] | Sort queue in ascending order. Use linux's " ``` - 開始測試 輸到脫褲了!,linux 的 merge sort 大約只要我的6成時間。 ![](https://i.imgur.com/K0QG0TE.png) 原因可從註解中略知一二 ```cpp * This mergesort is as eager as possible while always performing at least * 2:1 balanced merges. Given two pending sublists of size 2^k, they are * merged to a size-2^(k+1) list as soon as we have 2^k following elements. * * Thus, it will avoid cache thrashing as long as 3*2^k elements can * fit into the cache. Not quite as good as a fully-eager bottom-up * mergesort, but it does use 0.2*n fewer comparisons, so is faster in * the common case that everything fits into L1. ``` :::danger 文字訊息不要用圖片展現。考量: 1. 對視覺障礙者更友善 2. 後續資訊檢索的便利 :notes: jserv ::: ## 新增 Shuffle command 從 ADD_COMMAND 這 macro 可知道只要新增 shuffle 命令 , add_cmd 就會吃到 do_shuffle 函式 ```cpp #define ADD_COMMAND(cmd, msg) add_cmd(#cmd, do_##cmd, msg) ``` 使用 Fisher–Yates shuffle ```cpp void q_shuffle(struct list_head *head) { if (!head || list_empty(head)) return; srand(time(NULL)); int size = q_size(head); struct list_head *ptr = head->prev; element_t *target, *last; for (int i = size - 1; i >= 1; i--, ptr = ptr->prev) { int idx = rand() % (i + 1); // random index in range [0, i] last = container_of(ptr, element_t, list); target = container_of(get_node(head, idx), element_t, list); char *tmp = last->value; last->value = target->value; target->value = tmp; } } ``` ## 增加 web command - 首先引用 [tiny-web-server](https://github.com/7890/tiny-web-server) 的 tiny.c 並稍做修改。 除了刪減用不到的程式碼之外,也將其中的 main function 簡化成回傳 listenfd ,當成一個外部的接口。 ```cpp int get_listenfd(int argc, char **argv) { if (argc > 1 && (!strcmp(argv[1], "-h") || !strcmp(argv[1], "--help"))) { print_help(); return 0; } // struct sockaddr_in clientaddr; int default_port = DEFAULT_PORT, listenfd; // connfd; char buf[256]; char *path = getcwd(buf, 256); // socklen_t clientlen = sizeof clientaddr; if (argc == 2) { if (argv[1][0] >= '0' && argv[1][0] <= '9') { default_port = atoi(argv[1]); } else { path = argv[1]; if (chdir(path) != 0) { perror(path); exit(1); } } } else if (argc == 3) { default_port = atoi(argv[2]); path = argv[1]; if (chdir(path) != 0) { perror(path); exit(1); } } printf("serve directory '%s'\n", path); listenfd = open_listenfd(default_port); if (listenfd > 0) { printf("listen on port %d, fd is %d\n", default_port, listenfd); } else { perror("ERROR"); exit(listenfd); } // Ignore SIGPIPE signal, so if browser cancels the request, it // won't kill the whole process. signal(SIGPIPE, SIG_IGN); return listenfd; } ``` - 在 tiny.c 加入處理遠端輸入的 process() 函式 ,將收到的 command 取出整理格式並且回傳。 ```cpp char *process(int fd, struct sockaddr_in *clientaddr) { #ifdef LOG_ACCESS printf("accept request, fd is %d, pid is %d\n", fd, getpid()); #endif http_request req; parse_request(fd, &req); int status = 200; // handle_request(fd, req.function_name); char *p = req.function_name; /* Change '/' to ' ' */ while (*p) { ++p; if (*p == '/') { *p = ' '; } } #ifdef LOG_ACCESS log_access(status, clientaddr, &req); #endif char *ret = malloc(strlen(req.function_name) + 1); strncpy(ret, req.function_name, strlen(req.function_name) + 1); return ret; } ``` - 模仿老師的範例,在 linenoiseEdit 稍做修改 ,將 listenfd 加入 set 之中,利用 select() 同時監控 stdin_fd 和 listenfd,若 listenfd != 0 ,就可以接收遠端的輸入並當成 command line 處理。 ```cpp while (1) { signed char c; int nread; char seq[3]; fd_set set; FD_ZERO(&set); FD_SET(listenfd, &set); FD_SET(stdin_fd, &set); int rv = select(listenfd + 1, &set, NULL, NULL, NULL); struct sockaddr_in clientaddr; socklen_t clientlen = sizeof clientaddr; int connfd; switch (rv) { case -1: perror("select"); /* an error occurred */ continue; case 0: printf("timeout occurred\n"); /* a timeout occurred */ continue; default: if (listenfd && FD_ISSET(listenfd, &set)) { connfd = accept(listenfd, (SA *) &clientaddr, &clientlen); char *p = process(connfd, &clientaddr); strncpy(buf, p, strlen(p) + 1); close(connfd); free(p); return strlen(p); } else if (FD_ISSET(stdin_fd, &set)) { nread = read(l.ifd, &c, 1); if (nread <= 0) return l.len; ...... ``` - 新增 do_web 函式 ```cpp static bool do_web(int argc, char *argv[]) { if (!listenfd) { listenfd = get_listenfd(argc, argv); report(3, "Launching tiny web server"); } else report(3, "Tiny web server has already been launched"); return true; } ``` - 使用 curl 測試 ```shell curl http://localhost:9999/it/cat/3 curl http://localhost:9999/ih/dog/4 ``` ```shell cmd> new l = [] cmd> web serve directory '/home/tommy/desktop/linux/lab0-c' listen on port 9999, fd is 3 Launching tiny web server cmd> accept request, fd is 4, pid is 14574 127.0.0.1:52928 200 - 'it cat 3' (text/plain) l = [cat cat cat] cmd> accept request, fd is 4, pid is 14574 127.0.0.1:52930 200 - 'ih dog 4' (text/plain) l = [dog dog dog dog cat cat cat] cmd> ``` ## 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤 - 使用 make valgrind 進行編譯時產生 memory leak 的訊息 ```shell ==17406== 10 bytes in 1 blocks are still reachable in loss record 1 of 3 ==17406== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==17406== by 0x4A5150E: strdup (strdup.c:42) ==17406== by 0x110E2A: linenoiseHistoryAdd (linenoise.c:1236) ==17406== by 0x1119BD: linenoiseHistoryLoad (linenoise.c:1325) ==17406== by 0x10CB4E: main (qtest.c:1113) ==17406== ==17406== 160 bytes in 1 blocks are still reachable in loss record 2 of 3 ==17406== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==17406== by 0x110DEA: linenoiseHistoryAdd (linenoise.c:1224) ==17406== by 0x1119BD: linenoiseHistoryLoad (linenoise.c:1325) ==17406== by 0x10CB4E: main (qtest.c:1113) ==17406== ==17406== 193 bytes in 19 blocks are still reachable in loss record 3 of 3 ==17406== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so) ==17406== by 0x4A5150E: strdup (strdup.c:42) ==17406== by 0x110D9E: linenoiseHistoryAdd (linenoise.c:1236) ==17406== by 0x1119BD: linenoiseHistoryLoad (linenoise.c:1325) ==17406== by 0x10CB4E: main (qtest.c:1113) ==17406== ``` - 從訊息中可以看到在 linenoiseHistoryLoad() 之中的某個環節呼叫了 strdup 去動態配置記憶卻沒有被 free 掉。 ```cpp /* Add an heap allocated copy of the line in the history. * If we reached the max length, remove the older line. */ linecopy = strdup(line); if (!linecopy) return 0; if (history_len == history_max_len) { free(history[0]); memmove(history, history + 1, sizeof(char *) * (history_max_len - 1)); history_len--; } history[history_len] = linecopy; history_len++; ``` - 觀察程式碼,history 這個 global pointer 會在 呼叫 freeHistory() 時被釋放記憶體。 ```cpp static void freeHistory(void) { if (history) { int j; for (j = 0; j < history_len; j++) free(history[j]); free(history); } } ``` - 繼續追蹤程式碼,發現 freeHistory() 是經由 linenoise() 所開啟的連鎖呼叫而被呼叫的。 過程如下: linenoise() -> linenoiseRaw() -> enableRawMode() -> linenoiseAtExit() -> freeHistory() - 觀察 run_console() 中的程式碼,發現 qtest 在沒有 input file 的時候的時候才會經由 linenoise() 讀取命令,並且此時會需要藉由 linenoiseHistoryLoad() 將使用者之前的歷史紀錄 load 進來。 ```cpp if (!has_infile) { char *cmdline; while ((cmdline = linenoise(prompt)) != NULL) { interpret_cmd(cmdline); linenoiseHistoryAdd(cmdline); /* Add to the history. */ linenoiseHistorySave(HISTORY_FILE); /* Save the history on disk. */ linenoiseFree(cmdline); } } else { while (!cmd_done()) cmd_select(0, NULL, NULL, NULL, NULL); } ``` - 然而,再觀察 qtest.c 的 main function ,會發現他在任何情況下都會呼叫 linenoiseHistoryLoad(),導致我們使用檔案輸入時,由於不呼叫 linenoise() 讀取命令,所以也不會呼叫到 freeHistory(),造成了 memory leak。 ```cpp /* Trigger call back function(auto completion) */ linenoiseSetCompletionCallback(completion); linenoiseHistorySetMaxLen(HISTORY_LEN); linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ ``` - 解決方案 : 在沒有輸入檔案時才呼叫 linenoiseHistoryLoad() ```cpp= if(!infile_name) linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */ ``` - 再次執行 make valgrind 已經沒有了錯誤訊息 ## 研讀論文〈Dude, is my code constant time?〉 論文中提出了工具 **dudect**,可以用來檢測一段程式是否以 constant time 執行。 雖然現在已經出現很多評估效能的工具,例如作者提到的 ctgrind, ctverif, Valgrind,但是這些工具往往需要針對不同的硬體去做 modeling,且 CPU 供應商往往對產品細節不願透漏太多,這導致建模變得困難。 於是論文提出了以 leakage detection 方式,針對兩組 input data 的執行時間做統計分析,觀察他們的 distribution 是否有顯著差異,因而避開了對硬體的 dependency。 論文中採用的 Welch’s t-test 是 t-test 的其中一種變形,其在針對兩個 variance 不同的 sample 時能有更好的 numeric stability 當 t-test 試圖否定『 兩個 timing distributions 相同 』的假設失敗時,代表無法證明平均值的差異,implies 執行時間為constant time。 ### 可能的缺陷: 套用這種統計的方式可能需要很大量的測資才能夠反映出較真實的 distribution,測試上沒有直接測量時間的方法來得容易。