contributed by < bonianlee
>
作業要求
建立新的「空」佇列,將其 linked list 的 next
與 prev
指向本身
實作流程
malloc
分配記憶體空間,並以 head
指向它malloc
會回傳 NULL
並被 head
指向,此時讓 q_new
回傳 NULL
if (head)
判斷式會執行,參考 list.h
中的函式 INIT_LIST_HEAD
,將其 linked list 的 next
與 prev
指向本身,做初始化的動作list.h
參考函式 INIT_LIST_HEAD
釋放所有由佇列佔用的記憶體空間
實作流程
list_head *l
不存在 (NULL
) ,則不執行記憶體釋放list_head *l
存在,參考 list.h
中的函式 list_empty
,判斷 linked list 開頭 (head) 有沒有接著其他的 node ,若有則使用 q_remove_head
與 q_release_element
來連續清除記憶體空間,最後再將「空」佇列 l
佔用的記憶體空間清除實作程式碼
列出對應的 GitHub commit 超連結即可。
:notes: jserv
q_remove_head
q_release_element
:::
在佇列開頭 (head) 插入 (insert) 給定的新節點
實作流程
malloc
配置 element_t
的記憶體空間,若空間分配失敗,則回傳 false
strlen
計算引數 s
的字串長度 (包含空格) ,作為分配記憶體空間給 element->value
的依據,如果 value
的記憶體配置失敗,記得要先將 element
的空間釋放掉,才可以回傳 false
strncpy
將 s
複製給 element->value
,再參考 list.h
中的 list_add
將 element
插入 (insert) 到佇列開頭 (head)list.h
參考函式 list_add
在佇列尾端 (tail) 插入 (insert) 給定的新節點
實作流程
重複 q_insert_head
實作流程的 1 , 2 步驟,惟步驟 3 改成使用 list_add_tail
將新的 element
插入 (insert) 到在佇列尾端 (tail)
list.h
參考函式 list_add_tail
在佇列開頭 (head) 移去 (remove) 給定的節點
實作流程
head
不存在,或者為「空」佇列,則回傳 NULL
list_first_entry
取得開頭的 element ,並使用 strncpy
將 element->value
複製給 sp
list_del_init
將開頭 (head) 的 element 移去 (remove)實作程式碼
list_first_entry
與 list_del_init
程式碼lsit_first_entry
list_del_init
在佇列尾端 (tail) 移去 (remove) 給定的節點
實作流程
重複 q_remove_head
實作流程的步驟,惟步驟 2 的 list_first_entry
改成使用 list_last_entry
,取得尾端 (tail) 的 element
list.h
中的 list_last_entry
計算目前佇列的節點總量
head
不存在,則傳回 0list.h
中的巨集 list_for_each
逐一走訪鏈結串列,每存取到 temp
則遞增 count
,最後回傳 count
踩雷紀錄:迭代所使用的指標 temp
不要指向另外 malloc
的記憶體空間,因為若沒有 free
掉 temp
所佔用的記憶體空間,當 $ make test
執行重複呼叫 q_size
的 Trace 測試檔時,會導致 q_free
後仍有記憶體空間被佔用而沒被釋放掉,將 temp
改成指向 NULL
,則不會佔用到記憶體空間
移走佇列中間的節點並釋放之
參考你所不知道的 C 語言: linked list 和非連續記憶體
思考方向
fast
和 slow
指向 head->next
,其中 fast
的前進速度較快,一次走兩格節點,而 slow
的速度較慢,一次走一格節點,如此一來,只要當 if (fast == head || fast->next == head)
判斷式成立,指標 slow
所指向的節點即為佇列中間的節點,將之移走 (remove) 並且釋放掉 (release) 佔用的記憶體空間即可struct list_head
是雙向的 linked list ,所以除了使用龜兔賽跑那樣同向的指標移動之外,還可以從頭跟尾互相接近的方式來找出佇列中間的節點位置,這種策略能夠減少指標移動的次數實作流程 (採用頭尾逼近)
head
至少要接一個節點,才可以執行 delete mid 的動作,因此當 if(!head || list_empty(head))
判斷式滿足,則傳回 false
forward
與 backward
, forward
指向 head->next
,而 backward
指向 head->prev
,我使用 while
迴圈來不斷移動指標,若滿足判斷式 forward != backward && forward != backward->prev
,則使 forward = forward->next
和 backward = backward->prev
,即互相靠近,並且每次只前進一個節點forward == backward
head
外,連接了奇數個節點,則這種情形就會發生,此時 forward
和 backward
指向的節點即為佇列中間的節點forward == backward->prev
head
外,連接了偶數個節點,則這種情形就會發生,此時 backward
指向的節點即為佇列中間的節點if
判斷式做區別,即可找出佇列中間的節點,並做移去 (remove) 與釋放 (release) 記憶體的動作,這邊我使用 list.h
中的 list_del
與 list_first_entry
,再搭配 q_release_element
來完成踩雷紀錄: list.h
內的 list_del
做的事情只有將節點從 linked list 中孤立出來,並沒有做記憶體釋放的動作,因此我使用 list.h
中的 list_first_entry
找出佇列中間 element 的前一個 element 的地址,再用 q_release_element
釋放掉佇列中間 element 佔用的記憶體空間
在已經排序的狀況,移走佇列中具備重複內容的節點並釋放它們
注意資訊科技詞彙翻譯
:notes: jserv
遍歷:著重在「完全」走訪每個節點
本項 q_delete_dup
的實作目的是要找出特定內容的節點並釋放之,使用「遍歷」可能會令焦點模糊不清,已修正不當的詞彙描述,謝謝教授!
:cat2: ccasdqwe
實作流程
head
指向的「空」節點外,至少還要連接兩個以上的節點才有可能發生重複內容的狀況,因此先做例外排除 if (!head || list_empty(head) || list_is_singular(head))
,若滿足判斷式就傳回 false
curr
和 safe
,用來作為 list_for_each_safe
的引數,另外還宣告了布林變數 diff
,用來在之後的迭代過程判斷是否要繼續執行刪除的動作,將它初始化為 false
list.h
中的巨集 list_for_each_safe
來做 curr
與 safe
的curr->next
的指標 next
,並宣告 curr_entry
與 next_entry
分別指向它們所屬的 element!strcmp(curr_entry->value, next_entry->value)
用來比較 curr_entry
與 next_entry
的 value
是否相同,若相同 strcmp()
會回傳 0 ,加入邏輯運算子 !
就會得到 true
的結果,如此一來,當 if 判斷式滿足,即代表兩個相鄰 element 的 value
重複出現,此時將 curr
移去 (remove) 並且釋放掉 (release) 它占用的記憶體空間,最後令 diff = true
,代表下一個 element 也是待釋放的目標if ((next != head) && (!strcmp(curr_entry->value, next_entry->value)))
不再滿足,且 diff
仍為 true
,代表 curr
所指向的 element 為連續重複內容的最末端 element ,將它釋放掉後,令 diff = false
代表已刪除完其中一組重複內容的 element實作程式碼
交換佇列中鄰近的節點
實作流程
q_swap
是在進行節點兩兩對調的動作,因此佇列除了 head
指向的節點外,若沒有額外連接兩個以上的節點,就無法執行對調的動作,故先使用 if
判斷式做例外處理left
與 right
,分別指向 head->next
跟 head->next->next
,方便之後進行節點對調的動作next
與 prev
的重新指向
將 head
指向的節點改成與 2 號節點相鄰, 1 號節點接在 2 號節點的後面, 3 號節點再去與 1 號節點做 next
與 prev
的重新指向,最後成功對調 1 號與 2 號節點,此時 right
指向 2 號節點,而 left
則指向 1 號節點
移動指標並重複步驟 3
使用判斷式判斷 left->next
跟 left->next->next
是否為 head
所指向的節點,若滿足其中一個,則跳出 while
迴圈,代表剩餘的節點數已經不足 2 個,無法進行對調的動作
如果 left->next
跟 left->next->next
都不是 head
所指向的節點,代表佇列尚未完全地完成兩兩對調,故移動指標並繼續進行步驟 3 的對調動作,直到跳出 while
迴圈的條件被滿足為止,最終結果如下圖
實作程式碼
以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點
實作流程
例外處理
q_reverse
的目的是要將佇列以反向順序重新排列 (除了 head
所指向的節點之外) ,因此至少要額外連接 2 個以上的節點,才可以執行此動作,故先排除掉可能發生的例外
排列節點
q_reverse
可以使用 list.h
中的 list_for_each_safe
與 list_move
很簡單的達成,方法是透過 list_for_each_safe
遍歷 –->逐一走訪佇列,過程中用 list_move
不斷把節點移至開頭 (head) ,過程如下圖所示
實作程式碼
以 k 個節點為小組 (k-group) 進行分組,並以反向順序重新排列小組內的鏈結串列
實作流程
例外處理
q_reverseK
會進行反向順序的重新排列,因此除了 head
所指向的節點外,至少還要連接兩個以上的節點,才能執行排列的動作,故先做例外排除
同理, k < 2
無法執行排列的動作,因此也要做例外排除
k == 2
與 k > 2
的情形
k == 2
時 q_reverseK
的功能與 q_swap
相同,是在進行節點兩兩對調的動作,因此直接呼叫 q_swap
即可
若 k > 2
,我的作法是先數好總共要做幾次的反向順序重排,再由 for
迴圈執行相同的反向順序重排流程,其中變數 count
代表 head
以外的節點總數, repeat_times
則為反向順序重排流程所要執行的次數
k 個節點為小組進行反向順序重新排列
宣告指標 sub_head
與 cut_pos
,分別都指向 head
,它們之後會作為引數提供給 list_cut_position
跟 list_splice_init
使用
為了圖形美觀,環狀佇列頭尾相連的鏈結省略不畫
假設 k = 3 ,則移動指標 cut_pos
至節點 3 的位置,並以 list_cut_position
將節點們移至另一個佇列
使用 q_reverse
將 head_temp
為頭端 (head) 的鏈結串列進行反向順序重新排列
使用 list_splice_init
,將 head_temp
為頭端 (head) 的鏈結串列除了 head_temp
指向的節點以外,其他的節點移回原佇列
最後移動指標 sub_head
與 cut_pos
,若 ( count / k ) > 1
,則重複上述的流程
實作程式碼
以遞增順序來排序鏈結串列的節點
參考 < 你所不知道的 C 語言: linked list 和非連續記憶體 > 與 < Merge Sort 與它的變化 >
head
所指向的節點外,至少要連接 2 個以上的節點才能夠進行排序,因此做例外處理next
指向 NULL
,即斷開 next
方向的環狀結構,再使用撰寫好的 merge_sort
進行遞增順序的排序merge_sort
過程中切斷的 linked list 指標重新指向若佇列中任意節點的右端存在嚴格大於 (strictly greater) 的值 (value) ,則將該節點移去 (remove)
參考 < KHLee529 > 的實作
實作流程
element->value
的大小,過程中不斷更新 (或者說紀錄) 擁有最大值的字串,並由 max
指向它,當碰到比 max
大小還要小的 value
,則將之移除 (remove and release)total
與 n_del
分別紀錄 element 總數和移除的 element 總數,最後傳回兩者之差,即為剩下的 element 總數實作程式碼
將所有佇列合併 (merge) 成一個以遞增順序排列好的鏈結串列
參考 < chiangkd > 的實作
list_for_each_entry_safe
逐一走訪完每個節點,每當存取到 c_cont
就斷開部分連結,並以 mergeTwoLists
將 sorted
與 c_cont->q->next
合併prev
被 mergeTwoLists
打亂了,只有 next
的指向是正確的list_splice
將處理完的鏈結串列交還給 head
所屬的 element將佇列從中間切斷,產生的兩個子佇列再個別切斷中間的鏈結,以此種方式迭代,直到切成單個節點為止,再以 mergeTwoLists
進行遞增順序的合併
將兩個串列以遞增的順序合併在一起
list_sort.c
>作業要求
參考 < Risheng1128 > 的 < 研讀 Linux 核心原始程式碼 list_sort.c >
研讀原始程式碼 list_sort.c
發現主要是由三個函式 merge()
、 merge_final()
及 list_sort()
組成,接著要個別分析它們的作用
在分析之前,先探討函式屬性 __attribute__((nonnull()))
的作用,因為三個函式在宣告函式原型時都擁有這個屬性,參考 < __attribute__((nonnull))
function attribute > 可以得知 nonnull
的作用是當特定函式的引數為 NULL
時,編譯器會發出警告訊息
This function attribute specifies function parameters that are not supposed to be null pointers. This enables the compiler to generate a warning on encountering such a parameter.
merge()
merge()
原始碼cmp
分析list_cmp_func_t
,根據 < linux/include/linux/list_sort.h > ,得知 cmp
是一個函式指標cmp
指向回傳型態為 int
的函式,該函式的引數為 void *
和 2 個 const struct list_head *
宣告指標 ( line 5 )
宣告兩個指標,一個是指向 struct list_head
資料型態的指標 head
,另一個則是指向指標的指標 tail
,並且將它初始化指向 head
的地址
for 迴圈 ( line 7 )
沒有設定判斷式,為無限迭代的迴圈,使用 cmp
來判斷下一個節點是要從 a
或者 b
來取得,假如 linked list a
已經沒有節點時,則接上 linked list b
,反之,假如 linked list b
已經沒有節點時,則接上 linked list a
merge_final()
將所有節點的 prev
接回前一個節點
list_sort()
首先閱讀此函式的註解
由此可知,此 merge sort 總是等到 linked list 的長度比例為 2 : 1 時才會開始進行合併的動作,舉例來說,有兩個大小為 的 linked list 時, merge sort 不會直接進行合併,而是會等到有第三個 時,才開始合併,並且是以 和 兩個 linked list 進行合併,滿足前述的 2 : 1
若以這種方式進行 merge sort ,假如被合併的 linked list 都可以被放進 cache 裡,則可以避免 Cache thrashing 的問題
參考 < komark06 > 的方法引入原始程式碼,並且加入新的 qtest
命令 list_sort
實作流程
list_sort.h
qtest.c
新增函式 cmp()
qtest.c
的 do_sort()
函式首先要在 lab0-c 專案裡面建立 lsit_sort.c
及 lsit_sort.h
,前者包含 Linux 核心原始程式碼,而後者則是包含 lab0-c 所缺少的必要定義
以下為 lsit_sort.h
的內容
接著在 Makefile 中的 OBJS
新增 list_sort.o
在 qtest.c
上建立函式 cmp()
作業要求
當 queue.c
的實作基本上完成後,執行 $ make valgrind
進行動態分析,發現 trace-13-malloc.cmd
沒有通過測試,因此針對這個檔案進行分析,使用命令 valgrind -q --leak-check=full ./qtest -f traces/trace-13-malloc.cmd
對檔案 trace-13-malloc.cmd
進行測試
從錯誤訊息中發現有 still reachable 類型的記憶體遺失情形發生,且不是在 queue.c
實作中造成的,因此根據錯誤訊息的追隨路徑,開始檢查 qtest.c
中有無不合理的操作,但找了許久還是沒有頭緒,參考了 KHLee529 同學的筆記後,才發現 qtest.c
中的 q_quit
敘述第一行為 return true;
,也就是說這個函式第一行以後的敘述毫無作用,很明顯語意上有誤
still reachable: 程式結束時有尚未釋放的記憶體,且還被指標所指向,通常會發生在全域變數
q_quit
當我將錯誤的語意清除後重新執行 valgrind -q --leak-check=full ./qtest -f traces/trace-13-malloc.cmd
,先前 still reachable 類型的記憶體遺失情形也隨之解決了
使用圖形化工具 < Massif > 來查看執行 trace-13-malloc.cmd
時記憶體的分配狀況,輸入命令 valgrind -v --tool=massif ./qtest -f traces/trace-13-malloc.cmd
,產生了檔案 massif.out.85066
,接著使用 massif-visualizer 打開檔案,輸入命令 massif-visualizer ./massif.out.85066
,最終結果為下圖
從動態記憶體配置的圖形發現,佔用的記憶體最終都歸零 –->歸還了,可見 qtest
在 trace-13-malloc.cmd
執行結束後,過程中佔用的記憶體都已成功被釋放
注意用語,請查詢辭典,理解「歸零」的意涵是否可對應到本例 malloc/free 操作。
:notes: jserv
歸零:將儀表或計量裝置的指示值調至零點或起點
本例的操作是對記憶體進行分配與釋放 (歸還) ,記憶體總量沒有減少,因此不適合用「歸零」來描述,已修正錯誤的詞彙使用,謝謝教授!
:cat2: ccasdqwe