---
tags: linux2022
---
# 2022q1 Homework1 (lab0)
contributed by < `jj97181818` >
## queue.c
### q_new
建立新的「空」佇列
在想這裡應該要宣吿指向 `struct list_head` 或是 `element_t` 的 pointer,來回改了幾次,在後面實作到 q_insert_head 的 function 時,看了 list.h 裡面的 list_add function 的實作,要新增在最前面的新節點是接在 head 後面的,所以其實 head 本身不需要是 `element_t`,因為他的目的是要指向第一個節點,不需要存任何 value。
```c
struct list_head *q_new()
{
struct list_head *node = malloc(sizeof(struct list_head));
if (!node) {
return NULL;
}
INIT_LIST_HEAD(node);
return node;
}
```
### q_free
釋放佇列所佔用的記憶體
先用 `list_for_each_safe` iterate 所有節點,因為該 function 允許刪除節點,然後用 `list_entry` 透過成員位址 node(struct list_head)存取到該節點(element_t)的開頭位址,並用 queue.c 中的 `q_release_element` 來 free 節點的 value 和節點本身。在所有節點的記憶體都釋放之後,再來 free head。
```c
void q_free(struct list_head *l)
{
struct list_head *node;
struct list_head *safe;
list_for_each_safe (node, safe, l) {
element_t *p = list_entry(node, element_t, list);
q_release_element(p);
}
free(l);
}
```
### q_insert_head
在佇列開頭 (head) 加入 (insert) 給定的新元素 (以 LIFO 準則)
先宣告一個指向 element_t 的指標 node,並配置記憶體給它。然後再配置記憶體給 node 的成員 value,大小為 s 本身的長度再加上 1('\0'),如果沒有成功就將已配置給該節點的記憶體釋放。複製 s 的值到 node 的成員 value 中,再呼叫 `list_add` 將新節點插入 list 的最前面。
:::info
自己原本 `list_add(&(node->list), head);` 這樣寫 commit 的時候會被 pre-commit hook 拒絕,但是改 `list_add(&node->list, head);` 就會順利過了,但兩個是會做一樣的事情。
```
Following files need to be cleaned up:
queue.c
queue.c:58:5: error: Memory leak: node [memleak]
return true;
^
Fail to pass static analysis.
```
> TODO: 寫一個更小且可重現 (reproducible) 的測試案例,提交到 Cppcheck 專案的錯誤追蹤。
> :notes: jserv
:::
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head) {
return false;
}
element_t *node = malloc(sizeof(element_t));
if (!node) {
return false;
}
node->value = malloc((strlen(s) + 1) * sizeof(char));
if (!node->value) {
free(node);
return false;
}
strncpy(node->value, s, strlen(s) + 1);
list_add(&node->list, head);
return true;
}
```
### q_insert_tail
在佇列尾端 (tail) 加入 (insert) 給定的新元素 (以 FIFO 準則)
和 `q_insert_head` 一樣,除了 `list_add` 改成呼叫 `list_add_tail`。
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head) {
return false;
}
element_t *node = malloc(sizeof(element_t));
if (!node) {
return false;
}
node->value = malloc((strlen(s) + 1) * sizeof(char));
if (!node->value) {
free(node);
return false;
}
strncpy(node->value, s, strlen(s) + 1);
list_add_tail(&node->list, head);
return true;
}
```
### q_remove_head
在佇列開頭 (head) 移去 (remove) 給定的節點
檢查 head 是否為 NULL 或是 empty。`list_entry` 能取得第一個節點的起始位址,`list_del` 刪除第一個節點的連結。然後將要刪除節點的 value 複製給 sp,再將 sp 的最後一個字元改為 \0。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (head == NULL || list_empty(head)) {
return NULL;
}
element_t *removed_element = list_entry(head->next, element_t, list);
list_del(head->next);
if (sp && removed_element->value) {
strncpy(sp, removed_element->value, bufsize);
sp[bufsize - 1] = '\0';
}
return removed_element;
}
```
### q_remove_tail
在佇列結尾 (tail) 移去 (remove) 給定的節點
和 `q_remove_head` 一樣,除了要刪掉的節點從 `head->next` 改成 `head->prev`。
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (head == NULL || list_empty(head)) {
return NULL;
}
element_t *removed_element = list_entry(head->prev, element_t, list);
list_del(head->prev);
if (sp && removed_element->value) {
strncpy(sp, removed_element->value, bufsize);
sp[bufsize - 1] = '\0';
}
return removed_element;
}
```
### q_size
計算目前佇列的節點總量
```c
int q_size(struct list_head *head)
{
if (!head)
return 0;
int len = 0;
struct list_head *li;
list_for_each (li, head)
len++;
return len;
}
```
### q_delete_mid
移走佇列中間的節點
透過快慢指標,先將 fast 和 slow 的起始位置設定在第一個節點,然後 fast 一次移動兩步,slow 一次移動一步,當 fast 或 fast->next 等於 head 的位址時,slow 所在的位置即為中間節點。先用 `list_del` 移除中間節點的連結,再用 `list_entry` 取得該節點起始位置,呼叫q_release_element 將該節點的值和節點本身的記憶體釋放掉。
```c
bool q_delete_mid(struct list_head *head)
{
if (head == NULL || list_empty(head)) {
return false;
}
struct list_head *fast = head->next, *slow = head->next;
while (fast != head && fast->next != head) {
fast = fast->next->next;
slow = slow->next;
}
list_del(slow);
element_t *p = list_entry(slow, element_t, list);
q_release_element(p);
return true;
}
```
### q_delete_dup
在已經排序的狀況,移走佇列中具備重複內容的節點
走訪整個 queue,當現在走到的節點與下一個節點的值相同時,就將現在的節點刪除,然後設定 `duplicate = true`。當現在走到的節點與下一個節點不同,且 `duplicate` 的值是 `true` 時,也要將現在走到的節點刪除,這是為了要刪除連續重複節點的最後一個。
```c
bool q_delete_dup(struct list_head *head)
{
if (!head) {
return false;
}
element_t *cur;
element_t *safe;
bool duplicate = false;
list_for_each_entry_safe (cur, safe, head, list) {
if (cur->list.next != head &&
!strcmp(cur->value,
list_entry(cur->list.next, element_t, list)->value)) {
list_del(&cur->list);
q_release_element(cur);
duplicate = true;
} else {
if (duplicate) {
list_del(&cur->list);
q_release_element(cur);
duplicate = false;
}
}
}
return true;
}
```
### q_swap
交換佇列中鄰近的節點
走訪整個 queue,先將節點刪除連結,再將它連結加到下一個節點的後面,然後 `safe` 要成為 `node` 節點的下一位,因為接下來 `node` 會存取 `safe` 的位置,需要讓 `node` 指向兩兩 swap 中的前面那一個。
```c
void q_swap(struct list_head *head)
{
struct list_head *node, *safe;
list_for_each_safe (node, safe, head) {
if (node->next == head) {
return;
}
list_del(node);
list_add(node, safe);
safe = node->next;
}
}
```
### q_reverse
以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點
走訪整個 queue,先將節點刪除連結,再將它連結加到原本 queue 的最後一個節點 `last` 後面。一直重複這個動作,直到走訪到 `last` 結束。
```c
void q_reverse(struct list_head *head)
{
if (!head || !list_empty(head)) {
return;
}
struct list_head *node, *safe;
struct list_head *last = head->prev;
list_for_each_safe (node, safe, head) {
if (node == last) {
return;
}
list_del(node);
list_add(node, last);
}
}
```
### q_sort
以遞增順序來排序鏈結串列的節點
參考了 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists) 中, Merge Two Sorted Lists 的 indirect pointer 寫法、[Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 的 merge sort 寫法,以及 [mm0809 的共筆](https://hackmd.io/@2QYhHFatQmWFUgzITEcRjA/B1lsUihy9#q_sort) q_sort 的寫法。
1. 先讓最後一個節點的 `next` 指向 NULL,以 `next` 的方向來看,queue 變成不是 circular。
2. 然後呼叫 `mergeSortList`,找 queue 的中間節點,將 queue 分成兩半,在遞迴呼叫自己,將分開的兩半分別再繼續分成兩半,直到分成只有一個節點的時候(只有自己就是已排序的狀態),開始呼叫 `merge`。
3. `merge` 會將兩個 queue 依據值由小到大合併,一直呼叫 `merge` 直到整個 queue 都被 merge 完成。
4. 因為前面的動作只有專注在節點的 `next` 有連接到對的節點,所以最後需要將 `prev` 修正成對的節點。
```c
struct list_head *merge(struct list_head *l1, struct list_head *l2)
{
struct list_head *head = NULL;
struct list_head **ptr = &head;
for (; l1 && l2; ptr = &(*ptr)->next) {
if (strcmp(list_entry(l1, element_t, list)->value,
list_entry(l2, element_t, list)->value) < 0) {
*ptr = l1;
l1 = l1->next;
} else {
*ptr = l2;
l2 = l2->next;
}
}
*ptr = (struct list_head *) ((uintptr_t) l1 | (uintptr_t) l2);
return head;
}
struct list_head *mergeSortList(struct list_head *head)
{
if (!head || !head->next) {
return head;
}
struct list_head *fast = head->next, *slow = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
}
fast = slow->next;
slow->next = NULL;
slow = head;
struct list_head *l1 = mergeSortList(slow);
struct list_head *l2 = mergeSortList(fast);
return merge(l1, l2);
}
void q_sort(struct list_head *head)
{
if (!head || list_empty(head)) {
return;
}
head->prev->next = NULL;
head->next = mergeSortList(head->next);
struct list_head *cur = head->next;
struct list_head *prev = head;
while (cur != NULL) {
cur->prev = prev;
prev = cur;
cur = cur->next;
}
prev->next = head;
head->prev = prev;
}
```
## 引入 lib/list_sort.c
先將 [`lib/list_sort.c`](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 和 [`include/linux/list_sort.h`](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h) 放到 lab0 的專案下,然後在 `Makefile` 加入要編譯的 list_sort.c。
### 切換 sort
宣告一個 sortmode 變數,用來切換要執行的 sort 函式。
```c
/* original written sort: 0, lib/list_sort.c: 1 */
static int sortmode = 0;
```
在 `qtest.c` 中加入 option 參數 sort,根據不同的 sortmode,來切換 sort。
```c
add_param("sort", &sortmode,
"Switch between lib/list_sort.c and the original written sort", NULL);
```
原本執行 q_sort 的地方,根據 sortmode 來確定要執行原本自己寫的 sort 或 `lib/list_sort.c`。
因為實作 compare function 時不會用到 list_sort 的第一個參數,所以第一個參數為 NULL,第二個參數和原本呼叫 q_sort 傳入的參數一樣,第三個是放 compare function 的 function pointer。
```diff
if (exception_setup(true)) {
+ if (sortmode == 0) {
q_sort(l_meta.l);
+ }
+ else {
+ list_sort(NULL, l_meta.l, cmp);
+ }
}
```
### compare function
在 `list_sort.h` 有定義 compare function 回傳值必須為整數,參數有三個,第一個是 priv,但用不到,第二、第三個參數為指向 struct list_head 的指標,透過這兩個指標所在節點的值去比大小。
```c
typedef int __attribute__((nonnull(2,3))) (*list_cmp_func_t)(void *,
const struct list_head *, const struct list_head *);
```
實作出的 compare function:(在 `qtest.c`)
```c
static int cmp(void * priv, const struct list_head *a, const struct list_head *b) {
return strcmp(list_entry(a, element_t, list)->value,
list_entry(b, element_t, list)->value);
}
```
### likely 和 unlikely 巨集
`lib/list_sort.c` 有用到在 [linux/compiler.h](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h) 下的巨集:likely, unlikely,將兩個巨集直接搬到 `list_sort.c` 當中:
```c
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
```
### 將型別 u8 -> uint8_t
在 `merge_final` 中會用到型別 `u8`,因為未定義 `u8` 所以會錯誤,將其改成 `uint8_t`,並加上標頭檔 `#include <stdint.h>` 即可。
### 移除不需要的程式碼
將原本 linux 相關的標頭檔刪除
然後移除:
```c
EXPORT_SYMBOL(list_sort);
```
### 抑制 cppcheck 檢查
cppcheck 認定變數 head 沒有指派任何值,但是 head 在後面是有被指派值的。
```
list_sort.c:18:23: style: Variable 'head' is not assigned a value. [unassignedVariable]
struct list_head *head, **tail = &head;
^
Fail to pass static analysis.
```
解決方法是在該變數宣告的上一行加上:
```c
// cppcheck-suppress unassignedVariable
struct list_head *head, **tail = &head;
```
抑制 cppcheck 檢查該行 unassigned 變數。
## 比較 lib/list_sort.c 與自己實作 sort 的效能
分別測試自己寫的 sort 和引入到本專案的 `lib/list_sort.c` 各自 sort 字串,並測試 5 次做平均。
+ `trace-original-sort.cmd`:
```bash
# original sort
option verbose 1
option sort 0
new
ih RAND 1000000
time sort
free
```
+ `trace-kernel-sort.cmd`:
```bash
# lib/list_sort.c
option verbose 1
option sort 1
new
ih RAND 1000000
time sort
free
```
執行 `./qtest -f traces/trace-original-sort.cmd && ./qtest -f traces/trace-linux-sort.cmd`
得到的結果:
| sort 的字串數量 | original sort | linux sort |
| -------- | -------- | -------- |
| 100000 | 0.029(s) | 0.024(s) |
| 1000000 | 0.637(s) | 0.489(s) |
kernel 的 sort 表現得比較好,分別快了 20.8% 和 30.2%。
## 研讀 Linux 核心原始程式碼的 lib/list_sort.c
+ 用 buttom up 實作 merge sort 對 cache 較友善,因為過程中就是一直合併,cache 被參照到的機會是更大的。
+ 而 top down 是會先做 partition 再來 merge,但 partition 本身對 cache 不友善,在 cache 移進移出(內容不斷更新),導致 cache thrashing。
### 合併方式
合併方式是當有 $3 * 2^k$ 個節點時,合併前兩個 $2^k$ 變成 $2^{k + 1}$,並留下一個 $2^k$ 不動,維持著合併:不合併為 2 : 1 的比例,因為只要 $3 * 2^k$ 可以放得進 L1 cache,可以避免 cache thrashing。
`count` 為 pending list 中節點的數量,在 `count` 變 `count + 1` 後,可以觀察到第 `k` 個 bit 會改為 1,0 ~ `k - 1` 個 bit 會變 0,此時會將 2 個 $2^k$ 合併,並留下一個 $2^k$。
### 何時合併
每當 `count + 1`,可能會有兩種情況:
#### 1. 當 `count + 1` 後為 $2^k$,就不合併(只有 leading 1 是 1,其餘都為 0)
+ 例子:
`count` = 1($0001_2$)
`count + 1` = 2($0010_2$)
因為 `count + 1` 為 2 是 2 的倍數,所以此種情況不合併。
#### 2. 當 count + 1 後不為 $2^k$,就合併
+ 例子:
`count` = 2($0010_2$)
`count + 1` = 3($0011_2$)
因為 `count + 1` 為 3 不是 2 的倍數,所以此種情況要合併。
+ 可以觀察到在 `count` 變 `count + 1` 後,第 k 個 bit 會改為 1,0 ~ `k - 1` 個 bit 會變 0。而這裡的 k 為 0,所以會將 2 個 $2^0$ 合併,並留下一個 $2^0$,也就是合併 2 個 1 為 2,留一個 1 不合併。
以下是 count 從 0 一直加到 16 merge 的狀況:
(括號內是當次被合併的節點加起來的節點數量,用加號來分隔尚未合併的節點,黃色底則是告知此次合併的是 1 + 1, 2 + 2 或 4 + 4 等。)
| count 變化 | count 二進位 | merge | pending 上的節點|
| -------- | -------- | -------- | -------- |
| 0 -> 1 | 0000 -> 0001 | no($2^0$) | 1 |
| 1 -> 2 | 0001 -> 0010 | no($2^1$) | 1 + 1 |
| 2 -> 3 | 0010 -> 001==1== | yes | (2) + 1 |
| 3 -> 4 | 0011 -> 0100 | no($2^2$) | 2 + 1 + 1 |
| 4 -> 5 | 0100 -> 010==1== | yes | 2 + (2) + 1|
| 5 -> 6 | 0101 -> 01==1==0 | yes | (4) + 1 + 1|
| 6 -> 7 | 0110 -> 011==1== | yes | 4 + (2) + 1|
| 7 -> 8 | 0111 -> 1000 | no($2^3$) | 4 + 2 + 1 + 1|
| 8 -> 9 | 1000 -> 100==1== | yes | 4 + 2 + (2) + 1|
| 9 -> 10 | 1001 -> 10==1==0 | yes | 4 + (4) + 1 + 1|
| 10 -> 11 | 1010 -> 101==1== | yes | 4 + 4 + (2) + 1|
| 11 -> 12 | 1011 -> 1==1==00 | yes | (8) + 2 + 1 + 1|
| 12 -> 13 | 1100 -> 110==1== | yes | 8 + 2 + (2) + 1|
| 13 -> 14 | 1101 -> 11==1==0 | yes | 8 + (4) + 1 + 1|
| 14 -> 15 | 1110 -> 111==1== | yes | 8 + 4 + (2) + 1|
| 15 -> 16 | 1111 -> 10000 | no($2^4$) | 8 + 4 + 2 + 1 + 1 |
### list_sort
```c
__attribute__((nonnull(2,3)))
void list_sort(void *priv, struct list_head *head, list_cmp_func_t cmp)
{
struct list_head *list = head->next, *pending = NULL;
size_t count = 0; /* Count of pending */
if (list == head->prev) /* Zero or one elements */
return;
/* Convert to a null-terminated singly-linked list. */
head->prev->next = NULL;
```
+ priv: 從 `merge` 函式可以看到 priv 會被當作 cmp 的參數傳入,在其他地方不會用到。
+ head: 傳入指向 struct list_head 的指標,和原本自己寫的 `q_sort` 傳入的參數一樣
+ cmp: compare function,以 function pointer 的型別傳入
cmp 參數有考慮到通用性,但會增加 function call 的成本。
list 指向 head 的第一個節點,pending 指向 NULL,先檢查是否沒有任何元素或只有一個元素,然後將 head 前一個節點指向的下一個位置指向 NULL,將雙向 linked list 變成單向的。
```c
do {
size_t bits;
struct list_head **tail = &pending;
/* Find the least-significant clear bit in count */
for (bits = count; bits & 1; bits >>= 1)
tail = &(*tail)->prev;
/* Do the indicated merge */
if (likely(bits)) {
struct list_head *a = *tail, *b = a->prev;
a = merge(priv, cmp, b, a);
/* Install the merged result in place of the inputs */
a->prev = b->prev;
*tail = a;
}
/* Move one element from input list to pending */
list->prev = pending;
pending = list;
list = list->next;
pending->next = NULL;
count++;
} while (list);
```
在 while 迴圈中,會先讓 tail 往前移動到待 merge 的節點,然後在 if 判斷是否需要 merge,最後移動 pending 和 list 的位置,直到 list 沒有節點。pending 從沒有節點,然後一次次將節點從 list 中搬到 pending,等到 list 沒有節點就代表現階段結束了。
```c
/* End of input; merge together all the pending lists. */
list = pending;
pending = pending->prev;
for (;;) {
struct list_head *next = pending->prev;
if (!next)
break;
list = merge(priv, cmp, pending, list);
pending = next;
}
/* The final merge, rebuilding prev links */
merge_final(priv, cmp, head, pending, list);
}
EXPORT_SYMBOL(list_sort);
```
### merge
```c
__attribute__((nonnull(2, 3, 4))) static struct list_head *
merge(void *priv, list_cmp_func_t cmp, struct list_head *a, struct list_head *b)
{
// cppcheck-suppress unassignedVariable
struct list_head *head, **tail = &head;
for (;;) {
/* if equal, take 'a' -- important for sort stability */
if (cmp(priv, a, b) <= 0) {
*tail = a;
tail = &a->next;
a = a->next;
if (!a) {
*tail = b;
break;
}
} else {
*tail = b;
tail = &b->next;
b = b->next;
if (!b) {
*tail = a;
break;
}
}
}
return head;
}
```
當 `cmp(priv, a, b) <= 0` 表示 a 的值小於 b,因為由小排序到大,所以先接 a 再接 b,`cmp(priv, a, b) > 0` 表示 a 的值大於 b,則是先接 b 再接 a。
其中 `head` 會在排序過 list 的最前面,並回傳回去。
### 排序過程圖示
以 4, 3, 2, 1 的 list 為例做 list_sort 排序,隨著 `count` 增加,`pending` 內節點數量漸漸增加,而 `list` 內的節點數量漸漸減少:
#### count = 0 -> count = 1
+ list 指向 head->next:
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
head:right:e -> node4:w
node4:left:w -> head:e
node4:right:e -> node3:w
node3:left:w -> node4:e
node3:right:e -> node2:w
node2:left:w -> node3:e
node2:right:e -> node1:w
node1:left:w -> node2:e
list -> node4:n
}
```
+ 因為 bits 為 0,不需 merge。`list->prev = pending` 因為 pending 為 NULL,所以 list->prev 也指向 NULL:
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
head:right:e -> node4:w
node4:right:e -> node3:w
node3:left:w -> node4:e
node3:right:e -> node2:w
node2:left:w -> node3:e
node2:right:e -> node1:w
node1:left:w -> node2:e
list -> node4:n
}
```
+ pending 節點數量 + 1,list 節點數量 - 1,count + 1,`pending->next = NULL`:
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
head:right:e -> node4:w
node4:right:e -> node3:w[color="white"]
node3:left:w -> node4:e
node3:right:e -> node2:w
node2:left:w -> node3:e
node2:right:e -> node1:w
node1:left:w -> node2:e
list -> node3:n
pending -> node4:n
tail -> pending:n
}
```
#### count = 1 -> count = 2
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
head:right:e -> node4:w
node4:right:e -> node3:w[color="white"]
node3:left:w -> node4:e
node3:right:e -> node2:w
node2:left:w -> node3:e
node2:right:e -> node1:w
node1:left:w -> node2:e
list -> node3:n
pending -> node4:n
tail -> node4:left:s
}
```
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
head:right:e -> node4:w
node4:right:e -> node3:w[color="white"]
node3:left:w -> node4:e
node3:right:e -> node2:w[color="white"]
node2:left:w -> node3:e
node2:right:e -> node1:w
node1:left:w -> node2:e
list -> node2:n
pending -> node3:n
}
```
#### count = 2 -> count = 3
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
a [label="a", style="normal", color="white"]
b [label="b", style="normal", color="white"]
head:right:e -> node4:w
node4:right:e -> node3:w[color="white"]
node3:left:w -> node4:e
node3:right:e -> node2:w[color="white"]
node3:right:e -> node4:e
node2:left:w -> node3:e
node2:right:e -> node1:w
node1:left:w -> node2:e
list -> node2:n
pending -> node3:n
tail -> pending
a -> node3:n
b -> node4:n
}
```
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
a [label="a", style="normal", color="white"]
b [label="b", style="normal", color="white"]
head:right:e -> node4:w
node4:right:e -> node3:w[color="white"]
node3:right:e -> node2:w[color="white"]
node3:right:e -> node4:e
node2:left:w -> node3:e
node2:right:e -> node1:w
node1:left:w -> node2:e
list -> node2:n
pending -> node3:n
tail -> pending
a -> node3:n
b -> node4:n
}
```
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
head:right:e -> node4:w
node4:right:e -> node3:w[color="white"]
node3:right:e -> node2:w[color="white"]
node3:right:s -> node4:e
node2:left:w -> node3:e
node2:right:e -> node1:w[color="white"]
node1:left:w -> node2:e
list -> node1:n
pending -> node2:n
tail -> pending
}
```
#### count = 3 -> count = 4
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
head:right:e -> node4:w
node4:right:e -> node3:w[color="white"]
node3:right:e -> node2:w[color="white"]
node3:right:s -> node4:e
node2:left:w -> node3:e
node2:right:e -> node1:w[color="white"]
node1:left:w -> node2:e
list -> node1:n
pending -> node2:n
tail -> node3:left:s
}
```
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
head:right:e -> node4:w
node4:right:e -> node3:w[color="white"]
node3:right:e -> node2:w[color="white"]
node3:right:s -> node4:e
node2:left:w -> node3:e
node2:right:e -> node1:w[color="white"]
node1:left:w -> node2:e
pending -> node1:n
tail -> node3:left:s
}
```
#### count = 4
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
next [label="next", style="normal", color="white"]
a [label="a", style="normal", color="white"]
b [label="b", style="normal", color="white"]
head:right:e -> node1:w
node4:right:e -> node3:w[color="white"]
node3:right:e -> node2:w[color="white"]
node3:right:s -> node4:e
node2:left:w -> node3:e
node1:left:w -> head:e
pending -> node2:n
next -> node3:n
list -> node1:n
a -> node2:n
b -> node1:n
}
```
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
next [label="next", style="normal", color="white"]
a [label="a", style="normal", color="white"]
b [label="b", style="normal", color="white"]
head:right:e -> node1:w
node4:right:e -> node3:w[color="white"]
node3:right:e -> node2:w[color="white"]
node3:right:s -> node4:e
node2:left:w -> node3:e
node1:left:w -> head:e
pending -> node2:n
next -> node3:n
list -> node1:n
tail -> node1:n
a -> node2:n
b -> node2:n
}
```
```graphviz
digraph ele_list {
rankdir=LR
node[shape=record]
head [label="head|{<left>prev|<right>next}", style="bold"]
node4 [label="4|{<left>prev|<right>next}", style="bold"]
node3 [label="3|{<left>prev|<right>next}", style="bold"]
node2 [label="2|{<left>prev|<right>next}", style="bold"]
node1 [label="1|{<left>prev|<right>next}", style="bold"]
list [label="list", style="normal", color="white"]
pending [label="pending", style="normal", color="white"]
tail [label="tail", style="normal", color="white"]
next [label="next", style="normal", color="white"]
a [label="a", style="normal", color="white"]
b [label="b", style="normal", color="white"]
// head:left -> node4
// head:right -> node1
// node1:left -> head
// node1:right -> node2
// node2:left -> node1
// node2:right -> node3
// node3:left -> node2
// node3:right -> node4
// node4:left -> node3
// node4:right -> head
// pending -> node2:n
// next -> node3:n
// list -> node1:n
// tail -> node1:n
// a -> node2:n
// b -> node2:n
head:right:e -> node1:w
node1:left:w -> head:e
node1:right:e -> node2:w
node2:left:w -> node1:e
node2:right:e -> node3:w
node3:left:w -> node2:e
node3:right:e -> node4:w
node4:left:w -> node3:e
list -> node1:n
pending -> node2:n
a -> node2:n
b -> node4:n
tail -> node4:n
}
```
## 在 qtest 提供新的命令 shuffle
### 實作
在 qtest.c 中的 `console_init()` 加入:
```c
ADD_COMMAND(shuffle,
" | Shuffle nodes in queue");
```
然後另外宣告一個 `do_shuffle()`:
```c
static bool do_shuffle(int argc, char *argv[])
{
if (argc != 1) {
report(1, "%s takes no arguments", argv[0]);
return false;
}
if (!l_meta.l)
report(3, "Warning: Try to access null queue");
error_check();
if (exception_setup(true))
q_shuffle(l_meta.l);
exception_cancel();
show_queue(3);
return !error_check();
}
```
在 queue.c 中宣告 `shuffle()`:
利用 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm) 演算法來實作洗牌(shuffle)
1. 先用 `q_size` 取得 queue 的大小 `len`。
2. 隨機從 0 ~ (len - 1) 中抽出一個數字 `random`,然後 `old` 將指向從前面數來第 random 個節點,`new` 會指向最後一個未被抽到的節點,將 `old` 和 `new` 指向的節點的值交換,再將 len - 1。
3. 隨著 len 大小變小,已經被抽到過,並交換值到 queue 後面的會愈來愈多,直到所有的節點都已經被抽到過,shuffle 就結束了。
```c
void q_shuffle(struct list_head *head)
{
if (!head || list_empty(head)) {
return;
}
struct list_head *node;
struct list_head *last = head;
for (int len = q_size(head); len > 0; len--) {
int random = rand() % len;
node = head->next;
for (int i = 0; i < random; i++) {
node = node->next;
}
last = last->prev;
element_t *choose_entry = list_entry(node, element_t, list);
element_t *last_entry = list_entry(last, element_t, list);
char *temp = choose_entry->value;
choose_entry->value = last_entry->value;
last_entry->value = temp;
}
}
```
### 統計方法驗證 shuffle
[Pearson's chi-squared test](https://en.wikipedia.org/wiki/Pearson%27s_chi-squared_test) 能檢驗虛無假說(Null hypothesis),即某特定事件在樣本中觀察到的頻率分佈與特定理論分佈一致,事件必須是互斥且機率為 1。
如果要 shuffle 三個不同的數字,會出現六種可能的結果,彼此是互斥的,且發生機率加起來為 1。那假設 shuffle 是公平的(六種結果發生的機率相同),並遵守 Uniform distribution,那虛無假說為:
+ $H_0$(虛無假說): shuffle 的結果發生的機率相同,遵守 Uniform distribution
+ $H_1$(對立假說): shuffle 的結果發生的機率至少有一個不同
接下來透過 Pearson's chi-squared test 來驗證 shuffle 三個數字的頻率是符合 Uniform distribution:
#### 1. 先計算 chi-squared test statistic $X^2$
$$
X^2 = \sum_{i=1}^n {(O_i - E_i)^2 \over 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
```
E: 166666
O: {'abc': 166391, 'bac': 166829, 'bca': 166807, 'acb': 166790, 'cab': 166862, 'cba': 166321}
0.45375181500726003
0.15941463765855063
0.11928647714590858
0.0922563690254761
0.23049692198768795
0.7141528566114265
sum: 1.76935907743631
```
在測試 shuffle 1000000 次後,六種結果各自的頻率如下表:
|| 觀察到的頻率| 期待的頻率 |${(O_i - E_i)^2 \over E_i}$|
| -------- | -------- | -------- |---|
| [1, 2, 3] | 166391 | 166666 |0.45375181500726003|
| [2, 1, 3] | 166829 | 166666 |0.15941463765855063|
| [2, 3, 1] | 166807 | 166666 |0.11928647714590858|
| [1, 3, 2] | 166790 | 166666 |0.0922563690254761|
| [3, 1, 2] | 166862 | 166666 |0.23049692198768795|
| [3, 2, 1] | 166321 | 166666 |0.7141528566114265|
|Total|||1.76935907743631|
$X^2$ = 1.76935907743631
#### 2. 決定自由度(degrees of freedom)
對於 N 個隨機樣本而言,自由度為 N - 1。我們 shuffle 3 個數字會有六種結果,因為加起來機率為 1 ,所以實際上可以自由變換的變數只有五個,其中一個結果的機率為 1 減去另外五個結果發生的機率,所以自由度為 5。
#### 3. 選擇顯著水準
+ [顯著水準(Significance level)α](https://en.wikipedia.org/wiki/Statistical_significance) 代表在虛無假說($H_0$)為真下,錯誤地拒絕 $H_0$ 的機率,即型一錯誤發生之機率。
α = P(拒絕 $H_0$ | $H_0$ 為真)
我們 alpha 設定為常見的 0.05。
+ [P value](https://zh.wikipedia.org/wiki/P%E5%80%BC)
從[卡方分布表](https://zh.wikipedia.org/wiki/%E5%8D%A1%E6%96%B9%E5%88%86%E4%BD%88)找我們的 P value,我們的自由度為 5,$X^2$ 為 1.76935907743631,因為 1.61 < 1.76935907743631 < 2.34,於是 P value 介於 0.9 和 0.8 之間。
![](https://i.imgur.com/GDtpa8I.png)
#### 4. 統計檢定的結果不拒絕虛無假說
對於要拒絕的虛無假說($H_0$),觀察到的結果必須具有統計顯著性,即觀察到的 P value 小於預先指定的顯著水準 α。
因為 P value(0.8~0.9)> alpha(0.05),統計檢定的結果不拒絕虛無假說($H_0$)
也就是樣本資料不拒絕 shuffle 的結果遵循 Uniform distribution,因為沒有足夠證據推翻。
從圖表來看 shuffle 的頻率是大致符合 Uniform distribution 的。
![](https://i.imgur.com/uwrfQA1.png)
### 測試程式
看到在 `scripts/driver.py` 有用到 `subprocess.call()` 來產生新 process 執行指定的命令,然後等待命令完成後取得 return code,後續再檢查 return code 是否為 0 來得知測試是否成功。
因為這裡的測試我想取得 stdout,所以改用 [`subprocess.run()`](https://docs.python.org/3/library/subprocess.html#subprocess.run) ,會執行參數中指定的命令,然後等待命令完成後,會回傳一個 CompletedProcess instance。
取得 `CompletedProcess.stdout` 再做編碼後,可以得到 stdout 的字串。
後續再將得到的輸出字串取出 shuffle 過後的結果,放到 `nums` 變數中。
:::spoiler python script
```python
import subprocess
import re
import random
from itertools import permutations
import random
# 測試 shuffle 次數
test_count = 1000000
input = "new\nit 1\nit 2\nit 3\nit 4\n"
for i in range(test_count):
input += "shuffle\n"
input += "free\nquit\n"
# 取得 stdout 的 shuffle 結果
command='./qtest -v 3'
clist = command.split()
completedProcess = subprocess.run(clist, capture_output=True, text=True, input=input)
s = completedProcess.stdout
startIdx = s.find("l = [1 2 3 4]")
endIdx = s.find("l = NULL")
s = s[startIdx + 14 : endIdx]
Regex = re.compile(r'\d \d \d \d')
result = Regex.findall(s)
def permute(nums):
nums=list(permutations(nums,len(nums)))
return nums
def chiSquared(observation, expectation):
return ((observation - expectation) ** 2) / expectation
# shuffle 的所有結果
nums = []
for i in result:
nums.append(i.split())
# 找出全部的排序可能
counterSet = {}
shuffle_array = ['1', '2', '3', '4']
s = permute(shuffle_array)
# 初始化 counterSet
for i in range(len(s)):
w = ''.join(s[i])
counterSet[w] = 0
# 計算每一種 shuffle 結果的數量
for num in nums:
permutation = ''.join(num)
counterSet[permutation] += 1
# 計算 chiSquare sum
expectation = test_count // len(s)
c = counterSet.values()
chiSquaredSum = 0
for i in c:
chiSquaredSum += chiSquared(i, expectation)
print("Expectation: ", expectation)
print("Observation: ", counterSet)
print("chi square sum: ", chiSquaredSum)
```
產生直方圖的程式:
```python
import matplotlib.pyplot as plt
import numpy as np
permutations = counterSet.keys()
counts = counterSet.values()
x = np.arange(len(counts))
plt.bar(x, counts)
plt.xticks(x, permutations)
plt.xlabel('permutations')
plt.ylabel('counts')
plt.title('Shuffle result')
plt.show()
```
:::
測試 [1, 2, 3, 4] 做 shuffle 1,000,000 次的直方圖:
![](https://i.imgur.com/p9TYxio.png)
:::info
藉由測試程式發現自己最初寫的 shuffle 程式寫錯了,在所有 shuffle 組合中,只有其中 2 ~ 3 種組合會大量的出現,猜想是因為我在每次 shuffle 時都重設亂數種子,而亂數種子是根據時間當作參數,測試時會在短時間跑很多次,因為時間很短,所以很多次的 shuffle 都會取得一樣的時間參數,導致跑出來的只有少少的幾種組合。
把重設亂數種子刪除後,跑出來的結果就正常了,較平均地出現每種組合。
:::
## 在 qtest 提供新的命令 web
有參考 [PinLin 的 web 實作](https://hackmd.io/@PinLin/linux2022-lab0#%E6%96%B0%E5%A2%9E%E5%91%BD%E4%BB%A4-web)
先試跑 [tiny-web-server](https://github.com/7890/tiny-web-server) 專案,可以在瀏覽器看到跑該專案的目錄下有什麼檔案
,預設的 port 為 9999。
![](https://i.imgur.com/rH1YJSf.png)
先按照[整合 tiny-web-server](https://hackmd.io/@sysprog/linux2022-lab0#%E6%95%B4%E5%90%88-tiny-web-server)的引導,有三種方法,其中第三種方法最符合我們的需求。
### 一、嘗試用 select() 同時處理 stdin 及 socket
#### 在 `linenoiseEdit` 中加入以下程式碼:
```c
while (1) {
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 (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;
}
break;
}
}
```
```c
int select(int nfds, fd_set *restrict readfds,
fd_set *restrict writefds, fd_set *restrict exceptfds,
struct timeval *restrict timeout);
```
+ 查詢 [select(2)](https://man7.org/linux/man-pages/man2/select.2.html) ,可得知 select 這個 system call 有五個參數:
+ nfds: 要被設為三組(readfds, writefds, exceptfds)中 file descriptor 最高編號再加 1
+ readfs: 集合中的 file descriptors 會被監看是否準備好被 read
+ writefds: 集合中的 file descriptors 會被監看是否準備好被 write
+ exceptfds: 集合中的 file descriptors 會被監看是否準備好例外情況
+ timeout: 是 timeval 結構,指定 `select()` 要等一個 file descriptor 變成 ready 多少時間。如果 timeout 設定為 NULL,`select()` 將會無限期的等待一個 file descriptor 變成 ready。
```c
int accept(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict addrlen);
```
+ 查詢 [accept(2)](https://man7.org/linux/man-pages/man2/accept.2.html) ,可得知 accept 這個 system call 有三個參數:
+ sockfd: 監聽中的 socket file descriptor
+ addr: 連線進來的 client 位址
+ addrlen: addr 的長度
```c
ssize_t read(int fd, void *buf, size_t count);
```
+ 查詢 [read(2)](https://man7.org/linux/man-pages/man2/read.2.html),可得知 accept 這個 system call 有三個參數:
+ fd: 從 file descriptor 讀取
+ buf: 將讀入的 bytes 讀進緩衝區 buf
+ count: 讀取 count bytes
這個無限迴圈中,先宣告一個 fd_set 的 `set`,然後先用 `FD_ZERO` 將該 `set` 中的 file descriptor 移除,也就是初始化這個 file descriptor set。再用 `FD_SET` 將 `listenfd` 和 `stdin_fd` 加到 `set` 中。
用 `select` 來監視 `set` 中的 `FD_SET` 將 `listenfd` 是否準備好被讀取了,回傳值 `rv` 如果是 -1 代表有錯誤發生,0 則代表超過設置的等待時間,成功時,則為三個集合中包含的 file descriptor 數量。
`FD_ISSET` 會檢查 file descriptor 是否在 `set` 中。
+ 當 `listenfd` 在 `set` 中:
因為已經用 `select()` 檢查過 file descriptor 是否可讀,可讀代表有新的連線正在等待被 `accept()`,`accept()` 會回傳一個新的 file descriptor `connfd` 作為後續新的連線,原本的還是會留著用 `accept()` 接受新的連線。
將 `connfd` 和 `clientaddr` 當作參數傳入 `process()`,並回傳一個字串 `p`,並用 `strncpy()` 複製 `p` 的字串內容到 `buffer`,然後結束與 client 的連線,用 `close()` 來關閉。
+ 當 `listenfd` 在 `set` 中:
讀取 stdin file descriptor 1 byte 的大小到 c 裡面,如果 read 回傳的數字小於等於 0,就回傳目前 edited line 的長度。
### 二、嘗試修改 console.c,讓程式讀到 web 命令後手動按下 Enter 鍵繼續
+ 將 socket 改為 non-blocking,防止程式停止接收使用者輸入:
```c
int flags = fcntl(listenfd, F_GETFL);
fcntl(listenfd, F_SETFL, flags | O_NONBLOCK);
```
+ 修改 run_console():
```c
if (!has_infile) {
char *cmdline;
while ((cmdline = linenoise(prompt)) != NULL) {
int connfd = accept(*listenfd, (SA *)&clientaddr, &clientlen);
if (connfd == -1) {
interpret_cmd(cmdline);
linenoiseHistoryAdd(cmdline); /* Add to the history. */
linenoiseHistorySave(HISTORY_FILE); /* Save the history on disk. */
linenoiseFree(cmdline);
}
else {
cmdline = process(connfd, &clientaddr);
interpret_cmd(cmdline);
free(cmdline);
close(connfd);
}
}
} else {
while (!cmd_done())
cmd_select(0, NULL, NULL, NULL, NULL);
}
```
+ 透過 HTTP 的 GET method 來傳送想要執行的 function,process() 處理 URL 字串並將 function name 與 parameter 以跟 cmdline 一樣的格式回傳 ([function name][space][parameter]):
```c
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) != '\0') {
++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;
}
```
### 三、在分析完 console.c 後,發現使用 cmd_select() 能夠滿足需求
#### 1. 先將 tiny-web-server 的 `tiny.c` 加入專案中
+ 改名為 `tinyweb.c`,並編輯 `Makefile`,將 `tinyweb.o` 也變成需要的目標檔:
```diff
OBJS := qtest.o report.o console.o harness.o queue.o \
random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \
- linenoise.o
+ linenoise.o tinyweb.o
```
#### 2. 更改 `tinyweb.c` 中的 `process` 函式,其中的 `function_name` 改成 `filename`
```c
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) != '\0') {
++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;
}
```
#### 3. 因為我們需要同時控制 web 輸入及使用者輸入,必須關閉 linenoise API 才能達成,在輸入 web 命令後將 linenoise 關閉,並儲存監聽的 file descriptor:
在 `console.c` 加入:
```c
static bool do_web(int argc, char *argv[])
{
listenfd = socket_init();
noise = false;
return true;
}
```
在 `console.c` 中的 `init_cmd()` 加入 web command:
```c
ADD_COMMAND(web, " [port] | Read commands from a tiny-web-server");
```
#### 4. 更改 `console.c` 的 `run_console()` ,依照 linenoise 開啟與否來選擇要使用 `linenoise()` 還是 `cmd_select()`
有兩種得到命令(command)的方式,一種是 standard input,從終端機輸入 command,另一種是讀檔,例如:執行 `make check` 的時候會執行一些範例測試,就會需要讀檔。
是標準輸入時,會看 noise 來決定要用 `linenoise()` 或是 `cmd_select()`。
```c
// 讀 standard input
if (!has_infile) {
char *cmdline;
// 使用 linenoise()
while (noise && (cmdline = linenoise(prompt)) != NULL) {
interpret_cmd(cmdline);
linenoiseHistoryAdd(cmdline); /* Add to the history. */
linenoiseHistorySave(
HISTORY_FILE); /* Save the history on disk. */
linenoiseFree(cmdline);
}
// 使用 cmd_select()
if (!noise) {
while (!cmd_done()) {
cmd_select(0, NULL, NULL, NULL, NULL);
}
}
// 要讀檔案
} else {
while (!cmd_done()) {
cmd_select(0, NULL, NULL, NULL, NULL);
}
}
```
#### 5. 更改 `console.c` 的 `cmd_select()`,新增對應的處理
如果 readfds 為 NULL,那就給它一個集合 `local_readset`,然後將 `readfds` 初始化,再將 standard input 的 file descriptor `infd` 加到 `readfds` 集合裡。如果 web 還沒準備好 listen,就將 listen 的 file descriptor `listenfd` 也加到 `readfds`。
在 `console.c` 的 `push_file()` 中,stdin 的 file descriptor 設定為 `STDIN_FILENO`,所以當 `infd` 為 `STDIN_FILENO` 時,代表是 stdin 的情況,這裡先將 prompt 的內容:`cmd> ` 輸出,再將緩衝區的內容都輸出。
然後 `nfds` 和系統呼叫 `select()` 一樣,要是 `infd` 和 `listenfd` 中數字比較大的再加一。使用 `select()` 去監視,如果在任何一個 file descriptor 準備好之前時間就到期了,會回傳 0,出現錯誤是回傳 -1。
+ 如果 `infd` 在 `readfds` 中,就是用 cmdline 取得 command。
先將 `infd` 從 `readfds` 中移除,然後讀取一行內容,將它放進 `interpret_cmd()` 中,`interpret_cmd()` 會將讀到字串解析成 command line,然後執行命令。
+ 如果 `listenfd` 在 `readfds` 中,就是用 socket 的方式取得 command。
先將 `listenfd` 從 `readfds` 中移除,然後使用系統呼叫 `accept()` 取得一個新的 file descriptor `connfd` 做後續的連線。然後在 `process()` 中處理連線得到的 URL 字串,將 request 的 filename 轉成和 cmdline 的格式相同,然後一樣放進 `interpret_cmd()` 執行。
```diff=546
int cmd_select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout)
{
int infd;
fd_set local_readset;
if (cmd_done())
return 0;
if (!block_flag) {
/* Process any commands in input buffer */
if (!readfds)
readfds = &local_readset;
/* Add input fd to readset for select */
infd = buf_stack->fd;
+ FD_ZERO(readfds);
FD_SET(infd, readfds);
+ /* If web not ready listen */
+ if (listenfd != -1)
+ FD_SET(listenfd, readfds);
if (infd == STDIN_FILENO && prompt_flag) {
printf("%s", prompt);
fflush(stdout);
prompt_flag = true;
}
if (infd >= nfds)
nfds = infd + 1;
+ if (listenfd >= nfds)
+ nfds = listenfd + 1;
}
if (nfds == 0)
return 0;
int result = select(nfds, readfds, writefds, exceptfds, timeout)
if (result <= 0)
return result;
infd = buf_stack->fd;
if (readfds && FD_ISSET(infd, readfds)) {
/* Commandline input available */
FD_CLR(infd, readfds);
result--;
- if (has_infile) {
char *cmdline;
cmdline = readline();
if (cmdline)
interpret_cmd(cmdline);
- }
+ } else if (readfds && FD_ISSET(listenfd, readfds)) {
+ FD_CLR(listenfd, readfds);
+ result--;
+ int connfd;
+ struct sockaddr_in clientaddr;
+ socklen_t clientlen = sizeof(clientaddr);
+ connfd = accept(listenfd,(SA *) &clientaddr, &clientlen);
+ char *p = process(connfd, &clientaddr);
+ if (p)
+ interpret_cmd(p);
+ free(p);
+ close(connfd);
+ }
return result;
}
```
#### 6. 修正讓程式編譯的過
+ 將 `tinyweb.c` 的 `main()` 移除
避免專案中有兩個 main function 的定義
+ 新增 `tinyweb.c` 的標頭檔:`tinyweb.h`
```c
#ifndef TINYWEB_H
#define TINYWEB_H
#include <netinet/in.h>
/* Simplifies calls to bind(), connect(), and accept() */
typedef struct sockaddr SA;
int open_listenfd(int port);
char *process(int fd, struct sockaddr_in *clientaddr);
#endif
```
+ 在 `console.c` 引入 `tinyweb.h`
```c
#include "tinyweb.h"
```
+ 將 `tinyweb.c` 中 `process()` 以下這行移除
```c
handle_request(fd, req.filename);
```
+ `console.c` 中更改 do_web 的部分
```diff
+ int noise = true;
+ int listenfd;
static bool do_web(int argc, char *argv[])
{
+ int port = 9999;
- listenfd = socket_init();
+ listenfd = open_listenfd(port);
noise = false;
return true;
}
```
修改到這裡,目前功能是在我們下 `web` 指令的時候,將 `linenoise` 關掉,改用 `cmd_select()`。
而且還會再多輸出一次 `cmd> web`
```
cmd> web
cmd> web
```
+ 在 `console.c` 的 `cmd_select()` 加入
```c
set_echo(0);
```
讓他不要多輸出一次 `cmd> web`
+ 更改 `console.c` 的 `do_web()`
```c
static bool do_web(int argc, char *argv[])
{
int port = 9999;
if (argc == 2) {
if (argv[1][0] >= '0' && argv[1][0] <= '9') {
port = atoi(argv[1]);
}
}
listenfd = open_listenfd(port);
if (listenfd > 0) {
printf("listen on port %d, fd is %d\n", port, listenfd);
noise = false;
} else {
perror("ERROR");
exit(listenfd);
}
return true;
}
```
#### 7. 用 curl 下 command
用 curl 下 command 是正常運作。
```shell
$ curl http://localhost:9999/new
$ curl http://localhost:9999/ih/1
$ curl http://localhost:9999/ih/2
$ curl http://localhost:9999/ih/3
$ curl http://localhost:9999/sort
$ curl http://localhost:9999/quit
```
![](https://i.imgur.com/Mggb0AY.png)
#### 8. 用瀏覽器下 command
+ 要解決沒有回傳給 client 的問題
因為 server 沒有回傳任何東西給 client 就結束連線了,所以會發生瀏覽器以為是網路問題,多次重送 request 給 server。所以要來寫給 client 的 response。
在 tiny-web-server 專案中的 `tiny.c` ,有個處理 request 的 function: `handle_directory_request()`,參考它來實作要回傳給 client 的 response。
在 `tinyweb.c` 中加入的 `send_response()`。
```c
void send_response(int out_fd)
{
char *buf =
"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></head><body><table>\n";
writen(out_fd, buf, strlen(buf));
}
```
然後在 `tinyweb.c` 中的 `process()` 去呼叫 `send_response()`:
```c
send_response(fd);
```
就可以解決上述問題了。
+ 要解決 favicon.ico 的問題
![](https://i.imgur.com/pcXczr1.png)
從瀏覽器發 request 後,可以看到上圖 favicon.ico 的 status 是 404。因為部分瀏覽器會要求 request 中要提供網頁圖案的對應資訊,而我上面的 response 沒有提供。
在 [I'm getting favicon.ico error](https://stackoverflow.com/questions/31075893/im-getting-favicon-ico-error) 有提供做法,就是將下面這行加進去 head 裡面即可:
```c
<link rel="shortcut icon" href="#">
```
因此修改 `send_response()`,將上面的程式加進去:
```c
void send_response(int out_fd)
{
char *buf =
"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";
writen(out_fd, buf, strlen(buf));
}
```
再次嘗試,就都是 200 OK 了!
![](https://i.imgur.com/8WZ59gx.png)
### 檢驗是否正確
:::spoiler testWeb.c
```c
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#define TARGET_HOST "127.0.0.1"
#define TARGET_PORT 9999
/* length of unique message (TODO below) should shorter than this */
#define MAX_MSG_LEN 1024
static const char *msg_dum = "GET /new HTTP/1.1\n\n";
int main(void)
{
int sock_fd;
char dummy[MAX_MSG_LEN];
//struct timeval start, end;
sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket");
exit(-1);
}
struct sockaddr_in info = {
.sin_family = PF_INET,
.sin_addr.s_addr = inet_addr(TARGET_HOST),
.sin_port = htons(TARGET_PORT),
};
if (connect(sock_fd, (struct sockaddr *) &info, sizeof(info)) == -1) {
perror("connect");
exit(-1);
}
send(sock_fd, msg_dum, strlen(msg_dum), 0);
recv(sock_fd, dummy, MAX_MSG_LEN, 0);
shutdown(sock_fd, SHUT_RDWR);
close(sock_fd);
printf("%s\n", dummy);
int count = 0, last_pos = 0;
for (int i = 0; i < strlen(dummy); i++) {
if (dummy[i] == '\n') {
count++;
}
if (count == 3) {
last_pos = i + 1;
break;
}
}
char *answer = "l = []";
// printf("%s", dummy + last_pos);
printf("%ld %ld\n", strlen(answer), strlen(dummy + last_pos));
// if (strncmp(answer, dummy + last_pos, strlen(dummy + last_pos))) {
// puts("message validation failed");
// exit(-1);
// }
// else {
// puts("success");
// }
return 0;
}
```
:::
更改 [bench.c](https://github.com/sysprog21/kecho/blob/master/bench.c),傳送 HTTP request 給開著的 web server,並接收 HTTP response,查看是否與預期的結果相同。
在 testweb.c 中,傳送 new 的命令,但是回傳回來除了執行結果還多了換行和一個隨機的字元...
```
HTTP/1.1 200 OK
Content-Type: text/plain
l = []
v
```
:::spoiler report 和 report_noreturn
```c
extern int connfd;
void report(int level, char *fmt, ...)
{
if (!verbfile)
init_files(stdout, stdout);
int bufferSize = 4096;
char buffer[bufferSize];
if (level <= verblevel) {
va_list ap;
va_start(ap, fmt);
vfprintf(verbfile, fmt, ap);
fprintf(verbfile, "\n");
fflush(verbfile);
va_end(ap);
if (logfile) {
va_start(ap, fmt);
vfprintf(logfile, fmt, ap);
fprintf(logfile, "\n");
fflush(logfile);
va_end(ap);
}
va_start(ap, fmt);
vsnprintf(buffer, bufferSize, fmt, ap);
va_end(ap);
}
if (connfd) {
int len = strlen(buffer);
buffer[len] = '\n';
buffer[len + 1] = '\0';
send_response(connfd, buffer);
}
}
void report_noreturn(int level, char *fmt, ...)
{
if (!verbfile)
init_files(stdout, stdout);
int bufferSize = 4096;
char buffer[bufferSize];
if (level <= verblevel) {
va_list ap;
va_start(ap, fmt);
vfprintf(verbfile, fmt, ap);
fflush(verbfile);
va_end(ap);
if (logfile) {
va_start(ap, fmt);
vfprintf(logfile, fmt, ap);
fflush(logfile);
va_end(ap);
}
va_start(ap, fmt);
vsnprintf(buffer, bufferSize, fmt, ap);
va_end(ap);
}
if (connfd) {
send_response(connfd, buffer);
}
}
```
:::
因為原本的 web 實作中,HTTP 的 body 只會回傳 ok,現在要將原本輸出到標準輸出的結果也當作 HTTP response 傳回給 client。
在 `qtest.c` 的 do_operation 系列的函式,常常呼叫 report.c 的 `report` 和 `report_noreturn`,為了將執行結果輸出到標準輸出,其中兩者差別在是否有輸出換行,輸出後有需要換行就會呼叫 `report`,不需要會呼叫 `report_noreturn`。
在 `report` 和 `report_noreturn` 檢查是否有連線,如果有連線,就將結果一起寫進 response 中,如此一來即可回傳執行結果。
## 運用 Valgrind 排除 qtest 實作的記憶體錯誤
參考 [AmyLin0210](https://hackmd.io/@AmyLin0210/rJpekw9kc#%E9%81%8B%E7%94%A8-Valgrind-%E6%8E%92%E9%99%A4-qtest-%E5%AF%A6%E4%BD%9C%E7%9A%84%E8%A8%98%E6%86%B6%E9%AB%94%E9%8C%AF%E8%AA%A4) 的思路
執行 `make valgrind` 會出現(節錄部分輸出):
```
--- Trace Points
+++ TESTING trace trace-01-ops:
# Test of insert_head and remove_head
==186384== 11 bytes in 1 blocks are still reachable in loss record 1 of 3
==186384== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==186384== by 0x4A4B3BE: strdup (strdup.c:42)
==186384== by 0x110A1A: linenoiseHistoryAdd (linenoise.c:1236)
==186384== by 0x1115AD: linenoiseHistoryLoad (linenoise.c:1325)
==186384== by 0x10C765: main (qtest.c:972)
==186384==
==186384== 102 bytes in 19 blocks are still reachable in loss record 2 of 3
==186384== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==186384== by 0x4A4B3BE: strdup (strdup.c:42)
==186384== by 0x11098E: linenoiseHistoryAdd (linenoise.c:1236)
==186384== by 0x1115AD: linenoiseHistoryLoad (linenoise.c:1325)
==186384== by 0x10C765: main (qtest.c:972)
==186384==
==186384== 160 bytes in 1 blocks are still reachable in loss record 3 of 3
==186384== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==186384== by 0x1109DA: linenoiseHistoryAdd (linenoise.c:1224)
==186384== by 0x1115AD: linenoiseHistoryLoad (linenoise.c:1325)
==186384== by 0x10C765: main (qtest.c:972)
==186384==
--- trace-01-ops 5/5
```
在輸出訊息中有看到 `still reachable`,在 [K01: lab0 的 Valgrind 使用案例](https://hackmd.io/@sysprog/linux2022-lab0#Valgrind-%E4%BD%BF%E7%94%A8%E6%A1%88%E4%BE%8B) 中有提到:
> still reachable: 程式結束時有未釋放的記憶體,不過卻還有指標指著,通常會發生在 global 變數
查看發生錯誤訊息的路徑:
+ 在 `qtest.c` 中第 972 行,呼叫到 `linenoiseHistoryLoad` function:
```c=972
linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */
```
+ 在 `linenoise.c` 中第 1325 行(在 `linenoiseHistoryLoad` function),有呼叫到 `linenoiseHistoryAdd` function:
```c=1309
int linenoiseHistoryLoad(const char *filename)
{
FILE *fp = fopen(filename, "r");
char buf[LINENOISE_MAX_LINE];
if (fp == NULL)
return -1;
while (fgets(buf, LINENOISE_MAX_LINE, fp) != NULL) {
char *p;
p = strchr(buf, '\r');
if (!p)
p = strchr(buf, '\n');
if (p)
*p = '\0';
linenoiseHistoryAdd(buf);
}
fclose(fp);
return 0;
}
```
+ 在 `linenoise.c` 中第 1224 和 1236 行(在 `linenoiseHistoryAdd` function)會出現問題:
在 history 為 NULL 時,配置記憶體給 history,然後將傳進來的 line 參數用 strdup 複製出新的字串,再將新字串放到 history 裡面。
從 manual 可以看到 strdup 是用 malloc 配置新字串的記憶體:
>DESCRIPTION
The strdup() function returns a pointer to a new string which is a duplicate of the string s. Memory for the new string is obtained with malloc(3), and can be freed with free(3).
`static char **history = NULL;`,history 是 static 靜態變數,存放在記憶體的固定位置,不會隨函數的結束而被回收。因此要自行將 history 回收,而 `linenoise.c` 中有 `freeHistory` 這個 function,只有被 `linenoiseAtExit` 呼叫到,因為只有在要離開、不需要用到了才會將 history 回收。
```c=1208
/* This is the API call to add a new entry in the linenoise history.
* It uses a fixed array of char pointers that are shifted (memmoved)
* when the history max length is reached in order to remove the older
* entry and make room for the new one, so it is not exactly suitable for huge
* histories, but will work well for a few hundred of entries.
*
* Using a circular buffer is smarter, but a bit more complex to handle. */
int linenoiseHistoryAdd(const char *line)
{
char *linecopy;
if (history_max_len == 0)
return 0;
/* Initialization on first call. */
if (history == NULL) {
history = malloc(sizeof(char *) * history_max_len);
if (history == NULL)
return 0;
memset(history, 0, (sizeof(char *) * history_max_len));
}
/* Don't add duplicated lines. */
if (history_len && !strcmp(history[history_len - 1], line))
return 0;
/* 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++;
return 1;
}
```
回來看 `qtest.c` 可以發現 `main` 函式會呼叫到 `run_console`,但因為最初執行 `make valgrind` 就是要讀檔測試的,例如:`traces/trace-01-ops.cmd`,而不是直接從 command line 輸入,所以在 `run_console` 中,我們會執行 21~25 行的部分,是用 `cmd_select` 而不會呼叫到 linenoise 的函式。
```c=
bool run_console(char *infile_name)
{
if (!push_file(infile_name)) {
report(1, "ERROR: Could not open source file '%s'", infile_name);
return false;
}
if (!has_infile) { // 讀 standard input
char *cmdline;
while (noise && (cmdline = linenoise(prompt)) != NULL) {
interpret_cmd(cmdline);
linenoiseHistoryAdd(cmdline); /* Add to the history. */
linenoiseHistorySave(HISTORY_FILE); /* Save the history on disk. */
linenoiseFree(cmdline);
}
if (!noise) {
while (!cmd_done()) {
cmd_select(0, NULL, NULL, NULL, NULL);
}
}
} else { // 要讀檔案
while (!cmd_done()) {
cmd_select(0, NULL, NULL, NULL, NULL);
}
}
return err_cnt == 0;
}
```
但是在 `qtest.c` 中我們無論是用讀檔的方式或是從 command line 輸入的方式,都會呼叫到 linenoise 相關的函式,所以應該改成在沒有檔案的時候,才去呼叫 linenoise 相關的函式,才能避免讀檔會發生記憶體洩漏的情況:在 `qtest.c` 的 `main` 呼叫了 `linenoiseHistoryLoad`,卻又沒能在 `run_console` 中使用 linenoise 釋放記憶體。因此改成以下即可解決:
```diff
int main(int argc, char *argv[])
{
...
+ if (!infile_name) {
/* Trigger call back function(auto completion) */
linenoiseSetCompletionCallback(completion);
linenoiseHistorySetMaxLen(HISTORY_LEN);
linenoiseHistoryLoad(HISTORY_FILE); /* Load the history at startup */
+ }
...
}
```
再來是測試複雜度的測試中,如果失敗,輸出訊息中也會看到 `still reachable` 的提示:
```
+++ TESTING trace trace-17-complexity:
# Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant
Probably constant time
ERROR: Probably not constant time
Probably constant time
Probably constant time
==26260== 32 bytes in 1 blocks are still reachable in loss record 1 of 30
==26260== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==26260== by 0x10DF36: malloc_or_fail (report.c:189)
==26260== by 0x10EA5D: add_cmd (console.c:90)
==26260== by 0x10EDE7: init_cmd (console.c:431)
==26260== by 0x10D67C: main (qtest.c:965)
==26260==
```
在 `add_cmd()` 中會透過 `malloc_or_fail()` 配置記憶體給新的 command,然後在 `finish_cmd()` 中呼叫 `do_quit()` 將記憶體釋放,所以可能是因為沒有順利呼叫到 `finish_cmd()` 導致記憶體沒有被順利釋放。
只要在 `qtest.c` 以下改成:
```diff
bool ok = true;
ok = ok && run_console(infile_name);
- ok = ok && finish_cmd();
+ ok = finish_cmd() && ok;
```
原本當 `ok` 為 false 時,就不會呼叫到 `finish_cmd()`,因此改變 `ok` 和 `finish_cmd()` 的順序,就一定能順利呼叫到 `finish_cmd()`。
### Massif
用 Massif 來測量執行程式會需要用到多少 heap memory,並透過它來看記憶體洩漏的情況。
+ 在終端機輸入:
`valgrind -v --tool=massif ./qtest -f traces/trace-01-ops.cmd`
會得到 `massif.out.18935` 的檔案,每次執行後面的五位數字都會不一樣
+ 在終端機輸入:
`massif-visualizer ./massif.out.18935`
就可以透過 `massif-visualizer` 來看剛剛產生出來的檔案
+ 可以看到原本動態記憶體是沒有被釋放完全的,沒有歸零。
![](https://i.imgur.com/eszhS49.png)
+ 在修改之後,動態記憶體最終歸零了。
![](https://i.imgur.com/ujIz76v.png)
## dudect
dudect 中的 `cpucycles.h` 是使用 rdtsc 取得 timestamp:
```c
#include <stdint.h>
static inline int64_t cpucycles(void)
{
#if defined(__i386__) || defined(__x86_64__)
unsigned int hi, lo;
__asm__ volatile("rdtsc\n\t" : "=a"(lo), "=d"(hi));
return ((int64_t) lo) | (((int64_t) hi) << 32);
#elif defined(__aarch64__)
uint64_t val;
asm volatile("mrs %0, cntvct_el0" : "=r"(val));
return val;
#else
#error Unsupported Architecture
#endif
}
```
在 dudect 中的 `constant.c` 有使用到 `cpucycles.h` 中定義的 cpucycles:
```c
...
case test_insert_head:
for (size_t i = drop_size; i < n_measure - drop_size; i++) {
char *s = get_random_string();
dut_new();
dut_insert_head(
get_random_string(),
*(uint16_t *) (input_data + i * chunk_size) % 10000);
before_ticks[i] = cpucycles();
dut_insert_head(s, 1);
after_ticks[i] = cpucycles();
dut_free();
}
break;
...
```
因為 rdtsc 只會給 timestamp,沒辦法告訴我們一個操作需要花多少時間,所以這裡是在 `dut_insert_head(s, 1)` function 執行前,先取得 timestamp,在 function 離開後再取得一次 timestamp,即可得知相對的時間變化,粗略得知該 function 執行多少時間。
先將程式[鎖定到一個 CPU 執行](https://hackmd.io/suYZlvVVSNOjdmy4JzZU6A?view#%E6%95%88%E8%83%BD%E5%88%86%E6%9E%90),降低 CPU 的干擾因素。
嘗試使用 `clock_gettime` 代替 `rdtsc`,各自執行十次,幾乎都是非 constant time。
下表為按照 insert_head, insert_tail, remove_head, remove_tail 順序,測試出來是否為 constant time:(1: True, 0: False)
| | rdtsc | clock_gettime() |
| -------- | -------- | -------- |
| 1 | 1010 | 1010 |
| 2 | 1110 | 1010 |
| 3 | 1111 | 1100 |
| 4 | 1110 | 1010 |
| 5 | 1110 | 1110 |
| 6 | 1000 | 1010 |
| 7 | 1000 | 1010 |
| 8 | 1100 | 1011 |
| 9 | 1000 | 1110 |
| 10 | 1000 | 1110 |
因為不確定自己寫的 insert_head, insert_tail, remove_head, remove_tail function 是不是常數時間,後來有換成將這些 function 內容直接改成 return 0(因為一定會是常數時間),然後分別用 rdtsc 和 clock_gettime() 測試,但是跑出來的結果也幾乎都辨識為非常數時間
有在想 function 直接 return 0 是否速度會太快,會影響到測試之類的,有再將 insert_head, insert_tail, remove_head, remove_tail function 改成執行 10000 次的迴圈,但跑出來也是有非常數時間。
有想過不直接看最後結果是否為常數時間,直接拿 rdtsc 和 clock_gettime() 所執行的時間,各自做一些簡單的統計,像是平均、標準差(如果標準差較小,可能比較穩定(?)),然後將時間分佈做個直方圖來比較,但不確定這麼做是否有意義。
查看即時 CPU 的頻率:
`watch -n.1 "grep \"^[c]pu MHz\" /proc/cpuinfo"`
使用的 CPU 是 2.9 GHz,換算 rdtsc 的 cycle 變成 ns:
1 / 2.9G * $10^9$(ns / cycle) $\approx$ 0.3448(ns / cycle)
+ `test_tries` 跑一次會測量特定 function 91 * 110(頭尾個去掉 20) = 10010 次,下圖是測量 insert_head 10010 次每次的執行時間,大部分都落在 100~200 ns。
![](https://i.imgur.com/CY8I93d.png)
但有時候會出現 outlier,像是開頭的執行時間 1991 ns。
![](https://i.imgur.com/byifFBA.png)
+ 紀錄 fix class 和 random class 兩者執行時間的分佈:
![](https://i.imgur.com/UFe5tfr.png)
crop 的大小
$1 - 0.5^{(\frac{10x}{100})}$ = $1 - 0.5^{(\frac{x}{10})}$
![](https://i.imgur.com/Et5SiC3.png)