contributed by <MetalheadKen
>
In progress…
這句話不用說,共筆除了自己初始的創作,還要接受他人的檢視和批評,從而予以調整和改進,總是「進行中」
Some of the skills tested are :
Implementing a queue
Resources
The task is to modify the code in queue.h
and queue.c
to fully implement the following functions
q_new
: Create a new, empty queueq_free
: Free all storage used by a queueq_insert_head
: Attempt to insert a new element at the head of the queue (LIFO discipline)q_insert_tail
: Attempt to insert a new element at the tail of the queue (FIFO discipline)q_remove_head
: Attempt to remove the element at the head of the queueq_size
: Compute the number of elements in the queueq_reverse
: Reorder the list so that the queue elements are reversed in order.Precautions
NULL
. An empty queue is one pointing to a valid structure, but the head
field has value NULL
malloc
to allocate space and then copying from the source to the newly allocated spaceq_insert_tail
and q_size
will require that your implementations operate in time q_reverse
should not allocate or free any list elements. Instead, it should rearrange the existing elementsqueue.h
依據題意,為了讓函式 q_insert_tail
和 q_size
的時間複雜度為 ,在 struct queue_t
裡新增成員 tail
和 size
,使得在 runtime 的時候可以動態紀錄 queue 的尾端和其大小
q_new
需注意若 malloc
失敗時需回傳 NULL
q_free
釋放 queue 的空間,注意記得一併釋放節點和字串所佔的記憶體空間
q_insert_head
分別建立節點和字串的空間,之後進行字串複製和節點的串接
需注意若 malloc
失敗時,在回傳 NULL 前需釋放之前已經配置好的記憶體空間
goto
來作錯誤處理,可參考
紀錄節點數量
q_insert_tail
大致上與先前的 q_insert_head
的實作方向相同
因題意規定其時間複雜度需為 ,這邊使用 tail
來串接新節點
撰寫「有品味」的程式碼
tail
的 data type 為 list_ele_t**
,使得可以減少 branch penalty
紀錄節點數量
q_remove_head
sp
不為 NULL 時代表需把移除的字串複製到 sp
中sp
的大小,因而最多只需複製長度為 bufsize - 1
,並注意要在結尾上加上 terminating null byteq_size
size
動態紀錄 queue 的大小,並在此函式直接回傳 size
的內容new
command 的情況下直接使用 size
command,所以需判斷若 queue 為 NULL queue 或 empty queue 則直接回傳數值 0可改寫上方程式碼為更簡潔的形式:
LGTM.
MetalheadKenFeb 22, 2019
q_reverse
prev
, curr
, next
來紀錄反轉的過程
prev
紀錄 queue 前一個的節點curr
紀錄 queue 當前的節點,在反轉過程中,curr
的下一個節點應指派為 curr
的前一個節點next
紀錄 queue 下一個的節點head
和 tail
需重設方向console.[ch]
: Command-line interpreter for qtestreport.[ch]
: Printing of information at different levels of verbosityharness.[ch]
: Customized version of malloc and free to provide rigorous testing frameworkqtest.c
: Testing code for queue codescripts/driver.py
: The driver program, run qtest
on a standard set of tracestraces/trace-XX-CAT.cmd
: Trace files used by the driver. These are input files for qtestMakefile
: Builds the evaluation program qtest
console.[ch]
會把所有的 command, parameter, file 用 singly linked list 串連起來。console.h
用以下 structure 來表示之
透過 add_cmd
函式可將 command 串連到 linked list 裡。可以發現在串接的時候順便進行按 alphabetical order 的 insertion sort
一開始使用者執行 qtest
後,輸入命令時,會先執行 console.c
中的 interpret_cmd
並呼叫 parse_args
來將輸入的字串以空白為區隔,轉換為 argc
和 argv
,如下
之後在函式 interpret_cmda
中,利用迴圈走訪 singly linked list,並利用 strcmp
找出函式相對應的字串。可以發現在 qtest
中使用函式指標的方式可以很簡單方便的維護使用者可輸入的命令
在 qtest
中有提供參數 -f <filename>
來讀取 trace file。當使用者輸入命令後,在 qtest.c
中的 main
函式會呼叫 getopt
來解析使用者先前輸入命令的後面所添加的參數,節錄如下
getopt
是一個可以很方便的去 parse 從 command line 傳過來的參數的函式,該函式會一直讀取參數直到沒有更多的參數可以讀時才回傳 -1
int getopt(int argc, char *argv[], const char *optstring)
int argc
: the argument count passed to the main()
functionchar *argv[]
: the argument array passed to the main()
functionconst char *optstring
: a string containing the legitimate option characters
getopt
回傳數值後
optstring
找不到 option 字元則回傳 ?
,在 optstring
設定需帶參數的 option 沒有帶參數的話則回傳 :
optarg
變數儲存該 option 後面所帶的參數optind
變數儲存下次呼叫 getopt
時要處理的 argv 的 indexoptopt
變數儲存當 getopt
找不到 option 字元或是缺少某些參數時的該 optiongetopt
是 POSIX standard 但是並不是 C standard 並在 glibc 實作中的某些行為是 GNU extensions,這點需要特別注意在 console.c
中定義以下 structure 來儲存每個要使用的檔案的 file descritptor
RIO在 CS:APP 第 10.5 章提及,全名為 Robust I/O
Ref: System-Level I/O (preview version)
並在 push_file
函式中把開啟檔案的 file descriptor 放進 struct RIO_ELE
中的 fd
,若無指定開啟檔案的路徑則選擇為標準輸入 (也就是 stdin
)
harness.[ch]
中藉由替換掉 malloc
和 free
的實作方式使得可以檢查到 allocation error
從 harness.h
中可以見到 qtest
使用 macro 來把 malloc
和 free
替換成 test_malloc
和 test_free
用 marco 來把函式庫替換成自定義的函式雖然是個簡單方便的用法,但是若使用其他的函式庫裡面有呼叫到 malloc
或 free
就無從追蹤了,如 strdup
就是一個例子。若想解決這個問題則可以使用 dynamic linker 來抽換掉函式庫中呼叫的 malloc
或 free
而從 test_malloc
所配置出來的記憶體都會被紀錄在 struct BELE
中,structure 在 harness.c
中定義如下並以 doubly linked list 來串接節點
在 structure 中 payload[0]
是一個 struct hack,在 GCC manual 中稱呼為 Arry of Length Zero,藉由這種方式可以做到讓 structure 中的成員可以動態的配置記憶體又不用因為多次呼叫 malloc
造成記憶體碎片化
用法如下
這樣的手法其實就是利用 GNU C compiler 預設不會去檢查 Array index out of bounds 的問題來做到的,但是使用這個技巧需要思考以下的問題
sizeof
不能用在 incomplete type 上clang does not support the gcc extension that allows variable-length arrays in structures. This is for a few reasons: one, it is tricky to implement, two, the extension is completely undocumented, and three, the extension appears to be rarely used.
另外在 ISO C90 可以使用 Array of Length One 來做到同樣的事情,而從 ISO C99 開始支援 Flexible Array Members,請參照
As a special case, the last element of a structure with more than one named member may have an incomplete array type; this is called a flexible array member.
test_malloc
把經由 malloc
配置出來的新的記憶體空間,將其串連到 doubly linked list allocated
中,其中配置出記憶體空間的寫法如下
可以看到除了 size
和 sizeof(block_ele_t)
以外,還多了 sizeof(size_t)
,這是因為在 qtest
中,藉由在尾端多配置出一個空間並填入 magic number 來查看若當該數值有被更動過的話,即表示出現 memory corruption (array access out of bounds),以及偵測是否是由 test_malloc
配置出來的記憶體空間,而前一個成員 magic_header
也是基於此用途的。harness.c
中的 test_malloc
節錄如下
在 fail_allocation
中,實作了當亂數產生出來的數小於 0.01 乘上預先設定好的失敗機率 (fail_probability
) 的話,即 malloc failure,為何需要如此實作?
MetalheadKen
其中為了方便取得到 payload
的尾端來指派 magic number (MAGICFOOTER
),qtest
實作了 find_footer
函式,如下
藉由傳進來的 block_ele_t*
的開頭,在加上 payload size 和該 structure 本身的大小來取得到 payload
的尾端
test_free
會把要釋放的記憶體空間從 doubly linked list allocated
移除,並在呼叫 free
來釋放該記憶體空間。該函式節錄如下
依照上述程式碼可以發現到呼叫 free
來釋放記憶體空間前會先把 magic number 指派為 MAGICFREE
,並把 payload
清為 FILLCHAR
,是因為在 glibc 的 malloc
和 free
實作中為了加速等原因,free
並不會清除該記憶體空間,且也並不會釋放給 OS,僅僅是修改該 memory chunk 的資訊以便之後再次使用
Ref: Malloc Tutorial
由於呼叫 free
的傳進來的參數為該記憶體空間 payload
,qtest
為了藉由 payload
取得到 block_ele_t*
的開頭,使得可以之後呼叫 free
來釋放整塊記憶體空間,實作了 find_header
,該函式節錄如下
總的來說,qtest
是藉由送出 signal 和使用 signal handler 來處理 exception
在 qtest.c
的 main
函式中可以發現呼叫了 queue_init
來設定 signal 和 signal handler,如下
signal
函式簡單來說就是當收到 signal number 時會呼叫相對應的函式,SIGSEGV
和 SIGALRM
意思如下
Signal | Value | Comment |
---|---|---|
SIGSEGV | 11 | Invalid memory reference |
SIGALRM | 14 | Timer signal from alarm() |
Ref: man pages
而對應 signal 的 handler 如下
其中,sigsegvhandler
和 sigalrmhandler
皆會呼叫到 trigger_exception
,該函式實作如下
可以發現到當進行 signal handler 時該函式會呼叫 siglongjmp
,而 siglongjmp
會跳到先前呼叫 sigsetjmp
的地方,為在 harness.c
的 exception_setup
當中
alarm(0)
會把正在 pending 的 alarm 給取消掉,而 report_event
是在 report.c
的函式,該用途為回報錯誤訊息
在上面的 exception_setup
中可以發現在呼叫 sigsetjmp
後會把 jmp_ready
指派為 true
,並呼叫 alarm(time_limit)
來設定 alarm clock
且對應的函式為 exception_cancel
,該函式的用途為把先前設定好的 alarm 給取消掉,並把 jmp_ready
指派為 false
其中以 qtest.c
中的 do_reverse
為例,可以發現到在呼叫 exception_setup
後呼叫 q_reverse
來進行 queue 反轉,但若沒在設定 alarm 的時間內 (time_limited
) 呼叫 exception_cancel
來把 alarm 關掉,在超過時間後即會發送 SIGALRM
,即為超時