# 2025q1 Homework1 (lab0) contributed by <[JimmyChongz](https://github.com/JimmyChongz)> {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 安裝 Ubuntu 24.04 [在 Windows 11 安裝 Ubuntu 24.04.1 LTS](https://hackmd.io/@Jimmy-Xu/SkAXnK7LJx) ## 環境 ```bash $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 24.04.2 LTS Release: 24.04 Codename: noble $ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 ``` ## queue.c 的疑難雜症 ### q_new > commit: [ab7b291](https://github.com/sysprog21/lab0-c/commit/ab7b291b65738ade55561cbd638d73ac36378d02) #### 我沒注意記憶體分配失敗的處理。 記憶體配置不能保證成功,而是可能返回 null 指標。若直接使用 malloc 回傳的值而不檢查分配是否成功,則有可能會造成 segmentation fault on the null pointer dereference。 ### q_free > commit: [1f7493b](https://github.com/sysprog21/lab0-c/commit/1f7493b5451b8462a81e44668a6a608ca2c1539c) #### 怎麼完整 free 掉整個 Linked List 而不會發生 Memory leak ? 使用迴圈來釋放鏈結串列中所有的節點。另外,若 `struct` 中含有**動態配置的記憶體指標**,則要先 `free` 該指標所指向的記憶體空間,再 `free` 該結構體。還有 `head` 的結構與節點不同,`head` 並沒有 `element_t` 結構,故只需單獨 `free`。 ```clike free(list_entry(tmp, element_t, list)->value); free(list_entry(tmp, element_t, list)); ``` 善用 Linux kernel API `q_release_element` > commit: [84ee2f1](https://github.com/sysprog21/lab0-c/commit/84ee2f1407da8bccdea176d0be8baa2ec59c943d) ### q_insert > commit: [a202b1e](https://github.com/sysprog21/lab0-c/commit/a202b1e11ecb64ddb73a5e9f22d65e7a3651489c) > #### 為什麼不能直接 newNode->value = s ? Ans: 這樣只會讓 `newNode->value` 指向 `s`,但不會複製內容,若之後修改 `s`,那 `newNode->value` 也會跟著改變。例如: ```c char *s; char str[] = "Hello World!"; s = str; printf("s: %s, str: %s\n", s, str); strcpy(str, "123"); printf("s: %s, str: %s\n", s, str); ``` ``` // output s: Hello World!, str: Hello World! s: 123, str: 123 ``` 又或者當 s 被釋放(即 free(s)),而 s 與 `newNode->value` 都指向同一塊記憶體空間,此時 `newNode->value` 就會變成 **dangling pointer**。如果之後程式碼使用 `newNode->value`,就會出現不可預期的錯誤。 #### 那怎麼解決? 解決方法:使用 `strdup` 或 `strncpy` 來賦值給 `newNode->value` - `strdup(s)` : 複製傳入參數 `s` 指向的字串到一塊新的記憶體空間。意思就是說此函數會先 malloc `s` 指向字串的 size,再透過 `memcpy` 將 `s` 指向的字串複製到先前 malloc 的記憶體空間。 ![截圖 2025-02-27 上午10.09.20](https://hackmd.io/_uploads/H12UKHT5Jx.png) - `strncpy(str1, str2, n)` : 將 `str2` 的前 `n` 個字元複製到 `str1` 中。要注意預留一個 byte 的空間給 null character。 ![截圖 2025-02-27 上午10.44.42](https://hackmd.io/_uploads/rkYsbUTqJe.png) #### 那 `strdup` 跟 `strncpy` 用哪個會比較好? `strdup` 比較好,因為 `strdup` 會自動分配記憶體,而 `strncpy` 只是單純複製字串,**但仍然需要手動分配記憶體**,例如: ```c newNode->value = malloc(strlen(s) + 1); // 需要自己分配記憶體 strncpy(newNode->value, s, strlen(s)); newNode->value[strlen(s)] = '\0'; // 確保結尾有 '\0' ``` ### q_remove > commit: [a202b1e](https://github.com/sysprog21/lab0-c/commit/a202b1e11ecb64ddb73a5e9f22d65e7a3651489c) #### 為什麼參數有 `char *sp` 跟 `size_t bufsize` ? - `sp` 是一個指向字串緩衝區的指標,若不為 `NULL`,則函式應將被移除元素之成員 (即 `node->value`) 複製到這個字串緩衝區。 ***用途*:** 讓使用者可以立刻取得移除節點所儲存的字串,而不需透過訪問回傳的 `element_t` 結構體取得。 ***如果 sp == NULL*:** 代表 *呼叫者* 不需要移除節點所儲存的字串,只需要刪除節點並回傳該元素的指標。 - `bufsize` 是 `sp` 的最大可用空間(包含 `\0` ),避免 **緩衝區溢位(buffer overflow)**。 ***用途*:** 確保 `strncpy()` 不會超過 `sp` 的範圍,並手動添加 `\0` 來確保字串結尾安全。 ### q_delete_mid > commit: [a8f4303](https://github.com/sysprog21/lab0-c/commit/a8f430359a1ef246afdcd8f2cfc8e94062728ca7) #### 基於下方的程式碼 `deleteMiddle` 還有沒有更好的寫法? ```clike int len = 0; struct ListNode **indirect = &head; while (*indirect) { len++; indirect = &(*indirect)->next; } int pos = len / 2; len = 0; indirect = &head; while (len != pos) { len++; indirect = &(*indirect)->next; } *indirect = (*indirect)->next; ``` 利用「快慢指標」,讓 fast 指標以兩倍速在走訪整個鏈結串列,slow 則以一倍速走訪鏈結串列,當 fast 完成走訪整個鏈結串列時,slow 指標剛好會停在中間的位置。例如: ```clike struct ListNode *fast = head; struct ListNode *slow = head; while (fast && fast->next) { fast = fast->next->next; slow = slow->next; } slow = slow->next; ``` ### q_delete_dup > commit: [a8f4303](https://github.com/sysprog21/lab0-c/commit/a8f430359a1ef246afdcd8f2cfc8e94062728ca7) 思路:用巢狀迴圈,外層迴圈用於檢測兩兩節點的值是否相同,若相同則進入第二層迴圈,在第二層迴圈中刪除重複的節點。 #### 還有改進空間嗎? 用 Hash Table ... ### q_swap > commit: [244dc15](https://github.com/sysprog21/lab0-c/commit/244dc156ad2ab4a00350e9184d3b5a046a638850) 思路:利用 `list_for_each(cur, head)` 去走訪整個鏈結串列,再透過 `list_move(cur, cur->next)`,由此一來每一輪的 `cur->next` 都會被插入到 `cur` 前面,又執行完 `list_move` 後,`cur` 無形中會自動跑到下一個節點,如下圖所示: ![截圖 2025-03-04 晚上8.40.59](https://hackmd.io/_uploads/ryBkruEiyl.png) #### 依照上述做法會出現問題,當鏈結串列長度為奇數時,會多換一次,導致最後一個節點被移到最前面,怎麼解決? 多加一個判斷式,當 cur->next == head 時,就 break迴圈,避免發生交換即可。 > commit: [8904527](https://github.com/sysprog21/lab0-c/commit/89045277c7ccbfe472c76b45c06e59e6b708563d) #### 當完成 `q_reverseK` 時,即可將 `q_swap` 寫成: ```c q_reverseK(head, 2); ``` ### q_reverse > commit: [244dc15](https://github.com/sysprog21/lab0-c/commit/244dc156ad2ab4a00350e9184d3b5a046a638850) 作法:利用 `list_for_each_safe(cur, next, head)` 去走訪整個鏈結串列,再透過 `list_move(cur, head)` 將每一個點都重新插入 `head`,即可完成 reverse 操作。 ### q_reverseK > commit: [fcd4d2c](https://github.com/sysprog21/lab0-c/commit/fcd4d2c084a539ea43d10ecd245a91b373d28a0c) 作法:先得出鏈結串列的長度 `count`,當 `count` 大於 K 時,準備 `prev` 跟 `next` 兩個輔助指標做 Reverse K-group,做完後要 `count -= k`。 ### q_descend > commit: [13ffad8](https://github.com/sysprog21/lab0-c/commit/13ffad81caea17b9c5625fb63ec21e286171380b) #### 有更好的做法,怎麼做? 我最一開始的思路是先找出串列中的最大值 `max`,再利用遞迴找出 `max->next` 子串列的最大值回傳給 `max->next`,如下程式碼所示,但會 Time out 時間複雜度為 $O(n^2)$ 等級。 用兩個指標來進行反向走訪: - `max`:追蹤目前反向走訪過程中遇到的最大值節點(初始指向 `head->prev`,即尾部節點)。 - `cur`:負責走訪 `max` 前方的節點(初始指向 `max->prev` )。 走訪過程: - 如果 `cur` 所指的值**小於或等於** `max`,則刪除該節點。 - 如果 `cur` 所指的值**大於** `max`,則更新 `max` 為 `cur`,確保 `max` 始終指向目前反向走訪過程中遇到的最大值。 - 移動 `cur` 到 `prev`,繼續走訪。 如此一來就可以有效地保留遞減序列的節點,並刪除不符合條件的節點。 ### q_ascend > commit: [13ffad8](https://github.com/sysprog21/lab0-c/commit/13ffad81caea17b9c5625fb63ec21e286171380b) > 作法與 `q_descend` 相似,一樣是透過反向走訪的思路,差別在走訪過程的條件,`q_ascend` 要刪除**大於等於** `min` 的值,若 `cur` 所指的值**小於** `min`,則更新 `min` 為 `cur`,以確保 `min` 始終指向目前反向走訪過程中遇到的最小值。如此一來就可以有效地 **保留遞增序列的節點,並刪除不符合條件的節點**。 ### q_sort > commit: [e5f1033](https://github.com/sysprog21/lab0-c/commit/e5f10336478bebd2c575aebf3f9e6d7671de41a8) 我原先參考了 [第二周測驗一 的 list_quicksort 實作](https://hackmd.io/@sysprog/linux2025-quiz2),我將其轉換成 Linux kernel 風格的鏈結串列: ```c struct list_head list_less, list_greater; element_t *pivot; element_t *item = NULL, *is = NULL; INIT_LIST_HEAD(&list_less); INIT_LIST_HEAD(&list_greater); pivot = list_first_entry(head, element_t, list); list_del(&pivot->list); if (!descend) { list_for_each_entry_safe (item, is, head, list) { if (strcmp(item->value, pivot->value) < 0) list_move_tail(&item->list, &list_less); else list_move_tail(&item->list, &list_greater); } q_sort(&list_less, false); q_sort(&list_greater, false); list_add(&pivot->list, head); list_splice(&list_less, head); list_splice_tail(&list_greater, head); } ``` #### 使用 quick sort 為什麼會超時? 因為在 Worst Case 的情況下,Quick Sort 的時間複雜度會來到 $O(n^2)$ 等級。例如在全部節點值都相同的情況下,使用 **Singely-Linked-List** 示意: 令 $n$ 表示傳入的串列長度,第一個節點當作 `pivot` ![截圖 2025-03-06 中午12.02.04](https://hackmd.io/_uploads/Hk5rCqLjJl.png) 再將剩餘的 $n-1$ 個節點做分割,比 `pivot` 小的節點插入到 `list_less` 串列中,反之則插入 `list_greater` 串列中。在此例中,當執行完 `list_for_each_entry_safe` 迴圈後,並無法分割串列,因為全部的節點都相同,因此都會被插入到 `list_greater` 中,還是得到長度為 $n-1$ 的子串列。 ![截圖 2025-03-06 中午12.10.22](https://hackmd.io/_uploads/ry54loIoye.png) 再遞迴呼叫 `q_sort`,所以都是透過設 `pivot` 在做分割串列,每回合只切一個節點,由此可推得時間函數式為 $T(n) = T(n-1) + cn$ ,其中 $c$ 為常數,$cn$ 表示執行 `list_for_each_entry_safe` 迴圈的時間,經化簡後得 $T(n) = O(n^2)$ \begin{gather} T(n) = T(n-1) + cn\\=T(n-2) + n + (n-1)\\ = T(n-3) + cn + c(n-1) + c(n-2)\\ ...... \\ = T(0)+cn+c(n-1)+c(n-2)+...+c \\ \because T(0) = 0 \\ \therefore T(n) = c[n+(n-1)+(n-2)+...+1] \\ = \frac {cn(n-1)}{2} = O(n^2)\end{gather} 故無法滿足時間複雜度 $O(nlogn)$ 的要求。 #### 如何優化? 改用 **Merge Sort** ,因為不管在什麼情況下,時間複雜度皆為 $O(nlogn)$ 等級。 > 參考 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 之 Merge Sort #### 這樣寫 Merge Sort,會出現 Segmentation Fault,是什麼問題? ```c=234 if (!head || list_empty(head) || list_is_singular(head)) return; struct list_head *fast = head->next; struct list_head *slow = head->next; while (fast != head && fast->next != head) { fast = fast->next->next; slow = slow->next; } LIST_HEAD(left); list_cut_position(&left, head, slow); q_sort(head, descend); q_sort(&left, descend); struct list_head *merged = mergeTwoLists(&left, head, descend); INIT_LIST_HEAD(head); list_splice(merged, head); ``` 使用 [Valgrind](https://valgrind.org) 分析,執行以下命令: ```bash $ valgrind -q --leak-check=full ./qtest -v 3 -f traces/trace-04-ops.cmd ``` 發現是 Stack Overflow ,看起來是第 248 列的遞迴沒寫好。 ``` cmd> sort ==4012223== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==4012223== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==4012223== Can't extend stack to 0x1ffe8010b8 during signal delivery for thread 1: ==4012223== no stack segment ==4012223== ==4012223== Process terminating with default action of signal 11 (SIGSEGV) ==4012223== Access not within mapped region at address 0x1FFE8010B8 ==4012223== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==4012223== at 0x1104B2: q_sort (queue.c:248) ``` 先用簡單的例子來追蹤程式碼: ![截圖 2025-03-07 上午11.17.42](https://hackmd.io/_uploads/B1LwH1uiyx.png =70%x) 在執行完 while loop 後: ![截圖 2025-03-07 上午11.20.38](https://hackmd.io/_uploads/BJBGL1uoyg.png =70%x) :::warning TODO: Explain how list_cut_position(&left, head, slow) exactly do ...... > 將 `head` 到 `slow` (**包含 `slow`**) 的節點接到 `left` 上,若 `left` 不為空的話,則會將其原本的節點都覆蓋過去,即原本節點都會被 "Remove"(不會被釋放)。 ::: 在執行完 `list_cut_position(&left, head, slow);` 後: ![截圖 2025-03-07 上午11.38.59](https://hackmd.io/_uploads/B10L9kdo1e.png =70%x) 此時,再執行 `q_sort(&left, descend);` 時,會發現 `left` 就是原本的 `head`,根本沒切到,導致無限遞迴,造成 Stack Overflow ! #### 修正: 把 `list_cut_position(&left, head, slow);` 修改即可。 ```diff - list_cut_position(&left, head, slow); + list_cut_position(&left, head, slow->prev); ``` 接著,**Merge Sort** 會用到 `merge_two_lists` 輔助函數,用來合併由 `q_sort` 函數切割成兩半的子串列,比較特別的是要「 做 ascending / descending 的判斷 」。其中,為了要確保是 **Stable Sorting**,因此當遇到重複的數字時應保持原排列順序,以 ascending 為例,當目前左半子串列的值小於等於當前右半子串列的值時,應優先合併左半子串列的值。 ### q_merge > commit: [3e931f4](https://github.com/sysprog21/lab0-c/commit/3e931f4b8f70c3cac27a86e0d1667f6bf1492851) 先了解 `queue_contex_t` 結構體定義: ![截圖 2025-03-08 上午9.11.45](https://hackmd.io/_uploads/BkQDKMYiyg.png =70%x) 思路:首先,新增一個指標指向第一條 queue,作為合併的目標。接下來,將其餘所有 queue 依序合併到第一條 queue 中。我使用 `list_for_each_entry_safe` 走訪從第二條開始的每一條 queue,並透過 `merge_two_lists` 將當前走訪到的 queue 合併至第一條 queue。同時,需即時更新第一條 queue 的 size。當所有 queue 都合併完成後,回傳第一條 queue 的最終 size。 ## 如何驗證 queue.c 實作的 method? ### 在終端機輸入 make test 後,會發生什麼? 執行 [scripts/driver.py](https://github.com/sysprog21/lab0-c/blob/master/scripts/driver.py) 這個 Python 程式。快速瀏覽發現這個自動評分程式就是將 traces/trace-XX-CAT.cmd 這樣形式的命令序列提供給 `qtest.c` 內的命令直譯器,然後依據給定的檢驗標準,逐一判定該給測試者多少分數。 ## 整合網頁伺服器 ### File descriptor (fd) > [Reference 1](https://kkc.github.io/2020/08/22/file-descriptor/) > [Reference 2](https://man7.org/training/download/lusp_fileio_slides.pdf) File Descriptor 是一個非負整數,代表 File Descriptor Table 中的一個 entry 的索引。File Descriptor Table 是儲存在 PCB (即 Linux 中的 `task_struct`)中的一個資料結構,用來記錄程序所開啟檔案的相關資訊。每個 entry 會指向一個 Open File Table 的項目,該表中包含與檔案操作有關的詳細資料,例如檔案的偏移量、開啟模式等。Open File Table 中的每個項目則會對應到一個 i-node(Linux 檔案系統中的資料結構),而 i-node 中則紀錄了檔案的屬性資訊,例如檔案的路徑、大小、權限等。可以使用以下的 function 來查看當前 process 的 File Descriptor Table: ```c void list_open_fds() { char path[512]; // 增加緩衝區大小 snprintf(path, sizeof(path), "/proc/%d/fd", getpid()); DIR *dir = opendir(path); if (!dir) { perror("opendir"); return; } struct dirent *entry; printf("Open file descriptors for process %d:\n", getpid()); printf("FD\tType\t\tTarget\n"); printf("---------------------------------------------\n"); while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] == '.') continue; // Skip "." and ".." char fd_path[512], target_path[512]; // 增加緩衝區大小 int written = snprintf(fd_path, sizeof(fd_path), "/proc/%d/fd/%s", getpid(), entry->d_name); // 檢查是否發生截斷 if (written < 0 || written >= sizeof(fd_path)) { fprintf(stderr, "Path truncated: %s\n", entry->d_name); continue; } // Get the target of the symbolic link ssize_t len = readlink(fd_path, target_path, sizeof(target_path) - 1); if (len != -1) { target_path[len] = '\0'; } else { snprintf(target_path, sizeof(target_path), "Unknown"); } // Get the type of the file descriptor struct stat statbuf; if (stat(fd_path, &statbuf) == 0) { char *type; if (S_ISREG(statbuf.st_mode)) { type = "Regular File"; } else if (S_ISDIR(statbuf.st_mode)) { type = "Directory"; } else if (S_ISCHR(statbuf.st_mode)) { type = "Character Device"; } else if (S_ISBLK(statbuf.st_mode)) { type = "Block Device"; } else if (S_ISFIFO(statbuf.st_mode)) { type = "FIFO (Pipe)"; } else if (S_ISSOCK(statbuf.st_mode)) { type = "Socket"; } else { type = "Unknown"; } printf("%s\t%-15s\t%s\n", entry->d_name, type, target_path); } else { printf("%s\tUnknown\t\t%s\n", entry->d_name, target_path); } } closedir(dir); } ``` 查看建立 socket 前跟後 FD Table 的差異,以及當有新的 client 連進來後的差異: :::spoiler 完整測試程式碼 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <errno.h> #include <sys/select.h> #include <dirent.h> #include <sys/stat.h> #define SERVER_PORT 1234 #define BUF_SIZE 1024 #define MAX_CLIENTS 100 void list_open_fds() { char path[512]; // 增加緩衝區大小 snprintf(path, sizeof(path), "/proc/%d/fd", getpid()); DIR *dir = opendir(path); if (!dir) { perror("opendir"); return; } struct dirent *entry; printf("Open file descriptors for process %d:\n", getpid()); printf("FD\tType\t\tTarget\n"); printf("---------------------------------------------\n"); while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] == '.') continue; // Skip "." and ".." char fd_path[512], target_path[512]; // 增加緩衝區大小 int written = snprintf(fd_path, sizeof(fd_path), "/proc/%d/fd/%s", getpid(), entry->d_name); // 檢查是否發生截斷 if (written < 0 || written >= sizeof(fd_path)) { fprintf(stderr, "Path truncated: %s\n", entry->d_name); continue; } // Get the target of the symbolic link ssize_t len = readlink(fd_path, target_path, sizeof(target_path) - 1); if (len != -1) { target_path[len] = '\0'; } else { snprintf(target_path, sizeof(target_path), "Unknown"); } // Get the type of the file descriptor struct stat statbuf; if (stat(fd_path, &statbuf) == 0) { char *type; if (S_ISREG(statbuf.st_mode)) { type = "Regular File"; } else if (S_ISDIR(statbuf.st_mode)) { type = "Directory"; } else if (S_ISCHR(statbuf.st_mode)) { type = "Character Device"; } else if (S_ISBLK(statbuf.st_mode)) { type = "Block Device"; } else if (S_ISFIFO(statbuf.st_mode)) { type = "FIFO (Pipe)"; } else if (S_ISSOCK(statbuf.st_mode)) { type = "Socket"; } else { type = "Unknown"; } printf("%s\t%-15s\t%s\n", entry->d_name, type, target_path); } else { printf("%s\tUnknown\t\t%s\n", entry->d_name, target_path); } } closedir(dir); } typedef struct { int client_fd; char client_ip[INET_ADDRSTRLEN]; int client_port; } client_info_t; int main() { list_open_fds(); // Create a socket descriptor int server_fd; if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("Socket creation failed"); return -1; } list_open_fds(); // Eliminates "Address already in use" error from bind int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) { perror("Error setting SO_REUSEADDR on socket"); close(server_fd); return -1; } // Set up server address struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Bind socket if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("Bind failed"); close(server_fd); return -1; } // Listen for connections if (listen(server_fd, SOMAXCONN) < 0) { perror("Listen failed"); close(server_fd); return -1; } printf("伺服器已啟動,等待連接...\n"); // Initialize client array and file descriptor sets client_info_t clients[MAX_CLIENTS]; for (int i = 0; i < MAX_CLIENTS; i++) { clients[i].client_fd = -1; // Mark as unused } fd_set read_fds; // read_fds represent a set of file descriptors int max_fd = server_fd; // init the highest-numbered file descriptor to server_fd while (1) { // Clear and set file descriptor sets FD_ZERO(&read_fds); // Clear FD_SET(server_fd, &read_fds); // Add active client sockets to read_fds for (int i = 0; i < MAX_CLIENTS; i++) { if (clients[i].client_fd != -1) { FD_SET(clients[i].client_fd, &read_fds); if (clients[i].client_fd > max_fd) { max_fd = clients[i].client_fd; } } } // Use select to monitor sockets if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) { // max_fd + 1 -> nfds perror("Select failed"); continue; } // Check for new connection if (FD_ISSET(server_fd, &read_fds)) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd < 0) { perror("Accept failed"); continue; } // Get client socket information char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); int client_port = ntohs(client_addr.sin_port); printf("客戶端 %s:%d 已連接\n", client_ip, client_port); list_open_fds(); // Find an empty slot for the new client int i; for (i = 0; i < MAX_CLIENTS; i++) { if (clients[i].client_fd == -1) { clients[i].client_fd = client_fd; strncpy(clients[i].client_ip, client_ip, INET_ADDRSTRLEN); clients[i].client_port = client_port; break; } } if (i == MAX_CLIENTS) { printf("太多客戶端,拒絕連接\n"); close(client_fd); } else if (client_fd > max_fd) { max_fd = client_fd; } } // Check for data from clients for (int i = 0; i < MAX_CLIENTS; i++) { if (clients[i].client_fd != -1 && FD_ISSET(clients[i].client_fd, &read_fds)) { char buffer[BUF_SIZE]; ssize_t bytes_received = recv(clients[i].client_fd, buffer, BUF_SIZE - 1, 0); if (bytes_received <= 0) { if (bytes_received < 0) { perror("Connection error"); } else { printf("客戶端 %s:%d 已斷開連線\n", clients[i].client_ip, clients[i].client_port); } close(clients[i].client_fd); clients[i].client_fd = -1; continue; } buffer[bytes_received] = '\0'; printf("收到來自 %s:%d 的訊息: %s\n", clients[i].client_ip, clients[i].client_port, buffer); // Send echo message back to client if (send(clients[i].client_fd, buffer, bytes_received, 0) < 0) { perror("Connection error"); close(clients[i].client_fd); clients[i].client_fd = -1; continue; } printf("訊息已送出...\n"); } } } // Cleanup close(server_fd); printf("伺服器關閉\n"); return 0; } ``` ::: :::spoiler 預期輸出 ``` Open file descriptors for process 305949: FD Type Target --------------------------------------------- 0 Character Device /dev/pts/5 1 Character Device /dev/pts/5 2 Character Device /dev/pts/5 3 Directory /proc/305949/fd 19 Regular File /home/jimmy/.vscode-server/data/logs/20250428T121627/remoteTelemetry.log 20 Regular File /home/jimmy/.vscode-server/data/logs/20250428T121627/ptyhost.log 21 Character Device /dev/ptmx 22 Regular File /home/jimmy/.vscode-server/data/logs/20250428T121627/remoteagent.log 23 Character Device /dev/ptmx 24 Character Device /dev/ptmx Open file descriptors for process 305949: FD Type Target --------------------------------------------- 0 Character Device /dev/pts/5 1 Character Device /dev/pts/5 2 Character Device /dev/pts/5 3 Socket socket:[3259968] 4 Directory /proc/305949/fd 19 Regular File /home/jimmy/.vscode-server/data/logs/20250428T121627/remoteTelemetry.log 20 Regular File /home/jimmy/.vscode-server/data/logs/20250428T121627/ptyhost.log 21 Character Device /dev/ptmx 22 Regular File /home/jimmy/.vscode-server/data/logs/20250428T121627/remoteagent.log 23 Character Device /dev/ptmx 24 Character Device /dev/ptmx 伺服器已啟動,等待連接... 客戶端 140.116.245.209:46472 已連接 Open file descriptors for process 305949: FD Type Target --------------------------------------------- 0 Character Device /dev/pts/5 1 Character Device /dev/pts/5 2 Character Device /dev/pts/5 3 Socket socket:[3259968] 4 Socket socket:[3259974] 5 Directory /proc/305949/fd 19 Regular File /home/jimmy/.vscode-server/data/logs/20250428T121627/remoteTelemetry.log 20 Regular File /home/jimmy/.vscode-server/data/logs/20250428T121627/ptyhost.log 21 Character Device /dev/ptmx 22 Regular File /home/jimmy/.vscode-server/data/logs/20250428T121627/remoteagent.log 23 Character Device /dev/ptmx 24 Character Device /dev/ptmx ``` ::: ### select system call > [Manual page](https://man7.org/linux/man-pages/man2/select.2.html) #### 運作原理: Select 系統呼叫透過 fd_set 位元陣列來監控多個 file descriptor,檢查它們是否準備好進行讀取(readfds)、寫入(writefds)或是否有例外狀況(exceptfds),若已準備好,則使用 FD_SET() 將準備好之文件的 fd 對應到該 fd_set 的 bit 設為 1,告訴 select() 幫我監聽這個 fd 是否可讀。例如:使用 accept() 系統呼叫得到一個 socket_fd 假設是 4,那麼因為 socket_fd 是要接收客戶端的訊息,所以 readfds 的第 4 個 bit 就設為 1,也就是告訴 select() 幫我監聽這個 fd 是否可讀,若沒有 client 連線,則 select() 會 block 程式,直到有 fd 就緒可讀或 Timeout。另外,**當 select() return 後,會將 readfds 中除了可讀 fd bit 之外的所有 bit 清 0**。此外 fd_set 的 size 由 nfds 記錄,表示目前共監聽幾個 file descriptors,而監聽的上限則由巨集 FD_SETSIZE 限制。另外,我透過以下方式來查看 fd_set 的狀態: ```c void print_fd_set(fd_set *fds, int max_fd) { printf("fd_set contents (up to %d):\n", max_fd); for (int i = 0; i <= max_fd; i++) { if (FD_ISSET(i, fds)) { printf("fd %d: 1, ", i); } else { printf("fd %d: 0, ", i); } } } ``` 接著,寫程式來實驗看看 Select: :::spoiler server 完整測試程式碼 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <errno.h> #include <sys/select.h> #define SERVER_PORT 1234 #define BUF_SIZE 1024 #define MAX_CLIENTS 100 void print_fd_set(fd_set *fds, int max_fd) { printf("fd_set contents (up to %d):\n", max_fd); for (int i = 0; i <= max_fd; i++) { if (FD_ISSET(i, fds)) { printf("fd %d: 1, ", i); } else { printf("fd %d: 0, ", i); } } puts(""); printf("-----------------------------------------------------------------\n"); } typedef struct { int fd; char ip[INET_ADDRSTRLEN]; int port; } client_info_t; int main() { // Create a socket descriptor int server_fd; if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("Socket creation failed"); return -1; } // Eliminates "Address already in use" error from bind int opt = 1; if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) != 0) { perror("Error setting SO_REUSEADDR on socket"); close(server_fd); return -1; } // Set up server address struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(SERVER_PORT); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // Bind socket if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("Bind failed"); close(server_fd); return -1; } // Listen for connections if (listen(server_fd, SOMAXCONN) < 0) { perror("Listen failed"); close(server_fd); return -1; } printf("伺服器已啟動,等待連接...\n"); // Initialize client array and file descriptor sets client_info_t clients[MAX_CLIENTS]; for (int i = 0; i < MAX_CLIENTS; i++) { clients[i].fd = -1; // Mark as unused } fd_set read_fds; // read_fds represent a set of file descriptors int max_fd = server_fd; // init the highest-numbered file descriptor to server_fd printf("Init read_fds:\n"); print_fd_set(&read_fds, max_fd); while (1) { // Clear and set file descriptor sets FD_ZERO(&read_fds); // Clear printf("After execute FD_ZERO:\n"); print_fd_set(&read_fds, max_fd); FD_SET(server_fd, &read_fds); // pull up the server_fd(index) bit in read_fds printf("After execute FD_SET with server_fd:\n"); print_fd_set(&read_fds, max_fd); // Add active client sockets to read_fds for (int i = 0; i < MAX_CLIENTS; i++) { if (clients[i].fd != -1) { FD_SET(clients[i].fd, &read_fds); if (clients[i].fd > max_fd) { max_fd = clients[i].fd; } } } printf("After execute FD_SET with client_fd:\n"); print_fd_set(&read_fds, max_fd); // Use select to monitor sockets if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) { // max_fd + 1 -> nfds perror("Select failed"); continue; } printf("After call select:\n"); print_fd_set(&read_fds, max_fd); // Check for new connection if (FD_ISSET(server_fd, &read_fds)) { struct sockaddr_in client_addr; socklen_t client_len = sizeof(client_addr); int client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len); if (client_fd < 0) { perror("Accept failed"); continue; } printf("After accept client connection:\n"); print_fd_set(&read_fds, max_fd); // Get client socket information char client_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, INET_ADDRSTRLEN); int client_port = ntohs(client_addr.sin_port); printf("客戶端 %s:%d 已連接\n", client_ip, client_port); // Find an empty slot for the new client int i; for (i = 0; i < MAX_CLIENTS; i++) { if (clients[i].fd == -1) { clients[i].fd = client_fd; strncpy(clients[i].ip, client_ip, INET_ADDRSTRLEN); clients[i].port = client_port; break; } } if (i == MAX_CLIENTS) { printf("太多客戶端,拒絕連接\n"); close(client_fd); } else if (client_fd > max_fd) { max_fd = client_fd; } } // Check for data from clients for (int i = 0; i < MAX_CLIENTS; i++) { if (clients[i].fd != -1 && FD_ISSET(clients[i].fd, &read_fds)) { char buffer[BUF_SIZE]; ssize_t bytes_received = recv(clients[i].fd, buffer, BUF_SIZE - 1, 0); if (bytes_received <= 0) { if (bytes_received < 0) { perror("Connection error"); } else { printf("客戶端 %s:%d 已斷開連線\n", clients[i].ip, clients[i].port); } close(clients[i].fd); clients[i].fd = -1; continue; } buffer[bytes_received] = '\0'; printf("收到來自 %s:%d 的訊息: %s\n", clients[i].ip, clients[i].port, buffer); // Send echo message back to client if (send(clients[i].fd, buffer, bytes_received, 0) < 0) { perror("Connection error"); close(clients[i].fd); clients[i].fd = -1; continue; } printf("訊息已送出...\n"); print_fd_set(&read_fds, max_fd); } } } // Cleanup close(server_fd); printf("伺服器關閉\n"); return 0; } ``` ::: ::: spoiler client 完整測試程式碼 ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h> #include <errno.h> #define CONNECT_HOST "140.116.245.209" #define CONNECT_PORT 1234 #define BUF_SIZE 1024 int main() { // Create socket int client_fd = socket(AF_INET, SOCK_STREAM, 0); if (client_fd < 0) { perror("Socket creation failed"); return 1; } // Set up server address struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); // Initialize server_addr structure to zero server_addr.sin_family = AF_INET; server_addr.sin_port = htons(CONNECT_PORT); // Convert the CONNECT_HOST from string to binary if (inet_pton(AF_INET, CONNECT_HOST, &server_addr.sin_addr) <= 0) { perror("Invalid address"); close(client_fd); return 1; } // Connect to server if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) { perror("Connection failed"); close(client_fd); return 1; } // Get local address and port struct sockaddr_in local_addr; socklen_t addr_len = sizeof(local_addr); if (getsockname(client_fd, (struct sockaddr*) &local_addr, &addr_len) < 0) { perror("Getsockname failed"); close(client_fd); return 1; } // Get local address char local_ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &local_addr.sin_addr, local_ip, INET_ADDRSTRLEN); int local_port = ntohs(local_addr.sin_port); // ntohs: Network to Host Short (16-bits integer) printf("本機: %s:%d\n", local_ip, local_port); while(1) { // Get input char input[BUF_SIZE]; printf("輸入訊息: "); if (fgets(input, BUF_SIZE, stdin) && input[0] == '\n') { printf("輸入的訊息不得為空\n"); continue; } // Remove newline character("\n") from input input[strcspn(input, "\n")] = 0; // Send echo message back to client if (send(client_fd, input, strlen(input), 0) < 0) { perror("Send failed"); close(client_fd); return 1; } printf("訊息已傳送...\n"); // Receive response char buffer[BUF_SIZE]; // To store recevied message ssize_t bytes_received = recv(client_fd, buffer, BUF_SIZE - 1, 0); if (bytes_received < 0) { perror("Receive failed"); break; } else if (bytes_received == 0) { printf("伺服器已斷開連線\n"); break; } else { buffer[bytes_received] = '\0'; printf("收到訊息: %s\n", buffer); } } // Cleanup close(client_fd); printf("客戶端已中止...\n"); return 0; } ``` ::: \ 預期輸出: - 在尚未有客戶端連線時,可以看到 `read_fds` 中 `fd 3` 被設為 1,而程式 block 在 select(),因為還尚未有 fd 可讀。 ```bash jimmy@NEAT:~/linux2025/socket_exam$ ./server_select 伺服器已啟動,等待連接... Init read_fds: fd_set contents (up to 3): fd 0: 0, fd 1: 0, fd 2: 0, fd 3: 1, ----------------------------------------------------------------- After execute FD_ZERO: fd_set contents (up to 3): fd 0: 0, fd 1: 0, fd 2: 0, fd 3: 0, ----------------------------------------------------------------- After execute FD_SET with server_fd: fd_set contents (up to 3): fd 0: 0, fd 1: 0, fd 2: 0, fd 3: 1, ----------------------------------------------------------------- After execute FD_SET with client_fd: fd_set contents (up to 3): fd 0: 0, fd 1: 0, fd 2: 0, fd 3: 1, ----------------------------------------------------------------- ``` :::info 為什麼在尚未有客戶端連線時,server_fd 是不可讀的? > server_fd 是監聽狀態的 socket,因此,在當有新的 client 執行 connect() 後,也就是有一個 connection 在等待被 accept() 時,監聽 socket 才可讀。 ::: - 在當有客戶端連線時,可以發現多了以下的輸出內容,首先是 select 發現 `fd 3` 可讀了(因為有新的連線),當新的連線進來後,因為新連線的 fd 值會大於 max_fd,因此需將 max_fd 更新為最新連線的 fd,接著就重複程式中迴圈的流程(參考上述 server 完整測試程式碼)。 ```bash After call select: fd_set contents (up to 3): fd 0: 0, fd 1: 0, fd 2: 0, fd 3: 1, ----------------------------------------------------------------- After accept client connection: fd_set contents (up to 3): fd 0: 0, fd 1: 0, fd 2: 0, fd 3: 1, ----------------------------------------------------------------- 客戶端 140.116.245.209:36360 已連接 After execute FD_ZERO: fd_set contents (up to 4): fd 0: 0, fd 1: 0, fd 2: 0, fd 3: 0, fd 4: 0, ----------------------------------------------------------------- After execute FD_SET with server_fd: fd_set contents (up to 4): fd 0: 0, fd 1: 0, fd 2: 0, fd 3: 1, fd 4: 0, ----------------------------------------------------------------- After execute FD_SET with client_fd: fd_set contents (up to 4): fd 0: 0, fd 1: 0, fd 2: 0, fd 3: 1, fd 4: 1, ----------------------------------------------------------------- ``` 註:由於 select 能監視的 file descriptor 有限(被巨集 FD_SETSIZE 限制,通常為 1024),故現在需支援大量服務的系統不再使用,改用 poll(2) 或 epoll(7) 替代。 ## shuffle 實作 ### q_shuffle > commit: [484b7ea](https://github.com/sysprog21/lab0-c/commit/484b7ea9ed552612c31aacd5081eb9da5ef354a7) 參考 [你所不知道的 C 語言:Fisher–Yates shuffle](https://hackmd.io/@sysprog/c-linked-list#Fisher–Yates-shuffle) 演算法實作,透過 Pencil-and-paper method 對 Linux Kernel 的環狀雙向鏈結串列進行隨機洗牌,每次隨機選取一個節點並將其移動到 `new` 串列,直到所有節點都被移動。 ### do_shuffle > commit: [484b7ea](https://github.com/sysprog21/lab0-c/commit/484b7ea9ed552612c31aacd5081eb9da5ef354a7) 在 `qtest.c` 中新增 `do_shuffle` 函數,參考 `do_swap` 寫法,將 `q_swap(current->q);` 改成 `q_shuffle(current->q);`。詳細流程我還要弄清楚。 ### 分析 q_shuffle 是否 Uniform distribution 透過統計方法驗證 shuffle,利用作業提供的測試檔測試,結果如下: ```bash $ python3 test_shuffle.py Expectation: 41666 Observation: {'1234': 70538, '1243': 5611, '1324': 33258, '1342': 13650, '1423': 15627, '1432': 5872, '2134': 79863, '2143': 82082, '2314': 50809, '2341': 31226, '2413': 17591, '2431': 54725, '3124': 25432, '3142': 66487, '3214': 52806, '3241': 7783, '3412': 39009, '3421': 62571, '4123': 46892, '4132': 35171, '4213': 85991, '4231': 7836, '4312': 58659, '4321': 50511} chi square sum: 363008.4137186195 ``` 測試出來不太均勻,需要調整。 ### 解決方法 > commit: [e28bbdd](https://github.com/JimmyChongz/lab0-c/commit/e28bbdd2500f58dcfbe9b880eb7b6e7e4a47d041) 後來,我發現是程式碼中使用 `srand(time(NULL))` 的問題,因為這個函數會根據時間(1970 年 1 月 1 日到現在的秒數)初始化亂數種子,由於 `time(NULL)` 的精度只有秒,當在短時間內重複呼叫 `q_shuffle()` 時,其中的 `srand(time(NULL))` 會產生相同的亂數種子,導致每次產生的亂數「序列」相同。 **驗證:在短時間內重複呼叫 `srand(time(NULL))` 會產生相同的亂數種子,且每次產生的亂數「序列」相同。** :::spoiler 完整驗證程式碼 ```c #include <stdio.h> #include <time.h> #include <stdlib.h> void test() { srand(time(NULL)); printf("random seed: %lu\n", time(NULL)); for (int i = 0; i < 5; i++) { printf("%d ", rand()); } printf("\n"); } int main() { int count = 5; while (count-- > 0) { test(); } return 0; } ``` ::: :::spoiler 預期輸出(亂數種子相同、序列相同) ``` random seed: 1745983409 1042424400 1453791513 1018818495 61690579 736732418 random seed: 1745983409 1042424400 1453791513 1018818495 61690579 736732418 random seed: 1745983409 1042424400 1453791513 1018818495 61690579 736732418 random seed: 1745983409 1042424400 1453791513 1018818495 61690579 736732418 random seed: 1745983409 1042424400 1453791513 1018818495 61690579 736732418 ``` ::: 因此,當在執行 `test_shuffle.py` 時,在 1,000,000 次的洗牌中,會有部分的洗牌順序相同,導致不均勻洗牌。 再次測試: ```bash $ python3 test_shuffle.py Expectation: 41666 Observation: {'1234': 41430, '1243': 41914, '1324': 41566, '1342': 41802, '1423': 41328, '1432': 41663, '2134': 41660, '2143': 41646, '2314': 41720, '2341': 41667, '2413': 41614, '2431': 41952, '3124': 41714, '3142': 41739, '3214': 41585, '3241': 41503, '3412': 41947, '3421': 41556, '4123': 41554, '4132': 41802, '4213': 41511, '4231': 41509, '4312': 41626, '4321': 41992} chi square sum: 16.01344021504344 ``` ![image](https://hackmd.io/_uploads/SJgTrrkelg.png =65%x) 可以發現,1234 的各種排列組合出現的次數皆為四萬多次,均勻多了。 ## 讀論文 〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉 > 參考作業提供的 [論文重點提示](https://hackmd.io/@sysprog/linux2025-lab0-d#-論文%E3%80%88Dude-is-my-code-constant-time〉重點提示) 從引言的敘述中,推測 **Timing Leakage** 指的是程式執行時間與秘密數據之間的關聯性,導致攻擊者可以透過測量加密實作的執行時間來推測出機密資訊,例如:在 1996 年 Paul Kocher 發現 RSA 私鑰解密的運算執行時間取決於私鑰位元值,所以攻擊者就可以透過測量解密運算的執行時間來逐步恢復私鑰。這也是為什麼要探討 is my code constant time 的重要原因,以確保程式碼在不同輸入條件下的執行時間是固定的,以減少被 side-channel attacks 的可能性。 ## 參考論文提供的 [GitHub](https://github.com/oreparaz/dudect) 來修改 > commit: [42af940](https://github.com/JimmyChongz/lab0-c/commit/42af94069697c218a80297bed43e25bcb4ef464d) ### Macro 中的 `##` 是什麼? 參考規格書對 ## operator 的敘述: ![截圖 2025-03-15 下午1.03.50](https://hackmd.io/_uploads/SJvrctMh1l.png) 得知 ## 是在巨集中的「token 連接符號」,在 C 的預處理器中,## 可以將兩個 token 合併成一個 token,當參數為空時,C 預處理器會插入 placemarker,它最終會被刪除,不會影響程式碼的執行結果,例如: ```c #include <stdio.h> #define A(x) Value_##x #define B(x, y) x##y int main(int argc, const char * argv[]) { int A(1) = 9; int A(a) = 21; printf("%d, %d\n", Value_1, Value_a); int B(a, 1) = 21; int B(b, 1) = 9; printf("%d, %d\n", a1, b1); printf("%d\n", B(123, )); // 空參數由 placemarker 代替 } ``` ``` //output 9, 21 21, 9 123 Program ended with exit code: 0 ```