contributed by < akamayu-ouo >
qtest
qtest
linenoise
works將記憶體管理集中以減少重複的程式碼,出錯時也容易檢查。在這份作業中 queue_t
以及 list_ele_t
都需要動態配置記憶體,前者的記憶體管理由 q_new
和 q_free
兩個函式負責;後者由我自己增加的兩個函式負責。分別是:
list_ele_t *new_element(char *, list_ele_t *)
:list_ele_t*
,若輸入的字串為 NULL
或有其他錯誤,都輸出 NULL
。list_ele_t *del_element(list_ele_t *e)
:e
與 e->value
的記憶體,回傳 e->next
以方便使用。這個函式假設輸入不會是 NULL
值得注意的是根據 free(3)
The free() function frees the memory space pointed to by ptr, which must have been returned by a previous call to malloc(), calloc(), or realloc(). Otherwise, or if free(ptr) has already been called before, undefined behavior occurs. If ptr is NULL, no operation is performed.
free
在輸入為 NULL
時不會出問題,因此不做檢查。
queue_t
structure增加兩個變數: size
與 tail
動態地更新 size
與 tail
以達成常數時間的 q_size
以及 q_insert_tail
。
q_new
與 q_free
q_new
只有在配置記憶體成功的時候初始化內容。q_free
則從 head
開始依序使用 del_element
返還記憶體,最後再釋放佇列本身的空間。
定義一個 static 函數來簡化實做。參數 link
指向「要新增元素的記憶體位置」。
*link
*link
並回傳 false
tail
,回傳 true
如此一來, q_insert_head
與 q_insert_tail
都能簡化成一行
q_remove_head
將佇列的第一個元素去除,並交由 del_element
釋放其空間。並在 sp
不為 NULL
的時候將元素中的字串複製過去。
這邊有個地方不是很清楚,當 sp
不為 NULL
但元素字串是 NULL
時應該將 sp
更新為 NULL
、空字串 \0
抑或是完全不動它呢?還是我們能保證字串一定存在?但插入函式也都沒有定義字串不存在時的行為?
我目前的想法是:只要我保證佇列的 API 不會產生沒有字串的 list_ele_t
,那遇到這個情形就一定是使用者亂搞,什麼都不動就好。
附上原始的註解如下:
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.
可以利用 q_size(q)<n
將「佇列不存在」與「佇列長度小於 n 」的檢查合而為一,讓 edge cases 的處理更加簡潔。
先將 linked list 反轉,再更新 head
與 tail
。
基本上使用 merge sort 實做(一開始嘗試用 quick sort 硬幹,但忘記它最差情況會需要花 的時間,測試直接爆掉)。主要分為三個步驟:
head
出發,當兔子跑過整條 linked list 的時候烏龜會停在 linked list 中央。實做中另外定義了一個 struct range_t
這樣在傳遞參數的時候比較簡潔,程式碼也比較整齊。
qtest
用 make SANITIZER=1
編譯 qtest
。再於 qtest
中執行 help
得到了以下的錯誤:
從輸出第四行可知錯誤發生在 console.c 的第 306 行,並且是錯誤地存取了在 echo
這個變數的附近的地址。
變數 echo
宣告在第 59 行。
檢查程式碼,發現在 init_cmd
中 bool*
被轉成 int*
,造成之後在印出時被當作 int
讀取,讀到沒有配置的空間。
會需要轉成 int*
的原因推測是因為 PELE
中的 valp
是整數指標。
除了 echo
以外,還有一個變數 simulation
也有同樣的情況。將它們都改宣告為 int
,並移除型別轉換就好了。
然而這顯現了另一個問題,若將上述的兩個變數設定成其他整數,程式也不會出錯。但既然是「開關」行為的變數,值域應該只有 0
與 1
。要解決這個問題,可以在 parse 命令的時候檢查,也可以對有 bool
數值的選項另外增加 struct,但這兩個選項都會需要更動很多地方。
測試後發現當 .cmd_history
有東西的時候使用 qtest -f <FILENAME>
會出現 memory leak 。
從錯誤訊息推測是 history
沒有被釋放。也能發現出問題的記憶體是由 linenoiseHistoryAdd
配置。
有可能會釋放 history
的函式只有 linenoise.c
中的 freeHistory
以及前文提到的 linenoiseHistorySetMaxLen
。後者只有在 history
不為 NULL
時會重新配置記憶體,而它在主程式中被呼叫時 history
還未被初始化,因此它並不會對記憶體進行操作,僅僅是更新 history_max_len
而已。因此只有可能是 freeHistory
沒有被呼叫造成錯誤。
上圖是有機會執行到 freeHistory
的函式的函式 ,橢圓形表示該函式是 static 函式。
其中函式 enableRawMode
將 linenoiseAtExit
交給 atexit
註冊
根據 man page ,經由 atexit
註冊的函式會在主程式正常結束時被呼叫。
The atexit() function registers the given function to be called at normal process termination, either via exit(3) or via return from the program's main(). Functions so registered are called in the reverse order of their registration; no arguments are passed.
換句話說,若要將 history
釋放掉,程式執行期間至少要呼叫 enableRawMode
一次。否則 linenoiseAtExit
不會被註冊,主程式結束時也不會將空間歸還,造成 memory leak。
因為 enableRawMode
只有透過 linenoisePrintKeyCodes
或 linenoise
才能呼叫到,但前者沒有在任何地方被呼叫,後者只被 run_console
呼叫。然而run_console
只在沒有輸入檔案時會呼叫到 linenoise
。
以下是使用 gprof
紀錄到被呼叫的函式名稱(輸入內容都是 ./trace-13-perf.cmd
)。
$ ./qtest -f <FILENAME>
STDIN
輸入: $ cat <FILENAME> | ./qtest
可以看到前者沒有呼叫到 linenoise
。
若讀檔作為輸入,那就沒有紀錄歷史的必要,也不需要自動補全命令。在 qtest.c
中不呼叫自動補全以及跟歷史紀錄有關的設定。只要不拿記憶體,就不會有忘記釋放的問題。
TODO
我們先來看 select
的用途。根據 select(2) , select
是用於等待 檔案描述符(File descriptor)可動作的函式。
select() allows 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), or a sufficiently small write(2)) without blocking.
select()
能在時限(參數 timeout
)中等待「讀」、「寫」以及「例外」三組檔案描述符(分別為參數 readfds
、 writefds
與 exceptfds
),而變數 nfd
則是那三組描述符的數值上界。
當 select()
執行完成時,三組描述符中都只會剩下可動作的描述符(使用巨集 FD_ISSET
確認),回傳值則是總共可動作的描述符的數目。
console.c
中的 cmd_select()
是 select()
的 wrapper 。 它將 buf_stack
最上層的檔案描述符加入 readfds
,再使用 select()
確認其是否準備好了。若該描述符已經可以被讀取,當下又是在讀取檔案的話,cmd_select
會直接讀取並執行命令。
而 readline()
顧名思義就是從檔案描述符中讀取命令的函式。其中使用到與 CS:APP RIO 套件相同的邏輯——每次使用核心函式取得一塊輸入,暫存在記憶體中,在用完前都在 user space 讀取,不夠再去跟核心要資料,達到減少呼叫核心函式的目的。
使用 strace 來觀察程式呼叫核心函式的行為
先忽略輸出的前兩行,可以看到共呼叫 read
兩次,第一次讀取了 125 個位元組,這與輸入檔案的大小相同。而 8192 則是暫存區大小(定義於 console.c
的第 39 行)。
前兩行的 read
是動態載入器( dynamic loader )在載入動態函式庫。
其中 libm.so.6
是數學函式庫; libc.so.6
是 GNU 的 C 函式庫。檔案內容中的 ELF 來自「Executable and Linking Format」,是 Unix 與 類 Unix 系統的執行檔格式。
TODO
To Be Continued …