contributed by < OscarShiang >
linux2020
在閱讀 clang-format 的文件時,發現加入以下的程式碼後
可以在使用 vim 寫入檔案的時候直接執行 clang-format 來修改格式
但是在家目錄並沒有類似的檔案,所以在查看這個網站後發現在/usr/share/vim/addons/syntax/
有對應的clang-format.py
可以做使用,所以將.vimrc
中對應的路徑換為/usr/share/vim/addons/syntax/clang-format.py
後,就可以在寫入檔案時修改格式
請提交一個 pull request,關於 vim 和 clang-format 自動排版整合的建議,附於 README.md
檔案中
已提交 pull request
Oscar[color= orange]
qtest
實作的記憶體錯誤,並透過 Massif 視覺化 "simulation" 過程中的記憶體使用量,需要設計對應的實驗qtest
命令列功能,使其支援單行或多行編輯、歷史命令處理、命令自動補完 (例如輸入 h
之後按下 Tab 按鍵,自動接續補上 elp
,成為完整的 help
命令)。除了整合程式碼外,應當說明 antirez/linenoise 的運作原理,注意到 termios 的運用queue_t
根據作業要求,為了讓 q_insert_tail
與 q_size
的實作能在時間複雜度為 的條件下完成,所以我在 queue_t
的結構中增加:
tail
來記錄 queue
的最後一個元素的記憶體位置size
來儲存 queue
的元素個數q_new()
根據 Linux Programmer's Manual 對 malloc
回傳值的描述:
The malloc() and calloc() functions return a pointer to the allocated
memory, which is suitably aligned for any built-in type. On error,
these functions return NULL. NULL may also be returned by a successful
call to malloc() with a size of zero, or by a successful call to cal‐
loc() with nmemb or size equal to zero.
可以知道 malloc
會在取得長度為 0 或是在取得記憶體發生錯誤時回傳 NULL
所以我們可以透過檢查 malloc
的回傳值來確認是否取得成功
如果成功取得 queue_t
的空間,就將 head
與 tail
初始化為 NULL
, size
則初始化為 0
q_size()
根據作業描述中的實作,直接回傳 q->size
的數值來確保函式能在 也就是常數時間內完成
如果 q
或是 q->head
尚未取得記憶體位置時,直接回傳 0
改寫為以下等價但簡潔的形式:
已針對程式碼進行修改
Oscar
q_insert_head()
先確認 q
不是 NULL
,如果不是 NULL
的話就 malloc
元素的空間,如果 malloc
失敗就 return false
如果 malloc
成功,接著 malloc
字串的空間,並將字串的內容複製進入,最後將新的元素連接在 queue
上,並更新 q->size
的數值
q_insert_tail()
因為這個函式必須在常數時間(就是 的複雜度)內完成,所以我透過在 queue_t
的結構上宣告額外的 tail
指標來記錄最後一個元素的位置,達到快速插入元素的目的
如果這是 queue
中插入的第一個元素的話,就要額外更新 q->head
的值為 newt
以確保在執行其他操作的時候不會導致操作到 q->head == NULL
的情況發生
q_free()
利用 while
來走訪整個 queue
的結構,先將字串的空間釋放之後,再將該元素的空間也釋放出來,最後將 q
的空間也釋放
q_reverse()
根據 queue.c
中對於 q_reverse()
函式的描述
This function should not allocate or free any list elements
(e.g., by calling q_insert_head, q_insert_tail, or q_remove_head).
It should rearrange the existing ones.
所以我們可以利用 while
從頭開始走訪每個元素,並將當前拜訪的元素接在 tail
之後的方式,將整個 queue
反轉過來
最後將 q->head
與 q->tail
的值換成過來
q_remove_head()
在 queue.c
中有提到 q_remove_head
對於要刪除的字串所要進行的處理
If sp is non-NULL and an element is removed, copy the removed string to *sp
(up to a maximum of bufsize-1 characters, plus a null terminator.)
所以在我們調整 queue
的同時,如果需要將字串儲存起來的話,也要對字串進行處理,根據 Linux Programmer's Manual 對 strncpy
的說明:
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.
雖然在字串長度小於 strncpy
所輸入的參數 n
時會以 '\0'
填補,但為了避免 n
小於字串大小的情況發生,所以我一律在字串的最後額外指定為 '\0'
在排序演算法的部分,我使用 merge sort
來實作排序,因為 merge sort
的時間複雜度可以控制在 避免超出自動測試程式的時間限制
merge sort 的部分根據 Linked List Sort 所示範的來進行實作
在測試的過程中發現使用遞迴呼叫的 merge sort
會超出測試環境的 stack 大小發生 stack overflow 的狀況,因此將 merge
改為迭代實作
以 merge
將拆開的 queue
進行合併:
為了能夠使用 natural sort 來排序 queue
,我將 strnatcmp.[ch] 放入,並在 Makefile 中進行以下修改:
並以 mergeSort
來將 queue
切割並排序,最後回傳新的 queue
的開頭位置
最後以 q_sort
作為主要的入口,並在排序結束後更新 q->tail
的位置
參考 traces
目錄中的測試檔案並利用 natsort 所提供的 sorted-words
來打亂成下列的狀態,以 qtest
讀入檔案進行測試
但在進行測試時發現,自動測試系統會誤判 natural sort 的排序而印出下列訊息,而排序的情況則與 sorted-words
相同
這是本作業要求相當特別的地方,實際需要從 Natural sort order 去考量亂數字串產生器的設計
已在下方新增以 natural sort 進行排序的設定
Oscar
在 qtest.c
的 do_sort
函式中,會使用 strcasecmp
來檢測字串排序的正確性,但是本次作業使用 natural sort 進行排序,雖不影響自動評分系統的檢測,但仍可在設定中加入選項,以利自動評分系統檢測字串排序的正確性
如同 simulation
這個參數的設定一般,我在 console.c
中利用 add_params
函式設定使用 natural sort 的開關
在 console.c
的 add_params
中加入:
讓 qtest
在讀到 option natsort
時可以更改 natsort
的數值
透過 console.c
更改 natsort
的值後,為了在 qtest.c
中判斷是否要使用 natural sort 進行檢測,我在 console.h
中加入:
讓 qtest
可以根據 natsort
的數值決定是否開啟 natural sort 來檢測
並將 do_sort
函式改為
讓 qtest
可以判斷 queue
是否以 natural sort 進行排序
在執行自動測試程式的第 11 筆時候發現有記憶體未被釋放的錯誤訊息,使用 valgrind 進行測試,得到以下訊息:
根據訊息指出未被釋放的空間應該是 malloc
失敗之後才產生,因此修改 q_insert_head
與 q_insert_tail
中對於 malloc
失敗的字串空間進行釋放,解決 memory leak 的問題
為了能夠使用 massif ,須將 --show-leak-kind=all
這個指令刪除,並執行
並將輸出之 massif.out
檔案以下列方式開啟
在設定好 massif-visualizer 後,進行以下實驗:
在同樣進行以下檔案的指令時
正常的記憶體用量應該回到 0 KB 的使用量,也就是如下圖所示
也就是在程式結束的時候, Total Memory Heap Consumption
的使用會回到 0
但是當程式未將所有的記憶體空間歸還時就會發生 Total Memory Heap Consumption
最後沒有回到 0 的情況發生,也就是如下圖所示
所以從實驗可知,利用 massif-visualizer 可以幫助我們利用視覺化的圖表來檢視程式的記憶體使用過程,並透過圖表分析是否有 memory leak
的情況產生
根據 Dude, is my code constant time? 所提到的 dudect
是利用重複測量函式在執行不同測資時的 CPU 循環時間,並利用 Welch's t-test 分析資料是否有相同的母體平均值來判斷是否函式的執行複雜度是否為
利用 Student’s t-distribution 來分析程式的時間複雜度的好處是不會因為硬體差異而導致分析結果不同
程式實作的部分,在 dudect/constant.c
中的 measure
函式就是用來實作計算 CPU cycle 的,可以從程式碼中看到:
在執行測試函式的前後利用 cpucycles()
記錄數字來計算,並將計算的結果記錄在 before_ticks
與 after_ticks
的陣列中,以利進一步的統計分析
而在 cpucycle.h
的參考資料 Code Execution Times: IA-32/IA-64 Instruction Set Architecture 中有提到:
因為 Intel CPU 有計算 CPU cycle 的 counter ,所以在實作上就利用呼叫 rdtsc
來取得 cycle_high
與 cycle_low
的數值,並透過 bitwise 的操作將這兩個數值存在一個 64 bit 的 int 中
當取得了 CPU cycle 的測試資料後,透過在 fixture.c
的 doit()
函式的呼叫進行微分與 t test 的運算,如果計算後的 t
小於 t_moderate_threshold
時,表示執行時間符合 的限制,回傳 true
在 fixture.c
中預設進行嘗試的最大次數為 10 次,如果在 10 次的測試中,統計結果符合限制的話,就會跳出測試的迴圈並回傳結果,也就是 Probably constant time
的輸出結果
select
使用方法根據 Linux Programmer's Manual 中的說明:
select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for
some class of I/O operation (e.g., input possible).
而在程式中的使用是在 console.c
中的 cmd_select
函式,用來控制程式能在可讀取或寫入的狀態下才執行
在 run_console
函式中就在 cmd_done()
不成立的時候執行 cmd_select
來監測並等待直到可以執行
qtest
功能在 linenoise 的範例程式 example.c
中所示範的是利用 linenoise
這個函式來讀取整行的輸入
透過在 console.c
中尋找關於 cmd>
相關的輸出後發現在 run_console
函式中有 qtest
執行時等待輸入的相關函式迴圈:
在 cmd_select
函式中發現程式是透過 readline()
取得輸入字串,並執行 interpret_cmd
直譯輸入字串
所以為了引進 linenoise
我將該迴圈置換為:
但發現透過測試發現出現下列錯誤:
我認為是 interpret_cmd
在進行參數切割的時候出現錯誤,且因為每次執行測試的時候所錯誤的指令後面都會帶有兩個不固定的字元(例如上述 rU
等等),所以我認為是在切割字串的時候可能沒有將記憶體空間初始化所造成的。
在查看 parse_args
函式後,發現上列程式碼的第 9 行所取得記憶體空間,也就是接下來要將輸入分析與複製的空間沒有先清空
在該處加上 memset(buf, 0, len + 1)
後,程式可正常執行
但是在測試時發現改用 linenoise
作為輸入後,無法透過 -f
參數或是執行 source
指令將檔案讀入。
再次看 run_console
函式後發現 qtest
會透過 push_file
重新設定 RIO 的 fd
等參數,並利用相同的迴圈與 cmd_select
函式將資料讀入,但在 linenoise.c
中可見:
linenoise.c
中所預設使用的為 STDIN_FILENO
這個 file descriptor 所以導致無法將檔案內容讀入
所以我在 linenoise.c
裡加入新的 int
變數 readfds
儲存當前所使用的 file descriptor 並加入新的函式 linenoiseSetDescriptor
:
透過呼叫 linenoiseSetDescriptor
來變更 linenoise
所使用的 file descriptor
回到 console.c
中修改 push_file
以呼叫 linenoiseSetDescriptor
可是程式依然無法正常讀取檔案的內容。經過 gdb 檢查後發現使用檔案的 file descriptor 時, linenoise 無法將檔案的 file descriptor 以 enableRow
函式正常開啟,所以導致無法輸入
因為 linenoise
在不支援的 Terminal 上執行時會利用 fgets
從 stdin
取得輸入,所以透過將 linenoise
修改為:
將檔案的輸入引進 linenoise
中,並回傳每一行的指令,使得 -f
參數與 source
指令得以執行
根據原始的 qtest
之行為,在執行 source
指令後不會因為讀取完畢而結束程式,所以加入 run_source
這個 bool
變數來確保檔案讀取完畢後可以再度切換為原本的輸入模式
為了測試在加入 linenoise
套件之後是否有 memory leak 的問題發生,使用 make valgrind
檢測發現問題發生
從輸出訊息研判 memory leak 發生的原因在使用 linenoiseHistoryAdd
所取得的記憶體未被釋放,因此我檢查 linenoiseHistoryAdd
:
在該函式中可以看到, linenoise 主要是透過管理一個長度為 history_max_len 的字元指標陣列來存取從檔案中取得的記憶體位置的,但在程式結束後並沒有將對應的記憶體位置歸還,進而導致 memory leak 的情況發生
而在 linenoise.c
中也有對應的函式可以將這些記憶體空間釋放,在文件中有提到:
根據說明,在程式使用 exit() 結束時,會呼叫該函式來釋放記憶體空間
為了確定程式是否會正常呼叫,透過在 freeHistory
函式中加入輸出來測試,發現在使用檔案輸入時, freeHistory
並不會被正常執行
所以我利用強制在 console.c
的 run_console
函式要執行完畢前呼叫 linenoiseAtExit
,來確保記憶體空間都已被釋放
在輸入的操作上,原來的 console.c
與 linenoise
都是利用 open
來操作對應的 file descriptor ,如透過 stdin 輸入的 file descriptor: STD_FILENO
或是從檔案進行輸出的 file descriptor。
而在輸入的判斷上面,因為 console.c
中所使用的機制是利用 RIO_ELE
來儲存目前使用的 file desciptor 以及建立 buffer 空間,所以可以透過改變 file descriptor 的方式讓 qtest
得以自鍵盤或是檔案中取得輸入,並透過同一個迴圈,來處理不同方式的輸入
但在 linenoise.c
中,因為 linenoise
透過 termios
管理 terminal 的行為,而在 linenoiseRaw()
中,因為會透過 eableRawMode
開啟 termios
並進行設定
在 enableRawMode
這段程式碼中, termios
透過 bitwise 的方式,也就是透過操作 &=
進行設定,最後使用 linenoiseEdit
來啟動 history
, move cursor
, completion
等功能
而其中輸入的功能就是在第 33 行的地方,利用 read
函式將 STD_FILENO
的每一個字元輸入讀取進來,利用 linenoiseEditInsert
加進字串中,並根據不同的按鍵輸入進行不同的行為,最後在按下 Enter
離開這個函式。最後將字串的指標回傳出來。
歷史記錄的部分則可以透過 linenoiseHistoryAdd
可以得知
linenoise
透過一個名為 history
的這個指標的指標來管理一個長度為 history_max_len
的字元指標陣列,而這個一維陣列中所存放的就是利用 strdup
所儲存的之前使用的指令。
如果要加入一個新的歷史記錄在一個已經到達 history_max_len
大小的 history
陣列時,就會執行第 20 行之後的指令,也就是利用 memmove
將第 1 ~ history_max_len
個元素往前搬,將第 0 個元素覆蓋掉,並將新的指令存在 history[history_max_len]
的位置,也就是只會記錄最新的前 history_max_len
個指令
而在呼叫 linenoiseHistorySave
的時候就是將 history
所管理的陣列利用 fopen
開啟並用 fwrite
寫入
自動補全的機制在 linenoise/README.markdown
中有提到,透過呼叫 linenoiseSetCompletionCallback(completion)
將下列函式的指標指派給 linenoise
而在使用者進行輸入時, linenoiseEdit
將會呼叫我們所寫的 completion
函式,而根據輸入的結果,也就是根據 *buf
內容的不同,將不同的補全結果以 linenoiseCompletions
的形式儲存起來, linenoiseCompletions
的形式如下:
而在 linenoise.c
中是透過 linenoiseCompletion *lc
來管理一個動態長度的陣列,而跟據 *buf
目前的輸入,將 completion
函式中對應的補全結果加入到 *lc
中,在使用者按下 Tab
的時候,就將 *lc
中所儲存的結果輸出在螢幕上