contributed by < hankluo6
>
詳細閱讀 C Programming Lab ,依據指示著手修改 queue.[ch] 。
q_new
: 建立新的空佇列q_free
: 釋放佇列中所有空間q_insert_head
: 在佇列前端插入新元素q_insert_tail
: 在佇列後端插入新元素q_remove_head
: 刪除佇列前端元素q_size
: 計算佇列中元素數量q_reverse
: 倒轉佇列中元素q_sort
: 排序佇列中元素(遞增排序)queue.h
queue_t
要在 內完成 q_insert_tail
與 q_size
的話,必須在 queue_t
中加入 size
與 tail
欄位。
queue.c
q_free
q
本身也釋放。q_insert_head
free()
在 NULL
中無作用的特性,將釋放記憶體的部分寫在同區塊內,保持整潔與易讀性。strlen()
不會計算結尾 \0
字元,故使用 malloc
分配記憶體時,需額外給予 one byte 大小來容納 \0
字元。strncpy
時的長度須含括 *s
中所有字元,包含 \0
,理由同上。q->head
,如果為第一個element,則 q->tail
也須更新。strlen()
的計算方式在 man page 中有清楚解釋
The strlen() function calculates the length of the string pointed to by s, excluding the terminating null byte ('\0').
如果 strncpy
只複製字串長度 strlen(s)
的話,會造成 *news
字串中沒有結尾符號 \0
,取值時將會產生未定義行為。如 trace-03 出現以下亂碼。
q_insert_tail
q_insert_head
大致相同,須注意 q->tail
的更新需先檢查 q
是否為空。q_remove_head
*sp
的範圍,可以從 qtest.c 中的 do_remove_head
的程式碼中理解 *sp
跟 bufsize
的範圍而 string_length
可以透過 qtest 直譯器指令 option length [value]
來改變
所以 *sp
的第一個字元與最後一個字元為 \0
,其餘皆為 X
如果不加上 sp[bufsize - 1] = '\0'
的話,在 trace-06 會出現
原因是因為 qtest.c 中用來檢查回傳字串是否正確的 checks
變數,已經先將 checks[string_length]
設為 \0
,而 string_length + 1
就是 q_remove_head
中的 bufsize
,所以需要讓 sp[busize - 1] = '\0'
才能使 strcmp
通過。
q_size
*q
為 NULL
的情形中,不能取其成員。q_reverse
next
之節點,與其前後兩個節點。q->head->next
與 q->tail->next
用來記錄前面與中間的節點。rear
指標指向後方的節點q->head
與 q->tail
設定為正確位置queue
的結構while
迴圈前新增 tail
並將尾端相連while
迴圈中,執行反轉四步驟q->head->next == q->tail
跳出迴圈q->tail
與 q->head
替換,並將相連部分刪除即可。q_sort
q->tail
不一定為尾端節點,故需額外尋找尾端來更新。merge_sort
每次 merge_sort
都要回傳新的 head,可以使用 pointer to pointer
來改成更有「品味」的版本,
而 merge_sort
就不需要回傳 head
開啟 Address Sanitizer 後,在 trace17 會有記憶體錯誤的問題,從出現的檔案與其他檔案內容對照來看,大致推測為 simulation
的部分出現問題。
可以看到 init_cmd()
強制將 1 bytes 的 bool
型態 simulation
轉換為 int
型態,被限制使用的空間會被 Address Sanitizer 檢測出來。
只要將使用到 simulation
的部分改為 int
就行了。
上述 q_insert_head
或是 q_remove_head
中的 strncpy
少結尾字元的情形 Valgrind 能夠辨識出來
忘記釋放空間等常見錯誤也能發現,如 q_free
中忘記釋放 *q
時會顯示
從程式碼中可看到,simulation 模式會持續分配大量記憶體 (q_insert_head
),再量測 cpu 時間,最後釋放所有空間。
所以 massif 產生的記憶體使用量應為許多峰值的圖。
與預期結果不同,仔細看每段峰值橫跨不同的 snapshot,但 dut_insert_head
應該不會慢到橫跨不同的 snapshot。推測是 massif 的 snapshot 數量太少,將 snapshot 與 detailed snapshots 數量開到最大。
完美!每次 dut_insert_head
都將大量分配記憶體,計算完時間後立刻釋放,產生這種起伏巨大的圖。
起伏劇烈的原因可能為每次 queue 配置的記憶體大小不同,
實驗將 measure
裡 test_insert_tail
與 test_size
的 malloc
數量改為固定
可以看到後半段檢驗 q_size
的記憶體大小幾乎一樣,而前半段還是有高低起伏,推測是 snapshot 的時間不是剛好在 dut_insert_tail
結束的時間,所以每次紀錄的 heap size 都會變化。但可以知道劇烈起伏有很大的因素是 dut_insert_head
裡配置的記憶體大小影響的。
實驗是基於對執行時間的 Timing Leakage Detection,測量兩個不同 classes 的 input data,檢查是否有顯著性差異 ( statistically different )。
Student’s t-distribution 為一種當變異數未知時,用來估計常態分布總體的均值。隨著自由度的增加,該分布會趨近於常態分佈,在做統計檢定時相當有用。
t-test 為一種統計檢定,可以用來檢定在變異數未知且取樣數量少時,兩個來自常態分配母體的獨立樣本之期望值的差是否為某一實數。
可以將 t-test 分成 Student's t-test 以及 Welch's t-test,兩者的區別只是在統計量 t
的分母不同,Student's t-test 使用情形在兩母體之變異數近似,而 Welch's t-test 則是在兩母體變異數不同的情況下使用。
根據 wikipedia 所述:All such tests are usually called Student's t-tests, though strictly speaking that name should only be used if the variances of the two populations are also assumed to be equal。
變異數相同時稱為 Student's t-test ,不同時則必須稱為 Welch's t-test。
事實上,兩母體變異數是否有差異可以用 F-test 來檢定,在 dudect 中已經假設兩母體變異數不相等。
我們可以用統計檢定來解釋論文中使用的 t-test
此為一雙尾檢定,設定信賴水準為 95%,變異數未知,使用 Welch's t-test 來檢定。
算出 t 值後,需算出自由度 df,根據 Welch–Satterthwaite equation 來計算,這邊因為只有 2 個 sample variance,故直接將公式展開
注意自由度可能為小數,可以四捨五入或是無條件捨去(保守作法)。
最後查表看對應的自由度與 t value,只要 t value 落在信賴區間外(圖中紅色區域),表示拒絕虛無假設,也可以說是我們沒有足夠的證據證明兩者平均數相等。
在 dudect 中,主要計算時間的函式為 doit
,我們將重點放在 doit
中
variable
t
為 struct t_ctx
類型,用來記錄分布中的平均值、變異數與測試個數number_measurements
為測試的總數量before_ticks
與 after_ticks
用來記錄測試前時間與測試後時間exec_times
紀錄時間差classes
紀錄目前為論文中的 fix class
還是 random class
input_data
儲存 random class
中 q_insert
要使用多少個 node將 doit
分成幾個區塊,方便理解:
allocate
( line 0 ~ line 13 )前 14 行都在配置記憶體,特別注意 input_data
中每個測試資料皆有 chuck_size
bytes 的位置來儲存隨機數
prepare_inputs
( line 15 )prepare_inputs
會呼叫 randombytes
來產生隨機數,並將結果放在 input_data
中。
也會呼叫 randombit
來產生 0 或 1,並放入 classes[i]
中,如果為 0 則將 input_data 裡的該部分清除,此動作在實現論文中 fix class
的部分,而 class[i] == 1
則為論文中 random class
的部分。
此函式也會隨機產生 random_string
裡的值。
measure
( line 17 )measure
則是實際計算運行時間的部分,i
表示每一次的測試,透過 dut_insert_head
裡的第二個參數 *(uint16_t *) (input_data + i * chunk_size) % 10000)
來決定要 insert 多少個 node。
class[i] == 0
時,該參數為 0,為 fix class
的部分class[i] == 1
時,該參數為 0 ~ 9999 的隨機數,為 random class
的部分再來就是計算要測量的函式時間,將運行前時間與運行後時間放在 before_ticks
與 after_ticks
中,並記得釋放記憶體。
differentiate
and update_statistics
( line 18 ~ line 19 )這兩個函式只是計算每次測試 i
的時間差,並將結果與對應的 class 一起 push 至 t
中計算平均值與變異數。
report
( line 20 )套用 Welch’s test 公式,計算 t_value
,根據 t_threshold_moderate
決定假設是否成立。
free
( line 22 ~ line 28 )最後將分配的記憶體釋放,並回傳測試結果。
紀錄一些較難理解的地方
randombytes
randombytes
用來產生隨機數到指定的記憶體空間裡。但隨機數的是透過讀取 /dev/urandom
這個檔案裡的資料來產生。
不知道 dev/urandom
是幹嘛的,從 man page 裡可以找到
The character special files /dev/random and /dev/urandom (present since
Linux 1.3.30) provide an interface to the kernel's random number gener‐
ator.
再從 wikipedia 頁面中找資料
A counterpart to /dev/random is /dev/urandom ("unlimited"/non-blocking random source) which reuses the internal pool to produce more pseudo-random bits. This means that the call will not block, but the output may contain less entropy than the corresponding read from /dev/random.
原來 linux 核心中是依據環境噪聲產生 entropy,並放入 entropy pool,而 dev/urandom
就是從 entropy pool 中產生亂數的地方,且安全性比 dev/random
還低。
之後的 while
迴圈就很好理解,只要將隨機數讀到指定的記憶體就行了,特別注意 x += i
為指標的加法,用意是將指標位置從已經放完資料的地方移動到未賦值的地方。
t_push
t_push
使用 online 版本來計算平均值與變異數,可以防止 overflow 的問題。
使用 Welford's online algorithm 來計算變異數,需先計算 Sum of Squares,寫作 。在 struct t_ctx
中用 m2
成員來記錄,而在需要時再轉換為變異數。
report
report
函式用來檢查假設是否成立。從 report
中並不知道 t_threshold_bananas
的用處,但從論文作者原始碼 fixture.c 就能看到是用來區分 Definitely not constant time 與 Probably not constant time 用的。
threshold 的設計可以再更精準,需先計算自由度,再透過查表或計算 p value 來決定是否有顯著差異,可參考上方 Welch's t-test 的部分。
在論文中提到
when a t-test is coupled with cropping pre-processing, one is no longer
testing only for equality of means but also for higher order
statistical moments since the cropping is a non-linear transform
而在我們的 dudect 程式中,只有計算到 1st order statistical moments,需再增加 higher-order 的分析。
linenoise 的操作都是基於 linenoiseState
來運作的
接著來一個個分析裡面的函式
linenoise
linenoise 函式會在內部呼叫 linenoiseRaw
用來做編輯等處理,並回傳處理完的字串,也就是打完字按 Enter 後的內容,參數 prompt
的內容為一開始要輸出在 cmd 上的字串, (如 cmd>
)。
linenoiseRaw
可以看到先運行 enableRawMode
函式,並進入 linenoiseEdit
,最後再關閉 RawMode
。
enableRawMode
先來了解兩種 terminal mode,Raw Mode 與 Cooked Mode。
Cooked Mode 為一般 linux terminal 的模式,會根據使用者的特定輸入來產生特定行為,例如 Control character。
Raw Mode 則類似 Vim 編輯器一樣,使用者的輸入將全部以 Raw Data 的形式傳給 terminal,而這正是我們想要的。
再來看看 termios
這個 structure,從 man page 裡的說明
termios, tcgetattr, tcsetattr, tcsendbreak, tcdrain, tcflush, tcflow, cfmakeraw, cfgetospeed, cfgetispeed, cfsetispeed, cfsetospeed, cfset‐ speed - get and set terminal attributes, line control, get and set baud rate
The termios functions describe a general terminal interface that is provided to control asynchronous communications ports.
可以知道 termios
用來控制終端機介面裡的屬性,在 enableRawMode
函式裡更改相關屬性來達成 Raw Mode。而tcsetattr
與 tcgetattr
分別是用來接收與設置 terminal 當前屬性。
linenoiseEdit
linenoiseEdit
為主要操控游標的函式,先宣告 linenoiseState
變數,並將相關資訊放入到結構中。
接著進入 while
迴圈,除了 Enter
與 Ctrl_C
等字元外,其他左右移動,歷史紀錄等等操作皆在此迴圈中進行。
switch
以輸入字元 c
來選擇對應的行為。
linenoiseEditInsert
linenoiseEditInsert
為在 switch
裡 c
為非特定字元時會呼叫,用來插入字元到現在的字串中。依照游標是否在當前字串結尾分成兩種情況,相關操作都很直覺,看程式碼就能理解。要注意插入完字元後,需呼叫 refreshLine
來更新畫面上的字串。
refreshSingleLine
refreshLine
分成 SingleLine
與 MultLine
兩種,這邊介紹 SingleLine
版本。
函式前半段的 while
迴圈用來控制當終端機上的字元數過多時的情況,印出新的字元並將最舊的字元移除。
接著可以看到 abuf
這個 structure,當作 append buffer,用來記錄 refresh
中游標的所有移動,再一次性印到 terminal 上。
seq
用來儲存將要寫入的字串,之後進入 abAppend
將字串 push 到目前 append buffer 的尾端,在 snprintf
中寫入的字串稱為 ANSI escape code,用來處理游標操作。
最後在寫入到標準輸出上,印給使用者觀看。
linenoiseHistory
history
的概念很簡單,用一個 static char **history
來儲存每個歷史記錄內的字串,並呼叫 linenoiseHistoryAdd
來將新字串儲存到 history
中。
另外還提供搭配兩個函式 linenoiseHistorySave
與 linenoiseHistoryLoad
用來將 history
讀寫檔。
而要印出之前的 history
,就必須呼叫 linenoiseEditHistoryNext
函式,該函式會在輸入上下鍵時呼叫。
前兩行的 free
與 strdup
是為了將當前螢幕上的字串 l->buf
放入 history
中,並把接下來要印出來的字串從 history
中移除。也就是說當呼叫 linenoiseEditHistoryNext
後出現的字串,就已經不會儲存在 history
當中,這樣可以保證新輸入的字串會被放到 history
的尾端,而不是還儲存在 history
中間。
接著再透過 history_index
來選擇現在只向哪個 history
字串,並複製到 l->buf
就完成了。
completeLine
要使用 completeLine
前須先使用 linenoiseSetCompletionCallback
註冊 callback function,callback function 的寫法可參考 example.c 的範例程式,所有觸發相關的條件都必須寫在 callback function 裡。
而 linenoiseAddCompletion
則只儲存 completion 的結果 ,需透過 linenoiseCompletions
這個 structure 來儲存相關資料。
可以看到 lc->cvec
跟上面的 history
一樣用一個 pointer to pointer 來儲存每個 completion string。
最後,當輸入 Tab 鍵時,呼叫 completeLine
內部會呼叫 callback function,並將可以 completion 的字串通通放入 lc
當中,並進入 while
迴圈。while
迴圈會印出目前 completion 的字串,但是還不會把結果存入 ls
當中,而是進入 switch
等待下個輸入。
當下個輸入不是 Tab 或跳脫字元時,才會將結果寫入到 ls
中。
如果下個輸入是 Tab 的話,會依照 lc->cvec
裡的順序依序印出。
refreshShowHints
refreshShowHints
用來提示接下來可能的輸入,類似 linux 下按兩下 tab。要使用前一樣需先註冊 callback function,可 example.c 的範例程式,跟 completion 一樣,跟觸發相關的條件都必須寫在 callback function 裡。接著程式就會在 refresh
時呼叫 refreshShowHints
來顯示提示。
看程式碼應該就能理解,*hint
會傳需要提示的字串,更改顏色後透過 struct abuf
來添加到 append buffer 裡,這邊 ab
就是從 refresh
時宣告的 ab
傳來的,更新完 append buffer 後回到 refresh
函式就會將結果印到 terminal 上。
先找到 qtest 裡面 parse command 的地方,追蹤後發現在 console.c 裡 cmd_select
的 readline
部分
注意到這邊 file 與 STDIN 的輸入方式皆使用 readline
來控制,為了要使用 linenoise
需區分這兩種情況
將 cmd_select
中用到 readline
的部分改成 linenoise
實際測試會發現出現類似 newԆU
的隨機亂碼,debug 後發現 parse_args
中 *dst
沒有設置結尾字元,所以將 \0
手動新增到 *dst
結尾。
這邊不了解為甚麼使用 readline
就不用加 \0
在結尾,看不出來 readline
與 linenoise
輸出的字串有甚麼差別。
接著實作自動補齊,只要比較目前字串與目標字串相等,Tab 鍵就能持續選擇下個可能的字串。
再來是提示字串,可以簡單提示可輸入的參數
最後是歷史紀錄,只要在輸入字串後記錄到 history
中就好了
開啟 multiline mode (雖然用不太到)
readline
裡面的 read()
有可能會因為 signal 暫時中斷,但程式碼中好像未處理到關於 interrupt 的行為,查詢資料時翻閱到這篇很詳細的解釋 POSIX 中 read()
被 signal 中斷時的處理方式。
但為了要以第一手資料為主,翻閱 linux 裡 signal 的 man page 時看到這段
read(2) calls on "slow" devices. A "slow" device is one where the I/O call may block for an indefinite time, for example, a terminal, pipe, or socket. (A disk is not a slow device according to this definition.) If an I/O call on a slow device has already transferred some data by the time it is interrupted by a signal handler, then the call will return a success status (normally, the number of bytes transferred).
而在 read(2)
的 man page 裡又寫道
On error, -1 is returned, and errno is set appropriately. In this case it is left unspecified whether the file position (if any) changes.
兩者似乎對回傳值的定義有些不同(?),只好翻閱網路上的資料,根據 Linux 系統程式設計 - read()、write() 與 page cache 這篇文章與上面提到的 多工作業下的資料讀寫處理事項 所述,我的理解是當已經有資料傳輸後,signal 中斷時 read()
會將已經傳輸的 bytes 數回傳,而尚未傳輸資料時,則回傳 -1 並設置 erron
。
目前程式中的 readline
已經可以處理第一種情況,也就是已有資料傳輸時的情形,因為 buf_stack->cnt
會被設置為成功讀取字串的 bytes 數,就算 read
的時候被中斷,在 buf_stack->cnt == 0
時也會重新填補 buffer。
所以只要來應付第二種情況。在回傳值為 -1 時判斷是否為 EINTR 的 interrupt,將 read()
用 while
迴圈來判斷是否為 EINTR 的 error
linux2020