---
tags: Linux Kernel
---
# 2022q1 Homework1 (lab0)
contributed by < `kevinshieh0225` >
## 作業描述
本次作業 [lab0](https://hackmd.io/@sysprog/linux2022-lab0) 實作 FIFO 與 FILO 的佇列 queue 。為了使 remove, add 等操作維持在 O(1) 的時間複雜度,以 circular doubly linked list 的結構實現。相關知識背景務必詳閱[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list),會有更清晰的背景說明。
運用 [The Linux Kernel API - List Management Functions](https://www.kernel.org/doc/html/latest/core-api/kernel-api.html) 的基礎,讓學員學習 Linux 核心程式碼風格實作方式,也讓我們初探[物件導向作為一種態度](https://hackmd.io/@sysprog/c-oop?type=view)的實作技巧。
## 事先準備
### system
```bash
$ gcc -v
...
gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)
```
### github
在 GitHub 上 fork [lab0-c](https://github.com/kevinshieh0225/lab0-c) 並循序參考[作業指引](https://hackmd.io/@sysprog/linux2022-lab0)完成相關環境設定與實作。
#### [rebase on fork](https://medium.com/@topspinj/how-to-git-rebase-into-a-forked-repo-c9f05e821c8a)
```shell
git remote add upstream https://github.com/original-repo/goes-here.git
git fetch upstream
git rebase upstream/master
git push origin master --force
```
## queue 程式實作
修改 queue.c 的 function 已完成實作。需觀察 queue.h 介面對於 element_t 或 function 的描述,並搭配觀察 list.h 中對於 struct list_head 指標的描述來確定整體實作原理。
### Some issue
#### 1. malloc, free and robustness
(1) 透過動態記憶體配置來增減 element_t 的節點資料。而在我們選擇 delete element_t 時也必須用 free() 來將配置空間釋放。
(2) 任何使用動態記憶體配置的程式碼都務必確認是否成功,否則可能出現程式碼運行中的例外狀況。需確保 malloc() 回傳的內容:如果 heap 的記憶體空間不足時,其回傳值為 NULL。
(3) 注意 element_t 的 element:
```c
/* Linked list element */
typedef struct {
/* Pointer to array holding string.
* This array needs to be explicitly allocated and freed
*/
char *value;
struct list_head list;
} element_t;
```
我們可以發現,element_t 中包含了一個 char pointer 與 struct list_head。在配置 element_t 空間時,完整的 string value 是還未被宣告的。於是 char* 要額外宣告,element_t->value 再指向此字串位置。最後在釋放時也必須同時釋放 element_t 與 char*。
```c
void q_release_element(element_t *e)
{
free(e->value);
free(e);
}
```
#### 2. container_of
在我們實作的 queue ,是以 element_t 作為節點,將 char *value 與 list_head 作為 member,所以節點本身並不沒有紀錄節點串連,是透過 list_head 之間來完成 linked list 的串連。
```c
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
這麼做的好處是模組化,模組化提升程式碼的封裝、提升維護性。利用 list_head 實作 linked list 資料形式與功能,讓我們在設計新的資料結構時可以透過引用模組來實現功能,且只要所有 <s>associate</s> 關聯(association-uml)的 object 在程式碼上都遵守此份模組的規範,即可減少需維護的模組範圍。
:::warning
用通順的漢語書寫。
:notes: jserv
:::
`container_of` 使我們能夠從 member 的地址回推到原本物件地址。巨集原始碼如下:
```c
#define __LIST_HAVE_TYPEOF 1
#ifndef container_of
#ifdef __LIST_HAVE_TYPEOF
#define container_of(ptr, type, member) \
__extension__({ \
const __typeof__(((type *) 0)->member) *__pmember = (ptr); \
(type *) ((char *) __pmember - offsetof(type, member)); \
})
#else
#define container_of(ptr, type, member) \
((type *) ((char *) (ptr) -offsetof(type, member)))
#endif
#endif
struct list_head ptr;
element_t *node = container_of(ptr, element_t, list)
```
假設 ptr 是一個 element_t 的成員,我們可以使用 container_of 來回推 node 的地址。第一個參數放入該成員位址,第二個放入我要回推物件的 type,第三個值輸入 ptr 在該 struct 物件中的名稱為何。
### q_new / q_free / q_size
在 `q_new()` 我們希望建立一個新的 queue 資料物件。注意此時還未輸入節點,這是一個空的 queue。所以我們使用 `INIT_LIST_HEAD(head)` 先使節點中的 list_head 頭尾地址相連。
這裡在靜態分析時會出現由於 element_t *node 在配置空間後並未回傳,可能存在記憶體失去追蹤而未釋放的風險(但其實我們還是能夠取用該位置只是透過 `container_of` 來完成),故插入 cppcheck-suppress 來避免偵錯。
```c
struct list_head *q_new()
{
element_t *node = malloc(sizeof(element_t));
if (!node)
return NULL;
struct list_head *head = &(node->list);
INIT_LIST_HEAD(head);
// cppcheck-suppress memleak
return head;
}
```
:::danger
不要只張貼程式碼,你要闡述自己的作法和後續的改進
:notes: jserv
:::
`q_free()` 在傳入 queue 的起始位置,我們希望釋放整個 queue。我們透過 while loop 先到訪每個 list_head 位置,隨後取得 element_t 位置後,注意必須先指向下一個 list_head,再來釋放記憶體空間,否則 ptr 將指向不存在的位置,使迭代無法完成。
最後我們再釋放 queue head 的空間。無法在 while 中一氣呵成釋放的原因是 queue head 沒有配置 char * 的空間,存在形式和其他節點不同,故需獨立處理。
```c
void q_free(struct list_head *l)
{
if (!l)
return;
struct list_head *ptr = l->next;
while (ptr != l) {
element_t *node = container_of(ptr, element_t, list);
ptr = ptr->next;
q_release_element(node);
}
element_t *node = container_of(l, element_t, list);
free(node);
}
```
```c
int q_size(struct list_head *head)
{
int num = 0;
struct list_head *node;
if (head)
list_for_each (node, head)
num += 1;
return num;
}
```
### q_insert
在 queue 的頭或尾安插節點。首先先建立 element_t,在確保 malloc 都成功下再透過 list_add 函數將被建立的 struct list_head 安插在 linked list 中。
這裡除了需先確認 queue 有順利建成,再來確認 element_t, char * 的記憶體配置是否順利。由於要頻繁確認之故,所以程式碼顯得很冗長,還未想到更簡潔的方式處理這塊。
由於 char * 作為傳入的 parameter 無法透過 sizeof 來確保其字串長度的,可透過一層 while loop 來計算字串的長度。
:::warning
可以使用 strlen() 來確認 string 的大小:strlen 是以 counter 從 char * 逐一尋找最終非 NULLCHAR '\0' 位置,以回傳字串大小。
亦可參考 [數值系統](https://hackmd.io/@sysprog/c-numerics#%E9%81%8B%E7%94%A8-bit-wise-operator) 在 strlen 的 bitwise 操作嘗試長度取值。
:::
透過 memcpy 來複製字串,若使用 strcpy 會產生安全疑慮,因為該函數恐產生溢位風險,並無法通過靜態分析。
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *node = malloc(sizeof(element_t));
if (!node)
return false;
int charsize = strlen(s);
char *value = malloc(charsize + 1);
if (!value) {
free(node);
return false;
}
memcpy(value, s, charsize);
value[charsize] = '\0';
node->value = value;
list_add(&(node->list), head);
/* for q_insert_tail use :
** list_add_tail(&(node->list), head);
*/
return true;
}
```
### q_remove
移除節點。這裡特別注意必須題目敘述之 buffsize 大小。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *node = container_of(head->next, element_t, list);
/* for q_remove_tail use :
** element_t *node = container_of(head->prev, element_t, list);
*/
if (sp) {
memcpy(sp, node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(head->next);
return node;
}
```
### q_delete_mid
此題希望刪除 queue 中間節點。我使用快慢指摽的技巧,快指標走兩步慢指標走一步,即可讓快指標指向終點 (head node) 時,慢指標可以指在中間節點上。再來執行節點刪除。
```c
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *slowptr = head->next;
for (struct list_head *fastptr = head->next;
fastptr != head && fastptr->next != head;
fastptr = fastptr->next->next) {
slowptr = slowptr->next;
}
element_t *node = container_of(slowptr, element_t, list);
list_del(slowptr);
free(node->value);
free(node);
return true;
}
```
### q_delete_dup
希望刪除重複值節點。我透過設定 ckp 與 ptr 節點,讓 ckp 指在起始節點,讓 ptr 迭代尋值,若 ckp 與 ptr 指向的 字串相同,則我將該字串刪除,並讓 ptr 指向下一個節點;而若 ckp 與 ptr 不同,我則更新 ckp 位置到 ptr 所在,代表本位置以前的所有節點都是未重複過的。
我透過 `strcmp` 來比較字串大小,若相同則回傳值為 0。
這裏有可改進之處:由於每次發現重複值我便必須刪除節點並維護指標,如果發生連續性的重複字串時,其實更好的做法是找到迄今為止未重複之節點,一次性的刪除並維護指標位置,這樣能夠避免過多重複性但非必要的動作,可在日後改進。
```c
bool q_delete_dup(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *ckp = head->next;
element_t *ckpnode = container_of(ckp, element_t, list);
for (struct list_head *ptr = ckp->next; ptr != head; ptr = ckp->next) {
element_t *ptrnode = container_of(ptr, element_t, list);
if (strcmp(ckpnode->value, ptrnode->value) == 0) {
list_del(ptr);
q_release_element(ptrnode);
} else {
ckp = ptr;
ckpnode = container_of(ckp, element_t, list);
}
}
return true;
}
```
:::warning
原先的程式碼是將串列重複的節點刪除,但不刪除初始出現的重複節點(故在執行完演算法後讓串列無重複節點的狀況就好)
以下演算法是刪除所有重複的節點的版本。
:::
```c
/**
* @brief 刪除串列中重複節點
*
* ckp 指向紀錄節點,ckpnode 指向紀錄節點的除存 element_t ,為了取得 string。
* ckpisdup 紀錄是否有先前執行了刪除節點,告知我們要不要把當前的紀錄節點刪除。
*
* for (ptr 指向當前節點,往前檢查直到回到起點節點)
* - 如果檢查發現紀錄節點與當前節點相同:
* > 開始向前尋值並刪除節點直到不再是重複節點
* - 如果不相同:
* > 檢查是否在先前在刪除節點,如果是的話我就要把當前節點刪除
* > 最後一同更新當前節點,表示前面的串列都已經整理過
*
* 這裡程式碼缺點是易讀性很差,還沒想到更好的方式。
*/
bool q_delete_dup(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *ckp = head->next;
element_t *ckpnode = list_entry(ckp, element_t, list);
bool ckpisdup = false;
for (struct list_head *ptr = ckp->next; ptr != head; ptr = ckp->next) {
element_t *ptrnode = list_entry(ptr, element_t, list);
if (strcmp(ckpnode->value, ptrnode->value) == 0) {
list_del(ptr);
q_release_element(ptrnode);
ckpisdup = true;
} else {
if (ckpisdup) {
list_del(ckp);
q_release_element(ckpnode);
ckpisdup = false;
}
ckp = ptr;
ckpnode = list_entry(ckp, element_t, list);
}
}
if (ckpisdup) {
list_del(ckp);
q_release_element(ckpnode);
}
return true;
}
```
### q_swap / q_reverse
```c
/**
* @brief 兩兩交換節點
*
* 先確認是否有兩節點能夠互換
* 如果為真我先對 *next 的指標們進行更新
* 在完成以後在依緒透過回溯來更新 *prev 的指標。
*
* 這裡程式碼缺點是易讀性很差,還沒想到更好的方式。
*/
void q_swap(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *ptr = head;
while (ptr->next != head && ptr->next->next != head) {
// swap next pointer
struct list_head *tmp = ptr->next->next->next;
ptr->next->next->next = ptr->next;
ptr->next = ptr->next->next;
ptr->next->next->next = tmp;
// swap prev pointer
ptr->next->next->next->prev = ptr->next->next;
ptr->next->next->prev = ptr->next;
ptr->next->prev = ptr;
ptr = ptr->next->next;
}
}
```
###
```c
/**
* @brief DoublyLinkedList 節點反轉
*
* 從 head 出發一圈將 next 和 prev 指標位置互換。
*
* @Trick
* 由於這裡是少數 queue head 也需參與行動
* 但 head 也同時必須成為終止判定的指標
* 故我利用 do-while 的方式:
* 讓 head 先執行一次後指標前移
* 後續的執行再判定是否達到停止條件。
*/
void q_reverse(struct list_head *head)
{
struct list_head *ptr = head;
if (ptr && !list_empty(ptr)) {
do {
struct list_head *tmp = ptr->prev;
ptr->prev = ptr->next;
ptr->next = tmp;
ptr = ptr->prev;
} while (ptr != head);
}
}
```
### q_sort
此實作參考 jserv 在 linked list 上的程式碼。
特別可以注意,我是使用 single linked list 的邏輯在實作 mergesort
因為如果我們每次合併 node list 時都要把重新整理雙向串列,會浪費過多操作。
於是乾脆把順向串列整理好後,最後再整理反向串列。
```c
struct list_head *mergeTwoLists(struct list_head *L1, struct list_head *L2)
{
struct list_head *head = NULL, **ptr = &head, **cur;
for (cur = NULL; L1 && L2; *cur = (*cur)->next) {
// compare accending pair by pair
element_t *node1 = container_of(L1, element_t, list);
element_t *node2 = container_of(L2, element_t, list);
cur = (strcmp(node1->value, node2->value) <= 0) ? &L1 : &L2;
*ptr = *cur;
ptr = &(*ptr)->next;
}
*ptr = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2);
return head;
}
static struct list_head *mergesort_list(struct list_head *head)
{
if (!head || !head->next)
return head;
struct list_head *slow = head;
for (struct list_head *fast = head->next; fast && fast->next;
fast = fast->next->next)
slow = slow->next;
struct list_head *mid = slow->next;
slow->next = NULL;
struct list_head *left = mergesort_list(head), *right = mergesort_list(mid);
return mergeTwoLists(left, right);
}
void q_sort(struct list_head *head)
{
if (!head || list_empty(head))
return;
head->prev->next = NULL;
head->next = mergesort_list(head->next);
// reassign the prev ptr
struct list_head *ptr = head;
for (; ptr->next; ptr = ptr->next)
ptr->next->prev = ptr;
ptr->next = head;
head->prev = ptr;
}
```
:::warning
TODO: `mergesort_list` 使用遞迴實作,嘗試改為迭代 (iterative) 並探討系統堆疊使用量的變化 (作業說明介紹若干記憶體分析工具)。
:notes: jserv
:::
## q_sort 在遞迴與迭代之效能比較
### iterative merge sort
參考 [Merge Sort 與它的變化](https://hackmd.io/@lambert-wu/modified-merge-sort) 實作 iteration merge sort 的改進版:
```c
void q_sort(struct list_head *head)
{
if (!head || list_empty(head))
return;
head->prev->next = NULL;
int listsSize = 0;
struct list_head *lists[150000] = {NULL};
/* divide to single node
*
* split listnode first
* but if the node sequence is already in accending
* ignore spliting
*/
struct list_head *node = head->next
while(node){
struct list_head *iter = node;
while (iter && iter->next && iter->value < iter->next->value)
iter = iter->next;
lists[listsSize++] = node;
node = iter->next;
iter->next = NULL;
}
/* merge K sorted lists
*
* merge and sort two list node in array
* pair in front and end list node
* until all the list node had been merge in one list
*/
while (listsSize > 1) {
for (int i = 0, j = listsSize - 1; i < j; i++, j--)
lists[i] = mergeTwoLists(lists[i], lists[j]);
listsSize = (listsSize + 1) / 2;
}
// reassign the prev ptr
head->next = lists[0];
struct list_head *ptr = head;
for (; ptr->next; ptr = ptr->next)
ptr->next->prev = ptr;
ptr->next = head;
head->prev = ptr;
}
```
### perf stat
> 參考 `bakudr18` 的[效能分析實作](https://hackmd.io/@MR-9Qa9wQLWglSAUyd6UhQ/BkvLMLkeq?fbclid=IwAR1uB9XeTR-OhBCFq5o076k0CqEk1tQRvLwSbRn2YDMUfXSkXZCPdNzdf8s)
>
使用 `perf stat` 執行 500 次測試在迭代與遞迴版的 sort 上:
分別隨機產生 1000, 10000, 25000, 50000, 100000 個串列節點做 cache 觀察實驗:
```cmd
@REM trace-sort.cmd
option fail 0
option malloc 0
new
ih RAND 1000
@REM (10000, 25000, 50000, 100000)
sort
free
quit
```
```shell
$ sudo perf stat -r 500 -e cycles,instructions,\
cache-references,cache-misses \
./qtest -f traces/trace-sort.cmd
```
- [ ] `ih RAND 1000`
```
# recursive
429,4594 cycles ( +- 0.72% )
402,3140 instructions # 0.94 insn per cycle ( +- 0.13% )
13,2553 cache-references ( +- 0.67% )
3,4594 cache-misses # 26.098 % of all cache refs ( +- 0.89% )
0.0017419 +- 0.0000212 seconds time elapsed ( +- 1.22% )
# iterative
599,7217 cycles ( +- 0.66% )
529,7882 instructions # 0.88 insn per cycle ( +- 0.07% )
18,4345 cache-references ( +- 0.51% )
6,6911 cache-misses # 36.296 % of all cache refs ( +- 0.66% )
0.0023704 +- 0.0000218 seconds time elapsed ( +- 0.92% )
```
- [ ] `ih RAND 10000`
```
# recursive
2987,2129 cycles ( +- 0.71% )
2767,1574 instructions # 0.93 insn per cycle ( +- 0.01% )
105,0227 cache-references ( +- 0.22% )
18,9708 cache-misses # 18.064 % of all cache refs ( +- 2.13% )
0.0097144 +- 0.0000942 seconds time elapsed ( +- 0.97% )
# iterative
3490,4083 cycles ( +- 0.73% )
2860,1657 instructions # 0.82 insn per cycle ( +- 0.01% )
145,8666 cache-references ( +- 0.11% )
25,5875 cache-misses # 17.542 % of all cache refs ( +- 1.72% )
0.011597 +- 0.000110 seconds time elapsed ( +- 0.95% )
```
- [ ] `ih RAND 25000`
```
# recursive
8799,0769 cycles ( +- 0.36% )
6851,6095 instructions # 0.78 insn per cycle ( +- 0.01% )
309,8532 cache-references ( +- 0.09% )
109,2375 cache-misses # 35.255 % of all cache refs ( +- 0.72% )
0.034604 +- 0.000216 seconds time elapsed ( +- 0.63% )
# iterative
1,0853,8558 cycles ( +- 0.47% )
6881,3598 instructions # 0.63 insn per cycle ( +- 0.01% )
405,7416 cache-references ( +- 0.09% )
174,5545 cache-misses # 43.021 % of all cache refs ( +- 0.99% )
0.053943 +- 0.000370 seconds time elapsed ( +- 0.69% )
```
- [ ] `ih RAND 50000`
```
# recursive
2,0458,3259 cycles ( +- 0.21% )
1,3795,8839 instructions # 0.67 insn per cycle ( +- 0.01% )
694,3560 cache-references ( +- 0.07% )
335,3012 cache-misses # 48.290 % of all cache refs ( +- 0.38% )
0.080581 +- 0.000274 seconds time elapsed ( +- 0.34% )
# iterative
2,4660,8830 cycles ( +- 0.17% )
1,3685,5232 instructions # 0.55 insn per cycle ( +- 0.01% )
875,4610 cache-references ( +- 0.04% )
500,2342 cache-misses # 57.140 % of all cache refs ( +- 0.20% )
0.122214 +- 0.000227 seconds time elapsed ( +- 0.19% )
```
- [ ] `ih RAND 100000`
```
# recursive
4,5710,0302 cycles ( +- 0.08% )
2,7945,2035 instructions # 0.61 insn per cycle ( +- 0.00% )
1573,9750 cache-references ( +- 0.03% )
870,5131 cache-misses # 55.307 % of all cache refs ( +- 0.11% )
0.184705 +- 0.000225 seconds time elapsed ( +- 0.12% )
# iterative
5,8864,9910 cycles ( +- 0.14% )
2,7584,3506 instructions # 0.47 insn per cycle ( +- 0.00% )
1941,7409 cache-references ( +- 0.03% )
1354,6765 cache-misses # 69.766 % of all cache refs ( +- 0.16% )
0.290253 +- 0.000471 seconds time elapsed ( +- 0.16% )
```
以上實驗可以發現:
- 雖然遞迴與迭代的指令數不會差太多,但迭代所需的時脈週期卻更多。
- 迭代使用的 cache-references 更多,且 cache-misses 也通常更多。
遞迴基於 [divide and conquer](https://zh.wikipedia.org/wiki/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F#/media/File:Merge_sort_animation2.gif) 的特性,在合併排序上遞迴是更 cache-friendly 的,在降低 cache-references 和 cache-misses 之下,也讓執行效率更高。
### valgrind
本次實驗未用 valgrind 做系統性實驗,但紀錄使用基本指令,以便下次使用:
```shell
# memory usage test
$ valgrind --tool=massif ./qtest -f ./traces/trace-sort.cmd
# cache usage test
$ valgrind --tool=cachegrind ./qtest -f ./traces/trace-sort.cmd
$ ms_print massif.out.xxxxx
$ sudo apt install massif-visualizer
$ massif-visualizer massif.out.xxxxx
```
只是紀錄,對本次實驗幫助不大。
## qtest 新功能實作:Fisher-Yates shuffle
新增 qtest feature : shuffle
編輯 `console_init()` 並新增 `do_shuffle()`。
```c
static void console_init()
{
...
ADD_COMMAND(shuffle,
" | Shuffle the queue with Fisher–Yates shuffle "
"algorithm");
...
}
```
```c
/**
* @brief 對資料節點做 Fisher-Yates shuffle
*
* 參考網誌 [Fisher–Yates shuffle 洗牌算法]
* (https://gaohaoyang.github.io/2016/10/16/shuffle-algorithm/)
*
* 1. 檢查 argc 長度
* 2. 檢查 l_meta 不可為空
* 3. for loop 逐一進行 leave-one-out shuffle
* 從 [1,range] 挑出一個節點將他擺到佇列末端
* 每次執行後減少 range 大小
* 執行直到出現 error 或是 range == 0
*/
static bool do_shuffle(int argc, char *argv[])
{
if (argc != 1) {
report(1, "%s takes no arguments", argv[0]);
return false;
}
if (!l_meta.l)
report(3, "Warning: Calling size on null queue");
error_check();
bool ok = true;
set_noallocate_mode(true);
if (exception_setup(true)) {
int cnt = q_size(l_meta.l);
srand(time(NULL));
ok = ok && !error_check();
for (int range = cnt; ok && range > 0; range--) {
int randint = rand() % range + 1;
struct list_head *ptr = l_meta.l;
while (randint-- > 0)
ptr = ptr->next;
list_move_tail(ptr, l_meta.l);
ok = ok && !error_check();
}
}
exception_cancel();
set_noallocate_mode(false);
show_queue(3);
return ok;
}
```