2021q1 Homework1 (lab0)
contributed by < robertlin0401
>
作業說明
github repository
queue 實作
queue 結構
list_ele_t
用於表示 queue 中的元素,其中:
value
存放字元指標,即 C 語言的字串,此為 queue 中元素的內容
next
為 list_ele_t
的指標,指向 queue 中的下一個元素
queue_t
用於表示 queue 本身,其中:
head
為指向 queue 中第一個元素的指標
tail
為指向 queue 中最後一個元素的指標
size
紀錄 queue 中元素個數
- 圖示如下:
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
queue operations
- q_new:建立一個空 queue
- q_free:釋放 queue 所占用的記憶體空間,包含其元素內容之字串所使用的空間
- q_insert_head:將元素加入 queue 的前端
- q_insert_tail:將元素加入 queue 的尾端
- q_remove_head:將 queue 最前端的元素移除
- q_size:取得 queue 中的元素個數
- q_reverse:將 queue 中的元素順序反轉
- q_sort:將 queue 中的元素以遞增順序進行排序
以下針對部分 operations 之實作進行詳述:
q_insert_tail
- 第 35-39 行程式碼實作出將元素
newh
加入 queue 尾端的功能
- 若
queue_t
內未包含 tail
指標,則該段程式碼須改為以下實作,其中的 while 迴圈將達到 的時間複雜度
- 因此,我們需要使用
tail
指標增進程式的效能,以達到 時間複雜度
q_size
- 若
queue_t
內未紀錄 size
,則第 10 行程式碼須改為以下實作來計算 size,其中的 while 迴圈將達到 的時間複雜度
- 因此,我們需要在
queue_t
紀錄 size
以達到 時間複雜度
q_sort
版本一:遞迴式 quick sort
參考 2021 年春季 Linux 核心設計課程 quiz1 quicksort 實作
- 問題描述:在 trace-15 測資發生 TLE(Time Limit Exceeded)錯誤

- 推測原因
- 觀察 trace-15 測資可發現,欲排序的 queue 中元素為 [a a a … a b b b … b](a、b 各 1000000 個),對於 quick sort 而言,此為時間複雜度分析上的 worst case
- 在此 quick sort 實作中,此測資的 partition 如下
- 時間複雜度分析
- 令 為輸入 queue 的元素個數
- 左 partition 遞迴的時間複雜度為 ,右 partition 則為
- 總時間複雜度 , 為常數,故 ,確實為 quick sort 之 worst case 的時間複雜度
- 實際遞迴呼叫行為分析
- 由圖示可推知,左 partition 遞迴呼叫深度為 999999,右 partition 則為 1000000,最大遞迴深度達到
- 每次遞迴需要針對其左、右兩 partition 分別呼叫,所以左、右兩 partition(最上層的)總共向下遞迴呼叫了 次,加上最初針對左、右兩 partition 的遞迴呼叫,故總遞迴呼叫次數為 次,甚至達到了 的兩倍
- 綜上分析,在此情況下,quick sort 是極度費時且浪費 stack 空間的方法,故不適合應用於此
- 如何解決問題
- 根據上述分析可知,我們必須選擇其他排序演算法,以避免在遇到 worst case 的情形下,會耗費特別多資源的問題,意即需要選擇 worst case 之時間複雜度與 best case 是相同的演算法,常見的包含 merge sort、heap sort 與 radix sort
- 接著,我們從輸入字串進行分析,來決定使用何種排序法
- 首先,可以從 qtest.c 觀察到輸入字串僅由小寫的 a-z 組成,共 26 個字母
- 字串的排序邏輯為從第一個字元開始比較,若可以比較出大小,則該字串的大小便以此為依據,反之則比較第二個字元,以此類推
- 結合上述兩點,我們可以使用 radix sort 的觀念來實作排序
- 準備 27 個 buckets,分別表示 26 個字母以及終止字元 '\0'
- 從第一個字元開始,每回合取一個字元為依據,依此將字串放入所屬 bucket 中
- 檢查各 bucket(除 '\0' 以外),若存在多於一個字串則針對該 bucket 內容進行下一回合的 radix sort
- 最後,以 '\0' 的 bucket 最優先,接著按照 a-z 的順序,將字串以 FIFO 順序自 buckets 中取出,取出後的結果便會是排序完成的結果
- 時間分析
- 由於 buckets 數量為一常數,故此排序之時間複雜度僅與需比較之最大字串長度(令 )與輸入字串個數(令 )相關
- 又因字串之比較,若發現一字元可比較出大小,則無需比較剩餘字元,故僅在 且前 個字元皆相同的情況下,其時間複雜度會達到 以上,其餘情況下皆為 的時間複雜度
就實務上而言,時間複雜度達到 以上的情形是會發生的,但要在 為足夠大的數時,才會構成費時過多的問題,然而實際應用中並不會發生,原因如下:
- 若字串用作雜湊值,則因為 位數的 26 個字母之排列組合,共有 種可能性,因此 取值無需過大即足夠,相對地,在發生時間複雜度達到 以上的情形時, 也不會很大
- 若字串為有意義之單詞,則其長度本就受到限制,故同理, 也不會很大
版本二:遞迴式 radix sort
- 基於上述 radix sort 討論實作出此 function,經測試後可符合此問題之時間效率需求
- 是否需更改為非遞迴版本?
- 已知遞迴呼叫會花費較多的資源,故在效率議題上可以考慮將其改為非遞迴版本
- 然而,此實作中之遞迴呼叫次數存在限制,以下說明
- 每回合之遞迴呼叫最多會有 26 次,即針對 a-z 共 26 個 buckets
- 每次遞迴呼叫之最大深度即為需比較之最大字串長度(令 )少 1,且上述討論已得到 為有限數之結論
- 故最大遞迴總次數約為 次,其發生條件為資料個數達到 個,,且每層遞迴中,各 bucket 內的資料個數需相近
- 換言之,此情形之排序意義相似於:排序以 26 個字母任意排列的長度 k 之所有可能字串。而實務上此情況發生時,k 並不會是很大的數
- 綜上所述,在一般情形下並不至於發生如 stack overflow 等問題,因此我暫時選擇使用遞迴版本
版本三:非遞迴式 merge sort
參考:bobhsiao0306
同學的開發紀錄
- 觀察其他人的開發紀錄可以發現大部分人是使用 merge sort 進行實作,以上取自
bobhsiao0306
同學的非遞迴式實作並進行簡單改良
- 接著,我們可以從各項議題上與版本二的 radix sort 相比較
修正 qtest
之錯誤
- 問題描述:開啟 Address Sanitizer 後,先執行
qtest
,再於命令提示列輸入 help
命令,會產生 global buffer overflow
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
- 何謂 global buffer overflow?
- 針對全域資料的存取超出合法存取範圍之問題,可能導致未定義行為、程式崩潰,甚至有資訊安全之疑慮
參考:global buffer overflow
- 觀察問題來源
- 觀察藍字的錯誤訊息可發現,執行過程中自記憶體位址
0x55e5e09a5400
存取了 4 個位元組
- 觀察綠字的錯誤訊息可知在該記憶體位址的為一全域變數
echo
,其定義之大小為 1 個位元組,因此存取記憶體位址 0x55e5e09a5401
以右的位元組之行為為 overflow,並且在 console.c
的第 59 行可以找到該變數的宣告,其宣告為一 bool
型態的變數
- 接著從其 traceback 去檢查其使用問題所在
- traceback #0 顯示在
do_help_cmd
第 307 行存取 param_list
指向的物件 — plist
之成員時發生錯誤
- 檢查
param_list
與 echo
之關聯:echo
的指標在 init_cmd
中作為參數,強制轉型為整數指標傳入 add_param
,並在 add_param
中賦值給 param_list
中物件之成員 valp
使用
- 解決方法
- 既然需要將其作為 4 個位元組的變數使用,則概念上只需將
bool
型態進行位元延伸,使其成為 4 個位元組的整數型態,並且以整數 0 或 1 表示 false
或 true
- 所以實作上將
echo
改成整數宣告,即可解決問題
- 此外,全域變數
simulation
也有同樣問題,同理,也可以用此方法解決
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
simulation
為一 extern
變數,故 header file 之宣告與 source file 之定義皆須做修改
-
問題描述:使用 valgrind 進行執行時期的記憶體檢測可以發現有部分配置的記憶體空間未被釋放,以下為 trace-01 之執行的錯誤訊息
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
Image Not Showing
Possible Reasons
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
Learn More →
為了更有彈性的進行測試,我嘗試手動輸入測資,卻發現此方法不會觸發錯誤,故能夠推斷此錯誤產生的原因與讀檔進行輸入相關,此資訊應該會在後續分析有所幫助
-
觀察錯誤訊息可以發現,未被釋放的記憶體是在 linenoiseHistoryAdd
函式中(分別在 linenoise.c:1224 與 linenoise.c:1236)被配置的,其用途為記錄命令列的輸入歷史
- 配置字元指標的空間,用於指向各條歷史紀錄
- 使用
strdup
複製字串,即各條命令本身內容
- 後續將複製的字串存入歷史紀錄中(交由前面配置的指標所指)
-
接著,由於前面已經發現僅在讀檔進行輸入時未將此記憶體空間釋放,故必存在用於釋放歷史紀錄的記憶體空間的程式片段,找到它並進行追蹤
- 記憶體空間會在
freeHistory
函式中進行釋放
freeHistory
僅在 linenoiseAtExit
函式中被呼叫
linenoiseAtExit
僅在首次呼叫 enableRawMode
函式時會被 atexit
註冊,若被註冊則當程序正常終止時便會執行該函式。enableRawMode
函式之目的為開啟 raw mode,以改變特定按鍵輸入的行為,例如:改變 tab 鍵的行為以此實作命令自動補齊的功能,關於 raw mode 在此段落不進行深入討論,詳見 linenoise 運作原理 段落
參考 atexit man page
- 由於讀檔進行輸入不需要切換成 raw mode(因為輸入來自檔案,故不需要改變按鍵輸入的行為),故不會呼叫到
enableRawMode
函式,因此歷史紀錄的記憶體空間不會被釋放
-
解決方法
- 找到判別檔案輸入或命令列輸入的分支處,其位於 console.c 的
run_console
函式中,處理檔案輸入的部分為 else
block 中的程式碼
- 向下追蹤
cmd_done
與 cmd_select
的內容卻發現其中並無操作歷史紀錄的程式碼,故回頭檢查檔案輸入前後的歷史紀錄,發現檔案輸入的內容確實沒有被記錄,未被釋放的空間所存放的歷史紀錄是在執行 run_console
以前就載入完成的過往執行的紀錄
- 又因為檔案輸入的內容並不會記錄,故當使用檔案輸入時其實不需要載入過往的紀錄,因此找到載入歷史紀錄的函式呼叫,並加入讀檔與否的判斷式即可修正此問題,該段程式碼位於 qtest.c 的
main
函式中
linenoise 運作原理
TODO