---
tags: linux2022
---
# 2022q1 Homework1 (lab0)
contributed by < [ganoliz](https://github.com/ganoliz) >
## 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
$ lscpu
架構: x86_64
CPU 作業模式: 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 8
On-line CPU(s) list: 0-7
每核心執行緒數: 2
每通訊端核心數: 4
Socket(s): 1
NUMA 節點: 1
供應商識別號: GenuineIntel
CPU 家族: 6
型號: 158
Model name: Intel(R) Core(TM) i7-7700 CPU @ 3.60GHz
製程: 9
CPU MHz: 2436.942
CPU max MHz: 4200.0000
CPU min MHz: 800.0000
BogoMIPS: 7200.00
虛擬: VT-x
L1d 快取: 128 KiB
L1i 快取: 128 KiB
L2 快取: 1 MiB
L3 快取: 8 MiB
NUMA node0 CPU(s): 0-7
```
## 作業要求
* **實做 queue.c 的相關程式碼**:
* q_new: 建立新的「空」佇列;
* q_free: 釋放佇列所佔用的記憶體;
* q_insert_head: 在佇列開頭 (head) 插入 (insert) 給定的新節點;
* q_insert_tail: 在佇列尾端 (tail) 插入 (insert) 給定的新節點;
* q_remove_head: 在佇列開頭 (head) 移去 (remove) 給定的節點;
* q_release_element: 釋放特定節點的記憶體空間;
* q_size: 計算目前佇列的節點總量;
* q_delete_mid: 移走佇列中間的節點;
* q_swap: 交換佇列中鄰近的節點;
* q_reverse: 以反向順序重新排列鏈結串列;
* q_sort: 以遞增順序來排序鏈結串列的節點;
* **在 qtest 提供新的命令 shuffle,允許藉由 Fisher–Yates shuffle 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作**
* **在 qtest 提供新的命令 web,提供 web 伺服器功能,注意: web 伺服器運作過程中,qtest 仍可接受其他命令**
* **指出現有程式的缺陷 (提示: 和 RIO 套件 有關) 或者可改進之處 (例如更全面地使用 Linux 核心風格的鏈結串列 API),嘗試實作並提交 pull request**
## 開發過程
### q_new:
```c
struct list_head *q_new()
{
struct list_head *Queue = malloc(sizeof(struct list_head));
if (Queue == NULL)
return NULL;
INIT_LIST_HEAD(Queue);
return Queue;
}
```
**新增一個Queue**:初期寫法是使用queue指標指向自身,後來詳讀[你所不知道的 C 語言: linked list 和非連續記憶體](/YA7RMqd6RE2UL8OmmjU9PQ)與 list.h 的 The Linux Kernel API ,將寫法改為 macro 。
### q_free:
```c
void q_free(struct list_head *l)
{
struct list_head *temp;
if (l == NULL) {
return;
}
if (list_empty(l)) {
// printf("l is already release");
free(l);
return;
}
if (list_is_singular(l)) {
element_t *node = container_of(l->next, typeof(element_t), list);
q_release_element(node);
free(l);
return;
}
for (temp = l->next; temp != l;) {
element_t *node = container_of(temp, typeof(element_t), list);
temp = temp->next;
q_release_element(node);
}
free(l);
return;
}
```
**釋放Queue**:可以看到 q_free() 針對不同的節點數做了很多if判斷。
假如head沒有 element_t 節點,就將本身 new 出來的 list_head 釋放就可以了。若是有節點則使用 container_of(list_entry) 找出 element_t 的位置再將其 element_t 釋放,最後再將 head 釋放。
顯然程式碼是有改進空間的,針對不同節點的共同特性找出來,就可以精簡程式碼不用那麼多 if 判斷。
### q_insert_head
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (head == NULL)
return false;
element_t *Node = malloc(sizeof(element_t));
char *c = malloc(strlen(s) + 1);
if (Node == NULL || c == NULL) {
if (Node != NULL) {
free(Node);
}
if (c != NULL) {
free(c);
}
return false;
}
memcpy(c, s, strlen(s) + 1);
Node->value = c;
INIT_LIST_HEAD(&Node->list);
list_add(&Node->list, head);
return true;
}
```
**新增節點**:若是 malloc 不成功,要找出是 element_t 不成功還是 char 不成功,然後釋放掉另一個後回傳 false 。這是一個我很後面才改的小 Bug ,也讓我知道系統程式要考慮得很嚴謹,不能讓記憶體洩漏。
節點新增成功之後,將字串用 memcpy 複製到 element_t 的 value 成員。接著使用 API 初始化 element_t 的成員 list ,並將它的 list 新增到 head 的後面。
### q_insert_tail
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (head == NULL)
return false;
element_t *Node = malloc(sizeof(element_t));
char *c = malloc(strlen(s) + 1);
// struct list_head *temp;
if (Node == NULL || c == NULL) {
if (Node != NULL) {
free(Node);
}
if (c != NULL) {
free(c);
}
return false;
}
memcpy(c, s, strlen(s) + 1);
Node->value = c;
INIT_LIST_HEAD(&Node->list);
list_add_tail(&Node->list, head);
return true;
}
```
**新增節點至尾端**:基本上跟 q_inset_head 相同,只是使用 list.h 的 API 不一樣,改使用 list_add_tail 。
### q_remove_head
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (head == NULL || list_empty(head)) {
return NULL;
}
struct list_head *node = head->next;
element_t *element;
element = list_entry(node, typeof(element_t), list);
if (sp) {
memcpy(sp, element->value, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
list_del(node);
return element;
}
```
**移除從頭開始的第一個節點**: 將節點移除,若Queue不存在或是為空則回傳NULL。一開始搞不太清楚 char * sp 的用途還寫了很多 if-else 判斷式。稍微觀摩一下同學的程式碼就理解到 sp 。若有提供 sp ,就將 element_t 的 value 複製過去,複製直到 bufsize-1 的大小(遇到'\0'會自動停止)。值得注意的是若 element_t 的 value 大小超過 bufsize-1 其餘就會被砍掉,然後 * (sp+bufsize-1) 的位置一定要放'\0',否則系統不會知道結束位置在哪裡。
### q_remove_tail
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (head == NULL || list_empty(head)) {
return NULL;
}
struct list_head *node = head->prev;
element_t *element;
element = list_entry(node, typeof(element_t), list);
if (sp) {
memcpy(sp, element->value, bufsize - 1);
*(sp + bufsize - 1) = '\0';
}
list_del(node);
return element;
}
```
**從尾端移除節點**:基本上和從頭移除節點相同,只是存取的 node 點變成 head->prev,當然也可以使用 list_last_entry 來取得最後一個節點的位置。
### q_release_element
```c
void q_release_element(element_t *e)
{
free(e->value);
free(e);
}
```
**釋放節點**:毫無疑問!就是個釋放節點。
### q_size:
```c
int q_size(struct list_head *head)
{
if (!head)
return 0;
int len = 0;
struct list_head *li;
list_for_each (li, head)
len++;
return len;
}
```
**Queue的大小**:透過 list_for_eac 走訪每個 node,並每次對 len+1,最後返回 len 的長度。
### q_delete_middle
```c
bool q_delete_mid(struct list_head *head)
{
// https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
if (head == NULL || list_empty(head))
return false;
else if (list_is_singular(head)) {
// q_remove_head(head, NULL, 100);
element_t *temp = list_entry(head->next, element_t, list);
list_del(&temp->list);
q_release_element(temp);
return true;
}
struct list_head **indir = &head->next, *prev = NULL;
for (struct list_head *fast = head->next;
fast != head && fast->next != head; fast = fast->next->next) {
prev = *indir;
indir = &(*indir)->next;
}
struct list_head *next = (*indir)->next;
element_t *temp = list_entry(*indir, element_t, list);
list_del(*indir);
q_release_element(temp);
prev->next = next;
return true;
}
```
**移除中心點**: 參考[你所不知道的 C 語言: linked list 和非連續記憶體](/YA7RMqd6RE2UL8OmmjU9PQ)中所述之移除中心點方法,以快慢指標來實作( slow 走一格時 fast 走兩格),找到中心點後將中心點使用 list_del 刪除(其實這樣就能將其前面的節點指向後面)。
```graphviz
digraph test {
node[shape=record];
rankdir=LR;
head [label="{<left>prev|<right>next}", style=bold];
node1 [label="value|{<left>prev|<right>next}", style=bold];
node2 [label="value|{<left>prev|<right>next}", style=bold];
head->node1[label="prev"]
node1->node2[label="fast"]
}
```
### q_delete_dup
```c
bool q_delete_dup(struct list_head *head)
{
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
if (!head || list_empty(head))
return false;
if (list_is_singular(head)) {
return true;
}
bool duplicate = false;
struct list_head *current, *next;
list_for_each_safe (current, next, head) {
element_t *node = list_entry(current, element_t, list);
element_t *safe = list_entry(next, element_t, list);
if (next == head) {
if (duplicate) {
element_t *temp = node;
// node=list_entry(node->list.prev,element_t,list);
printf("remove node last:%c \n", *(temp->value));
list_del(&temp->list);
q_release_element(temp);
}
break;
}
if (strcmp(node->value, safe->value) == 0) {
element_t *temp = node;
// node=list_entry(node->list.prev,element_t,list);
duplicate = true;
// printf("remove node:%c \n",*(temp->value));
list_del(&temp->list);
q_release_element(temp);
}
else if (duplicate) {
element_t *temp = node;
// node=list_entry(node->list.prev,element_t,list);
printf("remove node last:%c \n", *(temp->value));
list_del(&temp->list);
q_release_element(temp);
duplicate = false;
}
}
return true;
}
```
**刪除重複的所有Node**:參考同學設置一個 bool duplicate 的想法,假如有重複就設 duplicate 為 true並刪除,若是 strcmp 之後未重複,則是判斷前面是否為 duplicate,若是則代表前面有出現過並把該node刪除。這部分程式碼還是有保留一些冗餘的部分,比如當 current == tail 且 next == head的情況下,需要用這段程式碼第一個if述句來特別開一個條件給它,寫起來不夠 general。
### q_swap
```c
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (head == NULL || list_empty(head) || list_is_singular(head))
return;
struct list_head *odd, *even, *temp;
odd = head->next, even = head->next->next;
while (odd != head && even != head) {
odd->next = even->next;
odd->prev->next = even;
temp = odd->prev;
odd->prev = even;
even->next->prev = odd;
even->next = odd;
even->prev = temp;
odd = odd->next;
even = odd->next;
}
```
**節點倆倆交換位置**:透過 odd 為奇數節點, even 為偶數節點走訪整個 Queue ,並兩兩指標交換。
```graphviz
digraph swap{
node[shape=record];
rankdir=LR;
head [label="{<left>prev|<right>next}", style=bold];
node1 [label="1|{<left>prev|<right>next}", style=bold];
node2 [label="2|{<left>prev|<right>next}", style=bold];
node3 [label="3 |{<left>prev|<right>next}", style=bold];
node4 [label="4|{<left>prev|<right>next}", style=bold];
node5 [label="5|{<left>prev|<right>next}", style=bold];
node6 [label="6|{<left>prev|<right>next}", style=bold];
head:right->node1
node1:right->node2
node2:right->node3[label="Step2"]
node3:right->node4 [label="Step1"]
node4:right->node5[label="Step5"]
node5:right->node6
node6:right->head
odd->node3
even->node4
head:left->node6
node6:left->node5
node5:left->node4[label="Step4"]
node4:left->node3[label="Step6"]
node3:left->node2[label="Step3"]
node2:left->node1
node1:left->head
}
```
根據圖,我們總共需要移動六個指標,首先移動
* 1. odd->next
* 2. odd->prev->next
* 3. odd->prev
* 4. even->next->prev
* 5. even->next
* 6. even->prev
然後因為現在 odd 實質上等於 even 的位置了,所以 odd 往 next 移動一次就等於新的要調整的 odd 了,然後 even 再順勢往 odd 的 next 再移動一次,就是新的要調整的 even 了(整體可讀性低了一些些)。
### q_reverse
```c
void q_reverse(struct list_head *head)
{
if (head == NULL || list_is_singular(head) || list_empty(head))
return;
struct list_head *li;
struct list_head *temp;
temp = head->prev;
head->prev = head->next;
head->next = temp;
list_for_each (li, head) {
temp = li->prev;
li->prev = li->next;
li->next = temp;
}
}
```
**Queue 翻轉**: 使用 list_for_each 走訪節點,然後將節點的 prev 與 next 對調(應該使用 list_for_each_safe 比較保險,因為有動到 Node 裡的東西),走完每個節點對調完即完成。
### q_sort
```c
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *temp = head->next;
head->prev->next = NULL;
head->prev = NULL;
head->next->prev = NULL;
head->next = NULL;
struct list_head *sortlist = mergesort_list(temp);
```
**MergeSort**: 參考 [你所不知道的 C 語言: linked list 和非連續記憶體](/YA7RMqd6RE2UL8OmmjU9PQ) 中的 merge sort 範例 ,在這個函式裡,我們先把 head 丟棄,避免它在我們後續使用 list_entrt 找 element_t 位置的時候指涉到 head 導致錯誤。接著我們把第一個元素 temp 丟進 mergesort_list() 裡:
```c
static struct list_head *mergesort_list(struct list_head *head)
{
if (!head)
return NULL;
if (head->next == NULL)
return head;
struct list_head *slow = head;
for (struct list_head *fast = head->next;
fast != NULL && fast->next != NULL; fast = fast->next->next) {
slow = slow->next;
}
struct list_head *mid = slow->next;
// element_t *t=list_entry(mid,element_t,list);
// printf("mid= %c \n",*(t->value));
mid->prev = NULL;
slow->next = NULL;
// element_t *temp_sl=list_entry(slow,element_t,list);
// printf("slow= %c \t",*(temp_sl->value));
struct list_head *left = mergesort_list(head), *right = mergesort_list(mid);
return mergeTwoLists(left, right);
```
在這個函式裡我們會使用遞迴把所有 element_t 變為一個個互不相聯的元素使用 delete_mid的方法。所以說我們斷開了所有 prev 跟 next 。接著遞迴呼叫 mergeTwoLists 來兩兩合併 List :
```c
static struct list_head *mergeTwoLists(struct list_head *L1,
struct list_head *L2)
{
struct list_head *head = NULL;
struct list_head **ptr = &head;
for (; L1 && L2; ptr = &(*ptr)->next) {
element_t *l1 = list_entry(L1, element_t, list);
element_t *l2 = list_entry(L2, element_t, list);
if (strcmp(l1->value, l2->value) < 0) {
*ptr = L1;
L1 = L1->next;
} else {
*ptr = L2;
L2 = L2->next;
}
}
*ptr = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2);
return head;
}
```
這裡就是參考老師的 merge two sorted lists ,非常漂亮。由於最後形成的是一個 Single-Linked-lists ,於是當我們最後回到 q_sort 裡時,需要將 prev 依序補上形成 Double-Linked-lists ,再把 head 接回來,大功告成。
```c
//struct list_head *sortlist = mergesort_list(temp);
// above ============================================================
struct list_head *prev = sortlist;
struct list_head *current = sortlist->next;
for (; current != NULL; current = current->next, prev = prev->next) {
current->prev = prev;
}
head->next = sortlist; // let head connect to value
sortlist->prev = head;
head->prev = prev; // finally prev will reach tail
prev->next = head;
```