# 2025q1 Homework1 (lab0) > contributed by < `RealBigMickey` > {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ***[點我到 2025q1 Homework1 (ideas)](https://hackmd.io/@BigMickey/linux2025-ideas)*** 我是成大資訊 116 乙的石維廉,以下是紀錄著我在 2025 年「Linux 核心設計」Lab0遇到的問題與開發的過程紀錄 > **May 30, 2025 更**: 撰寫時是我第一次接觸hackmd,操作上跟共筆的怎樣排版都還不太熟悉,所以整體很亂沒頭沒緒,接下來有時間會回來慢慢的修 Project [Github Link](https://github.com/RealBigMickey/lab0-c) ### 開發環境 ```clike bigmickey@Linuxer:~$ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 bigmickey@Linuxer:~$ 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): 6 On-line CPU(s) list: 0-5 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i5-9600KF CPU @ 3.70GHz CPU family: 6 Model: 158 Thread(s) per core: 1 Core(s) per socket: 6 Socket(s): 1 Stepping: 12 CPU(s) scaling MHz: 17% CPU max MHz: 4600.0000 CPU min MHz: 800.0000 BogoMIPS: 7399.70 ``` # Linux核心的連結串列排序: ## q_size ![image](https://hackmd.io/_uploads/HyYUdg1o1g.png) ![image](https://hackmd.io/_uploads/HJ8Utgyj1e.png) 遇到問題之 — hook 警告一些進去卻看不到的字元 嗯,看到心情就很差 輸入e進去後,預設是使用 vi 而不是 vim ,只是小事但我們都知道沒有人會自願用 vi 編輯。 ![image](https://hackmd.io/_uploads/ryFebexj1x.png) 嗯。 看到這個時我笑出來了 在這種情況下我也沒辦法,只好使用神器: **$ git commit --no-verify** ## q_delete_dup(): ![image](https://hackmd.io/_uploads/rJitc4xoye.png) 完成了 Leetcode 上對應的題目,努力的把程式寫的"有品味"。但一旦到了 lab0 這卻發現乍看 list_head 的定義中沒有 val 或其他儲存資料方式,又頓時敢到了無比困惑… - 向 GPT 求助 -> 雙手一攤,它不會 ❌ - 向同學求助 -> 甚至還沒開始做... ❌ - 向助教協助 -> 助教人非常好🥹 ✅ 助教說要用 list_entry 來看,因為它是侵入式結構。 再次仔細看過 list.h, queue.c & queue.h 此時才發現包覆 list_head 結構的結構叫 "element_t",而list_entry利用記憶體相鄰的特性去找到包著的結構。 ![image](https://hackmd.io/_uploads/H1VwQiejkx.png) list_entry 與 container_of 毫無差別, ![image](https://hackmd.io/_uploads/BJT9mjliJg.png) 依 GPT 的解釋,if 的兩種版本差在編譯器的版本,有些編譯器沒有 typeof 有些有。邏輯上一樣,不過有 typeof 的版本多個安全網,如果 type 上不一樣會丟出 error,另一版不會停止運行,造成錯誤。 ![image](https://hackmd.io/_uploads/S1TDBwUT1g.png) ## q_swap 開始抓到 lab0 的規律,寫起來更快、問題也沒有向前面幾個那麼多。Leetcode 寫完搬過來,再改成支援 doubly-linked-list 即可 ## q_reverse 非常的直觀,從head開始把所有的 next 與 prev 交換。 ## q_reverseK 第一版程式碼: ![image](https://hackmd.io/_uploads/Sy4H19-syx.png) 邏輯 - pp 在原地、pp2 先走到 k 節點位置 next_group,紀錄下一組的初始節點,交換兩者後再修正 *next 與 *prev 指標,最後設 pp 與 pp2 到 next_group。 *示意圖:* ![image](https://hackmd.io/_uploads/B1XU_PUpke.png) 問題:說實話我找不到,但程式就是困再迴圈中出不去。詢問 GPT 也沒啥用。 走頭無路時就該回頭! 第二版程式碼: ![image](https://hackmd.io/_uploads/rkIW6T-ske.png) 這個版本是把 Leetcode 對應題目寫完、修改、再搬過來的。第一版的邏輯依靠 *prev,這一版則是使用stack。 一指標跑過 k 個節點,把路過的節點放入一 stack 之中。 依序 pop 出 stack 裡的節點們,並適當更新他們的 *next 與 *prev 指標。 **優點:** - 能動、邏輯上比較好理解 **缺點:** - 邏輯上第一版若能動的話比較快 - 用 malloc 而使用到更多的記憶體 自己還不夠熟練,這個函式也是一直遇到問題並不停修正 ## q_sort 第一時間想到的排序法是 quicksort ,多數情況下是最快的排序法,第二是使用 Bubblesort 來進行排序,一來程式較為簡單,二來記憶體使用率比較低。但後來覺得不能逃避問題,想得到就該做,因此毅然決然使用 qsort。 先用一 pointer 掃過每個節點並加入一陣列之中,陣列大小從5開始,若空間不夠則將 realloc 擴大到兩倍。紀錄完成陣列後再使用 <stdlib.h> 中的 qsort 進行排列,最後再 free 掉創的矩陣。 ```c short int cmp_ascend(const void *a, const void *b) { struct list_head *A = *(struct list_head **) a; struct list_head *B = *(struct list_head **) b; const element_t *nodeA = list_entry(A, element_t, list); const element_t *nodeB = list_entry(B, element_t, list); return nodeA->value - nodeB->value; } short int cmp_descend(const void *a, const void *b) { struct list_head *A = *(struct list_head **) a; struct list_head *B = *(struct list_head **) b; const element_t *nodeA = list_entry(A, element_t, list); const element_t *nodeB = list_entry(B, element_t, list); return nodeB->value - nodeA->value; } void q_sort(struct list_head *head, bool descend) { if (list_empty(head)) return; struct list_head *p = head->next; short int a_max = 5; short int a_size = 0; struct list_head **arr = malloc(sizeof(struct list_head *) * a_max); while (p != head) { arr[a_size++] = p; if (a_size == a_max) { a_max *= 2; arr = realloc(arr, sizeof(struct list_head *) * a_max); } p = p->next; } if (descend) qsort(arr, a_size, sizeof(struct list_head *), cmp_descend); else qsort(arr, a_size, sizeof(struct list_head *), cmp_ascend); arr[0]->prev = head; head->next = arr[0]; for (int i = 0; i < a_size - 1; i++) { arr[i]->next = arr[i + 1]; arr[i]->next->prev = arr[i]; } arr[a_size - 1]->next = head; head->prev = arr[a_size - 1]; free(arr); } ``` **優點:** - 在 list 夠大的情況下仍比 bubblesort 快 **缺點:** - 須耗費大量記憶體儲存每個節點 - 應該有更好的作法,只是此時還沒想到 ![image](https://hackmd.io/_uploads/rJb5nkMs1e.png) 嘗試commit 時會遇到此問題,意思是當realloc失敗時目前的作法會導致 memory leak,得架個安全往才行 ```c arr = realloc(arr, sizeof(struct list_head *) * a_max); ``` **改為** ```c struct list_head **new = realloc(arr, sizeof(struct list_head *) * a_max); if (!new) { free(arr); arr = NULL; return; } ``` 後續問題-git hook - 若仔細看上面的程式碼會發現 cmp_descend 與 cmp_ascend 兩個函式的 return type 為 short int ,但 qsort 希望傳進去的函式回傳 int ,因此在 git push 時會被擋下來。 修正後進行 commit 後問題來了,git hook 會認為 cmp 為 typo 而無法進行此修正。 ![image](https://hackmd.io/_uploads/rkImy5fi1x.png) 理論上 q_reverseK 等也不是正確的英文單字,或許在設定檔中有允許這些函式的單字?後續嘗試了在函式前面加 "functions" 或將函式名用「\`」符號包住,但依然無法 commit。 前面教授提過函式命名規則,不應該亂縮寫,不過感覺 compare 縮成 cmp 還算常見? *- 最後決定把 compare 改成 cmp 暫時略過此問題。* **更:** ![image](https://hackmd.io/_uploads/H1sYsbmoye.png) 後來發現函式中的比較邏輯有大問題,當時寫的時候誤把字串當作整數而使用減法進行比較,已更正改用 strcmp 進行比較。 ## q_ascend 邏輯上不會太難,先從最後一個節點開始往前走,並開始紀錄當下出現過的最大節點,若比較小則跳過並釋放記憶體、比較大則接上 linked list。 我認為最大的難點是如何紀錄目前最大的數值,因每個節點中儲存的資料均為字串,無法向整數那麼容易紀錄。 **Idea💡:** 創一字串 max ,並分配足夠多的記憶體(這邊假設每一節點中的字串都 < 256 個字元) 將最後一節點的字串複製進去當作初始化,接下來比較節點大小時都使用 strcmp 函式去做。 *第一版程式碼:* ```c #define LONGEST_STRING_LENGTH 256 int q_ascend(struct list_head *head) { if (list_empty(head)) return -1; struct list_head **pp = &head->prev; struct list_head *prev = head; char *max = malloc(LONGEST_STRING_LENGTH); const element_t *last_e = list_entry(*pp, element_t, list); strcpy(max, last_e->value); while (*pp != head) { const element_t *e = list_entry(*pp, element_t, list); if (strcmp(max, e->value) < 0) { strcpy(max, e->value); (*pp)->next = prev; prev->prev = *pp; prev = *pp; *pp = (*pp)->prev; } else { struct list_head *temp = (*pp)->prev; list_del(*pp); *pp = temp; } } *pp = prev; free(max); return 0; } ``` ![image](https://hackmd.io/_uploads/Syw8_G7oyx.png) commit 時會被 hook 擋下,因為 strcpy 似乎是個不安全的函式 ``` strcpy: The strcpy built-in function does not check buffer lengths and may very well overwrite memory zone contiguous to the intended destination. In fact, the whole family of functions is similarly vulnerable: strcpy, strcat and strcmp. ``` 解決辦法✅: - 改用 strdup() 函式 ## q_descend 邏輯上與q_ascend相同,差別在: ```c if (strcmp(max, e->value) > 0) { ``` 一行,descend 中改為 ```c if (strcmp(max, e->value) < 0) { ``` ## q_merge 第一眼看到 q_merge 是有點不知所錯的,只有傳入一個 list_head *head,不確定具體是要什麼跟什麼合起來。 研讀queue.h 努力理解後,發現一結構 queue_context ```c typedef struct { struct list_head *q; struct list_head chain; int size; int id; } queue_contex_t; ``` 我認為每個 list_head 的頭都被這個結構包住,而 queue_context就好比一串項鍊,連接著一個個 queue。 若要合併,邏輯上就是反覆操作著「合併兩 list 」這件事,但及便是這樣仍有有兩種作法可實行: ### 一、依序合併 ![image](https://hackmd.io/_uploads/ByuHmOUakl.png) 先將A、B合併,再合併B、C,C、D...以此類推 每一次的合併一次得走 n 個節點,假設總共有 k 個 queue ,則總共得走 n * k次,時間複雜度: **- O(n) = n * k** **缺點:** - 執速度較慢 ### 二、兩兩合併 ![image](https://hackmd.io/_uploads/SkL4QOLpkg.png) ![image](https://hackmd.io/_uploads/HJ9G8_Ip1x.png) 兩兩一組的將每個節點合併,若 list 為奇數則跳過落單的。 每一次的合併一次得走 n 個節點,假設總共有 k 個 queue ,則總共得走 n log2(k)次,時間複雜度: **- O(n) = n * log k** **優點:** - 執行速度較快 ::: info ### **注意!** 以上所有函式都是先寫完並 commit 後,再隨後去改,因為一直不清楚怎麼去測各個函式才好,所以決定先把能做的地方做一做,後面再來修正。 ::: # Bug 地獄 > ![image](https://hackmd.io/_uploads/HyvlGO9pJx.png) > 時不時會無法執行 ```$ make test```,至今還不知道原因 再次重頭審視程式一遍,來修正一些它看到的潛在問題。例如: - list_head 為 NULL 指標 - list 中只有 head - 某些函式刪除節點時沒釋放記憶體 當我執行 ```& ./qtest```會出現以下錯誤訊息↓ ![image](https://hackmd.io/_uploads/Syz7LQuoke.png) Commit history 中有兩個 commit 沒有 change-ID,而這直接導致無法測試。原因是當初 commit 時加了指令 --no-verify,而使用該指令的原因均是檢查文法時遇到了問題而無法 commit 1. ".gitignore" 會被 git hook 擋下,它認為 gitignore 不是個合法單字 2. "kfree" 也會被 git hook 擋下,它認為 kfree 也不是合法單字 **解法:** - 執行```git rebase -i HEAD~20``` - 編輯兩個有問題的 commit - 文字間加個底線來繞過偵測↓ *(gitignore -> git_ignore 、 kfree -> k_free)* - 最後再 ```git push origin HEAD --force``` (雖然這麼做有點危險,但想不到什麼更好的方法QQ) 接下來開始執行 traces 中的各個檔案: traces-01-ops.cmd ✅ traces-02-ops.cmd <span style="color: red;">ERROR</span> - 修正 insert_tail 更改指標時邏輯錯誤 - 確保 q_delete_mid 的 value 記憶體也會被釋放 traces-03-ops.cmd <span style="color: red;">ERROR</span> ![image](https://hackmd.io/_uploads/H15Jkatiyx.png) - q_sort 的邏輯使用了一 array ,初衷是犧牲記憶體來提升速度,但很明顯這是不被允許的,因此重新寫了一次。新版使用最笨最不會出錯的 bubble sort 來進行排序。 (後面有再次實作 merge sort,泡沫排序速度太慢) ### q_merge ![image](https://hackmd.io/_uploads/HJaCl-3jJl.png) *(此時已經過5小時) 撞到了牆壁,看不出 bug 可能的點,gpt 更是沒用,房間充滿了無助感。* 我想說獨立開個新檔案,把要用的函式通通帶進來,再使用printf去細找問題。 但,最後仍無望。 ![image](https://hackmd.io/_uploads/r1oRPZnikx.png) 有尋求教授協助,但教授是請我研讀C語言規格書,並使用 GDB 來分析問題。 後來出去玩了幾個小時,回家後洗個澡靜下心來思考,我其實沒理解過 qtest 的原理。當我們寫 new 時,程式會關注並印出此 list,it 與 ih 指令也是擴充並印出此 list。那麼問題來了,執行 merge 後只會剩下一 list ,那此時 l 顯示的 list 究竟是被刪掉的 list 還是最後留下來的 list 呢?會顯示哪個? ### 錯誤一: **原來 q_merge 不會刪掉其他list...** ### 錯誤二: **q_merge 回傳值應為最後一 list 的 size** 用 for 迴圈走過整個 chain,將每個節點與第一個節點 merge,即修正完 q_merge。 traces-04-ops.cmd ✅ traces-05-ops.cmd ✅ traces-06-ops.cmd <span style="color: red;">ERROR</span> - q_delete_dup 函式有問題,遲遲找不太到問題,決定直接打掉重寫最快。很多問題來自當初寫函式時對list.h, queue.h 與各個結構還不夠熟悉,所以會操作錯誤或是漏掉一些細節e.g. 更新 q->size 或是善用提供的函式與 macro。 - q_descend 邏輯不變,但幾乎整個重寫,原本的作法是將節點刪掉並繼續往回走,但沒有刪清楚也沒有更新好各節點的 prev 與 next 指標,也並未回傳新串列的 size。 - q_ascend 與 q_descend 大同小異,簡單改 strcmp 判斷式中的大小寫即可。 - q_reverseK 也是動大刀,原先作法紙上行得通但礙於使用 malloc 違法就得換一個作法。新作法相似於 q_sort ,創一新 list_head,走過 k 個節點後開始往回走並一一加到新的 list_head 中,反覆操作,最後再把 list_head 與 原先的 head 結合 ✅ traces-07-string.cmd ✅ traces-08-robust.cmd ✅ traces-09-robust.cmd ✅ traces-10-robust.cmd ✅ traces-11-robust.cmd ✅ traces-12-robust.cmd ✅ traces-13-robust.cmd <span style="color: red;">ERROR</span> - 修正 q_free 嘗試釋放 NULL 指標 ✅ traces-14-perf.cmd <span style="color: red;">ERROR</span> ![image](https://hackmd.io/_uploads/HJKf2jAskx.png) 就如上面所述,前面 q_sort 撞牆後改使用了 bubblesort ,但 bubblesort O(n^2)似乎太慢,無法通過測驗。 後來一樣整個重寫,這次決定使用 mergesort O(n * log n) 來提升速度。 ![image](https://hackmd.io/_uploads/ryPiwZxnkl.png) **嗯。** 測驗後發現插入100000 時還沒問題,但一但插入1000000個元素就會Segmentation fault,猜測市樣本太大會發生stack overflow > 更:使用 iterative quicksort 會超時,嘗試將mergesort 改為iterative 也是超時以失敗結尾,之後再慢慢嘗試 #### <span style="color: red;">!!這個問題尚未解決!!</span> traces-15-pref.cmd <span style="color: red;">ERROR</span> ![image](https://hackmd.io/_uploads/Sk_A9be2Je.png) - 很奇妙,q_reverse 函式只有交換 next 與 prev 指標,卻會導致 memory leak ,而程式碼的邏輯也非常簡單明瞭。 ```c void q_reverse(struct list_head *head) { if (!head || list_empty(head)) return; struct list_head *p = head; do { struct list_head *temp = p->next; p->next = p->prev; p->prev = temp; p = temp; } while (p != head); } ``` ![image](https://hackmd.io/_uploads/S1Fm2blhkg.png) 雖然敘述這麼寫,但我的 q_sort 卻在 50000 就會 Segmentation fault,即便理論上使用的是個 O(n * log n)的 mergesort,這給了我蠻大的挫折感 #### <span style="color: red;">!!這個問題尚未解決!!</span> traces-16-pref.cmd ✅ *(即便 traces-16 裡插入了1000000個元素,也進行了 reverse,卻不會導致 memory leak)* - ![image](https://hackmd.io/_uploads/rJDFkflnke.png) 就也不知道為什麼會這樣 #### <span style="color: red;">!!這個問題尚未解決!!</span> *<a style="color:grey"> - Apr 2, 2025</a>* ![image](https://hackmd.io/_uploads/H1CDCU96kx.png) 四週後,把作業二與作業四完成後回頭修正作業一,發現怎麼執行 ```$ make test``` 出來 trace03 就會出錯?印象中之前已經修正過沒問題的。再次打開tester.cmd 製作測資跟假想,猛猛閱讀程式碼,很多細節已經被埋在記憶裡。 因為看不到輸出也就很難抓問題,再次整個函式抓出來建立一新 c 檔 [test.c](https://github.com/BigMickey69/Linux2025/blob/main/test.c),在控制的環境中去尋找問題。大概在這邊反覆耗了 ~6 小時,百思不得其解。一次更改測資終將 sort 刪除後發現過了,是 sort 指令導致的錯誤... <a style="color:red">原來後續更新的 sort 指令有問題,但問題比較深、 sort 之後乍看之下沒問題,猜測是指標更新上出了錯😮‍💨</a> 抓出來成 test2.c 獨立測試:![image](https://hackmd.io/_uploads/S1DX0Dcayg.png) 恩,prev 指標還原時錯誤 ```c void q_sort(struct list_head *head, bool descend) { ... for (cur = head->next; cur->next != NULL; cur = cur->next) { cur->prev = prev_node; prev_node = cur; } cur->next = head; // 少了這行 cur->prev = prev_node; head->prev = cur; } ``` 沒更新到最後一節點的 prev 指標,導致串列順序有問題,而順序有問題會導致 q_merge 在合併時指標更新錯誤 總算 traces-03-ops.cmd ✅ trace-17-complexity.cmd: ![image](https://hackmd.io/_uploads/B124cT9pJe.png) 有時候執行時會有幾個測出 constant time,有時候不會。發現執行後最小話 vscode 視窗會比較容易通過,真的非常玄。 即便測試其他 insert head/delete head/insert tail/delete tail 的程式碼仍有這個問題 ¯\\_(ツ)_/¯ 目前: --- TOTAL 83/100 ## 在 qtest 中新增指令 這在 quiz2 有出現,quiz2 測驗一的延伸問題一題如下 :::success 預期目標: - ... - 將程式碼整合進 lab0 提及的 qtest,並探討環狀雙向鏈結串列的排序效能,並善用 [perf](https://hackmd.io/@sysprog/linux-perf) 一類的效能分析工具,探討包含 Linux 核心 [list_sort](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 在內排序實作的效能表現並充分解釋 ::: ### quicksort 融入 lab0 與新增 qtest 指令 第一步驟是先修改測驗中給的範例程式碼: 將 listitem 改為 element_t,cmpint 改用 strcmp 基本上就能放入 queue.c 之中了。 在 queue.h 之中也得定義一行 ```c void list_quicksort(struct list_head *head); ``` 接下來進入 qtest.c ,新增一函式 do_quicksort ,程式碼這邊完全取自 do_sort 。來到 console_init 函式,新增一行: ```c ADD_COMMAND(quicksort, "**Assignment from N02** Sort queue in ascending order", ""); ``` 完成後重新執行 make 指令,測試 qtest : >![image](https://hackmd.io/_uploads/SyijQUH3kx.png) *help 畫面有 quicksort 指令* ![image](https://hackmd.io/_uploads/rydwNLB2Jl.png) *沒問題!* ![image](https://hackmd.io/_uploads/ByUYaLrnkx.png) 問題:這些改動 commit 到 lab0 中時會被擋下, 本地執行的話沒問題,但若希望 git push 上去就不能碰 queue.h,因此後來把程式寫進 qtest.c ### 利用 perf 分析 ![image](https://hackmd.io/_uploads/r1GK0nH21l.png) ![image](https://hackmd.io/_uploads/HyXy16S3kx.png) ![image](https://hackmd.io/_uploads/ByS-Z6r2yl.png) 可看見這個 mov 指令旁邊寫了 100% ,但這不代表這個指令花費了這部份全部的處理器時間,而是這邊是熱點,每次檢查時都在這邊。 這意味著時間都花在這個迴圈之中,也就是走過連結串列的迴圈中,算是在預料之內。 **如何簡短時間?** 既然時間都花在走過各連結,那麼解決改善的方向也很簡單,減少需要在串列上走的時間。 :::info **我的想法:** 每次呼叫 list_move_tail 都得走遍整個連結串列找到尾巴,浪費時間。 - 使用 lise_move: - 優:省時 - 缺:破壞 stable sort - 改用雙向連結串列: - 優:省時,尾巴為 head->prev - 缺:節點結構得花更多記憶體 ::: # 亂數生成 & Shuffle ### 生成亂數 每個人第一次碰程式時都會遇到的問題,電腦(至少目前)還不可能做到真正的隨機數,回想起去年程式設計一的洗禮,張燕光教授教我們利用 rand() 函式去生成一隨機數。 步驟如下:先決定一直種子,再丟入 rand 函式中,rand 會再套公式後吐出一答案,學校從二十年前都是這麼教的 #### rand() 又是如何生成亂數? ```c int rand(u_int *seed) { *seed = *seed * 1103515245 + 12345; return (*seed & RAND_MAX); } ``` 這是 c 教材中的模樣,舊版的 rand 函式。乘上 1103515245 再加 12345,因為只有32位元,最後會只取2^23的數字,好比一次 modulus 運算,這就是個典型的 linear congruential generator(LCG)。 一個理想的 LCG 要能在繞一圈前完整走過所有的數字,至於具體為什麼是 1103515245 的話我還不確定,但1103515245 % 4 = 1 而 12345 為奇數,符合能完整走過所有數字。 從 c 版本 2.36 以後,rand 函式就改變了,新邏輯如下: ```c int rand (void) { return (int) __random (); } // in stdlib/random.c long int __random (void) { int32_t retval; __libc_lock_lock (lock); (void) __random_r (&unsafe_state, &retval); __libc_lock_unlock (lock); ``` rand 呼叫 __random ,__random 上鎖全域資源並呼叫 __random_r 後解鎖,而 __random_r 才是重點: ```c int __random_r (struct random_data *buf, int32_t *result) { int32_t *state; if (buf == NULL || result == NULL) goto fail; state = buf->state; if (buf->rand_type == TYPE_0) { int32_t val = ((state[0] * 1103515245U) + 12345U) & 0x7fffffff; state[0] = val; *result = val; } else { int32_t *fptr = buf->fptr; int32_t *rptr = buf->rptr; int32_t *end_ptr = buf->end_ptr; uint32_t val; val = *fptr += (uint32_t) *rptr; /* Chucking least random bit. */ *result = val >> 1; ++fptr; if (fptr >= end_ptr) { fptr = state; ++rptr; } else { ++rptr; if (rptr >= end_ptr) rptr = state; } buf->fptr = fptr; buf->rptr = rptr; } return 0; fail: __set_errno (EINVAL); return -1; } ``` buf->rand_type 決定使用的演算法,舊的算法依然存在,但預設為 TYPE_3 ,有另一套邏輯去生成亂數,也可用```char *initstate(unsigned int seed, char *state, size_t n);```改變 rand_type。 這是 Linear Feedback Shift Register (LFSR),裡面使用了: - 一矩陣 - 兩個指向矩陣裡面的指標 - 當指標走到盡頭,將它送回起點,好比環狀鍊結串列 ```c val = *fptr += (uint32_t) *rptr; *result = val >> 1; ``` 將 buffer 中兩數值相加並存進 val,捨棄 LSB 後回傳解,剩下的程式碼是為了實現環狀 buffer 的部份就得看向 srand 的一串函式呼叫,從 srand(seed) → __srandom(seed) → __srandom_r(seed, &unsafe_state) ```c int __srandom_r (unsigned int seed, struct random_data *buf) { int type; int32_t *state; long int i; int32_t word; int32_t *dst; int kc; if (buf == NULL) goto fail; type = buf->rand_type; if ((unsigned int) type >= MAX_TYPES) goto fail; state = buf->state; /* We must make sure the seed is not 0. Take arbitrarily 1 in this case. */ if (seed == 0) seed = 1; state[0] = seed; if (type == TYPE_0) goto done; dst = state; word = seed; kc = buf->rand_deg; for (i = 1; i < kc; ++i) { /* This does: state[i] = (16807 * state[i - 1]) % 2147483647; but avoids overflowing 31 bits. */ long int hi = word / 127773; long int lo = word % 127773; word = 16807 * lo - 2836 * hi; if (word < 0) word += 2147483647; *++dst = word; } buf->fptr = &state[buf->rand_sep]; buf->rptr = &state[0]; kc *= 10; while (--kc >= 0) { int32_t discard; (void) __random_r (buf, &discard); } done: return 0; fail: return -1; } ``` 非常大略的邏輯: - state[0] 設為我們傳入的種子 - 利用 [Park-Miller RNG 演算法](https://en.wikipedia.org/wiki/Lehmer_random_number_generator)來填滿 state 矩陣 - 設定 fptr, rptr 兩指標 OK 回到我的目標:生成亂數。最好能夠均勻分佈,也很難旁敲側擊找出規律。 要利用 rand 函式生成亂數,關鍵莫過於傳入的種子,盡可能的利用各方面的「熵」一起運算,來疊加出一個熵更大更難回推的數字。思考過程沒有很有趣,我想了一下就我目前的知識與課堂中學到的結合,弄出個簡單公式。 ```c /* Try to get a random-ish seed, * with entropy from all different sources. */ struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); uint32_t val = ts.tv_sec ^ ts.tv_sec << 16; val = val ^ ts.tv_nsec ^ ts.tv_nsec << 16; unsigned int seed = (unsigned int)(time(NULL) ^ ((uintptr_t)&shuffle)) ^ val; srand(seed); ``` CLOCK_MONOTONIC 紀錄著從開機到當下的過的時間,而 time() 則是回傳電腦紀錄的日曆時間。因 CLOCK_MONOTONIC 有兩部份 -> 秒數與奈秒數,因此再多用了一次 << 16 來確保不會因為當下秒數過小而導致 MSB 都為 0 的狀況 &shuffle 傳入這個函式的地址,每次編譯時這些手動定義的函式他們的地址位置都會有些不一樣,進一步的去提高熵。這手法在本課某一次 quiz 中出現過的,當下看到有留下深刻的印象,因此想再這邊實做一下 ### 1. 先計算 chi-squared test statistic $$ X^2 = \sum_{i=1}^n \frac{(O_i - E_i)^2}{E_i} $$ - $X^2$:Pearson's cumulative test statistic - $O_i$:the number of observations of type i - $E_i$:the expected count of type i 創一新 c 檔,將 shuffle 函式與它需要的所有巨集與函式帶出來,在控制的環境下測試。shuffle 100,000次並將結果寫到 shuffle_out.txt 中,再用 python 來分析資料: | | 觀察到的頻率 | 期待的頻率 | ${(O_i - E_i)^2 \over E_i}$| |-------------|----------|----------|--------------| | 1234 | 4208 | 4166 | 0.410027 | | 1243 | 4120 | 4166 | 0.522667 | | 1324 | 4175 | 4166 | 0.016667 | | 1342 | 4119 | 4166 | 0.545307 | | 1423 | 4139 | 4166 | 0.183707 | | 1432 | 4231 | 4166 | 0.993307 | | 2134 | 4134 | 4166 | 0.256107 | | 2143 | 4157 | 4166 | 0.022427 | | 2314 | 4165 | 4166 | 0.000667 | | 2341 | 4138 | 4166 | 0.197227 | | 2413 | 4237 | 4166 | 1.187227 | | 2431 | 4099 | 4166 | 1.098907 | | 3124 | 4147 | 4166 | 0.092827 | | 3142 | 4111 | 4166 | 0.743707 | | 3214 | 4130 | 4166 | 0.322667 | | 3241 | 4175 | 4166 | 0.016667 | | 3412 | 4226 | 4166 | 0.844907 | | 3421 | 4219 | 4166 | 0.657307 | | 4123 | 4150 | 4166 | 0.066667 | | 4132 | 4260 | 4166 | 2.090667 | | 4213 | 4161 | 4166 | 0.007707 | | 4231 | 4172 | 4166 | 0.006827 | | 4312 | 4208 | 4166 | 0.410027 | | 4321 | 4119 | 4166 | 0.545307 | Total $X^2$ 為 11.239520 ### 2. 決定自由度(degrees of freedom) 在可能結果共有二十四種的情況下,我們知道二十四種結果總和機率為一,因此這次實驗的自由度為二十三,因為有固定數值 $$ P(x_n) = 1-P(x_1)-...-P(x_{n-1}) $$ 因此自由度總共有23個 ### 3. 選擇顯著水準 ```python from scipy.stats import chi2 ... chi_squared = 0 print(f"{'Permutation':<10} {'Observed':>9} {'Expected':>9} {'(O-E)^2/E':>12}") for perm, obs in sorted(counter.items()): contrib = pow(obs - expected, 2) / expected chi_squared += contrib print(f"{perm:<10} {obs:>9} {int(expected):>9} {contrib:>12.6f}") df = n - 1 p_value = 1 - chi2.cdf(chi_squared, df) ... ``` 利用 scipy.stats 中的函式 chi2.cdf 來計算 P value: 0.980609 利用[查表](https://www.obhrm.net/index.php/%E5%8D%A1%E6%96%B9%E5%88%86%E5%B8%83%E8%A1%A8_Chi-Square_Probabilities)來驗證此結果,23自由度中 $X^2$ 為 11.239520 在0.99與0.975之間,符合函式給的數值 ### 4. 統計檢定的結果不拒絕虛無假說 由於 ${P\ value}$ 沒有低於 顯著水準 α ,因此虛無假說 $H_0$ 無法被拒絕,也就是說無法否認 shuffle 的結果遵循 Uniform distribution 。 ![Figure_1](https://hackmd.io/_uploads/ByI135fR1g.png) ok,我很想說機率傾向於一致,不過感覺分佈上沒有很均勻。我認為此作法的熵已經足夠,小修正一下公式就能達到更好的結果。 更:嘗試了一小時,看來小修正很難帶來多大的進步,況且每次執行的 $X^2$ 也都很不一定,即使找到略為更好的公式也很難看到結果 ### 靈異現象: 測試 shuffle 函式時出現一個很玄的現象: ```c int main(void) { LIST_HEAD(my_list); const int count = 10; for (int i = 0; i < count; i++) { struct my_node *node = malloc(sizeof(struct my_node)); if (!node) { perror("malloc"); exit(EXIT_FAILURE); } node->data = i; INIT_LIST_HEAD(&node->list); list_add_tail(&node->list, &my_list); } printf("Original list:\n"); print_list(&my_list); shuffle(&my_list); printf("Shuffled list:\n"); print_list(&my_list); printf("Printed out reversed:\n"); // !!!這行!!! reversed_print_list(&my_list); ... } ``` 若將 reversed_print_list 改為註解,則程式能執行乍看之下沒問題,為了檢查指標是否正確才會寫個 reversed_print_list ,若加上這行,則程式會進入迴圈出不來,無限循環。 但是、但是... 它會循環在 print_list 處而不是 reversed_print_list ,換言之就論結果這行影響到了其他函式 ?-? >**個人揣測:** >- 程式確實有問題,而加上那行後或許編譯器在優化時會有些不一樣,導致這現象 >- 想不到第二種可能性 ```c void print_list(struct list_head *head) { struct list_head *pos; struct my_node *node; list_for_each(pos, head) { node = list_entry(pos, struct my_node, list); printf("%d ", node->data); } printf("\n"); } void reversed_print_list(struct list_head *head) { struct list_head *pos; struct my_node *node; for (pos = (head)->prev; pos != (head); pos = pos->prev) { node = list_entry(pos, struct my_node, list); printf("%d ", node->data); } printf("\n"); } ``` 兩函式也蠻簡易的,沒什麼能出錯的地方 後來抓到我寫錯的地方是 shuffle 有時會不小心把 dummyhead 也 shuffle 掉了,但這無法解釋這問題 # 以 Valgrind 分析記憶體問題 執行過就知道 qtest 中已經利用了 Valgrind 來分析記憶體,上面沒紀錄到但有幾個函式就遇到配置記憶體但忘記釋放的問題, Valgrind 有指出記憶體為釋放的問題 當時沒特別在 hackmd 上紀錄到,但 Github 上有修正問題的 commit 紀錄 ![image](https://hackmd.io/_uploads/rJfVaA8pkg.png) 若沒有 Valgrind 就會有點難抓到,畢竟看不到的東西非常抽象。 此時跑了一下make valgrind: ![image](https://hackmd.io/_uploads/SJjez1Daye.png) 預料之內,結果與```$ make test```一致,還是很多問題需要修正,要碼超時要碼 stack overflow,得慢慢除錯與修正 此時距離寫連結串列時已經過了5週左右,已經忘記很多細節的,中間先做完其他lab,現在回來看真的有點嚇到,不過不慌!一個蘿蔔一個坑,接下來就是腳踏實地的完成 # 整合 tiny-web-server 與 web 命令 小弟不材,花了不少時間在嘗試理解這部份的目標,究竟希望我做什麼? 前面敘述到將 qtest 部份與一個小型的 web-server 結合,且有辦法利用 curl(Client URL) 連線並傳送指令。我們看向 linenoise.c 檔 當使用者執行```./qtest```,程式一開啟就會開始使用 linenoise 來聆聽鍵盤在終端機輸入的內容(此 project 不是使用 stdin)。當終端處跳出```cmd> ```,那就意味著我們的程式正在使用 linenoise 也正在等待著下個 input 。再沒有特別修正的情況下,此時程式將卡在這無法做其他動作,若 web 伺服器端傳來指令,只能在下一次讀取到輸入判斷時一併執行 **Solution: 使用 ```select()```** select() 是個很實用且強大的系統呼叫,主要是提供同時兼顧並聆聽多個 file descriptors 的功用。 socket 端有個 Socket file descriptor。 linenoise 讀取資料時,會從 STDIN_FILENO 讀取,本身也是個 file descriptors (簡稱 fd), 實現 I/O Multiplexing 的函式 ```c int web_eventmux(char *buf) { fd_set listenset; FD_ZERO(&listenset); FD_SET(STDIN_FILENO, &listenset); int max_fd = STDIN_FILENO; if (server_fd > 0) { FD_SET(server_fd, &listenset); max_fd = max_fd > server_fd ? max_fd : server_fd; } int result = select(max_fd + 1, &listenset, NULL, NULL, NULL); if (result < 0) return -1; if (server_fd > 0 && FD_ISSET(server_fd, &listenset)) { FD_CLR(server_fd, &listenset); struct sockaddr_in clientaddr; socklen_t clientlen = sizeof(clientaddr); int web_connfd = accept(server_fd, (struct sockaddr *) &clientaddr, &clientlen); char *p = web_recv(web_connfd, &clientaddr); char *buffer = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n"; web_send(web_connfd, buffer); strncpy(buf, p, strlen(p) + 1); free(p); close(web_connfd); return strlen(buf); } FD_CLR(STDIN_FILENO, &listenset); return 0; } ``` - I/O Multiplexing Model ![image](https://hackmd.io/_uploads/Hk9MtBB0yl.png) ### FD_SET 與 select ```c ... FD_SET(STDIN_FILENO, &listenset); int max_fd = STDIN_FILENO; if (server_fd > 0) { FD_SET(server_fd, &listenset); max_fd = max_fd > server_fd ? max_fd : server_fd; } int result = select(max_fd + 1, &listenset, NULL, NULL, NULL); ... ``` FD_SET 將加 STDIN_FILENO 進 listenset 之中,將 max_fd 設為 STDIN_FILENO。在檢查 server_fd > 0 為有效數字後 FD_SET,最後再找出在這之中的 max_fd select 第一個參數為該檢查的 fd 數量,因此該傳入想檢查到的最大 max_fd + 1 。 select 從 0 開始檢查,直到掃了 max_fd + 1,再重頭一遍。即使還沒 FD_SET 過,通常還是會有幾個預設 fd ,如下: | FD | Use(通常)| | -------- | -------- | | 0 | stdin | | 1 | stdout | | 2 | stderr | 所以 server_fd 最少也是 3 看到這,仔細想想會發現個疑點❓: 這裡的 STDIN_FILENO 定義為 0 ,因為它使用stdin ```if (server_fd > 0)``` 這行已經檢查了 server_fd 是否存在,而 server_fd 一定>=3。 那麼為什麼要 ``` max_fd = max_fd > server_fd ? max_fd : server_fd;``` ? **個人猜測:** 我猜測這是 defensive coding 的一次案例,目前這個 project 的 STDIN_FILENO 是 0,但若哪一天系統做出了大改變,不再等於 0 ,那個 tenary 運算子就可以預防未來改變時 max_fd 錯誤的問題,可以少改程式碼,但代價就是會多幾行組合語言(但對效能的影響應該非常渺小) ```c ... int web_connfd = accept(server_fd, (struct sockaddr *) &clientaddr, &clientlen); // client 做出 request 時標示出來 printf("accepted request, fd is %d, pid is %d", web_connfd, getpid()); ... ``` ### favicon.ico 的問題 用瀏覽器送 request 時, e.g. ```http://localhost:9999/new``` ,它會向伺服器要求圖示 favicon.ico 檔: - Solution: ```c char *buffer = "HTTP/1.1 200 OK\r\n%s%s%s%s%s%s" // 加上下面這幾行 "Content-Type: text/html\r\n\r\n" "<html><head><style>" "body{font-family: monospace; font-size: 13px;}" "td {padding: 1.5px 6px;}" "</style><link rel=\"shortcut icon\" href=\"#\">" "</head><body><table>\n"; ``` 新增作業說明中的 debug lines: ```c // In web_recv ... char *client_ip = inet_ntoa(clientaddr->sin_addr); int client_port = ntohs(clientaddr->sin_port); printf("%s:%d 200 - '%s' (text/plain)\n", client_ip, client_port, req.filename); ... ``` ```c // In web_eventmux ... printf("accepted request, fd is %d, pid is %d", web_connfd, getpid()); ... ``` ### 問題:若 client 連線後沒提供指令呢? 若使用```netcat```連線且不提供任何指令,伺服器會在呼叫 read 等待輸入,因此卡住。 **Solution: 若**