contributed by < Risheng1128
>
作業要求
根據 list.h
參考函式 INIT_LIST_HEAD
,其功能為將 linked list 的 next
及 prev
指向本身,即初始化
malloc
分配記憶體空間,並由指標 new
指向malloc
會回傳 NULL
,此時回傳 NULL
INIT_LIST_HEAD
初始化這邊利用到了一個特殊的巨集 list_entry
,可以從 list.h
查到該巨集
可以看到 list_entry
會使用到 container_of
,參考 Linux 核心原始程式碼巨集: container_of可以得知 container_of
可以用來得到包含 ptr
之 object
的起始地址
head
為 NULL
→ return
linked list
,每經過一個節點,就對其使用 list_entry()
並釋放其空間head
的記憶體空間根據 list.h
參考函式 list_add
,查看原始碼可以得知其功能為把 node
節點加在 head
之後
malloc()
分配 element_t
大小的記憶體空間,若分配失敗則回傳 false
strlen()
取得字串長度,並分配要複製字串的空間
分配的空間大小要比字串長度多一個位元組,記得加入 '\0'
list_add()
將節點加在 head
的下一個
q_insert_head()
第 15 行trace-12-malloc.cmd
時,一開始都會有 memory leak 的問題,原本以為是 q_free()
有問題,結果最後發現問題是出在 q_insert_head()
和 q_insert_tail()
new
成功被分配空間而 new->value
分配空間失敗的情況new->value
分配失敗時記得要將 new
的空間也釋放掉q_insert_tail()
的思考幾乎都和 q_insert_head()
相同,兩者只差在插入的位置不同,也就使用不同函式
q_insert_head()
使用 list_add()
q_insert_tail()
使用 list_add_tail()
根據 list.h
參考函式 list_del
,其功能為將節點 node
從 linked list
上 remove,成為單獨的節點
head
為 NULL
或是 empty
,則回傳 NULL
list_entry
)並且 remove
該 elementbufsize
大小做複製q_remove_tail()
的作法和 q_remove_head()
大致相同,兩者只差在移除的位置不同
q_remove_tail()
: 被移除的節點位置為 head->prev
q_remove_head()
: 被移除的節點位置為 head->next
根據 list.h
參考巨集函式 list_for_each()
,其功能為走訪整個 linked list
利用 list_for_each()
可以完成 q_size()
rabbit
每一次迭代會走訪兩個節點turtle
每一次迭代會走訪一個節點rabbit
走到 head
或是 rabbit->next
為 head
表示已經抵達「終點」,此時 turtle
指到的位址剛好會是佇列正中間的節點list_del
可以將正中間的節點移出 linked listlist_entry
可以得到正中間 element 的地址,即可以進行記憶體空間釋放head
為 NULL
或 empty 或只有一個 element 時,則會回傳 false
curr
為指向當前節點的指標next
為指向 curr
下一個節點的指標key
表示是否發生資料相同的情況key
為 false
表示當前的 curr
和 next
沒有發生資料相同的情況key
為 true
表示當前的 curr
和 next
有發生資料相同的情況curr
和 next
的 element 地址
next
指向的節點進行移除並釋放空間,同時 key
被設置為 true
0
key
為 true
時,表示發生過資料相同的情況,但 curr
指到的節點尚未釋放,因此要記得釋放該空間
first
及 second
list_del_init
將 first
指向的節點從 linked list 取出list_add(first, second);
將 first
指向節點加到 second
指向節點的下一個位置,即兩節點交換位置first
或 first->next
為 head
時,表示已經到 linked list 的盡頭prev
, curr
及 next
,並依照 prev
→ curr
→ next
的順序指向不同節點next = curr->next;
(next
指向 curr
的下一個節點 )curr->next = prev;
( curr
的 next
指向原本的上一個節點 prev
)curr->prev = next;
( curr
的 prev
指向原本的下一個節點 next
)prev = curr;
( prev
往下移動一個節點 )curr = next;
經過這六個步驟,可以發現 head
的 prev
和 next
的變化
head
的 next
從指向 bear
(第一個 element)改成指向 meekat
(最後一個 element)head
的 prev
從指向 meerkat
(最後一個 element)改成指向 bear
(第一個 element)參考 quick-sort.c 使用 Linux Kernel API 實做 quick sort
參數介紹
less
: 存放資料比 pivot
小的 linked listgreater
: 存放資料比 pivot
大或相同的 linked list首先將 pivot
設定為 linked list 的第一個節點,並且從 linked list上移除
接著走訪整個節點,並讓每個節點和 pivot
的資料互相比較(使用 strcmp()
並根據其結果決定將節點加在 less
或是 greater
)
使用遞迴分別將 less
及 greater
進行比較和分割
將每次分割好的節點依照 less
→ head
→ greater
重組起來
結果 trace-14-perf.cmd
和 trace-15-perf.cmd
沒有通過QQ
注意排序操作期待是 stable sorting
jserv
好的,那學生試試看使用 merge sort 實作,不過想請問老師為什麼要使用 stable sorting ,是因為 unstable sorting 的效能比較不好,還是有其他的原因,謝謝老師!
「效能」只是評估的一個因素,stable sorting 和 unstable sorting 的應用場景不同,請回去讀書。
jserv
了解老師的意思了,謝謝老師
決定改成用 merge sort 試試看
merge sort 的實作被拆為三個函式 q_sort()
mergesort()
及 mergelist()
q_sort()
head
的指標改成 NULL
,目的在於把 doubly linked list 的終點從 head
改為 NULL
,變成單向 linked list
mergesort()
開始進行 merge sort
prev
會亂掉,因此需要走訪各個節點把所有 prev
接回來
mergesort()
left
及 right
兩個 linked list
mergelist()
合併 left
及 right
mergelist()
indirect
指向 pointer res
,並藉由修改指標完成 linked list 合併的動作
strcmp
判斷資料的大小
作業要求
為了讓篇幅小一點,這邊再開了一個新的筆記用來紀錄分析過程: 研讀 Linux 核心原始程式碼 list_sort
為了引入 Linux 核心原始碼,必須要先了解 Linux 核心的 merge sort 整個的運作流程,已將整個分析放在額外的筆記本上,根據分析結果,以下是我規劃的實作流程
實作流程
list_sort.h
qtest.c
新增函式 cmp()
qtest.c
的 do_sort()
函式建立 list_sort.c
及 list_sort.h
,前者包含 Linux 核心原始碼,後者包含必要的定義
首先需要將 lab0-c 所沒有的定義加進 list_sort.h
,如下所示
list_cmp_func_t (位於 include/linux/list_sort.h)
list_sort (位於 include/linux/list_sort.h)
likely() (位於 include/linux/compiler.h)
unlikely() (位於 include/linux/compiler.h)
以下為 list_sort.h
的程式碼
接著在 makefile 的 OBJS
新增 list_sort.o
在 qtest.c
上建立函式 cmp()
採雷小紀錄: 修改 list_sort()
的 prototype ,移除 __attribute__((nonnull(2,3)))
原因: 在測資 trace-10-robust.cmd
, 會輸入 NULL
給 head
,原本在 list_sort
裡新增以下程式,但編譯器會編不過,錯誤訊息如下
解法: 移除 __attribute__((nonnull(2,3)))
即可
最後為了可以切換 list_sort()
及 q_sort()
,在 list_sort.h
新增新的巨集 USE_LINUX_SORT
→ 如果 USE_LINUX_SORT
為 1
,則使用 list_sort()
,反之則使用 q_sort()
修改 qtest.c
裡的函式 do_sort()
,如下所示
最後結果: q_sort()
及 list_sort()
都可以正常執行
首先這邊新增了一個測資 trace-sort.cmd
,如以下所示,使用 qtest
提供的命令 time
可以算出排序使用的時間
輸入命令 make check
測試的示意如下 (這邊的排序是自己寫的 q_sort()
)
接著可以統計出 q_sort()
和 list_sort()
的排序的時間,這邊每一組測試三次
資料總數 | q_sort() 第一次 (s) |
q_sort() 第二次 (s) |
q_sort() 第三次 (s) |
list_sort() 第一次 (s) |
list_sort() 第二次 (s) |
list_sort() 第三次 (s) |
---|---|---|---|---|---|---|
100000 | 0.130 | 0.128 | 0.129 | 0.085 | 0.084 | 0.084 |
300000 | 0.504 | 0.496 | 0.492 | 0.305 | 0.306 | 0.306 |
500000 | 0.905 | 0.907 | 0.900 | 0.546 | 0.543 | 0.548 |
最後統計效能差異
資料總數 | q_sort() 平均 (s) |
list_sort() 平均 (s) |
list_sort() 和 q_sort() 效能比較 (%) |
---|---|---|---|
100000 | 0.129 | 0.084 | 34.88% |
300000 | 0.497 | 0.306 | 38.43% |
500000 | 0.904 | 0.546 | 39.60% |
接著探索 list_sort()
比較快的原因,這邊參考Linux 效能分析工具: Perf
這邊採用的資料點為 500000 筆
輸入命令 perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./qtest -f traces/trace-sort.cmd
→ 程式會執行 5 次,並且自動平均,以下為 q_sort()
及 list_sort()
執行的結果
首先是 q_sort()
的結果
接著是 list_sort()
的結果
將輸出的結果做成表格,如下表所示
q_sort() |
list_sort() |
|
---|---|---|
cycles | 3,211,924,749 | 2,235,674,206 |
instructions | 1,465,693,251 | 1,461,928,852 |
cache-references | 106,751,677 | 81,780,080 |
cache-misses | 74,305,695 | 56,981,650 |
insn per cycle | 0.46 | 0.65 |
這邊可以發現很有趣的事
list_sort()
的 cache miss 比 q_sort()
來的少很多作業目標
由於不知從何下手,因此起頭參考laneser的開發紀錄
command 是「命令」,instruction 是「指令」,二者語意不同。
jserv
已修正!
使用命令 valgrind -q --leak-check=full ./qtest -f traces/trace-01-ops.cmd
對檔案 trace-01-ops.cmd
進行測試
可以看到有出現 still reachable 類型的 memory lost
still reachable: 程式結束時有未釋放的記憶體,不過卻還有指標指著,通常會發生在 global 變數
跟著錯誤訊息的追隨路徑(main
→ linenoiseHistoryLoad
→ linenoiseHistoryAdd
)可以找到沒有被釋放的記憶體空間
錯誤訊息一共發生兩種
linenoiseHistoryAdd
第 10 行的位置,即 history = malloc(sizeof(char *) * history_max_len);
linenoiseHistoryAdd
第 22 行的位置,即 linecopy = strdup(line);
在終端機輸入命令 man strdup
可以得到其訊息,其回傳的空間會經過 malloc()
The strdup() function returns a pointer to a new string which is a duplicate of the string s. Memory for the new string is obtained with malloc(3), and can be freed with free(3).
查看了 linenoiseHistoryAdd
的原始碼後,發現 histroy
分配的每個指標指到的空間為 linecopy
分配的空間,因此只要將整個 history
釋放即可
接著在 lineoise.c
中發現 API linenoiseAtExit()
包含 freeHistory()
,可以用來釋放 history
實作步驟
static void linenoiseAtExit()
改成 void linenoiseAtExit()
,目的在於讓 console.c
裡的 finish_cmd()
可以呼叫該 API ,最後可以把 history
釋放linenoiseAtExit()
的宣告從 linenoise.c
移到 linenoise.h
console.h
裡 include linenoise.h
finish_cmd()
裡呼叫 linenoiseAtExit()
重新測試 valgrind -q --leak-check=full ./qtest -f traces/trace-01-ops.cmd
可以看到原本的錯誤訊息已經消失,表示 memory leak 已經解決
接著從第一筆測資測試到最後一筆測資
在測試到 trace-14-perf.cmd
時發生其他的問題,這邊先用最後出現問題的地方將不同的錯誤訊息分類
錯誤訊息一共有五種
report.c
的函式 calloc_or_fail()
,即 void *p = calloc(cnt, bytes);
,類型為 still reachablereport.c
的函式 strsave_or_fail()
,即 char *ss = malloc(len + 1);
,類型為 still reachablereport.c
的函式 malloc_or_fail()
,即 void *p = malloc(bytes);
,類型為 still reachableharness.c
的函式 test_malloc()
,即 block_ele_t *new_block = malloc(size + sizeof(block_ele_t) + sizeof(size_t));
,類型為 definitely lost
definitely lost: 真的 memory leak
qtest.c
的函式 do_sort
,即 if (strcasecmp(item->value, next_item->value) > 0) {
,類型為 Invalid read
Invalid read: 表示被讀取的地址為 process 不可以讀取的地址, size 8 表示 process 想要讀取 8 bytes
最後發現,以上全部的訊息是因為超出測試的時間,程式進中斷後結束時沒有把記憶體釋放而產生
首先查了一段時間後發現在 harness.c
裡有一個變數 time_limit
,將其數值增加(例如改成 5
) ,以上的問題都會解決
以 time_limit = 5
重新輸入命令 valgrind -q --leak-check=full ./qtest -f traces/trace-14-perf.cmd
測試後,可以發現通過測試,且之後測資經過測試後也都通過
最後透過 massif 查看記憶體的狀況,輸入命令 valgrind -v --tool=massif ./qtest -f traces/trace-01-ops.cmd
→ 產生了名為 massif.out.81335
的檔案
接著使用 massif-visualizer 打開檔案,輸入命令 massif-visualizer ./massif.out.81335
,以下為最終結果
可以看到建立的動態記憶體最後歸為 0
,表示所有記憶體已被釋放
作業目標
qtest
提供新的命令 shuffle
,允許藉由 Fisher–Yates shuffle 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作為了了解怎麼新增命令,這邊先分析整個 qtest 是如何從得到命令一直到使用 console.c
來添加以及執行命令的運作流程,並做紀錄
首先,從 main
可以得知 qtest 是如何接收使用者給出的命令,其中使用到了函式 getopt()
,輸入命令 man getopt
可以得到其描述
getopt is used to break up (parse) options in command lines for easy parsing by shell procedures, and to check for valid options. It uses the GNU getopt(3) routines to do this.
得知 qtest 如何得到命令後,接著繼續了解 console.c
如何添加命令
找到函式 console_init()
可以發現裡頭加入了所有佇列會用到的所有命令,因此可以在這裡定義 shuffle
命令
,開始分析巨集函式 ADD_COMMAND
,可以發現 ADD_COMMAND
被定義為, add_cmd(#cmd, do_##cmd, msg)
查看函式 add_cmd()
,可以看到一個有趣的結構 CELE
(位於 add_cmd()
第 10 行, cmd_ele
為結構 CLEL
的變數)
發現該函式做的事情就只是把輸入的命令存到一個名為 CELE
的結構 (從 11 行到 14 行)
分析結構 CELE
後,可以發現 CELE
為 linked list 的結構形式, 其中比較重要的變數為 operation
,被定義為函式指標,存放不同命令的函式地址。
由此可知,在一開始初始化的時候,所有命令會被存到一組 linked list 裡
最後開始分析 console.c
是如何執行命令
在 main
找到函式 run_console()
,仔細觀察原始碼後可以得知,執行命令一共分為兩種模式
run_console()
的第 8 行到第 15 行)run_console()
的第 16 行到第 19 行 )首先先討論第一種模式,在 run_console()
第 11 行可以找到函式 interpret_cmd()
,用途在於解析使用者輸入的命令並執行
parse_args()
interpret_cmda()
接著探討很重要的用來執行命令的函式 interpret_cmda()
,可以歸納以下幾點
interpret_cmd()
的第 9 行和第 10 行)interpret_cmd()
的第 12 行)接下來換成討論第二種執行情況,也就是直接讀取檔案並執行
從之前提到的函式 run_console()
,查看函式 cmd_select()
,可以歸類以下幾點
select.h
的函式 select()
詳細資料之後補上結論
getopt()
取得使用者對 qtest 的輸入console_init()
裡一路追蹤,得知了所有的命令一開始都會被儲存在一個 linked list 上從上述的分析,可以得知 qtest 從執行、初始化命令到執行命令的過程,並想出了增加命令的實作方法,如下所示
實作步驟
do_shuffle()
,作為命令 shuffle
的執行函式q_shuffle()
,用來實現 Fisher–Yates shuffle 演算法console_init()
裡新增 shuffle
命令在 qtest.c
新增函式 do_shuffle()
由於 queue.h
無法更改,因此將 q_shuffle()
宣告在 do_shuffle()
的上面
這邊參考 Fisher–Yates shuffle 裡提到的 Pencil-and-paper method (這邊使用 Fisher and Yates' original method) ,流程如下表所示
Range | Roll | Scratch | Result |
---|---|---|---|
A B C D | |||
1~4 | 2 | A C D | B |
1~3 | 3 | A C | B D |
1~2 | 1 | C | B D A |
1 | 1 | B D A C |
number
決定目前要取第幾個 element在 console_init()
裡新增 shuffle
命令
接著輸入命令 ./qtest
以及 help
,可以看到所有可使用的命令,可以發現 shuffle
已被加入
作業要求
首先查看了 tiny-web-server 後,發現有些步驟會用到 tiny-web-server
的函式 ,因此把 tiny.c
額外分出兩個檔案 tiny_function.c
及 tiny_function.h
,以下為 tiny_function.h
程式碼
tiny_function.h
修改 Makefile ,讓 lab0-c 可以使用 tiny-web-server 的函式,另外這邊新增命令 make tiny
,保留原本 tiny-web-server 的功能
開始新增命令 web
,在 init_cmd()
新增以下程式碼
另外,由於會使用到的函式分散在不同檔案,這邊宣告一些全域變數(位於 linenoise.h
)
實作函式 do_web()
,這邊參考 tiny.c
的函式 main()
的作法,使用預設的 port 9999
接著的步驟幾乎都是參考作業說明,跟著整合 tiny-web-server 的步驟慢慢做
修改 run_console()
,使其可以選擇使用 linenoise()
或是 cmd_select()
修改 cmd_select()
繼續照著作業說明實作,在 linenoiseEdit
新增程式碼
最後新增函式 url_process()
,透過 HTTP 的 GET method 傳送要執行的命令,這邊參考原本 tiny-web-server 的實作,不同在於 tiny-web-server 會讀取所在目錄的檔案,而 url_process()
指單純開啟空白頁面,實作如下
最後結果如以下所示,可以成功開啟網頁,以及接收使用者命令
不過還有一個小問題,開啟 server 之後,會經過很久的時間才能從終端機輸入命令,目前還沒有解決
這些都在該 git log 描述,不需要在此提及,有 git commit 簡述即可。
jserv
已修正!