owned this note
owned this note
Published
Linked with GitHub
# 2019q3 Homework2 (lab0)
contribyted by < `nckuoverload` >
###### tags: `sysprog2019`
## 作業要求
* 在 GitHub 上 fork [lab0-c](https://github.com/sysprog21/lab0-c)
* 參閱 [GitHub 設定指引](http://wiki.csie.ncku.edu.tw/github)
* ==詳細閱讀 [C Programming Lab](http://www.cs.cmu.edu/~213/labs/cprogramminglab.pdf)== (英文閱讀正是我們想強化的議題,你應該在共筆上摘要題目要求),依據指示著手修改 `queue.[ch]` 和連帶的檔案,測試後用 Git 管理各項修改。
* 在提交程式前,務必詳閱 [如何寫好 Git Commit Message](https://blog.louie.lu/2017/03/21/%E5%A6%82%E4%BD%95%E5%AF%AB%E4%B8%80%E5%80%8B-git-commit-message/)
* 不用理會 [Autolab](http://www.autolabproject.com/) 和檔案下載的描述,這兩者都是 CMU 專屬
* 除了修改程式,也要編輯「[作業區](https://hackmd.io/@sysprog/BkZ4h5xPB)」,增添開發紀錄和 GitHub 連結,提及如何逐步達到要求,以及如何改善效能
- [x] 解釋自動評分系統運作的原理
- [x] 提及 `qtest` 的行為和裡頭的技巧,特別是 signal handler
- [ ] 解析 Valgrind 的運作原理和針對本程式的使用
- [ ] 根據 [dudect](https://github.com/oreparaz/dudect) 工具以及提出的對應論文 [Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf),探討一種以實驗而非理論驗證時間複雜度的方法
- [ ] 留意 `MAGICHEADER`, `MAGICFREE`, `MAGICFOOTER`, `FILLCHAR` 等巨集的使用,並探討其動機和用法
* 截止日期:
* Oct 2, 2019 (含) 之前
* 越早在 GitHub 上有動態、越早接受 code review,評分越高
## 實驗環境
Operating System: `Ubuntu 16.04 64bit`
Compiler: `gcc 8.3.0`
## 測試結果
```shell
$ python ./scripts/driver.py -A
--- Trace Points
--- trace-01-ops 6/6
--- trace-02-ops 6/6
--- trace-03-ops 6/6
--- trace-04-ops 6/6
--- trace-05-ops 6/6
--- trace-06-string 7/7
please new a queue first
--- trace-07-robust 7/7
--- trace-08-robust 7/7
--- trace-09-robust 7/7
--- trace-10-malloc 7/7
--- trace-11-malloc 7/7
--- trace-12-malloc 7/7
--- trace-13-perf 7/7
--- trace-14-perf 7/7
--- trace-15-perf 7/7
--- TOTAL 100/100
```
## 實作紀錄
### queue_t
在`queue.h`中,修改`queue_t`結構。
新增`list_ele_t *tail;`用來指向尾巴。
新增`int size;`用來記錄目前的長度。
```cpp
typedef struct {
list_ele_t *head; /* Linked list of elements */
list_ele_t *tail;
int size;
} queue_t;
```
### q_new()
首先是建立一個queue ,過程沒太大問題,要注意`malloc` 有可能會返回`NULL` ,所以在第29行要針對這個情況特別處理。
```cpp=25
queue_t *q_new()
{
queue_t *q = malloc(sizeof(queue_t));
/* What if malloc returned NULL? */
if (q == NULL) {
free(q);
return NULL;
}
q->head = NULL;
q->tail = NULL;
q->size = 0;
return q;
}
```
### q_free()
釋放佇列,主要使用一個loop 去對每一個節點做處理。
```cpp=40
void q_free(queue_t *q)
{
/* How about freeing the list elements and the strings? */
/* Free queue structure */
// free(q-> value);
if (q == NULL) {
return;
}
list_ele_t *tmp = q->head;
while (q->head != NULL) {
tmp = q->head;
// free(q->head->value);
q->head = q->head->next;
free(tmp->value);
free(tmp);
}
free(q);
}
```
### q_insert_head(queue_t *q, char *s)
和`_insert_tail()` 可以搭配一起來看,主要在第一個`if` 判斷該佇列是否為空,第二、三個判斷`malloc` 是否返回`NULL` (在測資中,會針對這個部分做測試)。
從頭插入的部分主要是新增一個節點,並將該節點指向原本的head ,並將佇列的head 指向該節點,並且size 增加。
```cpp=66
bool q_insert_head(queue_t *q, char *s)
{
if (q == NULL) {
return false;
}
list_ele_t *newh;
/* What should you do if the q is NULL? */
newh = malloc(sizeof(list_ele_t));
if (newh == NULL) {
free(newh);
return false;
}
/* Don't forget to allocate space for the string and copy it */
/* What if either call to malloc returns NULL? */
newh->next = q->head;
newh->value = malloc(sizeof(char) * (strlen(s) + 1));
if (newh->value == NULL) {
free(newh->value);
free(newh);
return false;
}
strcpy(newh->value, s);
if (q->head == NULL) {
q->tail = newh;
}
q->head = newh;
q->size += 1;
return true;
}
```
### q_insert_tail(queue_t *q, char *s)
和`q_insert_head()` 相似,前三個判斷式相同效果,但應該有更好的寫法。
從尾端插入的部分,主要是將原本的尾端指向即將插入的節點,並將佇列中的`tail` 改成指向即將插入的節點。
```cpp=104
bool q_insert_tail(queue_t *q, char *s)
{
/* You need to write the complete code for this function */
/* Remember: It should operate in O(1) time */
if (q == NULL) {
return false;
}
list_ele_t *newQ;
newQ = malloc(sizeof(list_ele_t));
if (newQ == NULL) {
free(newQ);
return false;
}
newQ->value = malloc(sizeof(char) * (strlen(s) + 1));
if (newQ->value == NULL) {
free(newQ->value);
free(newQ);
return false;
}
strcpy(newQ->value, s);
q->tail->next = newQ;
q->tail = newQ;
newQ->next = NULL;
q->size += 1;
return true;
}
```
### q_remove_head(queue_t *q, char *sp, size_t bufsize)
主要要注意測試中,有兩種測試方式,一個為`rh [str]` 和`rhq` ,前者主要會和輸入的字串做判斷,在這個部分要特別注意**輸入字串的大小是否大於佇列中第一個節點的大小**,在第147行針對這個部分做處理,如果超過不處理的話會造成溢出(overflow)。
```cpp=139
bool q_remove_head(queue_t *q, char *sp, size_t bufsize)
{
/* You need to fix up this code. */
if (q == NULL || q->size == 0) {
return false;
}
if (bufsize != 0) {
memset(sp, '\0', bufsize);
if (strlen(q->head->value) > bufsize) {
strncpy(sp, q->head->value, bufsize - 1);
} else {
strcpy(sp, q->head->value);
}
}
list_ele_t *tmp = q->head;
q->head = q->head->next;
free(tmp->value);
free(tmp);
q->size--;
return true;
}
```
### q_size(queue_t *q)
使用額外的`int size` 來儲存佇列大小,為了符合題目的$O(1)$。
```cpp=165
int q_size(queue_t *q)
{
/* You need to write the code for this function */
/* Remember: It should operate in O(1) time */
if (q == NULL) {
return 0;
}
return q->size;
}
```
### q_reverse(queue_t *q)
總共寫了兩個版本,原先使用類似bubble sort 的演算法來做倒轉,簡單來說就是模擬bubble sort 最壞情況,每一個迴圈讓一個節點移到適當的位置,並讓其他節點往前移一個位置,複雜度為$O(n^2)$。但這樣做的話在test 會一直因為時間超過而無法通過。
第二個版本主要是多用了三個暫存變數,每一次將一個節點的指標轉向,並在最後處理`q`的`head` 和`tail`。也是很直觀的做法,但一開始因為題目敘述理解錯誤,誤解為不能多使用額外空間設置變數來處理。
```cpp=182
void q_reverse(queue_t *q)
{
/* You need to write the code for this function */
if (q == NULL || q->size < 2) {
return;
}
list_ele_t *temp;
list_ele_t *now = q->head->next;
list_ele_t *pre = q->head;
while (now != NULL) {
temp = now->next;
now->next = pre;
pre = now;
now = temp;
}
q->tail = q->head;
q->head = pre;
q->tail->next = NULL;
}
```
## 討論
### 解釋自動評分系統運作的原理
主要使用Makefile 來自動評分,Makefile 寫起來類似shell script 但又不盡相同,詳細可以參考[這篇範例](https://mropengate.blogspot.com/2018/01/makefile.html),或是網路上應該有許多其他解說。
在原始的Makefile 中,主要是編譯後,呼叫`scripts/driver.py` 來評分,所以也可以在`~/lab0-c` 中直接使用`python driver.py` 來評分。
`driver.py` 又可以使用四個參數來使用。
`h` : 主要用來說明有哪些參數,類似一般使用的help 。
`p` : 可以選擇用哪個檔案來評分。 (不太確定式不是這個意思)
`t` : 可以選擇要選第幾個腳本來測試,腳本都在`~/lab0-c/traces` 底下。
`v` : 有0~3的級別,可以選擇要顯示多少測試的腳本資料。
`A` : 自動評分,功能等同於使用 `make test` 。
`--valgrind` : 如果直接搜尋會找到 [Wikipedia](https://en.wikipedia.org/wiki/Valgrind) 的解說,是一款GNU 的工具,用來處理記憶體除錯。稍後會再針對這部分解釋。
### 提及 `qtest` 的行為和裡頭的技巧,特別是 signal handler
在 `qtest.c` 中,會有許多 method 用來做測試,每一個都會使用 `exception_setup() exception_cancel()` 兩個來對 exception 做處理,同時裡面也有使用到 signal 的機制。
首先會先執行 `queue_init()` 並且呼叫兩個 signal ,`SIGSEGV` 用來處理記憶體部分的例外狀況;`SIGALRM` 用來處理執行時間的例外狀況。
之後以 `do_new()` 為例,在第107行的部分,當要執行 `q_new()` 前,會先執行 `exception_setup(true)` ,用來開啟 signal ,並且在執行完成後使用 `exception_cancel()` 關閉。
`exception_setup()` 和 `exception_cancel()` 都在 `harness.c` 中。以下將分別敘述。
- exception_setup():
這邊可以分成兩個流程來說明,分別是正常運作和 `exception` 發生時的狀況。
- 正常運作: 在正常運作下,會從 `do_new()` 等執行 `exception_setup()` 跳入,因為是正常情況,所以 `sigsetjmp()` 並不會被執行,會直接跳到253 行執行,`jmp_ready` 參數主要是用來紀錄在 exception 發生後,是否可以執行 jump 。並且這時候會在257 行給予一個執行時間,程式必須在時間內完成,否則會觸發 signal 並且丟出 `sigalrmhandler` 讓 `qtest.c` 中的 `sigalrmhandler()` 去處理例外。並且會將狀態恢復到未執行 `q_new()` 前的狀態繼續往下執行。
- 例外狀況: 如果有發生例外狀況,因為一開始有先使用 `sigsetjmp` 來保存 stack 環境等,所以會把剩下的時間返回,並且將錯誤訊息印出,之後在兩個 handler 中都有使用到 `trigger_exception()` 去處理,`trigger_exception()` 會使用 `siglongjmp` 將環境恢復到原本儲存的狀態(尚未進入會發生例外狀況的 method 前的狀態)。
```cpp=239
bool exception_setup(bool limit_time)
{
if (sigsetjmp(env, 1)) {
/* Got here from longjmp */
jmp_ready = false;
if (time_limited) {
alarm(0);
time_limited = false;
}
if (error_message) {
report_event(MSG_ERROR, error_message);
}
error_message = "";
return false;
} else {
/* Got here from initial call */
jmp_ready = true;
if (limit_time) {
alarm(time_limit);
time_limited = true;
}
return true;
}
}
```
- exception_cancel():
當 method 順利執行完後,把 signal 關閉。
```cpp=267
void exception_cancel()
{
if (time_limited) {
alarm(0);
time_limited = false;
}
jmp_ready = false;
error_message = "";
}
```
小結, `qtest.c` 因為不能確保每次執行實作者在 `queue.c` 中實作的 method 都是可靠的,所以每次要進入 `queue.c` 的方法前,都會先使用 `sigsetjmp` 保存狀態,並且開啟 signal ,如果例外狀況沒發生,則會使用 `exception_cancel()` 去關閉所有上述機制。如果例外狀況發生,則會使用 `trigger_exception()` 中的 `siglongjmp()` 來恢復到進入有問題方法前的環境。
- 實驗
很簡單的實驗,可以把限制時間長度的 signal 關閉,並且把 `q_reverse` 的部分改回第一版本氣泡排序法去執行,可以發現並不會有時間錯誤的問題出現,在第13個測試 `trace-13-perf.cmd` 正常情況下會發生錯誤。
- `q_reverse()` using bubble sorting
```cpp
void q_reverse(queue_t *q)
{
/* You need to write the code for this function */
if (q == NULL) {
return;
}
int temp = q->size;
list_ele_t *t;
for (int i = 0; i < q->size; i++) {
temp--;
t = q->head;
for (int z = 0; z < temp; z++) {
*(t->value) ^= *(t->next->value);
*(t->next->value) ^= *(t->value);
*(t->value) ^= *(t->next->value);
t = t->next;
}
}
}
```
### 未完成
- [ ] 解析 Valgrind 的運作原理和針對本程式的使用
- [ ]根據 dudect 工具以及提出的對應論文 Dude, is my code constant time?,探討一種以實驗而非理論驗證時間複雜度的方法
- [ ]留意 MAGICHEADER, MAGICFREE, MAGICFOOTER, FILLCHAR 等巨集的使用,並探討其動機和用法
## 參考資料