KYG
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 2020q1 Homework1 (lab0) contributed by < [`KYG-yaya573142`](https://github.com/KYG-yaya573142/lab0-c) > > [H01: lab0](https://hackmd.io/JSFDnHn0Rpe0V7d-AqLAXA?view) ## 預期目標 * [C 語言程式設計](https://hackmd.io/@sysprog/c-programming) 議題,如[不定個數參數的處理](https://en.wikibooks.org/wiki/C_Programming/stdarg.h), [signal](https://en.wikibooks.org/wiki/C_Programming/signal.h), [setjmp/longjmp](https://en.wikibooks.org/wiki/C_Programming/setjmp.h) 和記憶體管理 * 學習 [GNU/Linux 開發工具](https://hackmd.io/@sysprog/gnu-linux-dev) - [Cppcheck](http://cppcheck.sourceforge.net/): 靜態程式碼分析工具 - [Valgrind](https://valgrind.org/): 動態程式分析工具 * 學習使用 Git 與 GitHub * 樹立一致且易於協作的程式開發規範 * 研究自動測試機制 * 接觸 [Linux Programming Interface](http://man7.org/tlpi/) ## 作業要求 * `q_new`: 建立新的「空」佇列 * `q_free`: 釋放佇列所佔用的記憶體 * `q_insert_head`: 在佇列開頭 (head) 加入 (insert) 給定的新元素 (以 LIFO 準則) * `q_insert_tail`: 在佇列尾端 (tail) 加入 (insert) 給定的新元素 (以 FIFO 準則),時間複雜度須為 $O(1)$ * `q_remove_head`: 在佇列開頭 (head) 移除 (remove) 給定的元素 * `q_size`: 計算佇列中的元素數量,時間複雜度須為 $O(1)$ * `q_reverse`: 以反向順序重新排列鏈結串列 (此函式不該配置或釋放任何鏈結串列元素) * `q_sort`: 以遞增順序來排序鏈結串列的元素 (此函式不該配置或釋放任何鏈結串列元素) ## 開發流程 ### queue.h 題目要求 `q_insert_tail` 和 `q_size` 的時間複雜度須為 $O(1)$,因此先修改 queue.h 中的 `struct queue_t`,新增 `list_ele_t *tail` 及 `int size` ```cpp /* Queue structure */ typedef struct { list_ele_t *head; /* Linked list of elements */ list_ele_t *tail; int size; } queue_t; ``` ### `q_size` 直接回傳 `q->size` 即可 ```cpp= int q_size(queue_t *q) { if (!q) return 0; return q->size; } ``` ### `q_new` 先確認是否成功 malloc,再初始化 queue ```cpp= queue_t *q_new() { queue_t *q = malloc(sizeof(queue_t)); if (!q) return NULL; q->head = NULL; q->tail = NULL; q->size = 0; return q; } ``` ### `q_free` 須注意在 `q_insert_head` 與 `q_insert_tail` 中,除了配置 elements 的記憶體之外,還會再配置記憶體來儲存字串,因此釋放記憶體的順序應該是 1. 用來儲存字串的空間 2. linked list elements 3. queue ```cpp= void q_free(queue_t *q) { if (!q) /* ignore NULL queue */ return; /* Free queue structure */ list_ele_t *tmp = q->head; while (tmp) { q->head = tmp->next; if (tmp->value) /* free the string if existed */ free(tmp->value); free(tmp); tmp = q->head; } free(q); } ``` ### `q_insert_head` & `q_insert_tail` 這兩個 function 寫起來非常相似,先配置 element 的記憶體空間,再配置 element 中用來儲存字串的空間 (記得確認 malloc 是否成功),儲存字串至 element 內,最後將 element 插入 queue 的頭/尾並把 `q->size` 加 1 實作上有幾點需特別注意: * `strlen` 回傳的長度**不包含** null terminator,因此 length 記得要加 1 回去 ```shell $ man strlen ... DESCRIPTION The strlen() function calculates the length of the string pointed to by s, excluding the terminating null byte ('\0'). ... ``` * 若在 malloc 字串所需空間時才失敗,記得要釋放 element 的空間再 return * 若為 empty queue 時,需特別處理 `q->head` 或 `q->tail` (使兩者皆指向目前新增的唯一 element) ```cpp= bool q_insert_head(queue_t *q, char *s) { if (!q) /* ignore NULL queue */ return false; list_ele_t *newh = malloc(sizeof(list_ele_t)); if (!newh) return false; /* allocate space for the string and copy it */ int length = strlen(s) + 1; newh->value = malloc(length * sizeof(char)); if (!newh->value) { free(newh); return false; } strncpy(newh->value, s, length); /* insert element at head of queue */ if (q->head == NULL) /* queue is empty */ q->tail = newh; newh->next = q->head; q->head = newh; (q->size)++; return true; } bool q_insert_tail(queue_t *q, char *s) { if (!q) /* ignore NULL queue */ return false; list_ele_t *newh = malloc(sizeof(list_ele_t)); if (!newh) return false; /* allocate space for the string and copy it */ int length = strlen(s) + 1; newh->value = malloc(length * sizeof(char)); if (!newh->value) { free(newh); return false; } strncpy(newh->value, s, length); /* insert element at tail of queue */ if (q->head == NULL) { /* no element in the queue */ q->head = newh; q->tail = newh; } else { q->tail->next = newh; q->tail = q->tail->next; } q->tail->next = NULL; (q->size)++; return true; } ``` ### `q_remove_head` 釋放記憶體的邏輯類似 `q_free`,釋放 elements 配置的記憶體前,需先處裡並釋放 element 中字串使用的空間 實作上需注意複製字串至 sp 的這個步驟,由於有最大空間為 bufsize 的限制,應該使用 `strncpy` 而非 `strcpy`,使用前先查閱說明 `man strncpy` > The strcpy() function copies the string pointed to by src, **including** the terminating null byte ('\0'), to the buffer pointed to by dest. The strings may not overlap, and the destination string dest must be large enough to receive the copy. **Beware of buffer overruns!** > The strncpy() function is similar, except that at most n bytes of src are copied. **Warning: If there is no null byte among the first n bytes of src, the string placed in dest will not be null-terminated.** > If the length of src is less than n, strncpy() writes additional null bytes to dest to ensure that a total of n bytes are written. 因此當 bufsize 小於字串長度時,將不會複製到 null terminator,要記得手動將其補上 ```cpp= bool q_remove_head(queue_t *q, char *sp, size_t bufsize) { if (!q || !q->head) /* ignore NULL and empty queue */ return false; list_ele_t *tmp = q->head; q->head = q->head->next; if (tmp->value) { if (sp != NULL) { strncpy(sp, tmp->value, (bufsize - 1)); sp[bufsize - 1] = '\0'; } free(tmp->value); } free(tmp); (q->size)--; return true; } ``` ### `q_reverse` 實作上可以分為以下步驟 1. 建立 `new` 指標,用來指向已經分類完成的 elements 2. `q->tail` 可以先指向 `q->head`,因為最後順序會反轉 3. 依序將 element 從原先的 queue 取出 4. 總是將取出的 element 放到 `new` 的**頭**,來達到反轉順序的目的 5. 最後將 `new` 指標接回去 `q->head` 即可 ```c= void q_reverse(queue_t *q) { if (!q || !q->head) /* ignore NULL and empty queue */ return; if (!q->head->next) return; list_ele_t *tmp; list_ele_t *new = NULL; q->tail = q->head; while (q->head) { /* detach element from the head */ tmp = q->head; q->head = q->head->next; /* reverse the queue */ tmp->next = new; new = tmp; } q->head = new; } ``` ### `q_sort` 根據作業說明的共筆中提供的資料,可以使用 bubble sort、insertion sort、selection sort、merge sort 等方式來排序 (將 `sort_method` 的部分替換成欲使用的演算法名稱即可,例如 `merge_sort`) ```c= void q_sort(queue_t *q) { if (!q || !q->head) /* ignore NULL and empty queue */ return; if (!q->head->next) return; q->head = sort_method(q->head); while (q->tail->next) { /* update the tail pointer */ q->tail = q->tail->next; } } ``` 至於題目中所謂的*以遞增順序來排序鏈結串列的元素*要怎麼做呢? 其實直接觀察 qtest.c 就可以知道答案了,其中 `do_sort` 這個方程式中很明確的使用 `strcasecmp` 來檢驗 `q_sort` 的結果,因此在我們的 `q_sort` 中一樣使用 `strcasecmp` 來排序就可以了 #### merge sort 概念上可以分為拆分 (`merge_sort`) 及合併 (`merge`) 兩個部分 拆分 `merge_sort`: 1. 將原先的 list 從中間分為兩半 2. 將分出來的兩個 list 再各自從中間分為兩半 3. 重複步驟 2 直到整個 list 都被拆成單一 element ```c= list_ele_t *merge_sort(list_ele_t *head) { /* merge sort */ if (!head || !head->next) return head; list_ele_t *slow = head; list_ele_t *fast = head->next; /* split list */ while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } fast = slow->next; slow->next = NULL; /* sort each list */ list_ele_t *l1 = merge_sort(head); list_ele_t *l2 = merge_sort(fast); /* merge sorted l1 and sorted l2 */ return merge(l1, l2); } ``` 合併 `merge` 1. 排序兩個單一 element 並合併為一個 list 2. 把兩個上述步驟產生的 list 排序並接合為一個 list 3. 重複步驟 2 直到接合所有 list 以下我分別使用不同的方式來實作 `merge` 的部分 **方式 1 - 使用遞迴的方式 merge** 這個方式的優點是程式碼很簡潔,但遞迴呼叫的方式導致記憶體使用量更龐大。 最明顯的例子就是當測試 traces-15-perf.cmd 時,因為配置的 elements 數非常龐大 (size = 2,000,000),會出現 segmentation fault (使用 AddressSanitizer 可以知道問題是 stack overflow) 註:後續會再使用 valgrind 來分析此問題 ```shell $ make SANITIZER=1 $ make test ... +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, reverse, and sort ASAN:DEADLYSIGNAL ================================================================= ==9205==ERROR: AddressSanitizer: stack-overflow on address 0x7ffcd0a27fd8 (pc 0x7f703599965e bp 0x7ffcd0a28840 sp 0x7ffcd0a27fa0 T0) ... ``` ```c= list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { if (!l1) return l2; if (!l2) return l1; if (strcasecmp(l1->value, l2->value) < 0) { l1->next = merge(l1->next, l2); return l1; } else { l2->next = merge(l1, l2->next); return l2; } } ``` **方式2 - pseudo head** 常見的 pseudo head 是先 malloc 一個空間來連接已經排序完成的 element,但在本作業中不允許在 `q_sort` 內呼叫 malloc,因此我改為使用指標的方式實作,如此一來在 traces-15-perf.cmd 時就不會錯誤 ```c= list_ele_t *merge(list_ele_t *l1, list_ele_t *l2) { if (!l1) return l2; if (!l2) return l1; list_ele_t *head = NULL; /* pseudo head */ list_ele_t *tmp = NULL; /* decide the first element and use it as pseudo head */ if (strcasecmp(l1->value, l2->value) < 0) { head = l1; l1 = l1->next; } else { head = l2; l2 = l2->next; } /* merge remaining elements to pseudo head */ tmp = head; while (l1 && l2) { if (strcasecmp(l1->value, l2->value) < 0) { tmp->next = l1; l1 = l1->next; } else { tmp->next = l2; l2 = l2->next; } tmp = tmp->next; } if (l1) tmp->next = l1; if (l2) tmp->next = l2; return head; } ``` ## 變更排序所用的函式為 natural sort 為了使用 [natural sort](https://github.com/sourcefrog/natsort),需先修改 Makefile 來將 strnatcmp.[ch] 加入 (相關的 dependency 會放在 .natsort 內) ``` ... NAT_DIR := natsort ... OBJS := qtest.o report.o console.o harness.o queue.o \ random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \ natsort/strnatcmp.o ... %.o: %.c @mkdir -p .$(DUT_DIR) @mkdir -p .$(NAT_DIR) $(VECHO) " CC\t$@\n" $(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $< ... clean: rm -f $(OBJS) $(deps) *~ qtest /tmp/qtest.* rm -rf .$(DUT_DIR) rm -rf .$(NAT_DIR) rm -rf *.dSYM (cd traces; rm -f *~) ``` 接著需要修改 queue.c 中使用的排序方式及 `make test` 時驗證 `q_sort` 結果的部分,也就是 queue.c 與 qtest.c 這兩個檔案的部分內容 * queue.c: 實際需要修改的地方是 merge sort 使用的 `merge`,將原先使用的 `strcasecmp` 修改為 `strnatcmp` 即可 * qtest.c: 字串排序結果是在 `do_sort` 中驗證的,將原先使用的 `strcasecmp` 修改為 `strnatcmp` 即可 至此已將排序函式變更為 natural sort,然而編譯時 Cppcheck 會報錯,如下 ```shell natsort/strnatcmp.c:112:14: style: The scope of the variable 'ca' can be reduced. [variableScope] nat_char ca, cb; ^ natsort/strnatcmp.c:112:18: style: The scope of the variable 'cb' can be reduced. [variableScope] nat_char ca, cb; ^ natsort/strnatcmp.c:170:0: style: The function 'strnatcasecmp' is never used. [unusedFunction] ^ Fail to pass static analysis. ``` 直接觀察 strnatcmp.c 相關的部分,區域變數 ca 和 cb 的用途是用來儲存接下來要比較的字元,但由於整個比較的過程都不會 (也不該) 修改到原本的字串,因此其實不用多宣告這兩個區域變數,也就是捨棄原本先宣告 `ca = a[ai]` 再使用 ca 的作法,改為直接使用 a[ai] 即可,修改完成後就可以正常編譯了 ```c= static int strnatcmp0(nat_char const *a, nat_char const *b, int fold_case) { int ai = 0; int bi = 0; int fractional, result; while (1) { /* skip over leading spaces or zeros */ while (nat_isspace(a[ai])) ++ai; while (nat_isspace(b[bi])) ++bi; /* process run of digits */ if (nat_isdigit(a[ai]) && nat_isdigit(b[bi])) { fractional = (a[ai] == '0' || b[bi] == '0'); if (fractional) { if ((result = compare_left(a + ai, b + bi)) != 0) return result; } else { if ((result = compare_right(a + ai, b + bi)) != 0) return result; } } if (!a[ai] && !b[bi]) { /* The strings compare the same. Perhaps the caller will want to call strcmp to break the tie. */ return 0; } if (fold_case) { if (nat_toupper(a[ai]) < nat_toupper(b[bi])) return -1; if (nat_toupper(a[ai]) > nat_toupper(b[bi])) return +1; } else { if (a[ai] < b[bi]) return -1; if (a[ai] > b[bi]) return +1; } ++ai; ++bi; } } ``` 最後是驗證的部分,原先在測試 `q_sort` 正確性的 trace files 中,無論是使用 `strcasecmp` 或是 `strnatcmp` 都會給出一樣的結果,因此根據 natural sort 提供的測試排序文件 [test-word](https://github.com/sourcefrog/natsort/blob/master/test-words) 來寫一個專門測試 natural sort 是否正確運作的 [traces/trace-18-natsort.cmd](https://github.com/KYG-yaya573142/lab0-c/blob/master/traces/trace-18-natsort.cmd),實測結果也顯示正確 ``` strcasecmp strnatcmp 1-02 | 1-02 1-2 | 1-2 1-20 | 1-20 10-20 | 10-20 fred | fred jane | jane pic01 | pic01 pic02 | pic02 pic02000 | pic02a pic02a | pic02000 pic05 | pic05 pic100 | pic2 pic100a | pic3 pic120 | pic4 pic121 | pic100 pic2 | pic100a pic3 | pic120 pic4 | pic121 tom | tom x2-g8 | x2-g8 x2-y08 | x2-y08 x2-y7 | x2-y7 x8-y8 | x8-y8 ``` ## 以 Valgrind 分析記憶體問題 先前使用遞迴的方式 merge 時遇到的問題一樣可以用 valgrind 分析,錯誤訊息明確的指出 stack overflow ``` +++ TESTING trace trace-15-perf: # Test performance of insert_tail, size, reverse, and sort ==27612== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==27612== Stack overflow in thread #1: can't grow stack to 0x1ffe801000 ==27612== Can't extend stack to 0x1ffe8010a8 during signal delivery for thread 1: ==27612== no stack segment ==27612== ==27612== Process terminating with default action of signal 11 (SIGSEGV) ``` ## Valgrind - Massif ### 使用介紹 [Massif](https://valgrind.org/docs/manual/ms-manual.html) 可以分析程式執行過程 heap 的使用狀態,使用的方法為 ```shell $ valgrind --tool=massif ./qtest ``` 以本作業來說,也可以選擇製作或使用已有的 trace file 來進行分析 ```shell $ valgrind --tool=massif ./qtest -f trace ``` 註:最初使用時會報錯 `Unknown option: --show-leak-kinds=all`,將 .valgrindrc 裡面 `--show-leak-kinds=all` 刪掉即可 (.valgrindrc 裡面紀錄的是預設使用的參數) 分析結果預設存於同目錄下的 massif.out.\<pid\> 檔案中 (可以使用參數 `--massif-out-file` 指定不同的檔案),得到分析結果後,可以進一步將其圖像化 ```shell $ ms_print massif.out.<pid> ``` 圖像中的 `:` 代表一般記錄點、`@` 代表詳細記錄點、`#` 代表峰值位置,y 軸為 heap 使用量,x 軸預設為 instruction 的執行數量,可使用參數 `--time-unit=<i|ms|B>` 更改 ``` --time-unit=<i|ms|B> [default: i] The time unit used for the profiling. There are three possibilities: 1. instructions executed (i), which is good for most cases; 2. real (wallclock) time (ms, i.e. milliseconds), which is sometimes useful; 3. bytes allocated/deallocated on the heap and/or stack (B), which is useful for very short-run programs, and for testing purposes, because it is the most reproducible across different machines. ``` Massif 可配合不同的參數調整分析的方式,詳見 [Manual](https://valgrind.org/docs/manual/valgrind_manual.pdf) ### 實測 heap 使用量 接下來分別以 `--time-unit=i`(預設) 與 `--time-unit=B` 兩種參數來分析以下自訂指令 ``` new ih head 100000 it tail 100000 reverse sort size quit ``` 觀察 `--time-unit=i` 的分析結果,可以看到程式的過程一共約執行了 429.1 Mi,然而峰值在位置 83.6 Mi 時就出現了,這是因為我們執行的指令實際上只有 `ih head 100000` 和 `it tail 100000` 會大量增加 heap 的使用量,其餘的 `reverse` `sort` `quit` 主要改變的是 stack 而不是 heap,這也是為何到達峰值後,heap 的使用量幾乎都維持一致直到程式結束 ``` $ ms_print massif.out.32595 -------------------------------------------------------------------------------- Command: ./qtest Massif arguments: (none) ms_print arguments: massif.out.32595 -------------------------------------------------------------------------------- MB 24.42^ # | :#::::::::::::::::::::::::::::::::::::::::::::::::::::: | ::#: :: | ::#: ::: | :::#: ::: | @:::#: ::: | @:::#: ::: | :@:::#: :::: | :@:::#: :::: | ::@:::#: ::::: | @::@:::#: ::::: | :@::@:::#: ::::: | :@::@:::#: ::::: | ::@::@:::#: ::::: | :::@::@:::#: :::::: | @::@::@:::#: :::::: | :@::@::@:::#: :::::: | :@::@::@:::#: ::::::: | ::@::@::@:::#: ::::::: |:::@::@::@:::#: ::::::: 0 +----------------------------------------------------------------------->Mi 0 429.1 Number of snapshots: 53 Detailed snapshots: [8, 9, 17, 26, 34 (peak)] ``` 觀察 `--time-unit=B` 的分析結果,可以發現 heap 先線性成長到峰值,接著再線性下降直到程式結束,這是因為 `ih head 100000` 和 `it tail 100000` 實質上就是重複 100000 次相同指令,所以是線性成長,接著 `quit` 時會使用 `q_free` 將記憶體逐一釋放,所以是線性下降 另外,原先 `--time-unit=i` 的分析結果在峰值後會出現一段 heap 用量幾乎沒有變動的區域,這個區域在改用 `--time-unit=B` 分析後會直接消失,這是因為此時 x 座標是 heap 的使用/釋放量,既然 heap 用量沒有改變,x 座標就不會移動 ``` $ ms_print massif.out.32584 -------------------------------------------------------------------------------- Command: ./qtest Massif arguments: --time-unit=B ms_print arguments: massif.out.32584 -------------------------------------------------------------------------------- MB 24.42^ # | ::#:: | :::#: : | :::::#: ::: | :@: :::#: :: : | :::@: :::#: :: :::: | :: :@: :::#: :: :::::: | :::: :@: :::#: :: :::::@: | ::::: :@: :::#: :: :::::@::: | :::::::: :@: :::#: :: :::::@::::: | :::: ::::: :@: :::#: :: :::::@:::::: | :: :: ::::: :@: :::#: :: :::::@::::::::: | ::::: :: ::::: :@: :::#: :: :::::@::::::::::: | :: ::: :: ::::: :@: :::#: :: :::::@:::::::::::: | :::: ::: :: ::::: :@: :::#: :: :::::@:::::::::::::@ | :: :: ::: :: ::::: :@: :::#: :: :::::@:::::::::::::@: | ::::: :: ::: :: ::::: :@: :::#: :: :::::@:::::::::::::@:::: | :::: :: :: ::: :: ::::: :@: :::#: :: :::::@:::::::::::::@:::::: | :: :: :: :: ::: :: ::::: :@: :::#: :: :::::@:::::::::::::@::::::@ | :::: :: :: :: ::: :: ::::: :@: :::#: :: :::::@:::::::::::::@::::::@:: 0 +----------------------------------------------------------------------->MB 0 48.47 Number of snapshots: 71 Detailed snapshots: [1, 23, 28 (peak), 38, 55, 65] ``` Massif 的結果會顯示峰值所在的 snapshot,分別是兩種分析結果的 34 及 28 這兩筆,挑選出來後直接比對,可以發現 heap 的使用量確實是一模一樣的,差別僅在 x 軸的不同 ``` --time-unit=i -------------------------------------------------------------------------------- n time(i) total(B) useful-heap(B) extra-heap(B) stacks(B) -------------------------------------------------------------------------------- 34 83,560,433 25,610,648 20,210,167 5,400,481 0 --time-unit=B -------------------------------------------------------------------------------- n time(B) total(B) useful-heap(B) extra-heap(B) stacks(B) -------------------------------------------------------------------------------- 28 25,611,112 25,610,648 20,210,167 5,400,481 0 ``` Massif 在特定的 snapshot 會做詳細的紀錄 (同時也是圖像上的 `@` 與 `#` 所在的位置),其中 heap 的總量可再被分為兩種 * useful-heap 程式執行時向系統請求的 heap 量 * extra-heap heap 用來記錄管理資訊的空間 (header 與 footer)、以及為了alignment 而增加的空間,這兩個也就是所謂的 internal fragmentation 挑選峰值的紀錄觀察,可以發現只有 78.91% 的 heap 使用量是 useful-heap,這代表 internal fragmentation 相對明顯 接著看下方的樹狀圖,先觀察前兩階的數據,可以發現 `q_insert_head` 與 `q_insert_tail` 都使用了 21.87% 與 17.57% 的用量,而它們的用量加起來幾乎就是 useful-heap 的總量,這符合前面觀察到的結果,因為整個測試過程中只有 `ih` 和 `it` 會新增 elements ``` -------------------------------------------------------------------------------- n time(B) total(B) useful-heap(B) extra-heap(B) stacks(B) -------------------------------------------------------------------------------- 26 24,457,704 24,457,240 19,300,056 5,157,184 0 27 24,951,528 24,951,064 19,689,714 5,261,350 0 28 25,611,112 25,610,648 20,210,167 5,400,481 0 78.91% (20,210,167B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc. ->78.87% (20,200,064B) 0x10C6E9: test_malloc (harness.c:137) | ->21.87% (5,600,000B) 0x10CC56: q_insert_head (queue.c:58) | | ->21.87% (5,600,000B) 0x10A77F: do_insert_head (qtest.c:213) | | ->21.87% (5,600,000B) 0x10B8E8: interpret_cmda (console.c:220) | | ->21.87% (5,600,000B) 0x10BE5C: interpret_cmd (console.c:243) | | ->21.87% (5,600,000B) 0x10C5CB: cmd_select (console.c:609) | | ->21.87% (5,600,000B) 0x10C672: run_console (console.c:630) | | ->21.87% (5,600,000B) 0x10AD97: main (qtest.c:771) | | | ->21.87% (5,600,000B) 0x10CCF5: q_insert_tail (queue.c:89) | | ->21.87% (5,600,000B) 0x10A4F8: do_insert_tail (qtest.c:298) | | ->21.87% (5,600,000B) 0x10B8E8: interpret_cmda (console.c:220) | | ->21.87% (5,600,000B) 0x10BE5C: interpret_cmd (console.c:243) | | ->21.87% (5,600,000B) 0x10C5CB: cmd_select (console.c:609) | | ->21.87% (5,600,000B) 0x10C672: run_console (console.c:630) | | ->21.87% (5,600,000B) 0x10AD97: main (qtest.c:771) | | | ->17.57% (4,500,000B) 0x10CC7C: q_insert_head (queue.c:63) | | ->17.57% (4,500,000B) 0x10A77F: do_insert_head (qtest.c:213) | | ->17.57% (4,500,000B) 0x10B8E8: interpret_cmda (console.c:220) | | ->17.57% (4,500,000B) 0x10BE5C: interpret_cmd (console.c:243) | | ->17.57% (4,500,000B) 0x10C5CB: cmd_select (console.c:609) | | ->17.57% (4,500,000B) 0x10C672: run_console (console.c:630) | | ->17.57% (4,500,000B) 0x10AD97: main (qtest.c:771) | | | ->17.57% (4,500,000B) 0x10CD1B: q_insert_tail (queue.c:94) | | ->17.57% (4,500,000B) 0x10A4F8: do_insert_tail (qtest.c:298) | | ->17.57% (4,500,000B) 0x10B8E8: interpret_cmda (console.c:220) | | ->17.57% (4,500,000B) 0x10BE5C: interpret_cmd (console.c:243) | | ->17.57% (4,500,000B) 0x10C5CB: cmd_select (console.c:609) | | ->17.57% (4,500,000B) 0x10C672: run_console (console.c:630) | | ->17.57% (4,500,000B) 0x10AD97: main (qtest.c:771) | | | ->00.00% (64B) in 1+ places, all below ms_print's threshold (01.00%) | ->00.04% (10,103B) in 1+ places, all below ms_print's threshold (01.00%) ``` ### 記憶體對齊 從 [你所不知道的 C 語言:記憶體管理、對齊及硬體特性](https://hackmd.io/@sysprog/c-memory?type=view) 可以知道在 64-bit 的系統下,malloc 會以 16 bytes 進行對齊,而原先 heap block 用來記錄管理資訊的空間 header 與 footer 會共佔 8 bytes,也就是說如果 elements 中儲存的字串低於 8 個字 (含 null terminator),一律會因為 alignment 要求而實際 malloc 8 bytes,同理當字串長度為 9~24 個字時皆會 malloc 24 bytes 這個現象也可以用 massif 觀察,使用以下的指令進行實驗,主要目的是驗證使用字串為 "h" 與 "t" 時的 heap 使用量其實與字串為 "head" 與 "tail" 時一樣 ``` new ih h 100000 it t 100000 free new ih head 100000 it tail 100000 quit ``` 從結果可以看到 heap 總使用量根本一樣,而字串為 "head" 與 "tail" 時,因為程式實際索取的記憶體大小較大,產生的 useful-heap 也確實比較高 ``` string: "h", "t" -------------------------------------------------------------------------------- n time(B) total(B) useful-heap(B) extra-heap(B) stacks(B) -------------------------------------------------------------------------------- 18 25,611,112 25,610,648 19,610,164 6,000,484 0 76.57% (19,610,164B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc. string: "head", "tail" -------------------------------------------------------------------------------- n time(B) total(B) useful-heap(B) extra-heap(B) stacks(B) -------------------------------------------------------------------------------- 28 25,611,112 25,610,648 20,210,167 5,400,481 0 78.91% (20,210,167B) (heap allocation functions) malloc/new/new[], --alloc-fns, etc. ``` 最後再追加一個比較,目的是呈現當字串長度為 9~24 個字時會 malloc 24 bytes 註:massif 沒辦法同時抓取複數峰值,因此只呈現圖像結果 ``` new ih h 100000 it t 100000 free new ih head 100000 it tail 100000 free new ih headhead 100000 it tailtail 100000 quit ``` 結果很明顯,字串長度 8 以內 heap 的使用量都一樣,字串長度一到 9 bytes 就能觀察到 heap 使用量提升 ``` MB 27.48^ # | # | :# | @ :: ::#: | :@: :: ::#:: | :@: :: : :::#:: | @:@::: :: : @:::#::: | @:@:: :::: :: @:::#:::: | :@:@:: @ : :: ::: :@:::#:::: | :@:@:: @ :: :: ::: ::@:::#::::: | :::@:@:: @: :: :: :::: ::@:::#:::::: | : :@:@:: @:: ::: :: ::::: :::@:::#:::::@ | :: :@:@:: @:: ::: :: :::::@ ::::@:::#:::::@: | :: :@:@:: @::: :::: :: :::::@ :::::@:::#:::::@:: | ::: :@:@:: @::: ::::: :: :::::@: :::::@:::#:::::@:: | :::: :@:@:: @::::: :::::: :: :::::@ @:::::@:::#:::::@::: | :::: :@:@:: @:::: ::::::: :: :::::@ : @:::::@:::#:::::@:::: | :::: :@:@:: @:::: ::::::: :: :::::@ : @:::::@:::#:::::@:::: | :::::: :@:@:: @:::: : :::::::: :: :::::@ :: :@:::::@:::#:::::@::::: |:: :::: :@:@:: @:::: :: @::::::: :: :::::@ ::@ ::@:::::@:::#:::::@:::::: 0 +----------------------------------------------------------------------->MB 0 152.0 ``` ## 分析 Makefile 閱覽 Makefile 有助於了解整個程式的架構,然而點開 Makefile 後發現~~根本看不懂~~需要花點時間來理解內容,以下會分段逐步理解 Makefile 的內容,許多用法在 [Makefile 語法和示範](https://hackmd.io/@sysprog/SySTMXPvl) 中已經有詳細示範,將不再重複說明 [完整的 Makefile 請參照 Github](https://github.com/KYG-yaya573142/lab0-c/blob/master/Makefile) * 首次執行 `make` 會自動安裝 git hook: 直接 `make` 預設會執行第一個 target,也就是 `all` 這個項目,所以除了編譯 `qtest` 之外,還會執行安裝 git hook 的腳本 ``` all: $(GIT_HOOKS) qtest $(GIT_HOOKS): @scripts/install-git-hooks @echo ``` * `make VERBOSE=1` 可以顯示詳細編譯訊息 * `@` 的作用是該行的指令不會被顯示 * `@true` 則追加該行指令直接被視為成功的作用 * 因此 `" LD\t$@\n"` 這行如果前面從 `@printf` 改為 `@` 會出錯,需要追加 `true` 讓這行直接視為執行成功 (這個例來說就是該行直接無效但不出錯) ``` # Control the build verbosity ifeq ("$(VERBOSE)","1") Q := VECHO = @true else Q := @ VECHO = @printf endif qtest: $(OBJS) $(VECHO) " LD\t$@\n" $(Q)$(CC) $(LDFLAGS) -o $@ $^ -lm ``` * Dependency 的部分相當有趣,查資料後發現本作業中使用類似 [Auto-Dependency Generation](http://make.mad-scientist.net/papers/advanced-auto-dependency-generation/) 的方法來處理 dependency 的問題 * `deps := $(OBJS:%.o=.%.o.d)` 使用自動變數替換,也就是每個 dependency 檔案名稱都是物件檔結尾加上 .d * `-MMD` 代表僅考慮 user header files 不包含 system head files,`-MF` 是指定輸出 dependency 檔案的名稱 * `-include $(deps)` 須放置在檔案的最後,告訴 `make` 需要 dependency 檔案的名稱,前置的 `-` 代表就算執行失敗也不強制結束 `make` 的執行 ``` OBJS := qtest.o report.o console.o harness.o queue.o \ random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \ deps := $(OBJS:%.o=.%.o.d) %.o: %.c @mkdir -p .$(DUT_DIR) $(VECHO) " CC\t$@\n" $(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $< -include $(deps) ``` * sanitizer 的支援 * 很單純的加入相關的編譯參數 ``` # Enable sanitizer(s) or not ifeq ("$(SANITIZER)","1") # https://github.com/google/sanitizers/wiki/AddressSanitizerFlags CFLAGS += -fsanitize=address -fno-omit-frame-pointer -fno-common LDFLAGS += -fsanitize=address endif ``` * valgrind 的支援 * `2>&1` 指的是將 stderr 的輸出重新導向到 stdout * `/dev/null` 概念類似黑洞,進去的東西都會消失 * `||` 代表若左邊執行出錯,就執行右邊的內容 * 因此 `valgrind_existence` 執行的意思就是 "檢查 valgrind 版本 (但無論成功失敗都不列出結果),如果檢查失敗就顯示右邊的錯誤訊息" * `$(MAKE)` 代表將這行當作 make 來執行,詳見 [Manual](https://www.gnu.org/software/make/manual/html_node/MAKE-Variable.html) * `$(eval expression)` 會先將 expression 展開,接著再將 expression 的內容視為 Makefile 的一部分並執行,與直接將 expression 寫在 Makefile 的差別為,後面的 shell 指令 `mktemp` 只有在 `make valgrind` 時才會執行並取得檔案名稱,詳見 [Manual](https://www.gnu.org/software/make/manual/html_node/Eval-Function.html) * `sed -i "s/alarm/isnan/g" $(patched_file)`,其中 `s/alarm/isnan/g` 不是路徑,是 `sed` 用來替換內容的表示法,這裡的意思是 "將 `$(patched_file)` 中的所有的 alarm 替換成 iasnan",詳見 [Manual](https://www.gnu.org/software/sed/manual/html_node/The-_0022s_0022-Command.html) * 由於 valgrind 會大幅度的增加程式的執行時間,將 `alarm` 去除可以避免在 `qtest` 中設置 timer,去除原先對執行時間的限制 * 相關的語法可以參閱 [sed - stream editor](http://manpages.ubuntu.com/manpages/precise/en/man1/plan9-sed.1.html) 以及 [the s command](https://www.gnu.org/software/sed/manual/html_node/The-_0022s_0022-Command.html) (感謝 [Randy870819](https://hackmd.io/@randy870819/system-prog-lab0) 提供) ``` valgrind_existence: @which valgrind 2>&1 > /dev/null || (echo "FATAL: valgrind not found"; exit 1) valgrind: valgrind_existence # Explicitly disable sanitizer(s) $(MAKE) clean SANITIZER=0 qtest $(eval patched_file := $(shell mktemp /tmp/qtest.XXXXXX)) cp qtest $(patched_file) chmod u+x $(patched_file) sed -i "s/alarm/isnan/g" $(patched_file) scripts/driver.py -p $(patched_file) --valgrind @echo @echo "Test with specific case by running command:" @echo "scripts/driver.py -p $(patched_file) --valgrind -t <tid>" ``` ## [dudect](https://github.com/oreparaz/dudect) dudect 以兩種不同類別的資料來測試程式的執行時間,再以統計的方式分析兩者的執行時間的平均值是否有差異,若無法證明平均值有差異,則程式的執行時間可能為常數 (constant time) dudect 的優點是不需要執行平台的硬體資訊 (這也是本題可以使用虛擬機跑的原因之一)。執行的過程可以分為三個步驟 1. 量測執行時間 使用的資料分為固定資料及隨機資料兩種 (fix-vs-random test),同時為了降低環境因素的影響,每次量測選定的資料種類也是隨機的,執行時間則是根據 CPU cycle 來計算 3. 後處理 每次量測時間後,可以在統計分析前先進行後處理,例如去除一定數值以外的資料 (cropping),來避免因程式執行過程中被系統插斷造成的正偏差。另外,可以根據後面想執行的統計分析,先在此步驟先進行資料處理 5. 統計分析 dudect 使用 [Welch's t-test](https://en.wikipedia.org/wiki/Welch's_t-test) 來分析兩種資料的結果,如果檢定結果無法否定 "兩種資料具有相同的平均數" 這個零假設 (null hypothesis),就代表兩種資料很有可能具有一樣的平均數,也就是程式執行時間很可能是常數 #### t-test * t-test 是一種假說檢定 (statistical hypothesis test),指零假設 (null hypothesis) 成立時的任一檢定統計會具有 Student’s t-distribution * Student's t-test 和 Welch's t-test 皆為雙樣本的 t-test 應用,Student's t-test 同時假設兩個母體 (population) 具有相同的變異數,Welch's t-test 則無此假設 * 本作業中使用的是 Welch's t-test,因為我們沒辦法得知母體的變異數 #### [Student’s t-distribution](https://zh.wikipedia.org/wiki/%E5%AD%A6%E7%94%9Ft-%E5%88%86%E5%B8%83#%E5%AD%A6%E7%94%9Ft-%E5%88%86%E5%B8%83%E7%BD%AE%E4%BF%A1%E5%8C%BA%E9%97%B4%E7%9A%84%E6%8E%A8%E5%AF%BC) * 用於根據小樣本來估計 "呈常態分布且變異數未知的總體" 的平均值 * 自由度 $\nu$ 越大,t-distribution 越接近常態分佈 * 也簡稱為 t-distribution ### 分析本作業中如何實作 dudect 從 qtest.c 裡面可以看到使用 `is_insert_tail_const` 與 `is_size_const` 來判斷,其函數的定義在 dudect/fixture.c 內,由於整個程式的架構非常複雜,現階段先釐清整體實作的架構,許多輔助函數的實作之後再探討 #### `is_size_const` ```c= bool is_size_const(void) { bool result = false; t = malloc(sizeof(t_ctx)); for (int cnt = 0; cnt < test_tries; ++cnt) { printf("Testing size...(%d/%d)\n\n", cnt, test_tries); init_once(); for (int i = 0; i < enough_measurements / (number_measurements - drop_size * 2) + 1; ++i) result = doit(1); printf("\033[A\033[2K\033[A\033[2K"); if (result == true) break; } free(t); return result; } ``` * `t_ctx` 是一個儲存統計用資料的資料結構,相關的意義可以在後續的 `t-push` 中得知 * mean 是平均值 * m2 是離均差之合 (aggregates of squared distance from the mean) * n 是資料數量 ``` typedef struct { double mean[2]; double m2[2]; double n[2]; } t_ctx; ``` * 第一個迴圈 `for (int cnt = 0; cnt < test_tries; ++cnt)` 代表最多會進行 10 次測試,如果結果為真,會直接跳離迴圈 (也就是結果顯示為 constant time) * `init_once` 會將 `t` 的所有資料成員初始化為 `0.0`,`q` 初始化為 `NULL` * 實際的計算與量測在 `doit` 中實施 * `doit` 一次會進行 `(number_measurements - drop_size * 2)` 次的測試,而統計需求共需 `enough_measurements = 10000` 次測試數據 #### `doit` ```c= static bool doit(int mode) { int64_t *before_ticks = calloc(number_measurements + 1, sizeof(int64_t)); int64_t *after_ticks = calloc(number_measurements + 1, sizeof(int64_t)); int64_t *exec_times = calloc(number_measurements, sizeof(int64_t)); uint8_t *classes = calloc(number_measurements, sizeof(uint8_t)); uint8_t *input_data = calloc(number_measurements * 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(input_data, classes)` * 總共準備 `number_measurements = 150` 個資料 * classes 的內容只會是 0 和 1 ,用來區分兩種資料類別 * `input_data` 的每個成員大小為 `chunk_size * sizeof(uint8_t)`,也就是 16 bytes * class 0 的 `input_data` 的前兩個 bytes 會被設置為 0 * 在此函數內會一同隨機設置用來測試的字串 `static char random_string[100][8]` * 產生隨機數值的方法應該是從[這裡](https://bench.cr.yp.to/tips.html)偷過來的 * `measure` * `assert` 是一個保護函數 [(man assert)](http://man7.org/linux/man-pages/man3/assert.3.html) * 根據 `mode` 的數值來判斷是要分析 `q_size` 還是 `q_insert_tail` ,另外這裡有用到 anonymous enum 的技巧 `enum { test_insert_tail, test_size }` * 從 `for (size_t i = drop_size; i < number_measurements - drop_size; i++)` 可以看出實際使用的資料會從頭尾再扣除 `drop_size = 20`,查閱[原始 Github 專案](https://github.com/oreparaz/dudect/tree/master/src)並比對內容,這麼做的目的就是 dudect 步驟二的 cropping,只是本作業中有對 cropping 的時間點稍作修改 * 原始 Github 專案共使用 first order uncropped、cropped、second order uncropped 三種方式來計算最大 t 值,本作業中大幅度省略至只使用單一閥值來 cropping * 為了避免和主測試函數的內容混淆,同時維持測試環境的一致性,這邊會先使用 `dut_new` 與 `dut_insert_head` 做出測試用的 linked-list * 實際測試時間的函數被包在 `before_ticks[i] = cpucycles()` 與 `after_ticks[i] = cpucycles()` 之間,執行時間以 CPU clock cycles 當基準計算 * 由 `*(uint16_t *) (input_data + i * chunk_size) % 10000)` 可以看出字串插入的隨機數量是根據 `input_data` 的前兩個 bytes 決定,而且最多不超過 10000。同時由此可以看出 class 0 的數據實際上根本不會插入字串 * `differentiate` * 計算執行時間 `exec_times[i] = after_ticks[i] - before_ticks[i]` * `update_statistics` * 選定納入使用的數據,並呼叫 `t-push` 對其進行前處理 * CPU cycle counter overflowed 的資料與 `drop_size` 涵蓋的資料會在此被過濾掉 * `t-push` * 使用 [Welford's online algorithm](https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Welford's_online_algorithm) 計算實行 Welch's t-test 所需的資料,計算結果儲存於 `t` 指向的資料結構中 * `report` * 使用 `t_compute` 計算出 t 值,判斷是否在閥值範圍內並回傳判斷結果 #### `cpucycles` ```cpp= 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); #else #error Unsupported Architecture #endif } ``` * 執行時間以 CPU clock cycles 當基準計算 * 詳細方法可參閱 [Code Execution Times: IA-32/IA-64 Instruction Set Architecture](https://www.intel.com/content/www/us/en/embedded/training/ia-32-ia-64-benchmark-code-execution-paper.html) (現階段我還看不太懂 [asm](https://linux.die.net/man/3/inline_asm) 的用法) #### `t_compute` ```c= double t_compute(t_ctx *ctx) { double var[2] = {0.0, 0.0}; var[0] = ctx->m2[0] / (ctx->n[0] - 1); var[1] = ctx->m2[1] / (ctx->n[1] - 1); double num = (ctx->mean[0] - ctx->mean[1]); double den = sqrt(var[0] / ctx->n[0] + var[1] / ctx->n[1]); double t_value = num / den; return t_value; } ``` * 實際計算 [Welch’s t-test](https://en.wikipedia.org/wiki/Welch%27s_t-test) 的函式 * 計算檢定量 $t \quad = \quad {\; \overline{X}_1 - \overline{X}_2 \; \over \sqrt{ \; {s_1^2 \over N_1} \; + \; {s_2^2 \over N_2} \quad }}$ * $\bar{X}$ 為平均值,也就是 `ctx->mean` * $s^2$ 為標準差平方,也就是 `ctx->m2` * $N$ 為資料數量,也就是 `ctx->n` * 本作業直接以固定的 t 值 `t_threshold_moderate = 10` 進行雙尾檢定 (two-tail test),也就是判斷 t 的絕對值是否在接受範圍內,來判斷是否為 constant time * 閥值越大,[I 型錯誤](https://zh.wikipedia.org/wiki/%E7%AC%AC%E4%B8%80%E5%9E%8B%E5%8F%8A%E7%AC%AC%E4%BA%8C%E5%9E%8B%E9%8C%AF%E8%AA%A4) (執行時間被誤認為非常數) 的機率會降低,但 [II 型錯誤](https://zh.wikipedia.org/wiki/%E7%AC%AC%E4%B8%80%E5%9E%8B%E5%8F%8A%E7%AC%AC%E4%BA%8C%E5%9E%8B%E9%8C%AF%E8%AA%A4) (執行時間被誤認為常數) 的機率會增加 * 原先的 Welch’s t-test 需再算出自由度 $\nu$ 來獲得對應的 t-distribution,根據設定的信賴區間及 t-distribution 決定 t 的閥值,再進行雙尾檢定 $\nu \quad \approx \quad {{\left( \; {s_1^2 \over N_1} \; + \; {s_2^2 \over N_2} \; \right)^2 } \over{ \quad {s_1^4 \over N_1^2 \nu_1} \; + \; {s_2^4 \over N_2^2 \nu_2 } \quad }},\quad\nu_1 = N_1-1,\quad\nu_2 = N_2-1$ ## RIO 套件 (Robust I/O) [RIO 套件](http://csapp.cs.cmu.edu/2e/ch10-preview.pdf)是 CS:APP 中提供的一種 system I/O [wrapper](https://en.wikipedia.org/wiki/Wrapper_library) 套件,其內容建立於原本的 `read` 與 `write` 之上,原始內容可詳見 [csapp.c](http://csapp.cs.cmu.edu/3e/ics3/code/src/csapp.c) 與 [csapp.h](http://csapp.cs.cmu.edu/2e/ics2/code/include/csapp.h) RIO 套件具有以下特點 * 被 signal handler 中斷後 (會導致 [EINTR](http://man7.org/linux/man-pages/man2/read.2.html) 錯誤),會自動嘗試重新執行,直到遇到 EOF 或是達到指定的讀取大小 * 具有 application-level buffer,可大幅降低實際呼叫 system call 的次數,進而提升效能 * 可以方便的用於 socket * thread-safe (之後研讀 CS:APP 第 12 章會再探討並補充進來) RIO 套件的精隨在於 `rio_read` 以及對應的 rio 結構體 `rio_t` ```cpp #define RIO_BUFSIZE 8192 typedef struct { int rio_fd; /* Descriptor for this internal buf */ int rio_cnt; /* Unread bytes in internal buf */ char *rio_bufptr; /* Next unread byte in internal buf */ char rio_buf[RIO_BUFSIZE]; /* Internal buffer */ } rio_t; /* * rio_read - This is a wrapper for the Unix read() function that * transfers min(n, rio_cnt) bytes from an internal buffer to a user * buffer, where n is the number of bytes requested by the user and * rio_cnt is the number of unread bytes in the internal buffer. On * entry, rio_read() refills the internal buffer via a call to * read() if the internal buffer is empty. */ static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n) { int cnt; while (rp->rio_cnt <= 0) { /* Refill if buf is empty */ rp->rio_cnt = read(rp->rio_fd, rp->rio_buf, sizeof(rp->rio_buf)); if (rp->rio_cnt < 0) { if (errno != EINTR) /* Interrupted by sig handler return */ return -1; } else if (rp->rio_cnt == 0) /* EOF */ return 0; else rp->rio_bufptr = rp->rio_buf; /* Reset buffer ptr */ } /* Copy min(n, rp->rio_cnt) bytes from internal buf to user buf */ cnt = n; if (rp->rio_cnt < n) cnt = rp->rio_cnt; memcpy(usrbuf, rp->rio_bufptr, cnt); rp->rio_bufptr += cnt; rp->rio_cnt -= cnt; return cnt; } ``` * 在使用 RIO 套件前,會先建立並初始化一個 `rio_t` 物件 * `rio_read` 在被呼叫後,會先檢查 `rio_t` 物件中的 buffer 是否還有內容可以被使用 * 若有 - 直接讀取 `rio_buf` * 若無 - 呼叫 `read` 讀取最多至 8192 bytes 的內容至 `rio_buf` * 也就是說使用者呼叫 `rio_read` 取得的內容總是會從 `rio_buf` 取得,`rio_read` 同時會負責更新 `rio_buf` 的內容 ### `qtest` 使用 RIO 套件的考量點 `qtest` 使用 RIO 套件的優勢會體現於 trace files 的讀取,若使用無緩衝的 `read` 來讀取指令,需要重複以 `read` 讀取單個字元直到遇到 `\n` 為止,如此一來會因為頻繁的 system call 造成效能低落 改用 RIO 套件的方法後,會將整個 trace file 的內容存入 `rio_buf`,接著 `readline` 會從 `rio_buf` 內讀出指令來執行,只有當 `rio_buf` 內容都使用完畢 (也就是跑完整個 trace file 的內容),才會再次將下一個 trace file 的內容完整存入 `rio_buf` ### `qtest` 實際使用 RIO 套件的方式 觀察 qtest.c 及 console.c 可歸納出整體的呼叫順序,`qtest` 首先會在 `run_console` 初始化 `rio_t` 物件的空間,接著 `cmd_select` 會負責從 `rio_buf` 取得輸入的字串並交給後續的 `interpret_cmd` 執行指令,`cmd_select` 中的 `readline` 則是實際讀取與更新 buffer 的函數 (等同於 RIO 套件的 `rio_read`) ``` main → run_console → cmd_select → interpret_cmd → parse_args ``` #### `run_console` ```cpp bool run_console(char *infile_name) { if (!push_file(infile_name)) { report(1, "ERROR: Could not open source file '%s'", infile_name); return false; } while (!cmd_done()) cmd_select(0, NULL, NULL, NULL, NULL); return err_cnt == 0; } static bool push_file(char *fname) { int fd = fname ? open(fname, O_RDONLY) : STDIN_FILENO; if (fd < 0) return false; if (fd > fd_max) fd_max = fd; rio_ptr rnew = malloc_or_fail(sizeof(rio_t), "push_file"); rnew->fd = fd; rnew->cnt = 0; rnew->bufptr = rnew->buf; rnew->prev = buf_stack; buf_stack = rnew; return true; } ``` * `rio_t` 物件在 `push_file` 內產生,但此時 `rio_buf` 還沒有內容 * 最後將產生的物件以 linked list 的方式插入 `buf_stack` 所指向 list * 觀察 console.c 中定義的 `rio_t` 資料結構以及 `buf_stack` 指標的使用方式,可以發現 console 將每個 `rio_t` 物件以 stack 的邏輯在管理,這也是為何新增及刪除 `rio_t` 物件的函式名稱分別叫做 `push_file` 及 `pop_file` ```cpp #define RIO_BUFSIZE 8192 typedef struct RIO_ELE rio_t, *rio_ptr; struct RIO_ELE { int fd; /* File descriptor */ int cnt; /* Unread bytes in internal buffer */ char *bufptr; /* Next unread byte in internal buffer */ char buf[RIO_BUFSIZE]; /* Internal buffer */ rio_ptr prev; /* Next element in stack */ }; static rio_ptr buf_stack; ``` #### `cmd_select` 流程略為複雜,直接以 flowchart 呈現其運作邏輯,`select` 的部分可參閱[下個章節的討論](#解釋-select-系統呼叫在本程式的使用方式) ```flow st=>start: cmd_select cond1=>condition: read_ready: complete line in rio_buf? cond2=>condition: cmd_done: buf_stack? sub1=>subroutine: readline (from rio_buf) sub2=>subroutine: readline (from file) op1=>operation: select op2=>operation: interpret_cmd op3=>operation: interpret_cmd op4=>operation: interpret_cmd e=>end: end st->cond1 cond1(yes)->sub1->op4(left)->cond1 cond1(no)->cond2(yes)->op1->sub2->op2->e cond2(no)->e ``` * `interpret_cmd` 會呼叫 `parse_args` 來將 `readline` 回傳的內容解析為指令及參數,最後根據指令及參數呼叫 `interpret_cmda` 來執行該指令內容 * 執行命令的部分,[作業導讀](https://hackmd.io/@sysprog/linux2020-lab0#qtest-%E5%91%BD%E4%BB%A4%E7%9B%B4%E8%AD%AF%E5%99%A8%E7%9A%84%E5%AF%A6%E4%BD%9C)的部分已經說明得很清楚了 #### `readline` ```flow st=>start: readline cond1=>condition: rio_buf? cond2=>condition: rio_buf? cond3=>condition: \n? op1=>operation: read: update rio_buf op2=>operation: read a char from rio_buf op3=>operation: test op4=>operation: EOF: pop_file e=>end: end st(right)->cond1 cond1(no, bottom)->op1->cond2 cond1(yes, right)->op2(right)->cond3 cond3(no)->cond1 cond3(yes)->e cond2(no)->op4->e cond2(yes)->op2 ``` * 邏輯類似 RIO 套件的 `rio_read` * 與 RIO 套件不同的地方是,被 signal 中斷而未讀取任何資料時 (`EINTR`),**不會**自動重新執行 `read`,而會被判斷成 `EOF` ## 解釋 `select` 系統呼叫在本程式的使用方式 ```cpp int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); ``` `fd_set` 是一種儲存 file descriptor (以下簡稱為 fd) 編號的集合,例如 fd 集合為 {0, 2, 7} 的 `fd_set` 以 binary 顯示會是 10000101。`fd_set` 須配合下面幾種 macro 來管理 * `FD_ZERO(fd_set *set)` - 清空 set 內容 * `FD_SET(int fd, fd_set *set)` - 新增 fd 至 set * `FD_CLR(int fd, fd_set *set)` - 將 fd 從 set 移除 * `FD_ISSET(int fd, fd_set *set)` - 確認 fd 是否可用 (在 `select` 返回後使用) `select` 會暫停程式並監測 `fd_set` 清單中紀錄的 fd,直到其中至少有一個 fd 變成可使用的狀態,返回 "可用 fd 的數量",並將相對應的 `fd_set` 清單內容修改為 "可用 fd 清單" (因此每次呼叫 `select` 前都須 `FD_ZERO` 初始化清單) * 監測至最大編號為 nfds 的 fd * 監測 readfds 中紀錄的 fd 是否可讀 * 監測 writefds 中紀錄的 fd 是否可寫 * 監測 exceptfds 中紀錄的 fd 是否有特殊狀態 * timeout 可以設定 `select` 要等待多久 [(詳見 man select)](http://man7.org/linux/man-pages/man2/select.2.html) 本作業中 `select` 使用在 `cmd_select` 內,當 `read_ready` 確認 `rio_buf` 無內容可用後,`select` 會暫停程式直到以下兩種情況發生 * 使用者輸入指令 (具體來說是按下的 ENTER 鍵) * trace file 可被讀取 (**想不到哪種情況會產生這個情況**) 最後 `readline` 才會根據 `select` 的結果決定是否能從 fd 讀取資料 ### `select` 於本作業中的必要性 以下列程式碼進行實驗 ```cpp int main(int argc, char *argv[]) { int fds = STDIN_FILENO; int nfds = fds + 1; int result = 0; fd_set readfds; char line[10]; memset(line, 0x0, 10); while (1) { FD_ZERO(&readfds); FD_SET(fds, &readfds); select(nfds, &readfds, NULL, NULL, NULL); if (FD_ISSET(fds, &readfds)) { result = read(fds, line, 10); line[result] = '\0'; printf("input: %s", line); } } } ``` 執行結果如下 ```shell test input: test hello input: hello ``` 但其實如果把 `select` 的部份去掉,輸出結果是一樣的,差別是 * 使用 `select` 時,程式會在 `select` 等待輸入,輸入後 `read` 會直接讀取剛剛輸入的數值 * 單獨使用 `read` 時,程式會在 `read` 等待,直到鍵入 ENTER 才會結束 同時我還進行了另一個實驗,目的是想要知道如果 `fork` 後 parent 和 child 都使用 `read` 來讀取輸入,會不會造成 race condition。測試的方式是執行程式後,等待一秒以上再輸入文字 (因為 parent 會先 `sleep(1)`),此時 parent 與 child 皆執行到 `read` 的位置,且 child 比 parent 先執行 `read` ```cpp #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/select.h> #include <sys/types.h> #include <unistd.h> int main(int argc, char *argv[]) { int fds = STDIN_FILENO; int nfds = fds + 1; int result = 0; pid_t pid; fd_set readfds; char line[10]; memset(line, 0x0, 10); if ((pid = fork()) == 0) { /* child runs the job */ FD_ZERO(&readfds); FD_SET(fds, &readfds); result = read(fds, line, 10); line[result] = '\0'; printf("child input: %s", line); } else { /* parent */ sleep(1); FD_ZERO(&readfds); FD_SET(fds, &readfds); result = read(fds, line, 10); line[result] = '\0'; printf("parent input: %s", line); } } ``` 執行結果如下 ``` child child input: child parent parent input: parent ``` 因此就算同時有兩個程式對同個 file descriptor 進行 `read`,不使用 `select` 仍不會產生混淆的狀態 根據上面的結果,我認為 `qtest` 中無需使用 `select`,單獨使用 `read` 也能達到一樣的結果 ### 移除 qtest 中的 `select` 直接修改 console.c:598 將 `select` 的部分移除,測試結果發現無論是 `make test` 的分數,或是手動輸入指令測試,程式執行結果都找不到差別 ```cpp=598 int result = 1; /* remove the usage of select() */ if (result <= 0) return result; infd = buf_stack->fd; if (1) { /* Commandline input available */ result--; cmdline = readline(); if (cmdline) interpret_cmd(cmdline); } return result; ``` ### 使用 `select` 的情境 在 [GNU - Waiting for Input or Output](https://www.gnu.org/software/libc/manual/html_node/Waiting-for-I_002fO.html) 中有提到 `select` 可以使用的情境,這同時可以說明為何 qtest 中不須使用 `select`,因為輸入端只有一個鍵盤,如果有額外的輸入端才必須使用 `select` > **Sometimes a program needs to accept input on multiple input channels** whenever input arrives. For example, some workstations may have devices such as a digitizing tablet, function button box, or dial box that are connected via normal asynchronous serial interfaces. > You cannot normally use read for this purpose, because this blocks the program until input is available on one particular file descriptor; input on other channels won’t wake it up. You could set nonblocking mode and poll each file descriptor in turn, but this is very inefficient. > A better solution is to use the select function. This blocks the program until input or output is ready on a specified set of file descriptors, or until a timer expires, whichever comes first. ## 使用 [antirez/linenoise](https://github.com/antirez/linenoise) 強化 `qtest` 命令列功能 ### linenoise 使用說明 linenoise 的 API 非常簡單,閱覽 [Github 的範例](https://github.com/antirez/linenoise/blob/master/example.c)就可以非常快上手 * 主要的 API,會將 `prompt` 的內容列出在 terminal,並把處理過後的輸入以字串的型態回傳 * 回傳的字串使用過後需 `free` ```cpp char *linenoise(const char *prompt); ``` * 多行編輯功能 * `linenoiseSetMultiLine(0)` 為單行編輯 * `linenoiseSetMultiLine(1)` 為多行編輯 ```cpp linenoiseSetMultiLine(1); ``` * 歷史紀錄功能,讓使用者可以用上下鍵使用曾經輸入過的內容 * `linenoiseHistoryAdd` 將字串紀錄到歷史紀錄中 * `linenoiseHistorySetMaxLen` 可以設置歷史紀錄最大的長度 * `linenoiseHistoryLoad` 開啟儲存歷史紀錄的檔案,儲存歷史紀錄於檔案前須先開啟該檔案 * `linenoiseHistorySave` 儲存歷史紀錄於檔案 ```cpp int linenoiseHistoryAdd(const char *line); int linenoiseHistorySetMaxLen(int len); int linenoiseHistoryLoad(const char *filename); int linenoiseHistorySave(const char *filename); ``` * 命令自動補完,使用者可以按下 `<TAB>` 鍵來根據已輸入的部分自動補完命令 * 在使用前需先以 `linenoiseSetCompletionCallback(completion)` 註冊 callback funtion * 被註冊為 callback 的函式有規定的格式,同時內容包含自動補完的判斷依據,如下 ```cpp void completion(const char *buf, linenoiseCompletions *lc) { if (buf[0] == 'h') { linenoiseAddCompletion(lc,"hello"); linenoiseAddCompletion(lc,"hello there"); } } ``` ### 整合並應用 linenoise #### 將 linenoise.[ch] 納入lab0 會特別提到這點是因為 git hook 的所有設定也會套用到 linenoise,也就是說執行 `git commit` 前會先對 linenoise.[ch] 做 clang-format 和 cppcheck,前者沒什麼問題,直接 `clang-format -i linenoise.[ch]` 就可以解決,後者則會需要對原始碼進行修改,為了節省時間我選擇直接修改腳本讓 cppcheck 忽略 linenoise.[ch],scripts/pre-commit.hook 的修改內容如下 ``` ... CPPCHECK_IGNORE="-ilinenoise/" ... # static analysis $CPPCHECK $CPPCHECK_IGNORE $CPPCHECK_OPTS >/dev/null if [ $? -ne 0 ]; then ... ``` #### 在 qtest 中使用 linenoise 原本的 `cmd_select` 會先 `select` 確認 fd 是否可用,然後才使用 `readline` 讀取指令。如果直接改成使用 `select` + `linenoise` 會導致指令需要輸入兩次 (準確來說是第一次的輸入會被 `linenoise` 捨棄),因此若要維持 `select` 的作用並同時使用 `linenoise`,勢必要修改 `linenoise` 的原始碼。根據[前面的討論結果](#select-於本作業中的必要性),我決定當 fd 為 stdin 時,不使用 `select` 並直接 `linenoise` 來讀取輸入的指令 (雖然這樣就失去 `cmd_select` 本來名稱所代表的用途) 實作時有幾點注意事項 * 需將 linenoise 回傳的字串結尾改成 `\n\0` (因為原本的 lab0 會根據 `\n` 來判斷是否為完整的輸入,而 linenose 預設結尾不含 `\n`) * linenoise 回傳的字串是使用 [`strdup`](http://man7.org/linux/man-pages/man3/strdup.3p.html) 產生的,記得 `free` * 也可以不使用 `linenoiseHistorySave`,只是這樣每次執行 qtest 時不會有上次的歷史紀錄 ```cpp int cmd_select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) { ... /* need to read command from fd */ infd = buf_stack->fd; if (infd != STDIN_FILENO) { /* use trace files */ /* prepare necessary data for select */ if (!readfds) readfds = &local_readset; FD_SET(infd, readfds); if (infd >= nfds) nfds = infd + 1; if (nfds == 0) return 0; int result = select(nfds, readfds, writefds, exceptfds, timeout); if (result <= 0) return result; if (readfds && FD_ISSET(infd, readfds)) { /* Commandline input available */ FD_CLR(infd, readfds); result--; cmdline = readline(); if (cmdline) interpret_cmd(cmdline); } return result; } else { /* use linenoise (stdin) */ cmdline = linenoise(prompt); linenoiseHistoryAdd(cmdline); /* Add to the history. */ linenoiseHistorySave( "linenoise/history.txt"); /* Save the history on disk. */ int len = strlen(cmdline); if (len >= (RIO_BUFSIZE - 2)) /* prevent overflow */ len = RIO_BUFSIZE - 2; memcpy(linebuf, cmdline, len + 1); /* copy content to inter buffer */ /* the original string got from linenoise is ended with \0, * change it to \n\0. */ linebuf[len] = '\n'; linebuf[len + 1] = '\0'; free(cmdline); /* free the memory allocated by linenoise */ cmdline = len ? linebuf : NULL; if (cmdline) interpret_cmd(cmdline); } return 0; } ``` 自動補完與歷史紀錄的功能需在使用前先設定好 callback,這個部分寫在 qtest.c 中,於 `run_console` 前執行 ```cpp void completion(const char *buf, linenoiseCompletions *lc) { if (buf[0] == 'f') { linenoiseAddCompletion(lc, "free"); } if (buf[0] == 'h') { linenoiseAddCompletion(lc, "help"); } ... if (buf[0] == 't') { linenoiseAddCompletion(lc, "time"); } } static void linenoise_init() { /* Set the completion callback. This will be called every time the * user uses the <tab> key. */ linenoiseSetCompletionCallback(completion); /* Load history from file. The history file is just a plain text file * where entries are separated by newlines. */ linenoiseHistoryLoad( "linenoise/history.txt"); /* Load the history at startup */ } int main(int argc, char *argv[]) { ... queue_init(); init_cmd(); console_init(); linenoise_init(); ... ok = ok && run_console(infile_name); ... } ``` ## 現有程式的缺陷 在繪製 `readline` 的流程圖的時候,我發現 `readline` 不像 RIO 套件有考慮到被 signal 中斷的狀況 (可參考 [RIO 套件](#RIO-套件-Robust-IO)中的 `rio_read`),這會造成一個邏輯問題 * 被 signal 中斷而**未讀取任何資料**時 (`EINTR`),**不會**自動重新執行 `read`,而會被判斷成 `EOF` ## 實作小型終端機命令解譯器 (待補) 我認為可以將此次作業的經驗用於改善以前寫的 [CS:APP shell lab](https://hackmd.io/1sOTAV6DRVm5E7UpYdBk1g),但短時間內沒辦法完成 TODO list: * 改善 git commit 的使用方式 (也應該考量如何加入 [git-good-commit](https://github.com/tommarshall/git-good-commit)) * 改善 signal handler 的使用方式 (例如考量使用 `siglongjmp`),我覺得我原先寫的 signal handler 肯定爛透了,整體的流程也該重新考慮 * 應該更詳細理解 [Asynchronous Signal Safety](http://man7.org/linux/man-pages/man7/signal-safety.7.html) 如何達成 * 重新編輯筆記,使之具有更高的可讀性 ## 參考資料 (待整理) [flowchart.js](https://github.com/adrai/flowchart.js) ###### tags: `linux 2020` `csapp`

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully