contributed by < ryanwang522
>
定義了一個單向 linked list:
在不存在環狀結構的狀況下,以下函式能夠對 linked list 元素從小到大排序:
首先,可以看到 sort
是一個遞迴函式,這個遞迴的終止條件是:只要輸入的 linked list (以下簡稱 list) 為空或是只有一個元素,就返回自己。
NULL
,代表 empty liststart != NULL
,且 start->next == NULL
,代表只有一個元素存在在該 list,返回他自己。注意 sort
這個函式一旦回傳,回傳的就是一串排序好的 list
left
和 right
,想到的是將 list 拆成兩個部分理工人應避免在報告中提及「感覺」,這不是文學創作,是分析推論和系統開發!
LL0
的選項中看到 left->next = NULL;
,如果是此選項的話就代表每次都把 list 最左邊的元素獨立出來,再分別對左右兩個 list 遞迴呼叫,先假設是這樣,繼續往下看。left
right
回傳後就分別指向 list 的第一個跟第二個元素。left
就指向由輸入 list 最左邊第一個元素所構成的 sublist,right
則指向由剩下的元素所構成的 sublist 的開頭,並且right = sort(right)
回傳後,right
就是 已經排序好的接者的問題就變成 – 要怎麼 combine left
、right
兩個 sublist 成為一個一樣是由小到大的 list, 並且讓 start 指向該 sorted list 的開頭。
left、right 分別指向左右兩個 sublist,且 right
是排序好的(right->data
是 right list 中的最小值),從其各自所指向的值來看,根據三一律,以下分成三種情況討論,來推測實作應該長怎樣:
left->data
< right->data
line12
的 if 敘述條件為真,並且一開始 merge
為 NULL
–> 進入 LL1
、LL3
left
是最小了,所以~把要回傳的開頭 start
指向最小元素才是 LL1
合理的選項。加上 merge
為 NULL
才進來 LL1
的目的猜測是要初始化 merge
,選擇
- start = left;
- [x]start = merge = left;
LL3
,選項 (c) (d) 沒有意義,且 left
已經被 merge
所暫存,合理選擇left = left->next;
left->data
= right->data
line12
的 if 敘述條件為 false,並且一開始 merge
為 NULL
–> 進入 LL4
、LL6
start
指向左右都是合理選項,先跳過left->data
> right->data
line12
的 if 敘述條件為 false,並且一開始 merge
為 NULL
–> 進入 LL4
、LL6
right
指向左右 list 中的最小,start
應該指向 right
,和 LL1
同理,選擇
- start = right;
- start = merge = right;
LL3
同理,選擇right = right->next;
從上面 left->data
< right->data
之後繼續推理,此時 left
為 NULL
,merge
和 start
指向原本的左串列,right
依然指向右串列開頭 (注意left
right
其中之一不為空,迴圈繼續)
line12
的 if 條件皆不成立 –> 進入 LL5
LL6
merge
所指的 node 連接到 right
的開頭,因此選擇merge->next = right;
LL6
right = right->next
right
跟 merge
就會不斷右移直到 right
跑到最末端指向 NULL
,此時 left
right
都為空,跳出迴圈 return 排序好的串列給上一層呼叫。從上面 left->data
>= right->data
之後繼續推理,此時 left
依然指向左元素,merge
和 start
指向原本的右串列的最小,right
指向右串列的下一個元素
line12
!right
為 false
if
後半部條件成立 (e.g. left:6, right:4->7) –> LL2
LL3
LL2
: 把 left
指到的 node 插在 merge
後 right
之前LL3
: left
指向 NULLif
後半部條件不成立 (e.g. 左元素已經放置完了,但 right 還沒走到底) –> 透過 LL5
LL6
持續平移直到 right
走到底
right
指到 NULL),!right
為真 –> LL2
LL3
LL2
: 把 left
指到的 node 插在 merge 後 right 之前LL3
: left
指向 NULL要注意的是,新增一個新節點到一串 list 的最前面 (e.g. push) 跟插入一個新節點到一串 list 的中間 (e.g. insert),兩著的實作是不一樣的
LL2
、LL5
其實是不同概念,只是實做起來剛好寫法相同根據原本的程式邏輯,實作 circular doubly-linked list 由右到左的 insertion sort
qsort
的結果比較。include/linux/list.h
的實作首先,先找到 list.h 本人, 裡面定義了 list_head
的結構
它的作用是拿來串接自己定義的結構 (i.e. 作為該 struct 其中一個 member)。
以 quiz1 作為例子,在結構中增加一個型態為 list_head
的變數,如下:
接著,我們可以利用以下的 macro,初始化一個 list_head
,用來代表一串 linked-list 的開頭。
把 macro 替代後發現
LIST_HEAD
是拿來宣告一個 list_head
型態的變數,並且進行初始化,將他的 prev
、next
都指向自己。接著來嘗試利用 list.h
提供的函式實作 push()
,將一個新的 node 串接到目前 linked-list 的開頭。
其中 list_add
函式在 list.h
中定義如下
__list_add()
,他會將一個新的 node 串接 prev
、next
兩個節點之間list_add
則是呼叫 __list_add(new, head, head->next);
new
這個 node 插入到當前的 head
、head->next
之間__list_add(new, head->prev, head);
看到這裡,簡單做了一些摘要
LIST_HEAD(list_start)
可以宣告一個 list_head
型態的變數並初始化,代表一串 list 的起始點list_start
由於是 list_head
型態,代表它其實是一個 dummy head,不存資料
list_start->next
表示第一個元素list_start->prev
表示最後一個元素假設我們有一串 Node
型態的資料,每一個都透過各自結構中的 list_head
中的 prev
、next
互相串聯,如果我們想遍歷這串 list,很直覺的用 for-loop:
接著遇到了一個問題,要怎麼從 list_head
型態的 curr
取得當前所屬 Node
結構中的 data
呢?接著我看到了一個 macro
從註解上來看,container_of
透過當前的 list_head
指標 (e.g. curr
),回傳一個指向 curr
所屬的結構的 pointer,示意圖如下:
一個部分一個部分來看:
gcc
extension – Statements in Expressions
gcc
的 online doc,{}
的回傳值為最後一個 expression。The last thing in the compound statement should be an expression followed by a semicolon; the value of this subexpression serves as the value of the entire construct.
int x = ({1; 2;}) + 3;
,x
的值為 2 + 3 = 5Referring to a Type with typeof
gcc
的 online doc,它可以以某一個變數的型態來宣告另一個變數
typeof (*x) y;
declares y with the type of what x points to.
Zero Pointer Dereference
((Node *)0)->list
,segmentation faultprintf("%zu\n", (size_t) &((Node *)0)->list)
,因為有 address-of 運算子的關係,這段程式碼可以正常執行,回傳 list
的這個 member 在 Node
結構中從起始位置到他的位址 offset。透過 list_entry(ptr, type, member)
將遍歷得到的 curr
減去他在 Node
結構中的 offset,就能得到該指向當前結構的指標。
因此現在我們能夠透過 list_head
結構成員遍歷整個 list,並取得結構中的 data
。
邏輯相同,先將 list 切成左右部分,把 left
插入到排序好的 right
中正確的位置。
list.h
,於是我稍微改寫 include/linux/list.h
,使其成為可以在 userspace 下使用的標頭檔,可參考 Github 中的 list.h
。因為有三種不同的 sorting 實作 (i.e. singular、doubly、list.h),避免 main
充斥著 if-else
, 所以我定義了一個函式介面,對主要的幾個函式進行封裝,並且在sort_orig.c
、sort_dbly.c
、sort_kernel_list.c
中對各自函式進行實作,最後在 main.c
中,在執行時期才透過 argument 參數決定要執行哪一個實作。
節點結構
next
sort.h
list.h
中節點結構跟前兩個差異太多,所以主要函式的參數及回傳值都運用 void *
統一include/linux/list.h
程式碼的方式,改寫上述排序程式Doubly Circular Linked List
Linux kernel list efficient generic linked-list
magical container_of macro