contributed by < 0xff07
>
queue_t
因為要求尾端插入與長度查詢的複雜度是 ,因此在資料結構中加入 tail
與 size
分別紀錄尾端與長度:
q_free
if (q) free(q)
這樣的 if
敘述是多餘,直接寫 free(q)
即可,詳見 free(3)
若 head
為空,則顯然記憶體已經釋放。若 head
非空,則「釋放 head
」可遞迴地視為「釋放串列第一個元素(迴圈中暫存於 tmp
) ,並釋放剩下所有元素形成的串列(q -> head -> next
)」 。
而再釋放串列中的元素之後,再釋放 q
即可。
若在 free
中傳入空指標,在測試程式中將出現 Attempt to free NULL
的錯誤訊息,並且扣分(儘管 man
中寫到傳入空指標是合法的)。因此這邊所有的 free
都事先檢測是否為空指標。
q_insert_head
使用 do{...} while(0)
配合 break
,達到 goto
的效果,並進行各種例外處理。並在插入後,將屁股尾端維護好。正在思考例外處理與判斷的邏輯是否能夠更精減(立刻回去翻上週作業嗚嗚)。
另外發現若將 14. 15 行改成 cpumpound-literal 的寫法:
在 git commit
時似乎會被檢查出 memory leak,跑出:
但該作法可以順利通過 qtest
。因此不是很確定這種用法是否將造成 memory leak,或是有什麼潛在的不良影響。相關資料待查。
目前的靜態分析工具是 cppcheck,可能會遇到 false alarm,可搭配 AddressSanitizer (ASAN) 確認是否有 leaks
q_insert_tail
大致上與前一個函數相同,紀錄尾端並在插入後維護好 tail
與 head
的指標。「新增 list_ele_t
」似乎可以寫成一個函式,但發現使用 strdup
時,即使 man
中提到:
The
strdup()
function returns a pointer to a new string which is a duplicate of the strings. Memory for the new string is obtained with malloc(3), and can be freed with free(3).
即 strdup
中的記憶體由 malloc
配置,但使用 strdup
時測試程式,仍會判定沒有正確配置記憶體(見「其他」),因此尚未測試。
q_remove_head
依照要求,除了釋放記憶體,必要時得將節點中存的字串進行複製。因此照題目要求寫出來:
q_size
直接回傳存好的值大小:
q_reverse
假定 ,則:
若串列為空,或是串列僅有一個元素。則自己就是自己的反轉
若已經有前 個元素反轉的串列 :
則將第 個元素加入反轉串列的頭部:
就能構造出前 個元素反轉的串列。以此遞推下去。
前 2 個 if
敘述描述的是第 1. 中的狀況; 而其後的程式用於處理 2. 中的狀況。
我覺得這樣的實作看起來並不一目瞭然。似乎有待改進。
在 harness.[ch]
當中,可以看到記憶體管理相關的內容。該記憶體管理機制相關說明如下:
將原先的 malloc
與 free
使用 test_malloc
與 test_free
取代。該部份可見 harness.h
最後面:
所以實際上,在 queue.c
中呼叫 malloc
與 free
時,其實式分別呼叫 test_malloc
與 test_free
兩個函式。以下都用 test_malloc
與 test_free
稱呼該二函式(而不是 malloc
與 free
)。
所有因呼叫 test_malloc
得來的記憶體,實際上是紀錄在一個名為 allocated
的一個 doubly-linked list 中。該 linked list 結點的結構為 block_ele_t
,定義在 harness.c
中:
在這個結構體中,最後一個成員 payload[0]
是實際給呼叫者使用的記憶體開頭。該成員使用到的 GCC 中 Array of Length Zero 特徵。該功能簡便處在於:若結構體中的最後一個成員是大小為 0 的陣列,那麼可以透過 malloc(sizeof(block_ele_t) + 希望 payload 後面接著多少記憶體)
來達成「最後一個成員大小可以任意指定」的效果(另外一個好處是 malloc
跟 free
一次就可以完成,不用替 payload
開頭的記憶體再 malloc
一次)。malloc_test
中可以發現以下程式:
這個技巧表現在程式的 malloc(size + sizeof(block_ele_t) + sizeof(size_t))
當中。但除了 sizeof(block_ele_t)
與 size
之外, 還多了一個 sizeof(size_t)
的大小。這是因為使用者實際可使用的記憶體,前後被兩個 size_t
整數包著,裡面各自紀錄著兩個 Magic Number,作為驗證這塊記憶體是否是由 test_malloc
分配的依據,以及作為記憶體是否有產生越界等不佳使用的依據。
查閱 C99/C11 規範中,是否有類似 gcc "Arrays of Length Zero" 的標準規範,又,Variable-length arrays (VLA) 為何在 Linux 核心不被允許呢?
位於記憶體之前的 magic number,實際上就是結構體中 magic_header
這個成員,其值會在 test_malloc
中被指定為 MAGICHEADER
,也就是 0xdeadbeef
; 在記憶體尾端的 size_t
整數,數值必定為 MAGICFOOTER
,也就是 0xbeefdead
。
因為要回傳給使用者的指標實際上是 payload
的位置,因在程式中另外有 find_footer
跟 find_header
這兩個函式作為輔助。前這傳入 block_ele_t*
,回傳位於 payload
尾端 magic number 的位址。如 test_malloc
中有一段指定尾端 magic number 的程式:
而後者則是傳入使用者呼叫後得到的記憶體開頭,反推該記憶體所屬的 block_ele_t
的開頭位置。這兩個函式除了用在 test_malloc
當中,也會用再 test_free
當中。在呼叫 test_free
時,使用者傳入的,實際上是 payload
的位置,但釋放記憶體時,除了該記憶體之外,該記憶體所屬的結構體,也要一併釋放。因此需要有一個尋找該記憶體所屬開頭的方法。
該二函數的原理不難理解。給定 block_ele_t
,尾端的 magic number,由 payload_size
進行推算即可; 對於反推記憶體所屬的結構體,則是利用 sizeof(block_ele_t)
反推,並且尋找反推過後的記憶體是否在 allocated
的清單中。若沒有,則推論為錯誤的配置。
而在 find_header
中有檢測傳入指標是否為空的設定。
我懷疑這是一個不能使用
calloc
跟strdup
的理由。但目前還沒確認該二函數中的malloc
是否有被影響到。
test_free
中用的原理類似:首先用 find_header
找出使用者準備釋放的記憶體,其所屬的結構體有沒有再 allocated
裡面。若傳入的指標為空指標,或是該記憶體不屬於任何一個節點,find_header
內部會分別傳出「試圖釋放空指標」或「記憶體損壞」等警告。
若順利找到該記憶體所屬的節點,接著檢驗尾端的 magic number 是否仍為 MAGICFOOTER
,作為記憶體有沒有被越界存取等狀況的判斷依據。若比對結果發現不合,也會發出該區段記憶體損壞的警告。
若確認結果無誤,則將記憶體區段前後的 magic number 都設成 MAGICFREE
。並且將記憶體內容都用 FHILLCHAR
填充。最後移出 allocated
這個 doubly-linked list。
不確定為什麼要先把 magic number 都改掉之後再釋放。我猜測是同樣大小的記憶體如果又被
malloc
,但裡面仍有 magic number 時,會造成誤判,以為不合法的記憶體其實是合法的。
提示: 思考 malloc() 得到的記憶體空間是否會跟輸入的空間一樣大?若不是,是否意味著我們要額外處理?
在分配記憶體時,每呼叫一次 test_malloc
,allocate_count
就會 +1; 而每呼叫一次 test_free
,該數值就會 -1。因此程式即將結束時,判斷該數值,即可知道是否有妥當地釋放記憶體。
輸入一個指令時,會經過下面這些 function call 一路到 parse_arg
。這個函數會解譯使用者的輸入:
之後,interpret_cmda
會在 cmd_list
這個 struct CELE
為元素的 singly linked list 中找對應的指令。cmd_list
會在 main
裡面的 cmd_init
跟 console_init
呼叫時初始化。
找到之後,struct CELE
這個結構體中有個 operation
成員,儲存每個指令的結構體中的 operation
是個包住待測試的函數的 wrapper:
程式執行到這邊之後,會將剛剛 parse 到的輸入傳給他。以下面這個指令為例:
parse_arg
完後,執行相應的測試程式時,call stack 如下:
有了上面的資訊後,可以知道設計好 do_*
函數,並在 console_init
中呼叫 add_cmd("<instruction>", <do_*>, "<documentation>")
,就可以增加新指令。比如說在 console.c
中增加:
並在 console_init
中多增加:
可參照 你所不知道的 C 語言:前置處理器應用篇 提及的 #
(Stringification/Stringizing; 字串化) 和 ##
(concatenation; 連結; 接續) 使用技巧,簡化上述程式碼
重新編譯之後,在 qtest
的命令列中輸入 hello
,就可以發現指令成功新增:
若使用 help
,則會發現對應的說明新增進去了:
select_cmda
裡面還有用 select()
,man select
對應的說明為:
select()
andpselect()
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). A file descriptor is considered ready if it is possible to perform a corresponding I/O operation (e.g., read(2) without blocking, or a sufficiently small write(2)).
但是忘記他要怎麼用,正在查。
最前面的 getopt
的迴圈用來解譯啟動 qtest
時有的 commandline arguement,並做出相應動作。接著進行 3 個初始化動作。首先是 queue_init
:
這邊的測試是用一個全域變數 q
作為測試的 linked-list。除了把錯誤次數的計數 fail_count
設成 0 之外,還註冊了 2 個訊號的 signa handler,其中 SIGSEV
(segmentattion fault 會發出的訊號)的處理常式是:
而另外一個 SIGALARM
的處理函式是:
可以發現兩者均使用了 trigger_exception
。而 trigger_exception
的定義,則是:
除了設定相關的變數,全域變數 error_message
指給錯誤訊息之外,還有一個 siglongjump
。而這個 siglongjmp
會跳到哪裡去呢?觀察一下那邊有 sigsetjmp
。會發現在 exception_setup
裡面:
可以發現:除了暫停 SIGALARM
的計時之外,還會進行 report_event
,猜測這是用來回報錯誤訊息。把他打開看看:
會發現這是一個接受不定量參數數目的函數,主要的功能是輸出錯誤訊息。
接著翻了 The Linux Programming Interface,發現了一些使用訊號的細節:
sigsetjmp
跟 siglongjmp
是能在 signal handler 內部使用的非區域跳躍。不使用 setjmp
, longjmp
的理由是:
The sa_mask field allows us to specify a set of signals that aren’t permitted to interrupt execution of this handler. In addition, the signal that caused the handler to be invoked is automatically added to the process signal mask. This means that a signal handler won’t recursively interrupt itself if a second instance of the same signal arrives while the handler is executing.
以及:
However, there is a problem with using the standard longjmp() function to exit from a signal handler. We noted earlier that, upon entry to the signal handler, the kernel automatically adds the invoking signal, as well as any signals specified in the act.sa_mask field, to the process signal mask, and then removes these signals from the mask when the handler does a normal return.
What happens to the signal mask if we exit the signal handler using longjmp()? The answer depends on the genealogy of the particular UNIX implementation.
即:當某個 signal handler 被觸發時,該訊號會在執行 signal handler 時會被遮罩住,並在 signal handler 回傳時恢復; 而在裡面使用 longjmp
時,解除訊號遮照的行為有可能不會發生(是否解除則依照實作決定)。為了保證在非區域跳躍後能夠恢復,所以規範了另一個能在 signal handler 中呼叫的 sigsetjmp
跟 siglongjmp
。
jmp_ready
的技巧:
Because a signal can be generated at any time, it may actually occur before the target of the goto has been set up by sigsetjmp() (or setjmp()). To prevent this possibility (which would cause the handler to perform a nonlocal goto using an uninitialized env buffer), we employ a guard variable, canJump, to indicate whether the env buffer has been initialized. If canJump is false, then instead of doing a nonlocal goto, the handler simply returns.
因為訊號有可能在任何時候發生,包含 sigsetjmp
正在處理而未處理完 sigjmp_buf
的時候。如果這時候 signal handler 又進行 siglongjmp
,那麼將產生錯誤。改良方法是設立一個 jmp_ready
變數,表示「sigjmp_buf
是否準備好」,並且在可能進行 siglongjmp
的 signal handler 中先檢查這個變數,以確保 sigjmp_buf
有正確地初始化。
看這個名字大概可以猜到是用來吃初始化測試用的 command line 的。裡面是:
前面是在初始化一堆全域變數,這些變數代表的功能暫時不知道。但後面的 add_cmd
可以猜測是加入新的指令。進去看看實作:
看到這邊,再參照 console.h
中的結構體:
可以發現:所有的 command line 指令是用一個 singlely-linked list 儲存的,而每個結構是用一個名稱(name
) ,一個指向對應功能的函數的 function pointer, 以及一個字串的說明(documentation
)構成。而插入的手法類似 insertion sort,將所有命令由字典序排好。
而另外一方面,add_param
也是用類似的運作方式:
而對應的結構體為 param_ele
跟 param_ptr
:
在 interpret_cmda
中,如果有找到指令,會呼叫對應的 struct CELE
中的 operation
成員。operation
每個函數的型別都是 bool (*do_something) (int, char**)
,對應的命名就是 do_<operation name>
的那些函數。在呼叫 operation
時,是用如下的方法:
如果收到 SIGSEGV 或是 SIGALRM,那麼就會跳進 signal handler 裡面; 而 signal handler 會執行 siglongjmp
。會跳到那邊呢? 可以注意到: 每個 do_<operation_name>
型式的函數,都會有如下面這段程式:
假定測試程式沒寫錯,那唯一會發出 SIGSEGV
當執行流程第一次(或說「不是因為 signal handler 跳來這邊」)時,會呼叫 exception_setup
。看裡面的內容:
這邊我覺得有點怪:如果
longjmp
到一個已經回傳的函數裡面,不是未定義行為嗎?或是siglongjmp
這方面有什麼不一樣的細節?
設計新的實驗並搭配 GDB 去驗證
可以知道:第一次呼叫他時,exception_setup
的功能是設立一個 sigjmp_buf
,並回傳 true
;
回到原來的 if(exception_setup(true))
中,如果是第一次回傳,那麼那麼 if
,會開始測試函數。如果測試函式的過程中,發生任何錯誤(也就是發出 SIGSEGV 或 SIGALRM 訊號),就會立刻跳回 signal handler。signal handler 會印出錯誤訊息,並進行 siglongjmp
。由 exception_setup
的程式可以知道又是跳到 exception_setup(true)
裡面(這裡我認為理解有誤。因為就我的理解,如果 longjmp
到已經回傳的函式中,是未定義行為)。但這時會回傳 false
,因此跳過測試函數的過程,直接結束測試並回傳 ok 值。
如果在執行作業要求的函數中,沒有發出任何訊號,就接著檢測回傳結果是否正確。這方面來說相對簡單:就是一堆 if
分狀況討論,如果不對,就印出對應的錯誤。