# 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


遇到問題之 — hook 警告一些進去卻看不到的字元
嗯,看到心情就很差
輸入e進去後,預設是使用 vi 而不是 vim ,只是小事但我們都知道沒有人會自願用 vi 編輯。

嗯。
看到這個時我笑出來了
在這種情況下我也沒辦法,只好使用神器: **$ git commit --no-verify**
## q_delete_dup():

完成了 Leetcode 上對應的題目,努力的把程式寫的"有品味"。但一旦到了 lab0 這卻發現乍看 list_head 的定義中沒有 val 或其他儲存資料方式,又頓時敢到了無比困惑…
- 向 GPT 求助 -> 雙手一攤,它不會 ❌
- 向同學求助 -> 甚至還沒開始做... ❌
- 向助教協助 -> 助教人非常好🥹 ✅
助教說要用 list_entry 來看,因為它是侵入式結構。
再次仔細看過 list.h, queue.c & queue.h
此時才發現包覆 list_head 結構的結構叫 "element_t",而list_entry利用記憶體相鄰的特性去找到包著的結構。

list_entry 與 container_of 毫無差別,

依 GPT 的解釋,if 的兩種版本差在編譯器的版本,有些編譯器沒有 typeof 有些有。邏輯上一樣,不過有 typeof 的版本多個安全網,如果 type 上不一樣會丟出 error,另一版不會停止運行,造成錯誤。

## q_swap
開始抓到 lab0 的規律,寫起來更快、問題也沒有向前面幾個那麼多。Leetcode 寫完搬過來,再改成支援 doubly-linked-list 即可
## q_reverse
非常的直觀,從head開始把所有的 next 與 prev 交換。
## q_reverseK
第一版程式碼:

邏輯
-
pp 在原地、pp2 先走到 k 節點位置 next_group,紀錄下一組的初始節點,交換兩者後再修正 *next 與 *prev 指標,最後設 pp 與 pp2 到 next_group。
*示意圖:*

問題:說實話我找不到,但程式就是困再迴圈中出不去。詢問 GPT 也沒啥用。
走頭無路時就該回頭!
第二版程式碼:

這個版本是把 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 快
**缺點:**
- 須耗費大量記憶體儲存每個節點
- 應該有更好的作法,只是此時還沒想到

嘗試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 而無法進行此修正。

理論上 q_reverseK 等也不是正確的英文單字,或許在設定檔中有允許這些函式的單字?後續嘗試了在函式前面加 "functions" 或將函式名用「\`」符號包住,但依然無法 commit。
前面教授提過函式命名規則,不應該亂縮寫,不過感覺 compare 縮成 cmp 還算常見?
*- 最後決定把 compare 改成 cmp 暫時略過此問題。*
**更:**

後來發現函式中的比較邏輯有大問題,當時寫的時候誤把字串當作整數而使用減法進行比較,已更正改用 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;
}
```

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 」這件事,但及便是這樣仍有有兩種作法可實行:
### 一、依序合併

先將A、B合併,再合併B、C,C、D...以此類推
每一次的合併一次得走 n 個節點,假設總共有 k 個 queue ,則總共得走 n * k次,時間複雜度:
**- O(n) = n * k**
**缺點:**
- 執速度較慢
### 二、兩兩合併


兩兩一組的將每個節點合併,若 list 為奇數則跳過落單的。
每一次的合併一次得走 n 個節點,假設總共有 k 個 queue ,則總共得走 n log2(k)次,時間複雜度:
**- O(n) = n * log k**
**優點:**
- 執行速度較快
::: info
### **注意!**
以上所有函式都是先寫完並 commit 後,再隨後去改,因為一直不清楚怎麼去測各個函式才好,所以決定先把能做的地方做一做,後面再來修正。
:::
# Bug 地獄
> 
> 時不時會無法執行 ```$ make test```,至今還不知道原因
再次重頭審視程式一遍,來修正一些它看到的潛在問題。例如:
- list_head 為 NULL 指標
- list 中只有 head
- 某些函式刪除節點時沒釋放記憶體
當我執行 ```& ./qtest```會出現以下錯誤訊息↓

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>

- q_sort 的邏輯使用了一 array ,初衷是犧牲記憶體來提升速度,但很明顯這是不被允許的,因此重新寫了一次。新版使用最笨最不會出錯的 bubble sort 來進行排序。 (後面有再次實作 merge sort,泡沫排序速度太慢)
### q_merge

*(此時已經過5小時)
撞到了牆壁,看不出 bug 可能的點,gpt 更是沒用,房間充滿了無助感。*
我想說獨立開個新檔案,把要用的函式通通帶進來,再使用printf去細找問題。
但,最後仍無望。

有尋求教授協助,但教授是請我研讀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>

就如上面所述,前面 q_sort 撞牆後改使用了 bubblesort ,但 bubblesort O(n^2)似乎太慢,無法通過測驗。
後來一樣整個重寫,這次決定使用 mergesort O(n * log n) 來提升速度。

**嗯。**
測驗後發現插入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>

- 很奇妙,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);
}
```

雖然敘述這麼寫,但我的 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)*
-

就也不知道為什麼會這樣
#### <span style="color: red;">!!這個問題尚未解決!!</span>
*<a style="color:grey"> - Apr 2, 2025</a>*

四週後,把作業二與作業四完成後回頭修正作業一,發現怎麼執行 ```$ 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 獨立測試:
恩,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:

有時候執行時會有幾個測出 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 :
>
*help 畫面有 quicksort 指令*

*沒問題!*

問題:這些改動 commit 到 lab0 中時會被擋下,
本地執行的話沒問題,但若希望 git push 上去就不能碰 queue.h,因此後來把程式寫進 qtest.c
### 利用 perf 分析



可看見這個 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 。

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 紀錄

若沒有 Valgrind 就會有點難抓到,畢竟看不到的東西非常抽象。
此時跑了一下make valgrind:

預料之內,結果與```$ 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

### 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: 若**