owned this note
owned this note
Published
Linked with GitHub
# 2022q1 Homework1 (lab0)
contributed by < [Eric-liau](https://github.com/Eric-liau) >
## 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 8
On-line CPU(s) list: 0-7
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 94
Model name: Intel(R) Core(TM) i7-6700HQ CPU @ 2.60GHz
Stepping: 3
CPU MHz: 900.002
CPU max MHz: 3500.0000
CPU min MHz: 800.0000
BogoMIPS: 5199.98
Virtualization: VT-x
L1d cache: 128 KiB
L1i cache: 128 KiB
L2 cache: 1 MiB
L3 cache: 6 MiB
NUMA node0 CPU(s): 0-7
```
## [作業要求](https://hackmd.io/@sysprog/linux2022-lab0)
依據指示著手修改 queue.[ch] 和連帶的檔案,測試後用 Git 管理各項修改,記得也該實作 q_sort 函式。
- [x] 在 GitHub 上 fork [lab0-c](https://github.com/sysprog21/lab0-c)
- [x] 依據上述指示著手修改 `queue.[ch]` 和連帶的檔案,測試後用 Git 管理各項修改,要能滿足 ``$ make test` 自動評分系統的所有項目。
- [x] 研讀 Linux 核心原始程式碼的 [lib/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 並嘗試引入到 `lab0-c` 專案,比較你自己實作的 merge sort 和 Linux 核心程式碼之間效能落差
- [x] 在 `qtest` 提供新的命令 `shuffle`,允許藉由 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作
- [x] 在 `qtest` 提供新的命令 `web`,提供 web 伺服器功能,注意: web 伺服器運作過程中,`qtest` 仍可接受其他命令
- [ ] 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer),修正 `qtest` 執行過程中的錯誤
- 先執行 `qtest` 再於命令提示列輸入 `help` 命令,會使開啟 Address Sanitizer 觸發錯誤,應予以排除
- [ ] 運用 Valgrind 排除 `qtest` 實作的記憶體錯誤,並透過 Massif 視覺化 “simulation” 過程中的記憶體使用量,需要設計對應的實驗
- [x] 解釋 [select](https://man7.org/linux/man-pages/man2/select.2.html) 系統呼叫在本程式的使用方式,並分析 [console.c](https://github.com/sysprog21/lab0-c/blob/master/console.c) 的實作,說明其中運用 CS:APP [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 的原理和考量點。可對照參考 [CS:APP 第 10 章重點提示](https://hackmd.io/@sysprog/CSAPP-ch10)
- [x] 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉,解釋本程式的 “simulation” 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 [Student’s t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 及程式實作的原理。注意:現有實作存在若干致命缺陷,請討論並提出解決方案;
- [ ] 指出現有程式的缺陷 (提示: 和 [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf) 有關) 或者可改進之處 (例如更全面地使用 Linux 核心風格的鏈結串列 API),嘗試實作並提交 pull request
:::danger
針對 2022 年作業要求,更新上述列表
:notes: jserv
:::
:::success
已更新
:::
## queue.c
### q_new
- `INIT_LIST_HEAD(head)`: 將 head 的 *\*prev* 和 *\*next* 分別指向 head。
```c
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (!head)
return NULL;
INIT_LIST_HEAD(head);
return head;
}
```
### q_free
想法:從 head node 開始,依序把每個`node` free 掉。
- temp : 因為 `container` 會被 free 掉,而 `current` 包含在 `container` 中,所以需要一個 `struct list_head` 來暫存 `current` 的值。
- `free(l)`: `list_for_each` 不會經過 `head` ,所以在迴圈結束後需要把 `head` free 掉。
- 在寫 `q_insert_head` 時提示 memory leak ,檢查後發現` container->value` 並未 free 掉,故加入 `free(container->value)`。
```c
void q_free(struct list_head *l)
{
struct list_head *current, temp;
element_t *container;
list_for_each(current, l){
container = list_entry(current, element_t, list);
temp = *current;
free(container->value);
free(container);
current = &temp;
}
free(l);
}
```
### q_insert_head
1. 檢查 `head` 是否為 `null`
2. 分配空間給 `new` 並檢查是否成功分配
3. 分配空間給 `new->value` 並檢查是否成功分配
4. 複製 s 到 `new->value`
5. 將 `new` insert 到 `queue` 中
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new = malloc(sizeof(element_t));
if (!new)
return false;
new->value = malloc(strlen(s) + 1);
if (!new->value) {
free(new);
return false;
}
strncpy(new->value, s, strlen(s) + 1);
list_add(&new->list, head);
return true;
}
```
### q_insert_tail
1. 檢查 `head` 是否為 `null`
2. 分配空間給 `new` 並檢查是否成功分配
3. 分配空間給 `new->value` 並檢查是否成功分配
4. 複製 s 到 `new->value`
5. 將 `new` insert 到 `queue` 中
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *new = malloc(sizeof(element_t));
if (!new)
return false;
new->value = malloc(strlen(s) + 1);
if (!new->value) {
free(new);
return false;
}
strncpy(new->value, s, strlen(s) + 1);
list_add_tail(&new->list, head);
return true;
}
```
### q_remove_head
1. 檢查 `head` 是否為 `NULL` 或 `empty`
2. 檢查 `sp` 是否為 `NULL`
3. 比較 `strlen(node->value) + 1` 和 `bufsize` 大小
- 若後者較大則把 `node->value` 完整複製到 `sp` 中
- 反之則只把 `node->value` 的前 `bufsize - 1` 個字元複製到 `sp`,並在 `sp` 的第 `bufsize` 個字元填上 `'\0'
4. 將 `head->next` 移除
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head)
return NULL;
if (list_empty(head))
return NULL;
element_t *node = list_entry(head->next, element_t, list);
if (!sp) {
list_del_init(head->next);
return node;
}
char *str = node->value;
int len = strlen(str) + 1;
if (len <= bufsize)
strncpy(sp, str, len);
else {
strncpy(sp, str, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
list_del_init(head->next);
return node;
}
```
### q_remove_tail
1. 檢查 `head` 是否為 `NULL` 或 `empty`
2. 檢查 `sp` 是否為 `NULL`
3. 比較 `strlen(node->value) + 1` 和 `bufsize` 大小
- 若後者較大則把 `node->value` 完整複製到 `sp` 中
- 反之則只把 `node->value` 的前 `bufsize - 1` 個字元複製到 `sp`,並在 `sp` 的第 `bufsize` 個字元填上 `'\0'
4. 將 `head->prev` 移除
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head)
return NULL;
if (list_empty(head))
return NULL;
element_t *node = list_entry(head->prev, element_t, list);
if (!sp) {
list_del_init(head->prev);
return node;
}
char *str = node->value;
int len = strlen(str) + 1;
if (len <= bufsize)
strncpy(sp, str, len);
else {
strncpy(sp, str, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
list_del_init(head->prev);
return node;
}
```
### q_release_element
未做更動
```c
void q_release_element(element_t *e)
{
free(e->value);
free(e);
}
```
### q_size
整個 list 走過一遍即可算出 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
利用快慢指標找到中間的 node
```c
struct list_head *find_mid(struct list_head *head)
{
struct list_head *slow = head->next;
for (struct list_head *fast = head->next;
fast != head && fast->next != head; fast = fast->next->next) {
slow = slow->next;
}
return slow;
}
bool q_delete_mid(struct list_head *head)
{
if (!head)
return false;
if (list_empty(head))
return false;
struct list_head *mid = find_mid(head);
mid->prev->next = mid->next;
mid->next->prev = mid->prev;
element_t *node = list_entry(mid, element_t, list);
free(node->value);
free(node);
return true;
}
```
### q_delete_dup
若 `current node` 的值等於 `first` 的值就把 `last` 指標移到 `current node` ,若不等於就檢查 `first` 和 `last` 是否為同一個 `node` ,若不是就把 `first` 和 `last` 之間的 `node` 全部刪除。
```c
bool q_delete_dup(struct list_head *head)
{
if (!head)
return false;
if (list_empty(head))
return true;
element_t *current, *first = list_entry(head->next, element_t, list),
*last = first;
list_for_each_entry (current, head, list) {
if (!strcmp(current->value, first->value))
last = current;
else {
if (first != last) {
first->list.prev->next = last->list.next;
last->list.next->prev = first->list.prev;
while (first != current) {
element_t *temp =
list_entry(first->list.next, element_t, list);
q_release_element(first);
first = temp;
}
}
first = current;
last = current;
}
}
if (first != last) {
first->list.prev->next = last->list.next;
last->list.next->prev = first->list.prev;
while (first != current) {
element_t *temp = list_entry(first->list.next, element_t, list);
q_release_element(first);
first = temp;
}
}
return true;
}
```
### q_swap
把 list 的 node 每兩個依序前後交換
```c
void q_swap(struct list_head *head)
{
if (!head)
return;
for (struct list_head *first = head->next, *second = head->next->next;
first != head && second != head;
first = first->next, second = first->next) {
struct list_head *prev = first->prev;
struct list_head *next = second->next;
prev->next = second;
second->prev = prev;
second->next = first;
first->prev = second;
next->prev = first;
first->next = next;
}
}
```
### q_reverse
把每個 node 的 `next` 和 `prev` 進行交換
```c
void q_reverse(struct list_head *head)
{
if (!head)
return;
struct list_head *current, *safe;
list_for_each_safe (current, safe, head) {
struct list_head *prev = current->prev;
struct list_head *next = current->next;
current->next = prev;
current->prev = next;
}
struct list_head *prev = head->prev;
struct list_head *next = head->next;
head->next = prev;
head->prev = next;
}
```
### q_sort
#### 遞迴版本
使用遞迴的 merge sort ,因為傳入的 list 有一個不存資料的 `head` ,所以宣告一個 `new_head` 作為切下來另一半 list 的 `head node`。在自己電腦的效能測試過了,但 github 上的仍無法通過。
```c
struct list_head *merge(struct list_head *left, struct list_head *right)
{
struct list_head head, **temp, *l = left->next, *r = right->next;
INIT_LIST_HEAD(&head);
while (l != left && r != right) {
if (strcmp(list_entry(l, element_t, list)->value,
list_entry(r, element_t, list)->value) < 0)
temp = &l;
else
temp = &r;
*temp = (*temp)->next;
list_move_tail((*temp)->prev, &head);
}
(l == left) ? list_splice_tail_init(right, &head)
: list_splice_tail(left, &head);
list_splice_tail(&head, right);
return right;
}
void q_sort(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *mid = find_mid(head);
struct list_head new_head;
INIT_LIST_HEAD(&new_head);
list_cut_position(&new_head, head, mid->prev);
q_sort(&new_head);
q_sort(head);
merge(&new_head, head);
return;
}
```
- 參考[這篇筆記](https://hackmd.io/@laneser/linux2022_lab0#q_sort)懷疑是 stackoverflow 導致效能測試無法通過
- 思考為什麼會發生 stackoverflow ,猜測是因為每次遞迴都會宣告一次 `new_head` 所導致的
- 改寫 mergesort ,讓 merge 的時候不考慮 `head node` ,如此就不需要每次遞迴都宣告一個 `new_head` 。
- 再次上傳後成功通過效能測試
```c
struct list_head *merge(struct list_head *left, struct list_head *right)
{
struct list_head *head = NULL, **temp = &head;
while (left && right) {
if (strcmp(list_entry(left, element_t, list)->value,
list_entry(right, element_t, list)->value) <= 0) {
*temp = left;
left = left->next;
} else {
*temp = right;
right = right->next;
}
temp = &(*temp)->next;
}
*temp = left ? left : right;
return head;
}
struct list_head *merge_sort(struct list_head *head)
{
if (!head)
return head;
if (!head->next)
return head;
struct list_head *fast = head, *slow = head;
while (1) {
if (!fast)
break;
if (!fast->next)
break;
slow = slow->next;
fast = fast->next->next;
}
slow->prev->next = NULL;
head = merge_sort(head);
slow = merge_sort(slow);
return merge(head, slow);
}
void q_sort(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
head->prev->next = NULL;
head->next = merge_sort(head->next);
struct list_head *current, *prev;
for (current = head->next, prev = head; current;
current = current->next, prev = prev->next)
current->prev = prev;
prev->next = head;
head->prev = prev;
return;
}
```
#### 非遞迴版本
依序將 list 裡的 node 存進 `space` 陣列中,當第 `space[i]` 與 `space[i-1]` 擁有同樣的 node 數(lsit 的長度一樣)時,將兩者進行 merge 並存到 `space[i-1]` 。
```c
void merge_sort_iter(struct list_head *head)
{
int num = q_size(head);
num = log2(num) + 1;
int stack[num];
struct list_head *space[num];
head->prev->next = NULL;
head->prev = NULL;
struct list_head *node = head->next;
int i = 0;
while (node) {
space[i] = node;
stack[i] = 0;
struct list_head *tmp = node;
node = node->next;
tmp->next = NULL;
int j = i - 1;
while (j >= 0) {
if (stack[j] == stack[j + 1]) {
space[j] = merge(space[j], space[j + 1]);
stack[j]++;
j--;
i--;
} else
break;
}
i++;
}
i--;
while (i) {
space[i - 1] = merge(space[i - 1], space[i]);
i--;
}
head->next = space[0];
struct list_head *current, *prev;
for (current = head->next, prev = head; current;
current = current->next, prev = prev->next)
current->prev = prev;
prev->next = head;
head->prev = prev;
return;
}
```
### 研讀 [linux/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c)
- 需要傳入 `list_sort()` 的三個參數
1. `priv` : 傳輸 `cmp` 所需參數的 pointer
2. `head` : 要排序的 list
3. `cmp` : 比較自定義大小的 function pointer,當 `a` 須置於 `b` 之後時會回傳大於 0 的值
- 以升序排列為例,當 `a` > `b` 時便會回傳大於 0 的值
- `list_sort` 為 stable sort ,故不必區分小於或等於 0 的分別
- `list_sort` 跟一般的 `bottom-up mergesort` 不同,後者每當出現 2 個 $2^k$ 大小的 list 時就會進行合併,前者則是當第 3 個 $2^k$ 大小的 list 出現時才進行合併。這樣做的好處是避免了 worst case 的出現,當要排序的 list 大小只比 $2^k$ 大一點點時,`bottom-up mergesort` 的最後一次合併就會出現極端的情況。舉個例子,當 list 大小為 9 時, `bottom-up mergesort`最後一次合併的 2 個 list 大小分別會是 8 和 1;而 `list_sort` 則會是 4 和 5 ,後者很明顯地會優於前者。事實上,不論要排序的 list 大小為何, `list_sort` 的合併都不會出現比 2 : 1 還糟糕的情況。
- 從 `list_sort` 的程式碼中,我發現了幾個自己實作的 merge sort 可以改進的地方
- sort 過的 list 可以用 `struct list_head()` 的 `prev` 指標來串聯,如此就不須宣告新的空間去存放
- 試著只用一個 integer `count` 去控制 list 的合併
- 上述兩點可以使 sort 少走訪一次整個 list (不必在一開始計算 list 的 node 數)
- 在最後一次的合併就把整個 list 的 `prev` 連接回去
- 可以使 sort 少走訪一次 list
- 修改後的程式碼
```c
void final_merge(struct list_head *left,
struct list_head *right,
struct list_head *head)
{
struct list_head **temp = &head;
while (left && right) {
if (strcmp(list_entry(left, element_t, list)->value,
list_entry(right, element_t, list)->value) <= 0) {
left->prev = *temp;
(*temp)->next = left;
left = left->next;
} else {
right->prev = *temp;
(*temp)->next = right;
right = right->next;
}
temp = &(*temp)->next;
}
(*temp)->next = left ? left : right;
while ((*temp)->next) {
(*temp)->next->prev = *temp;
temp = &(*temp)->next;
}
(*temp)->next = head;
head->prev = *temp;
return;
}
void merge_sort_iter(struct list_head *head)
{
struct list_head *pending = NULL;
struct list_head *list = head->next;
head->prev->next = NULL;
head->prev = NULL;
int count = 0;
while (list) {
list->prev = pending;
pending = list;
list = list->next;
pending->next = NULL;
struct list_head **tail = &pending;
for (int bits = count;; bits >>= 1) {
if (bits & 1) {
struct list_head *a = *tail, *b = a->prev;
a = merge(b, a);
a->prev = b->prev;
*tail = a;
} else
break;
}
count++;
}
list = pending;
pending = pending->prev;
while (pending) {
struct list_head *next = pending->prev;
if (!next)
break;
list = merge(pending, list);
pending = next;
}
final_merge(pending, list, head);
return;
}
```
- 測試兩者間效能差異
- 測試用的命令檔(由於第一次 sort 測出來的時間會跟之後的有明顯落差,故皆不採記第一次的結果)
```
option echo 0
option verbose 1
# fault
new
it RAND 300000
time sort
# 1
new
it RAND 300000
time sort
# 2
new
it RAND 300000
time sort
# 3
new
it RAND 300000
time sort
# 4
new
it RAND 300000
time sort
# 5
new
it RAND 300000
time sort
```
- 測試結果
- 原始版本
```
ericliau@ericliau-GL502VML:~/linux2022/lab0-c$ ./qtest -f traces/test1.cmd
cmd> option echo 0
# fault
Delta time = 0.242
# 1
Delta time = 0.381
# 2
Delta time = 0.396
# 3
Delta time = 0.391
# 4
Delta time = 0.386
# 5
Delta time = 0.383
```
- 修改後
```
ericliau@ericliau-GL502VML:~/linux2022/lab0-c$ ./qtest -f traces/test1.cmd
cmd> option echo 0
# fault
Delta time = 0.207
# 1
Delta time = 0.325
# 2
Delta time = 0.327
# 3
Delta time = 0.333
# 4
Delta time = 0.333
# 5
Delta time = 0.330
```
- 把修改後的 5 次測試時間加起來除以原始版本的 5 次測試時間,得出來的值約為 0.85 ,效能提昇了 15% 左右
### 引入 [linux/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c)
引入方法參考自 [2020leon](https://hackmd.io/@6649/linux2022-lab0#%E5%AF%A6%E5%81%9A%E4%BD%87%E5%88%97%EF%BC%88-queuec-%EF%BC%89)
1. 將 `list_sort.c` 和 `list_sort.h` 複製到 lab0-c 專案中。
- `list_sort.c` 直接從 [linux/list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 複製。
- `list_sort.h` 從 linux 核心中的 include/linux/list_sort.h 複製。
2. 將型別 `u8` 更改為 `uint8_t`。
- 必須引入標頭檔 `stdint.h`。
3. 在 `list_sort.h` 加入 `likely(x)` 及 `unlikely(x)` 巨集定義。
```c
# define likely(x) __builtin_expect(!!(x), 1)
# define unlikely(x) __builtin_expect(!!(x), 0)
```
4. 在 `list_sort.c` 中新增 `listcmp()` 函式。
```c
int listcmp(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);
}
```
5. 使 cppcheck 忽略 `list_sort.c` 中 `merge` 函式產生的 `head` 未賦值錯誤
- 加入 `// cppcheck-suppress unassignedVariable`
```c
// cppcheck-suppress unassignedVariable
struct list_head *head, **tail = &head;
```
- 於終端機輸入 `$ cppcheck --inline-suppr list_sort.c`
6. 修改 `qtest.c` ,新增一個 option 以便於在 `list_sort` 及自己實作的排序法間做切換。
- 加入 option
```c
...
static int listsort = 0;
...
static void console_init()
{
...
add_param("listsort", &listsort, "Use list_sort or not", NULL);
}
```
- 修改 `do_sort()` 函式
```c
bool do_sort(int argc, char *argv[])
{
...
if (exception_setup(true)){
if(!listsort)
q_sort(l_meta.l);
else
list_sort(NULL, l_meta.l, cmp);
...
}
```
測試 list sort 和自己的排序法效能落差,測出來自己的排序法慢了大概 10%
```
ericliau@ericliau-GL502VML:~/linux2022/lab0-c$ ./qtest -f traces/test.cmd
cmd> option echo 0
# fault
Delta time = 0.273
# user version
Delta time = 0.376
Delta time = 0.408
Delta time = 0.390
Delta time = 0.377
Delta time = 0.374
# list sort
Delta time = 0.358
Delta time = 0.342
Delta time = 0.340
Delta time = 0.342
Delta time = 0.344
```
而如果是用參考 list sort 後所修改的版本則大約快了 5%
```
ericliau@ericliau-GL502VML:~/linux2022/lab0-c$ ./qtest -f traces/test.cmd
cmd> option echo 0
# fault
Delta time = 0.207
# user version
Delta time = 0.323
Delta time = 0.321
Delta time = 0.327
Delta time = 0.336
Delta time = 0.333
# list sort
Delta time = 0.344
Delta time = 0.348
Delta time = 0.344
Delta time = 0.343
Delta time = 0.343
```
我還蠻意外會比 list sort 快的,具體比較快的原因待驗證
## qtest.c
### 實作 shuffle 演算法
使用 [Fisher–Yates shuffle](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle) 演算法,該演算法之 pseudocode 如下:
```
-- To shuffle an array a of n elements (indices 0..n-1):
for i from n−1 downto 1 do
j ← random integer such that 0 ≤ j ≤ i
exchange a[j] and a[i]
```
實作之程式碼如下:
```c
static void shuffle(struct list_head *head)
{
int num = q_size(head);
struct list_head *tmp, *current;
for(tmp = head->prev; num > 0; num--){
current = head;
for(int i = rand() % num; i >= 0; i--)
current = current->next;
char *value = list_entry(current, element_t, list)->value;
list_entry(current, element_t, list)->value = list_entry(tmp, element_t, list)->value;
list_entry(tmp, element_t, list)->value = value;
}
}
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(1, "list is null");
return false;
}
else if (list_empty(l_meta.l))
{
report(1, "list is empty");
return false;
}
else if (list_is_singular(l_meta.l))
{
report(1, "no need to shuffle");
return false;
}
shuffle(l_meta.l);
show_queue(3);
return true;
}
```
最後在 `console_init()` 添加指令:
```
ADD_COMMAND(shuffle, " | Shuffle the list");
```
### 提供 web 命令
因為要整合 [tiny-web-server](https://github.com/7890/tiny-web-server) ,所以先試著引入 `tiny.c` ,並分一個 `tiny.h` 出來存放其他檔案會用到的部份。
- 其中的 `process()` 需要進行修改,這部份稍後再提
```c
#ifndef __TINY_H
#define __TINY_H
#include <arpa/inet.h> /* inet_ntoa */
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/sendfile.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
typedef struct {
char filename[512];
off_t offset; /* for support Range */
size_t end;
} http_request;
/* Simplifies calls to bind(), connect(), and accept() */
typedef struct sockaddr SA;
int open_listenfd(int port);
char *process(int fd, struct sockaddr_in *clientaddr);
void parse_request(int fd, http_request *req);
#endif /* __TINY_H */
```
然後把 `tiny.c` 和 `tiny.h` 加到 `Makefile` 裡,在 `OBJS := `後面加入 `tiny.o`
```
OBJS := console.o qtest.o report.o harness.o queue.o \
random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \
linenoise.o tiny.o
```
接著基本上就照著[作業說明](https://hackmd.io/@sysprog/linux2022-lab0#%E6%95%B4%E5%90%88-tiny-web-server)完成
首先在 `linenoise.h` 中加入變數 `noise` 和 `listenfd`
```c
bool noise;
int listenfd;
```
修改 `linenoiseEdit()` ,在 `while(1)` 裡加入以下程式碼
```c
if (listenfd) {
fd_set set;
FD_ZERO(&set);
FD_SET(listenfd, &set);
FD_SET(stdin_fd, &set);
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
int rv = select(listenfd + 1, &set, NULL, NULL, NULL);
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;
}
} else {
nread = read(l.ifd, &c, 1);
if (nread <= 0)
return l.len;
}
```
修改 `run_console()` ,讓其依照 `linenoise` 開啟與否決定要用 `linenoise` 還是 `cmd_select`
```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) {
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;
}
```
修改 `cmd_select()` ,新增對於 web 端輸入的處理
```c
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--;
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;
}
```
修改 `tiny.c` 中的 `process()` , `process()` 會處理 http 請求,將其轉換成符合 `cmd` 的命令格式
```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;
char *p = req.filename;
/* Change '/' to ' ' */
while (*p && (*p) != '\0') {
++p;
if (*p == '/') {
*p = ' ';
}
}
#ifdef LOG_ACCESS
log_access(status, clientaddr, &req);
#endif
char *ret = malloc(strlen(req.filename) + 1);
strncpy(ret, req.filename, strlen(req.filename) + 1);
return ret;
}
```
最後在 `qtest.c` 中新增 `do_web()` 函式和 `web` 指令
- 加入 `set_echo(false);` 是為了避免在 `web` 開啟後從使用者端輸入指令會重複印出的情形
```c
static bool do_web(int argc, char *argv[])
{
noise = false;
set_echo(false);
listenfd = open_listenfd(9999);
printf("listen on port 9999, fd is %d\n", listenfd);
return true;
}
```
```c
ADD_COMMAND(web, " | Open web server");
```
執行結果
```
ericliau@ericliau-GL502VML:~/linux2022/lab0-c$ ./qtest
cmd> web
listen on port 9999, fd is 3
cmd> accept request, fd is 4, pid is 53178
127.0.0.1:58584 200 - 'new' (text/plain)
l = []
cmd> accept request, fd is 4, pid is 53178
127.0.0.1:58586 200 - 'ih a' (text/plain)
l = [a]
cmd> accept request, fd is 4, pid is 53178
127.0.0.1:58588 200 - 'ih b' (text/plain)
l = [b a]
cmd> accept request, fd is 4, pid is 53178
127.0.0.1:58590 200 - 'ih c' (text/plain)
l = [c b a]
cmd> ih d
l = [d c b a]
cmd> ih e
l = [e d c b a]
cmd> accept request, fd is 4, pid is 53178
127.0.0.1:58592 200 - 'sort' (text/plain)
l = [a b c d e]
```
## 開啟 [Address Sanitizer](https://github.com/google/sanitizers/wiki/AddressSanitizer),修正 `qtest` 執行過程中的錯誤
`qtest` 執行後並未發生錯誤,尚在尋找原因
## select 系統呼叫
`select` 的功用是同時監測多個 `file descriptor` ,直到其中一個準備好要進行 I/O 。而 `lab0` 使用 `select` 的地方在函式 `cmd_select()` 中,透過實作 `web` 指令,可以得知其主要用來偵測是由使用者端還是從 web 端輸入指令。
## 分析 console.c
先說結論 : 使用 RIO 套件可以在讀 cmd 檔的時候減少呼叫系統指令 `read` 的次數。
因為 RIO 套件支援 buffered I/O ,在讀行時只需要呼叫 1 次 `read` 系統指令將一個 buffer 大小的資料存進 buffer ,再從 buffer 中一個字一個字的尋找換行符即可。如果不使用 buffer ,因為需要尋找換行符的緣故,每次呼叫 `read` 只能讀一個字元,導致需要頻繁的呼叫 `read` 指令,使得 OS 必須不斷地在 kernel-mode 和 user-mode 間做切換。
## 研讀論文〈Dude, is my code constant time?〉
這篇論文使用 [dudect](https://github.com/oreparaz/dudect) 這個程式來測量某個程式執行時間是否為 constant time 。其中使用的 [Student’s t-distribution](https://en.wikipedia.org/wiki/Student%27s_t-distribution) 為一種估計期望值的統計學方法,用於樣本量不大且不知道標準差的情況。
該論文使用 leakage detection test ,因為是透過統計模型來推斷是否為 constant time,使得此方法並不局限於特定的 CPU 硬體,方法步驟如下:
1. 分別用固定和隨機的輸入資料進行多次測試
2. 去掉極端值然後進行 high-order preprocessing
3. 用統計方法判定是否符合常態時間,本專案使用 [Welch's t-test](https://en.wikipedia.org/wiki/Welch's_t-test)
### lab0 實作
lab0 檢查是否 constant time 的函式主要位於 `dudect/` 中,其呼叫路徑為 `is_XXX_const() -> TEST_CONST() -> doit()` ,測試步驟基本在 `doit()` 中完成。
```c
static bool doit(int mode)
{
int64_t *before_ticks = calloc(n_measure + 1, sizeof(int64_t));
int64_t *after_ticks = calloc(n_measure + 1, sizeof(int64_t));
int64_t *exec_times = calloc(n_measure, sizeof(int64_t));
uint8_t *classes = calloc(n_measure, sizeof(uint8_t));
uint8_t *input_data = calloc(n_measure * chunk_size, sizeof(uint8_t));
if (!before_ticks || !after_ticks || !exec_times || !classes ||
!input_data) {
die();
}
prepare_inputs(input_data, classes);
measure(before_ticks, after_ticks, input_data, mode);
differentiate(exec_times, before_ticks, after_ticks);
update_statistics(exec_times, classes);
bool ret = report();
free(before_ticks);
free(after_ticks);
free(exec_times);
free(classes);
free(input_data);
return ret;
}
```
`prepare_inputs()` 產生多個長度為 7 bytes 的隨機字串 (加上 `\0` 為 8 bytes),並將資料分成兩個 class。
`measure()` 將 `prepare_inputs()` 產生的字串根據不同操作進行測量,測量方式為分別記錄執行該操作前後當下的 cycle 數。
`differentiate()` 計算兩者 cycle 數的差值,即該操作執行所花費的 cycle 數。
`update_statistics()` 再用得到的執行 cycle 數分別算出兩個 class 的平均數和變異數。
`report()` 將所求出的值用 Welch’s t-test 算出 `t` 值,並判斷對應操作是否為 constant time 。