---
tags: linux2023
---
# 2023q1 Homework1 (lab0)
contributed by < [`Ahsen-lab`](https://github.com/Ahsen-lab) >
## 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-7500 CPU @ 3.40GHz
CPU family: 6
Model: 158
Thread(s) per core: 1
Core(s) per socket: 4
Socket(s): 1
Stepping: 9
CPU max MHz: 3800.0000
CPU min MHz: 800.0000
BogoMIPS: 6799.81
Virtualization features:
Virtualization: VT-x
Caches (sum of all):
L1d: 128 KiB (4 instances)
L1i: 128 KiB (4 instances)
L2: 1 MiB (4 instances)
L3: 6 MiB (1 instance)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-3
```
## 作業要求
> [作業要求](https://hackmd.io/@sysprog/linux2023-lab0)
[queue.c](https://github.com/sysprog21/lab0-c/blob/master/queue.c) 僅提供介面 ([interface](https://en.wikipedia.org/wiki/Interface-based_programming)) 但尚未有完整程式實作,需要由學員補完並逐步精進,包含以下:
* `q_new`: 建立新的「空」佇列;
* `q_free`: 釋放佇列所佔用的記憶體;
* `q_insert_head`: 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則);
* `q_insert_tail`: 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則);
* `q_remove_head`: 在佇列開頭 (head) 移去 (remove) 給定的節點;
* `q_remove_tail`: 在佇列尾端 (tail) 移去 (remove) 給定的節點;
* `q_size`: 計算目前佇列的節點總量;
* `q_delete_mid`: 移走佇列中間的節點,詳見 [LeetCode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/)
* `q_delete_dup`: 在已經排序的狀況,移走佇列中具備重複內容的節點,詳見 [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/)
* `q_swap`: 交換佇列中鄰近的節點,詳見 [LeetCode 24. Swap Nodes in Pairs](https://leetcode.com/problems/swap-nodes-in-pairs/)
* `q_reverse`: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點;
* `q_reverseK`: 類似 `q_reverse`,但指定每 k 個節點為一組,進行反向順序排列,詳見 [LeetCode 25. Reverse Nodes in k-Group](https://leetcode.com/problems/reverse-nodes-in-k-group/)
* `q_sort`: 以==遞增順序==來排序鏈結串列的節點,可參閱 [Linked List Sort](https://npes87184.github.io/2015-09-12-linkedListSort/) 得知實作手法;
* `q_descend`: 參閱 [LeetCode 2487. Remove Nodes From Linked List](https://leetcode.com/problems/remove-nodes-from-linked-list/)
* `q_merge`: 合併所有==已排序==的佇列,並合而為一個新的已排序佇列,詳見 [LeetCode 23. Merge k Sorted Lists](https://leetcode.com/problems/merge-k-sorted-lists/)
## 開發過程
:::danger
注意作業書寫規範:中英文間用一個半形空白字元區隔。
程式碼縮排亦有相關規範,請依循!
:notes: jserv
:::
### q_new
建立新的「空」佇列,使用 `malloc` 配置記憶體空間給 `head` ,再用 `INIT_LIST_HEAD` 巨集來初始化 `head` ,需要注意 `malloc` 有可能會 fail,若配置記憶體空間失敗,則回傳 `NULL` 。
```c
/*
* Create empty queue.
* Return NULL if could not allocate space.
*/
struct list_head *q_new()
{
struct list_head *head = malloc(sizeof(struct list_head));
if (!head)
return NULL;
INIT_LIST_HEAD(head);
return head;
}
```
### q_free
釋放佇列所佔用的記憶體
```c
void q_free(struct list_head *l)
{
if (!l)
return;
struct list_head *cur, *tmp;
element_t *elem = NULL;
list_for_each_safe (cur, tmp, l) {
elem = list_entry(cur, element_t, list);
q_release_element(elem);
}
free(l);
}
```
用 `list_for_each_safe` 巨集走訪每個節點並用 `list_entry` 巨集取出對應的 `element` ,然後釋放 `element` 所佔的記憶體空間。
### q_insert_head
在佇列開頭新增一個新節點
```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;
node->value = strdup(s);
if (!node->value) {
free(node);
return false;
}
list_add(&node->list, head);
return true;
}
```
我的作法是配置一段記憶體空間給一個新的 `element` ,再配置一段空間給 `element` 的成員變數 `char *value` 用來儲存字串。而因為在 [harness.h](https://github.com/sysprog21/lab0-c/blob/master/harness.h) 和 [harness.c](https://github.com/sysprog21/lab0-c/blob/master/harness.c) 檔案中,可見到對記憶體管理所進行的實作,會追蹤記憶體配置和釋放的狀態,將原先的 `malloc` 與 `free` 使用 `test_malloc` 與 `test_free` 取代,因此在配置記憶體空間時,只可用 `malloc` 來配置空間。
選擇使用 `strdup` 來配置記憶體空間給字串是因為根據 [strdup(3)](https://man7.org/linux/man-pages/man3/strdup.3.html),`strdup` 會使用 `malloc` 來配置記憶體給新的字串。
```c
char *strdup(const char *s);
```
>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()`, and can be freed with `free()`.
### q_insert_tail
在佇列尾端新增一個新節點
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *node = malloc(sizeof(element_t));
if (!node)
return false;
node->value = strdup(s);
if (!node->value) {
free(node);
return false;
}
list_add_tail(&node->list, head);
return true;
}
```
實作概念與 `q_insert_head` 相同,只是將 `list_add` 換成 `list_add_tail`。
### q_remove_head
移除佇列開頭的節點
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *elem = list_entry(head->next, element_t, list);
list_del_init(head->next);
if (sp) {
strncpy(sp, elem->value, bufsize);
sp[bufsize - 1] = '\0';
}
return elem;
}
```
首先檢查佇列是否存在以及是否為空佇列,若是的話則不做任何事,返回 `NULL` ,若不是的話則使用 `list_entry` 巨集取出開頭第一個 `element` ,再將該節點從佇列中移除,如果 `sp` 不為 `NULL` ,將第一個 `element` 中所存的字串複製給 `sp` 。
### q_remove_tail
移除佇列尾端的節點
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *elem = list_entry(head->prev, element_t, list);
list_del_init(head->prev);
if (sp) {
strncpy(sp, elem->value, bufsize);
sp[bufsize - 1] = '\0';
}
return elem;
}
```
實作概念與 `q_remove_tail` 相同,只是所移除的節點為 `head->prev` ,也就是倒數第一個節點。
### q_size
計算目前佇列的節點總量,設定一個變數 `len` ,初始值為 `0` ,再使用 `list_for_each` 巨集來遍歷佇列中的所有節點,每經過一個節點 `len` 就加一,最後回傳 `len` 的值,若佇列為 `NULL` 或 `empty` 則返回 `0`,
```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;
}
```
### q_delete_mid
移走佇列中間的節點,作法參考
>[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-Leetcode-2095-Delete-the-Middle-Node-of-a-Linked-List) 案例探討 : [Leetcode 2095. Delete the Middle Node of a Linked List](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/)
原本的案例探討是針對單向的 linked list,但為適用於雙向環狀 linked list ,若傳入的佇列為 `NULL` 或是空佇列,則回傳 `false`。
使用快慢指標的技巧來找出中間的節點,首先設定一個指標 `mid` ,以及一個指標 `fast` ,兩個指標都會指向 `head->next` ,然後使用迴圈走訪節點, `fast` 每次會前進兩個節點,而 `mid` 每次前進一個,當 `fast` 本身等於 `head` 或是 `fast->next` 等於 `head` 時,代表已走訪完佇列,這時 `mid` 會指向佇列中間的節點。
找到中間節點後,使用 `list_entry` 巨集來取得相對應的 `element`,然後用 `list_del_init` 巨集來移除節點並用 `free` 釋放記憶體空間,最後回傳 `true` 。
```c
bool q_delete_mid(struct list_head *head)
{
// https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/
if (!head || list_empty(head))
return false;
struct list_head *mid = head->next, *fast = head->next;
while (fast != head && fast->next != head) {
mid = mid->next;
fast = fast->next->next;
}
element_t *elem = list_entry(mid, element_t, list);
list_del_init(mid);
q_release_element(elem);
return true;
}
```
### q_delete_dup
在已經排序的狀況,移走佇列中具備重複內容的節點,若傳入的佇列為 `NULL` 或空佇列,回傳 `false`。
>以下實作有參考 [laneser](https://hackmd.io/@laneser/linux2022_lab0#q_delete_mid) 同學的作法。
使用 `list_for_each_entry_safe` 巨集來走訪佇列中的所有 `element` , `entry` 代表當前所指向的 `element` ,`safe` 則指向 `entry` 的下一個 `element`。
另外,在走訪節點的過程中需要考慮兩個情況:
1. `entry->value == safe->value`:
* 刪除 `entry`,並將 `needDel` 設成 `true`,表示所有與 `entry` 有相同字串的節點都是需要刪除的節點。
2. `entry->value != safe->value`:
* 若 `needDel == true` 表示 `entry` 也是需要刪除的節點。
* 若 `needDel == false` 表示 `entry` 無須刪除。
```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;
element_t *entry, *safe;
bool needDel = false;
list_for_each_entry_safe (entry, safe, head, list) {
if (&safe->list != head && strcmp(entry->value, safe->value) == 0) {
list_del_init(&entry->list);
free(entry->value);
free(entry);
needDel = true;
} else if (needDel) {
list_del_init(&entry->list);
free(entry->value);
free(entry);
needDel = false;
}
}
return true;
}
```
:::warning
詳閱 [CONTRIBUTING.md](https://github.com/sysprog21/lab0-c/blob/master/CONTRIBUTING.md),避免 `needDel` 這樣違反一致程式碼風格的用法。
:notes: jserv
:::
根據 [CONTRIBUTING.md](https://github.com/sysprog21/lab0-c/blob/master/CONTRIBUTING.md),使用 [snakecase](https://en.wikipedia.org/wiki/Snake_case) 風格來命名變數,變更如下:
```diff
@@ -5,18 +5,18 @@ bool q_delete_dup(struct list_head *head
return false;
element_t *entry, *safe;
- bool needDel = false;
+ bool need_del = false;
list_for_each_entry_safe (entry, safe, head, list) {
if (&safe->list != head && strcmp(entry->value, safe->value) == 0) {
list_del_init(&entry->list);
free(entry->value);
free(entry);
- needDel = true;
- } else if (needDel) {
+ need_del = true;
+ } else if (need_del) {
list_del_init(&entry->list);
free(entry->value);
free(entry);
- needDel = false;
+ need_del = false;
}
}
return true;
```
:::warning
善用 `q_release_element` 以縮減程式碼。`need_del` 可換為其他更符合題意的詞彙。
:notes: jserv
:::
參考老師的建議
- 以 `q_release_element` 縮減程式碼
- 將變數 `need_del` 改名為 `fund_dup` 以符合題意
- 將指標 `entry` 和 `safe` 改名為 `cur` 和 `tmp` 等更易懂的名稱
```diff
@@ -153,17 +153,17 @@ bool q_delete_dup(struct list_head *head)
if (!head || list_empty(head))
return false;
- element_t *entry, *safe;
- bool need_del = false;
- list_for_each_entry_safe (entry, safe, head, list) {
- if (&safe->list != head && strcmp(entry->value, safe->value) == 0) {
- list_del_init(&entry->list);
- q_release_element(entry);
- need_del = true;
- } else if (need_del) {
- list_del_init(&entry->list);
- q_release_element(entry);
- need_del = false;
+ element_t *cur, *tmp;
+ bool found_dup = false;
+ list_for_each_entry_safe (cur, tmp, head, list) {
+ if (&tmp->list != head && strcmp(cur->value, tmp->value) == 0) {
+ list_del_init(&cur->list);
+ q_release_element(cur);
+ found_dup = true;
+ } else if (found_dup) {
+ list_del_init(&cur->list);
+ q_release_element(cur);
+ found_dup = false;
}
}
return true;
```
最後放上修改後的程式碼
:::spoiler 完整程式碼
```c
/* Delete all nodes that have duplicate string */
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;
element_t *cur, *tmp;
bool found_dup = false;
list_for_each_entry_safe (cur, tmp, head, list) {
if (&tmp->list != head && strcmp(cur->value, tmp->value) == 0) {
list_del_init(&cur->list);
q_release_element(cur);
found_dup = true;
} else if (found_dup) {
list_del_init(&cur->list);
q_release_element(cur);
found_dup = false;
}
}
return true;
}
```
:::
### q_swap
交換佇列中鄰近的節點,若傳入的佇列為 `NULL` ,則不做任何動作。
這個函式中我主要是用 `list_move` 巨集來完成實作:
```c
/* list_move API */
static inline void list_move(struct list_head *node, struct list_head *head)
{
list_del(node);
list_add(node, head);
}
```
`list_move` 巨集會將 `node` 移到佇列的開頭,也就是會讓 `head->next` 指向 `node` ,利用這個功能,搭配迴圈走訪節點,一開始設定一個指標 `h` 指向 `head` 。
每次迴圈 `h` 會前進兩個節點 `h = h->next->next` ,將 `h->next->next` 當作新的 `head` 代入 `list_move` 中,就可達到交換佇列中鄰近的節點的目的。
```c
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head)
return;
struct list_head *h;
for (h = head; (h->next != head) && (h->next->next != head);
h = h->next->next)
list_move(h->next->next, h);
}
```
### q_reverse
以反向順序重新排列佇列
首先判斷傳入的佇列是否為 `NULL` 或空佇列或是僅有單一節點的佇列,若是的話則不做任何動作。
使用 `list_for_each_safe` 巨集來走訪佇列中的節點,並使用 `list_move` 巨集來將當前的節點移動到佇列的開頭,以此達到反向排列佇列的效果。
```c
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *cur, *tmp;
list_for_each_safe (cur, tmp, head) {
list_move(cur, head);
}
}
```
### q_reverseK
以每 `K` 個節點為一組進行反向排列該佇列
首先判斷傳入的佇列是否為 `NULL` 或空佇列或是僅有單一節點的佇列,以及 `K` 是否小於 `2` 或大於佇列的總長度,若是的話則不做任何動作。
與 `q_reverse` 的實作概念相同,但新增一個 `count` 變數來計算是否已走訪到第 `K` 個節點,若是的話將該節點設為 `tmp_head` ,也就是當作一個暫定的新的 `head` ,然後重置 `count` 為 `0`,如此一來 `list_move` 巨集會將 `tmp_head` 之後的節點一一移動到 `tmp_head` 的後面,也就是會重新開始反向排列 `tmp_head` 後的節點。
重複上述步驟便可達到以每 `K` 個節點為一組進行反向排列的目的。
```c
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head || list_empty(head) || list_is_singular(head) || k < 2 ||
k > q_size(head))
return;
struct list_head *cur, *tmp, *tmp_head = head;
int count = 0;
list_for_each_safe (cur, tmp, head) {
list_move(cur, tmp_head);
count++;
if (count == k) {
tmp_head = tmp->prev;
count = 0;
}
}
}
```
### list_append
這個函式被用在 `merge_two_lists` 函式中,目的是將兩個佇列串接在一起。
- `list` : 指向要被串接到後面的那個佇列
- `head` : 指向前面的那個佇列
與 `list_splice_tail` 函式不同的是, `list_append` 會將 `list` 所指的整個佇列,包含第一個節點,都接到 `head` 的後面,而 `list_splice_tail` 只會將 `list` 的下一個節點之後的節點合併到 `head` 。
```c
/**
* list_append() - Add all nodes from a list to the end of another list
* @list: pointer to the head of the list with the node entries to add
* @head: pointer to the head of the list to add the nodes to
*
* All nodes from @list, including the @list head, are added to the end of the
* list of @head.
*/
void list_append(struct list_head *list, struct list_head *head)
{
struct list_head *head_last = head->prev;
struct list_head *list_last = list->prev;
head_last->next = list;
list_last->next = head;
list->prev = head_last;
head->prev = list_last;
}
```
### merge_two_lists
:::warning
保持一致的命名風格。
:notes: jserv
:::
參考 [案例探討: LeetCode 21. Merge Two Sorted Lists](https://hackmd.io/@sysprog/c-linked-list#%E6%A1%88%E4%BE%8B%E6%8E%A2%E8%A8%8E-LeetCode-21-Merge-Two-Sorted-Lists) 中指標的指標的方式來實作此函式,用來合併兩個已排序的佇列。
指標用途:
- `e1` 、 `e2` : 分別指向 `l1` 和 `l2` 當前所指向的節點所連接的 element
- `merged_head` : 這個指標用來指向合併後的新佇列。
- `cur` : 比較 `l1` 和 `l2` 兩個節點中的字串, `cur` 會指向字典序 ( lexicographically ) 較小的節點。
- `merged_tail` : 指向合併後的佇列的尾端。
:::spoiler 完整程式碼
```c
/* Merge Two Sorted Lists */
struct list_head *mergeTwoLists(struct list_head *l1, struct list_head *l2)
{
struct list_head *merged_head = NULL, **merged_tail = &merged_head,
**cur = NULL;
element_t *e1 = NULL, *e2 = NULL;
while (l1->next != merged_head && l2->next != merged_head) {
e1 = list_entry(l1, element_t, list);
e2 = list_entry(l2, element_t, list);
cur = ((strcmp(e1->value, e2->value) <= 0) ? &l1 : &l2);
*merged_tail = *cur;
*cur = (*cur)->next;
list_del_init(*merged_tail);
list_add_tail(*merged_tail, merged_head);
merged_tail = &(*merged_tail)->next;
}
merged_head == l1->next ? list_append(l2, merged_head)
: list_append(l1, merged_head);
return merged_head;
}
```
:::
1. 使用一個迴圈來遍歷佇列 `l1` 和 `l2` ,比較 `l1` 和 `l2` 當前所指向的節點中的字串,選出字典序 ( lexicographically ) 較小的節點。
2. 接著用指標的指標 `cur` 指向選中的節點 ( 假設是 `l1` ) 的位址,而 `*merged_tail` 也會指向該節點,然後 `*cur` 會移向 `l1` 的下一個節點,因為 `cur` 中存放的是 `&l1` ,因此 `*cur = (*cur)->next` 就相當於 `l1 = l1->next` ,即 `l1` 會指向本身的下一個節點。
```c
e1 = list_entry(l1, element_t, list);
e2 = list_entry(l2, element_t, list);
cur = ((strcmp(e1->value, e2->value) <= 0) ? &l1 : &l2);
*merged_tail = *cur;
*cur = (*cur)->next;
```
3. 選中的節點 `*merged_tail` , 用 `list_del_init` 函式將其從原本的佇列移除,然後 `list_add_tail` 函式將其添加到新佇列的尾端,接著 `merged_tail` 會指向要存放下一個節點的空間,當下一個節點被選中後,會重複 1 ~ 3 的步驟。
```c
list_del_init(*merged_tail);
list_add_tail(*merged_tail, merged_head);
merged_tail = &(*merged_tail)->next;
```
4. 最後當迴圈結束時,將剩餘的節點全部串到新佇列的尾端,並回傳 `merged_head` 。
```c
merged_head == l1->next ? list_append(l2, merged_head)
: list_append(l1, merged_head);
return merged_head;
```
### merge_k_lists
參考 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list) 中的 [Merge Sort 的實作](https://hackmd.io/@sysprog/c-linked-list#Merge-Sort-%E7%9A%84%E5%AF%A6%E4%BD%9C),以 Merge Sort 的方式來合併多個已排序的佇列。
首先,檢查 `h` 是否為空佇列,如果是則直接回傳 `h`,表示無需合併,這同時也是遞迴函式的截止條件。
```c
if (list_empty(h))
return h;
```
接著, Merge Sort 主要分為兩大步驟,分割和合併:
#### 分割
使用前面提到的快慢指標的技巧來找出中間的節點,以便從中將佇列分割為兩個佇列,接著用遞迴的方式重複呼叫 `merge_k_lists` 函式,將佇列不斷分割為二,最後佇列將被分割為一個個單獨的節點。
```c
struct list_head *slow = h, *fast = h->next, *mid;
while (fast != h && fast->next != h) {
slow = slow->next;
fast = fast->next->next;
}
mid = slow->next;
mid->prev = h->prev;
h->prev->next = mid;
slow->next = h;
h->prev = slow;
slow = merge_k_lists(h);
fast = merge_k_lists(mid);
```
#### 合併
每個單獨的節點都可視為已排序的佇列,透過遞迴調用 `merge_two_lists` 函式來兩兩合併佇列,如此便可得到排序後的完整佇列。
```c
return merge_two_lists(slow, fast);
```
以下為 `merge_k_lists` 函式的完整程式碼
```c
struct list_head *merge_k_lists(struct list_head *h)
{
if (list_empty(h))
return h;
struct list_head *slow = h, *fast = h->next, *mid;
while (fast != h && fast->next != h) {
slow = slow->next;
fast = fast->next->next;
}
mid = slow->next;
mid->prev = h->prev;
h->prev->next = mid;
slow->next = h;
h->prev = slow;
slow = merge_k_lists(h);
fast = merge_k_lists(mid);
return merge_two_lists(slow, fast);
}
```
### q_sort
以==遞增順序==來排序佇列中的節點,首先判斷傳入的佇列是否為 `NULL` 或空佇列或是僅有單一節點的佇列,若是的話則不做任何動作。
接著給定一個指標 `new_queue` 指向 `head` 的下一個節點,並將 `head` 暫時移出佇列,因為 `head` 本身並沒有連接到 element,不包含任何字串因此不參與排序。
然後調用 `merge_k_lists` 函式 來排序 `new_queue` ,完成後用 `list_add` 巨集將 `head` 併回到佇列的頭部,如此即可完成佇列的排序。
```c
void q_sort(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *new_queue = head->next;
list_del_init(head);
new_queue = merge_k_lists(new_queue);
list_add(head, new_queue->prev);
}
```
### q_descend
`q_descend` 函式的主要功能為,對於佇列中的任何節點,如果其右側還有節點的值比它大,則將其從佇列中移除。
實做的概念為,通過將佇列反轉,以相反的順序遍歷佇列,從佇列的尾端開始遍歷,一旦找到一個節點比開頭節點(`first`)的值小,就從鏈表中刪除該節點 (`cur`) 。反之,如果找到一個節點比開頭節點的值大或相等,則將該節點設置為新的開頭節點。
最後,將佇列再次反轉以返回原始順序,並回傳佇列中剩餘節點的數量。
函式的流程如下:
1. 檢查佇列是否為 `NULL` 或是空佇列,若是則直接回傳 0。
2. 檢查佇列中是否只有一個節點,若是則直接返回 1。
3. 將佇列反轉,使其最右側的節點變成最左側的節點,方便從尾端開始遍歷佇列。
4. 將 `first` 指向佇列中第一個節點,初始時 `size` 設為 0,`deleted_nodes` 設為 0。
5. 使用 `list_for_each_entry_safe` 巨集從左到右遍歷佇列,對於每個節點執行以下操作:
- 將 `size` 加 1,代表經過了一個節點。
- 如果 `first` 指向的節點的值比當前節點的值大,代表存在某個右側節點的值比當前節點大,因此需要將當前節點從佇列中移除。
- 移除當前節點後,將 `deleted_nodes` 加 1,代表已經刪除了一個節點。
- 如果 `first` 指向的節點的值不比當前節點的值大,代表右側沒有比當前節點大的節點,因此將 `first` 指向當前節點。
6. 將佇列反轉回原本的順序。
7. 返回未被刪除節點的數量,即 `size` 減去 `deleted_nodes`。
```c
int q_descend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
if (!head || list_empty(head))
return 0;
if (list_is_singular(head))
return 1;
q_reverse(head);
element_t *cur, *tmp;
element_t *first = list_entry(head->next, element_t, list);
int size = 0, deleted_nodes = 0;
list_for_each_entry_safe (cur, tmp, head, list) {
size++;
if (strcmp(first->value, cur->value) > 0) {
list_del_init(&cur->list);
q_release_element(cur);
deleted_nodes++;
} else {
first = cur;
}
}
q_reverse(head);
return size - deleted_nodes;
}
```
### q_merge
觀察 `queue_contex_t` 結構,`chain` 會將所有佇列的頭部都串連在一起,`q` 會指向當前佇列的頭部。
```c
/**
* queue_contex_t - The context managing a chain of queues
* @q: pointer to the head of the queue
* @chain: used by chaining the heads of queues
* @size: the length of this queue
* @id: the unique identification number
*/
typedef struct {
struct list_head *q;
struct list_head chain;
int size;
int id;
} queue_contex_t;
```
`q_merge` 會將 `chain` 中所有的佇列合併為一個新的已排序佇列。
函式的流程如下:
1. 檢查 chain 是否為 `NULL` 或是空的 chain,若是則直接回傳 0。
2. 如果 chain 只有一個佇列,則直接返回這個已排序佇列的節點數量。
3. 如果 chain 有多個佇列,則取出第一個佇列(即 `queue_contex_t` 結構體),將其保存到 `head_chain` 指標中。
4. 然後從 head_chain 指針中的鏈表中取出第一個已排序鏈表,並將其從鏈表中刪除,將其指針保存在 queue 指針中。
5. 使用 `list_for_each_entry_safe` 遍歷 chain 中的所有佇列,每次取出一個已排序鏈表,然後將其從鏈表中刪除,將其指針保存在 entry 指針中。
6. 然後使用 merge_two_lists 函數將 queue 和 entry 這兩個已排序鏈表合併成一個新的已排序鏈表,並將其指針保存在 queue 指針中。
7. 將 entry 的節點數量添加到 total 變量中,然後重複執行步驟 5 和步驟 6,直到 head 鏈表中的所有節點都被遍歷完畢為止。
8. 最後,將 queue 指針指向的已排序鏈表添加到 head_chain 指針所指向的鏈表中,然後返回已排序鏈表的節點數量 total。
```c
int q_merge(struct list_head *head)
{
// https://leetcode.com/problems/merge-k-sorted-lists/
if (!head || list_empty(head))
return 0;
queue_contex_t *head_chain = list_entry(head->next, queue_contex_t, chain);
if (list_is_singular(head))
return head_chain->size;
struct list_head *queue = head_chain->q->next, *chain = NULL;
list_del_init(head_chain->q);
queue_contex_t *cur, *tmp;
int total = head_chain->size;
list_for_each_entry_safe (cur, tmp, head, chain) {
if (cur == head_chain)
continue;
chain = cur->q->next;
list_del_init(cur->q);
queue = merge_two_lists(queue, chain);
total += cur->size;
}
list_add(head_chain->q, queue->prev);
return total;
}
```
:::danger
注意作業書寫規範:中英文間用一個半形空白字元區隔。
程式碼縮排亦有相關規範,請依循!
:notes: jserv
:::
## 整合網頁伺服器
### 修復 `favicon.ico` 的問題
因為部分瀏覽器會要求 request 中要提供網頁圖案的對應資訊,而原始程式中的 response 為:
```c
char *buffer = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n";
```
其中並沒有包含關於 icon 的資訊。要修改這個問題,參考 [解決 `favicon.ico` 的問題](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-c#%E8%A7%A3%E6%B1%BA-faviconico-%E7%9A%84%E5%95%8F%E9%A1%8C) ,在 `cmd_select` 函式中,將 http response 的內容改為
```diff
@@ -617,7 +617,14 @@ static int cmd_select(int nfds,
accept(web_fd, (struct sockaddr *) &clientaddr, &clientlen);
char *p = web_recv(web_connfd, &clientaddr);
- char *buffer = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n";
+ char *buffer =
+ "HTTP/1.1 200 OK\r\n%s%s%s%s%s%s"
+ "Content-Type: text/html\r\n\r\n"
+ "<html><head><style>"
+ "body{font-family: monospace; font-size: 13px;}"
+ "td {padding: 1.5px 6px;}"
+ "</style><link rel=\"shortcut icon\" href=\"#\">"
+ "</head><body><table>\n";
web_send(web_connfd, buffer);
```
即可修復此問題。
### 確保 `web` 命令與 `linenoise` 套件可一起使用
目前一旦網頁伺服器啟動後,原本處理命令列操作的 `linenoise` 套件就無法使用。
觀察 `console.c` 中的程式碼,在 `do_web` 函式中,當網頁伺服器啟動時,會將 `use_linenoise` 設為 `false`
```c
web_fd = web_open(port);
if (web_fd > 0) {
printf("listen on port %d, fd is %d\n", port, web_fd);
use_linenoise = false;
} else {
perror("ERROR");
exit(web_fd);
}
```
而在 `run_console` 函式中,若 `use_linenoise = false` ,則不會啟用 `linenoise` 套件,會調用 `cmd_select` 函式來處理使用者輸入的命令,因此無法讓網頁伺服器與 `linenoise` 套件同時使用。
為了讓解決這個問題,首先我刪除了 `use_linenoise` 變數,也就是說不論是否有啟動網頁伺服器,都會使用 `linenoise` 套件。
```diff
@@ -404,7 +403,6 @@ static bool do_web(int argc, char *argv[])
web_fd = web_open(port);
if (web_fd > 0) {
printf("listen on port %d, fd is %d\n", port, web_fd);
- use_linenoise = false;
} else {
perror("ERROR");
exit(web_fd);
}
return true;
}
```
同樣的,也要修改 `run_console` 函式,移除關於 `use_linenoise` 的判斷,統一使用 `linenoise` 處理命令列輸入。
```diff
@@ -694,7 +691,7 @@ bool run_console(char *infile_name)
if (!has_infile) {
char *cmdline;
- while (use_linenoise && (cmdline = linenoise(prompt))) {
+ while ((cmdline = linenoise(prompt)) != NULL) {
interpret_cmd(cmdline);
line_history_add(cmdline); /* Add to the history. */
line_history_save(HISTORY_FILE); /* Save the history on disk. */
@@ -702,10 +699,9 @@ bool run_console(char *infile_name)
while (buf_stack && buf_stack->fd != STDIN_FILENO)
cmd_select(0, NULL, NULL, NULL, NULL);
has_infile = false;
- }
- if (!use_linenoise) {
- while (!cmd_done())
- cmd_select(0, NULL, NULL, NULL, NULL);
+
+ if (web_connfd > 0)
+ close(web_connfd);
}
} else {
while (!cmd_done())
```
但這樣又會有另一個問題,觀察程式等待輸入時的主要迴圈,位於 `linenoise()->line_raw()->line_edit()` 內的 `while(1)` 迴圈。
但 `linenoise` 是用 `read` 等待使用者輸入,無法接收 web 傳來的資訊。
因此要在 `line_edit()` 內的 `while(1)` 迴圈中嘗試用 `select()` 同時處理來自 `stdin` 及 `socket`的資訊。
> 參考 [整合 tiny-web-server](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-c#%E6%95%B4%E5%90%88-tiny-web-server)
首先判斷是否有啟用 `web` 命令,若 `web_fd > 0` ,表示網頁伺服器已啟用。
- 先將 socket 改為 non-blocking,防止程式停止接收使用者輸入:
```c
int flags = fcntl(web_fd, F_GETFL);
if (flags != (flags | O_NONBLOCK))
fcntl(web_fd, F_SETFL, flags | O_NONBLOCK);
```
- 接著,將 `web_fd` 和 `stdin_fd` 加入 `fd_set` 中,讓 `select` 可以同時處理來自使用者與網頁伺服器的命令。
```c
fd_set set;
FD_ZERO(&set);
FD_SET(web_fd, &set);
FD_SET(stdin_fd, &set);
int rv = select(web_fd + 1, &set, NULL, NULL, NULL);
```
- `select` 會監聽兩個 file descriptors(`web_fd` 和 `stdin_fd`)是否有 connect 或request 等事件發生。
- 如果是 `web_fd` 有事件發生,則代表有來自 web 的命令,並回傳一個 HTTP 200 OK 的回應。參考 `cmd_select` 中針對 web 命令的處理:
1. 使用 `accept` 函式等待客戶端的連接請求到達。
2. `web_recv` 函式會接收 HTTP 請求並回傳命令的字串。
3. 將命令字串複製到 `buf` 中。
4. 使用 `web_send` 將 response 回傳給客戶端 ( 記得修復 `favicon.ico` 的問題 )。
5. 最後回傳接收到的命令的長度。
```c
if (FD_ISSET(web_fd, &set)) {
web_connfd = accept(web_fd, (SA *) &clientaddr, &clientlen);
char *p = web_recv(web_connfd, &clientaddr);
strncpy(buf, p, strlen(p) + 1);
int len = strlen(p);
char *buffer =
"HTTP/1.1 200 OK\r\n%s%s%s%s%s%s"
"Content-Type: text/html\r\n\r\n"
"<html><head><style>"
"body{font-family: monospace; font-size: 13px;}"
"td {padding: 1.5px 6px;}"
"</style><link rel=\"shortcut icon\" href=\"#\">"
"</head><body><table>\n";
web_send(web_connfd, buffer);
free(p);
return len;
}
```
- 如果是 `stdin_fd` 有事件發生,則代表來自命令列的命令,此時程式會讀取使用者輸入的命令,最後會回傳讀取的命令的長度。
```c
else if (FD_ISSET(stdin_fd, &set)) {
nread = read(l.ifd, &c, 1);
if (nread <= 0)
return l.len;
}
```
而若是 `web` 命令未啟動,即 `web_fd <= 0` ,表示網頁伺服器未啟用,只需處理來自命令列的命令
```c
else {
nread = read(l.ifd, &c, 1);
if (nread <= 0)
return l.len;
}
```
:::spoiler 完整的實作
```diff
@@ -932,9 +935,56 @@ static int line_edit(int stdin_fd,
int nread;
char seq[5];
- nread = read(l.ifd, &c, 1);
- if (nread <= 0)
- return l.len;
+ if (web_fd > 0) {
+ int flags = fcntl(web_fd, F_GETFL);
+ if (flags != (flags | O_NONBLOCK))
+ fcntl(web_fd, F_SETFL, flags | O_NONBLOCK);
+
+ fd_set set;
+ FD_ZERO(&set);
+ FD_SET(web_fd, &set);
+ FD_SET(stdin_fd, &set);
+
+ int rv = select(web_fd + 1, &set, NULL, NULL, NULL);
+ struct sockaddr_in clientaddr;
+ socklen_t clientlen = sizeof(clientaddr);
+
+ switch (rv) {
+ case -1:
+ perror("select"); /* an error occurred */
+ continue;
+ case 0:
+ printf("timeout occurred\n"); /* a timeout occurred */
+ continue;
+ default:
+ if (FD_ISSET(web_fd, &set)) {
+ web_connfd = accept(web_fd, (SA *) &clientaddr, &clientlen);
+ char *p = web_recv(web_connfd, &clientaddr);
+ strncpy(buf, p, strlen(p) + 1);
+ int len = strlen(p);
+ char *buffer =
+ "HTTP/1.1 200 OK\r\n%s%s%s%s%s%s"
+ "Content-Type: text/html\r\n\r\n"
+ "<html><head><style>"
+ "body{font-family: monospace; font-size: 13px;}"
+ "td {padding: 1.5px 6px;}"
+ "</style><link rel=\"shortcut icon\" href=\"#\">"
+ "</head><body><table>\n";
+ web_send(web_connfd, buffer);
+ free(p);
+ return len;
+ } else if (FD_ISSET(stdin_fd, &set)) {
+ nread = read(l.ifd, &c, 1);
+ if (nread <= 0)
+ return l.len;
+ }
+ break;
+ }
+ } else {
+ nread = read(l.ifd, &c, 1);
+ if (nread <= 0)
+ return l.len;
+ }
```
:::
## 在 `qtest` 提供新的命令 `shuffle`
藉由 Fisher–Yates shuffle 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作。
我參考 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list) 中對於 [Fisher–Yates shuffle](https://hackmd.io/@sysprog/c-linked-list#Fisher%E2%80%93Yates-shuffle) 演算法的 [實作](https://hackmd.io/@sysprog/c-linked-list#%E5%AF%A6%E4%BD%9C)
- 首先在 `console_init` 函式中新增一個 `shuffle` 命令即描述
```c
ADD_COMMAND(shuffle,
"Shuffle all nodes in queue using the Fisher-Yates "
"shuffle algorithm",
"");
```
- 接著實作 `do_shuffle` 函式,定義 `shuffle` 命令的行為
1. `shuffle` 命令不接受參數,若有輸入參數會回傳 `false`
2. 若佇列為 `NULL` 則跳出警告並結束
3. `shuffle` 的實作中不允許配置新的記憶體,因此開啟 `set_noallocate_mode(true);`
4. 呼叫 `q_shuffle(current->q);` 來進行洗牌操作
5. 顯示洗牌後的結果並回傳成功與否
```c
static bool do_shuffle(int argc, char *argv[])
{
if (argc != 1) {
report(1, "%s takes no arguments", argv[0]);
return false;
}
if (!current || !current->q)
report(3, "Warning: Calling shuffle on null queue");
error_check();
set_noallocate_mode(true);
if (current && exception_setup(true))
q_shuffle(current->q);
exception_cancel();
set_noallocate_mode(false);
q_show(3);
return !error_check();
}
```
- `q_shuffle` 中定義了洗牌演算法的實作
1. 首先判斷傳入的佇列是否為 `NULL` 或空佇列或是僅有單一節點的佇列,若是的話則不做任何動作。
2. 使用時間戳作為亂數種子,初始化亂數產生器。
3. 獲取佇列的長度。
4. 初始化一個指標 `new` ,指向佇列的最後一個元素。
5. 進行 `len - 1` 輪的隨機交換操作,每一輪從原佇列中隨機選擇一個位置 `random`,從頭部開始遍歷鏈表,找到第 `ramdom` 個元素 `old`,然後交換 `old` 和 `new` 所對應的值。
6. 隨著每一輪的結束,`new` 遞減,指向原佇列中靠前的元素。
7. 當所有輪的交換操作完成後,佇列中的元素順序被打亂,保存在原佇列中。
```c
static void q_shuffle(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
srand(time(NULL));
int len = q_size(head);
int random;
char *tmp;
struct list_head *old, *new = head->prev;
element_t *entry_old, *entry_new;
while (len-- > 1) {
random = rand() % len;
old = head->next;
while (random--)
old = old->next;
entry_old = list_entry(old, element_t, list);
entry_new = list_entry(new, element_t, list);
tmp = entry_old->value;
entry_old->value = entry_new->value;
entry_new->value = tmp;
new = new->prev;
}
}
```
## 研讀論文〈[Dude, is my code constant time?](https://eprint.iacr.org/2016/1123.pdf)〉
### Simulation 程式實作驗證流程
在 `qtest` 中執行 `option simulation 1` 可開啟 “simulation” 模式,測試時間複雜度。
首先觀察 `do_option` 函式中的程式碼
```c
/* Find parameter in list */
param_element_t *plist = param_list;
while (!found && plist) {
if (strcmp(plist->name, name) == 0) {
int oldval = *plist->valp;
*plist->valp = value;
if (plist->setter)
plist->setter(oldval);
found = true;
} else
plist = plist->next;
}
```