---
tags: 2023 linux kernel
---
# 2023q1 Homework1 (lab0)
contributed by < [`Risheng1128`](https://github.com/Risheng1128) >
> [作業說明](https://hackmd.io/@sysprog/linux2023-lab0)
## 實驗環境
```shell
$ 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): 12
On-line CPU(s) list: 0-11
Vendor ID: GenuineIntel
Model name: 11th Gen Intel(R) Core(TM) i5-11400H @ 2.70GHz
CPU family: 6
Model: 141
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
Stepping: 1
CPU max MHz: 4500.0000
CPU min MHz: 800.0000
BogoMIPS: 5376.00
Virtualization features:
Virtualization: VT-x
Caches (sum of all):
L1d: 288 KiB (6 instances)
L1i: 192 KiB (6 instances)
L2: 7.5 MiB (6 instances)
L3: 12 MiB (1 instance)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0-11
$ gcc --version
gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
Copyright (C) 2021 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
```
## 佇列實作
### 使用的結構
實作之前,先分析目前 lab0-c 對管理佇列的設計
雙向鏈結串列
```ㄏ
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
佇列元素
```c
typedef struct {
char *value;
struct list_head list;
} element_t;
```
由上述兩種的結構,可以知道佇列的元素本身就是鏈結串列的節點,會產生如下的示意圖
```graphviz
digraph {
rankdir=LR;
splines=false;
node [shape="record"]
head [label="<list>head"]
element1 [label="<s>val|<list>list"]
element2 [label="<s>val|<list>list"]
element3 [label="<s>val|<list>list"]
dots [shape=none, label="..."]
head:list -> element1:list
element1:list -> head:list
element1:list -> element2:list
element2:list -> element1:list
element2:list -> dots
dots -> element2:list
dots -> element3:list
element3:list ->dots
element3:list -> head:list
head:list -> element3:list
}
```
接著是和去年不同的部份,現在可以管理多個佇列,主要是由於以下的結構實作
```c
typedef struct {
struct list_head *q;
struct list_head chain;
int size;
int id;
} queue_contex_t;
```
上述的結構為每個佇列的節點,成員說明如下:
- `q`: 指向佇列的第一個節點
- `chain`: 負責管理佇列的鏈結串列的節點
- `size`: 佇列節點的數量
- `id`: 鏈結串列編號
接著以下的結構為管理佇列的鏈結串列的第一個節點
```c
typedef struct {
struct list_head head;
int size;
} queue_chain_t;
static queue_chain_t chain = {.size = 0};
```
由上述的結構,可以得知目前管理佇列的鏈結串列示意圖如下:
```graphviz
digraph {
rankdir=LR;
splines=false;
node [shape="record"]
chain [label="<c>chain"]
queue1 [label="q|<c>chain|size|id"]
queue2 [label="q|<c>chain|size|id"]
queue3 [label="q|<c>chain|size|id"]
dots [shape=none, label="..."]
chain:c -> queue1:c
queue1:c -> chain:c
queue1:c -> queue2:c
queue2:c -> queue1:c
queue2:c -> dots
dots -> queue2:c
dots -> queue3:c
queue3:c ->dots
queue3:c -> chain:c
chain:c -> queue3:c
}
```
其中 `q` 則是對應到上述佇列的結構
### q_new
函式功能: 建立新的「空」佇列
:::info
函式流程
1. 使用 malloc 分配記憶體空間,並由指標 new 指向
2. 如果空間分配失敗 malloc 會回傳 NULL ,此時回傳 NULL
3. 使用函式 `INIT_LIST_HEAD` 初始化
:::
實際程式碼:
```c
struct list_head *q_new()
{
struct list_head *new = malloc(sizeof(*new));
if (!new)
return NULL;
// initialize the head
INIT_LIST_HEAD(new);
return new;
}
```
### q_free
函式功能: 釋放佇列所佔用的記憶體
:::info
函式流程
1. 如果傳入的 head 為 NULL ,直接結束函式
2. 走訪整個鏈結串列,每經過一個節點,就對其使用函式 `list_del` 移除該節點並釋放其空間
3. 釋放 head 的記憶體空間
:::
實際程式碼:
```c
void q_free(struct list_head *l)
{
if (!l)
return;
element_t *entry, *safe;
list_for_each_entry_safe (entry, safe, l, list) {
list_del(&entry->list);
q_release_element(entry);
}
// free list head
free(l);
}
```
### q_insert_head
函式功能: 在佇列開頭 (head) 插入 (insert) 給定的新節點
:::info
函式流程
1. 使用函式 `malloc` 分配 `element_t` 大小的記憶體空間,若失敗則回傳 `false`
2. 使用函式 `strdup` 建立複製字串資料
3. 使用函式 `list_add` 將節點加在 `head` 的下一個位置
:::
實際程式碼:
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *element = malloc(sizeof(*element));
if (!element)
return false;
// copy string
element->value = strdup(s);
if (!element->value) {
free(element);
return false;
}
// add node into doubly-linked list at head
list_add(&element->list, head);
return true;
}
```
其中函式 `strdup` 的實作如下
```c
/* Use undef to avoid strdup redefined error */
#undef strdup
#define strdup test_strdup
char *test_strdup(const char *s)
{
size_t len = strlen(s) + 1;
void *new = test_malloc(len);
if (!new)
return NULL;
return memcpy(new, s, len);
}
```
### q_insert_tail
函式功能: 在佇列尾端 (tail) 插入 (insert) 給定的新節點
:::info
函式流程
1. 使用函式 `malloc` 分配 `element_t` 大小的記憶體空間,若失敗則回傳 `false`
2. 使用函式 `strdup` 建立複製字串資料
3. 使用函式 `list_add_tail` 將節點加在 `head` 的前一個位置
:::
實際程式碼:
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *element = malloc(sizeof(*element));
if (!element)
return false;
// copy string
element->value = strdup(s);
if (!element->value) {
free(element);
return false;
}
// add node into doubly-linked list at tail
list_add_tail(&element->list, head);
return true;
}
```
### q_remove_head
函式功能: 在佇列開頭 (head) 移去 (remove) 給定的節點
:::info
函式流程
1. 如果佇列為 `NULL` 或是空佇列,則回傳 `NULL`
2. 移除佇列的第一個元素 (`head->next`) 並且取得其資料
3. 將資料複製到 buffer 中
:::
實際程式碼:
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
struct list_head *node = head->next;
element_t *element = list_entry(node, element_t, list);
list_del(node);
if (sp) {
strncpy(sp, element->value, bufsize - 1);
// add null terminator
sp[bufsize - 1] = 0;
}
return element;
}
```
### q_remove_tail
函式功能: 在佇列尾端 (tail) 移去 (remove) 給定的節點
:::info
函式流程
1. 如果佇列為 `NULL` 或是空佇列,則回傳 `NULL`
2. 移除佇列的最後一個元素 (`head->prev`) 並且取得其資料
3. 將資料複製到 buffer 中
:::
實際程式碼:
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
struct list_head *node = head->prev;
element_t *element = list_entry(node, element_t, list);
list_del(node);
if (sp) {
strncpy(sp, element->value, bufsize - 1);
// add null terminator
sp[bufsize - 1] = 0;
}
return element;
}
```
### q_size
函式功能: 計算目前佇列的節點總量
:::info
函式流程
1. 如果佇列為 `NULL` 則回傳 `0`
2. 走訪鏈結串列並計算走過的數量
:::
實際程式碼:
```c
int q_size(struct list_head *head)
{
if (!head)
return 0;
int count = 0;
struct list_head *node;
list_for_each (node, head)
count++;
return count;
}
```
### q_delete_mid
函式功能: 移走佇列中間的節點
:::info
函式流程
1. 如果佇列為 `NULL` 或是空佇列則回傳 `false`
2. 使用指標分別往前 (`forward`) 及往後 (`afterward`) 走,並找到中間的節點 (最後為 `forward` )
3. 移除中間的節點並釋放其記憶體空間
:::
實際程式碼:
```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 *afterward = head->next, *forward = head->prev;
// move point to find the middle node
for (; afterward != forward && afterward->next != forward;
afterward = afterward->next, forward = forward->prev)
;
// remove the middle node in queue
list_del(forward);
// delete the node
q_release_element(list_entry(forward, element_t, list));
return true;
}
```
### q_delete_dup
函式功能: 在**已經排序**的狀況,移走佇列中具備重複內容的節點
:::info
函式流程
1. 如果佇列為 `NULL` 則回傳 `false`
2. 走訪整個鏈結串列並取得 `curr` 和 `next` 的元素地址
```c
list_for_each_entry_safe (curr_entry, next_entry, head, list)
```
3. 當資料相同時,會對 `next` 指向的節點進行移除並釋放空間,同時 `flag` 被設置為 `true` ,字串比較則使用 [strcmp](https://man7.org/linux/man-pages/man3/strcmp.3.html) 函式,當資料相同時回傳 `0`
```c
while (&next_entry->list != head &&
!strcmp(curr_entry->value, next_entry->value)) {
list_del(&next_entry->list);
q_release_element(next_entry);
// update next pointer
next_entry = list_entry(curr_entry->list.next, element_t, list);
flag = true;
}
```
4. 當 `flag` 為 `true` 時,表示發生過資料相同的情況,但 `curr` 指到的節點尚未釋放,因此要記得釋放該空間
```c
// need remove current node
if (flag) {
list_del(&curr_entry->list);
q_release_element(curr_entry);
flag = false;
}
```
:::
實際程式碼:
```c
bool q_delete_dup(struct list_head *head)
{
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
if (!head)
return false;
bool flag = false;
element_t *curr_entry, *next_entry;
list_for_each_entry_safe (curr_entry, next_entry, head, list) {
while (&next_entry->list != head &&
!strcmp(curr_entry->value, next_entry->value)) {
list_del(&next_entry->list);
q_release_element(next_entry);
// update next pointer
next_entry = list_entry(curr_entry->list.next, element_t, list);
flag = true;
}
// need remove current node
if (flag) {
list_del(&curr_entry->list);
q_release_element(curr_entry);
flag = false;
}
}
return true;
}
```
:::warning
TODO: 用更精簡的方式撰寫程式碼。
:notes: jserv
:::
後來參考 [alanjian85](https://hackmd.io/@alanjian85/lab0-2023#2023q1-Homework1-lab0) 同學的作業,發現了更好的解法,兩者相同的部份都是使用指標 `prev` 及 `curr` 來判斷資料是否相同,而不同之處在於我原本的實作是移除 `curr` 指向的節點,後者則是移除 `prev` 的節點,如此一來可以節省 `curr` 的迴圈
修改後的程式碼如下所示,修改紀錄為 [commit](https://github.com/Risheng1128/lab0-c/commit/863444d138c17dd9698476af3a0e62376b2a67b3):
```c
bool q_delete_dup(struct list_head *head)
{
// https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
if (!head)
return false;
bool flag = false;
element_t *curr_entry, *next_entry;
list_for_each_entry_safe (curr_entry, next_entry, head, list) {
bool equal = &next_entry->list != head &&
!strcmp(curr_entry->value, next_entry->value);
if (flag || equal) {
list_del(&curr_entry->list);
q_release_element(curr_entry);
flag = equal;
}
}
// need remove current node
if (flag) {
list_del(&curr_entry->list);
q_release_element(curr_entry);
}
return true;
}
```
### q_swap
函式功能: 交換佇列中鄰近的節點
:::info
函式流程
1. 如果佇列為 `NULL` 則直接離開函式
2. 走訪鏈結串列,每次都將 `first` 指到的節點取出並加到 `second` 指到的節點後,如此一來就達到交換的功能
:::
實際程式碼:
```c
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head)
return;
struct list_head *first = head->next;
for (struct list_head *second = first->next;
first != head && second != head;
first = first->next, second = first->next) {
// can swap
list_del(first);
list_add(first, second);
}
}
```
### q_reverse
函式功能: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點
:::info
函式流程
1. 建立三個指標 prev, curr 及 next ,並依照 prev → curr → next 的順序指向不同節點
```graphviz
digraph {
rankdir=LR;
node[shape=record];
head [label="<list>head"]
element1 [label="<list>bear"]
element2 [label="<list>dolphin"]
element3 [label="<list>meerkat"]
dots [shape=none, label="..."]
prev [shape=none, label="prev"]
curr [shape=none, label="curr"]
next [shape=none, label="next"]
head:list -> element1:list
element1:list -> head:list
element1:list -> element2:list
element2:list -> element1:list
element2:list -> dots
dots -> element2:list
dots -> element3:list
element3:list -> dots
element3:list -> head:list
head:list -> element3:list
prev -> element3:list [color=red]
curr -> head:list [color=red]
}
```
2. `next` 指到目前節點的下一個
```graphviz
digraph {
rankdir=LR;
node[shape=record];
head [label="<list>head"]
element1 [label="<list>bear"]
element2 [label="<list>dolphin"]
element3 [label="<list>meerkat"]
dots [shape=none, label="..."]
prev [shape=none, label="prev"]
curr [shape=none, label="curr"]
next [shape=none, label="next"]
head:list -> element1:list
element1:list -> head:list
element1:list -> element2:list
element2:list -> element1:list
element2:list -> dots
dots -> element2:list
dots -> element3:list
element3:list -> dots
element3:list -> head:list
head:list -> element3:list
prev -> element3:list
curr -> head:list
next -> element1:list [color=red]
}
```
3. 交換目前節點的下一個及上一個節點
```graphviz
digraph {
rankdir=LR;
node[shape=record];
head [label="<list>head"]
element1 [label="<list>bear"]
element2 [label="<list>dolphin"]
element3 [label="<list>meerkat"]
dots [shape=none, label="..."]
prev [shape=none, label="prev"]
curr [shape=none, label="curr"]
next [shape=none, label="next"]
head:list -> element1:list [label="prev" , color=red]
element1:list -> head:list
element1:list -> element2:list
element2:list -> element1:list
element2:list -> dots
dots -> element2:list
dots -> element3:list
element3:list -> dots
element3:list -> head:list
head:list -> element3:list [label="next" , color=red]
prev -> element3:list
curr -> head:list
next -> element1:list
}
```
4. 更新 `prev` 及 `curr`
```graphviz
digraph {
rankdir=LR;
node[shape=record];
head [label="<list>head"]
element1 [label="<list>bear"]
element2 [label="<list>dolphin"]
element3 [label="<list>meerkat"]
dots [shape=none, label="..."]
prev [shape=none, label="prev"]
curr [shape=none, label="curr"]
next [shape=none, label="next"]
head:list -> element1:list
element1:list -> head:list
element1:list -> element2:list
element2:list -> element1:list
element2:list -> dots
dots -> element2:list
dots -> element3:list
element3:list -> dots
element3:list -> head:list
head:list -> element3:list
prev -> head:list [color=red]
curr -> element1:list [color=red]
next -> element2:list [color=red]
}
```
5. 完整走訪整個鏈結串列,即可完成反轉
:::
實際程式碼:
```c
void q_reverse(struct list_head *head)
{
if (!head)
return;
struct list_head *prev = head->prev, *curr = head, *next = NULL;
while (next != head) {
next = curr->next;
curr->next = prev;
curr->prev = next;
prev = curr;
curr = next;
}
}
```
### q_reverseK
函式功能: 類似 `q_reverse` ,但指定每 k 個節點為一組,進行反向順序排列
以下的函式流程以 `trace-06-ops.cmd` 的測資為例
> l = [e, e, d, c, b, a, a, a], k = 3
:::info
函式流程
1. 如果佇列為 NULL 或是空佇列則直接離開函式
2. 切開鏈結串列裡最後一個節點回到 head 的連線
```c
head->prev->next = NULL;
```
```graphviz
digraph {
rankdir=LR;
node[shape=record];
h [label="head"]
e1 [label="e"]
e2 [label="e"]
e3 [label="d"]
e4 [label="c"]
e5 [label="b"]
e6 [label="a"]
e7 [label="a"]
e8 [label="a"]
NULL [shape=none, label="NULL"]
h -> e1
e1 -> h
e1 -> e2
e2 -> e1
e2 -> e3
e3 -> e2
e3 -> e4
e4 -> e3
e4 -> e5
e5 -> e4
e5 -> e6
e6 -> e5
e6 -> e7
e7 -> e6
e7 -> e8 [color=red]
e8 -> e7
e8 -> NULL
h -> e8
}
```
3. 當 `count` 第一次和 `k` 相同且在執行函式 `q_reverse` 之前,此時從佇列頭開始,可以看到形成了一個單向環狀鏈結串列 [e, e, d] ,這麼做的目的是要滿足函式 `q_reverse` 輸入,也就是函式 `q_reverse`是**以輸入節點的下一個節點 (head->next) 開始反轉**
```graphviz
digraph {
rankdir=LR;
node[shape=record];
h [label="head"]
e1 [label="e"]
e2 [label="e"]
e3 [label="d"]
e4 [label="c"]
e5 [label="b"]
e6 [label="a"]
e7 [label="a"]
e8 [label="a"]
sub_head [shape=none, label="sub_head"]
sub_tail [shape=none, label="sub_tail"]
old_tail [shape=none, label="old_tail"]
next_head [shape=none, label="next_head"]
NULL [shape=none, label="NULL"]
h -> e1
e1 -> h
e1 -> e2
e2 -> e1
e2 -> e3
e3 -> e2
e3 -> h [color=red]
e4 -> e3
e4 -> e5
e5 -> e4
e5 -> e6
e6 -> e5
e6 -> e7
e7 -> e6
e7 -> e8
e8 -> e7
e8 -> NULL
h -> e8
old_tail -> h [color=red]
sub_head -> e1 [color=red]
sub_tail -> e3 [color=red]
next_head -> e4 [color=red]
}
```
4. 接著開始反轉,需注意的是原本的 `sub_head` 及 `sub_tail` 指向節點的相對位置會相反
```graphviz
digraph {
rankdir=LR;
node[shape=record];
h [label="head"]
e1 [label="e"]
e2 [label="e"]
e3 [label="d"]
e4 [label="c"]
e5 [label="b"]
e6 [label="a"]
e7 [label="a"]
e8 [label="a"]
sub_head [shape=none, label="sub_head"]
sub_tail [shape=none, label="sub_tail"]
old_tail [shape=none, label="old_tail"]
next_head [shape=none, label="next_head"]
NULL [shape=none, label="NULL"]
h -> e3
e3 -> h
e3 -> e2
e2 -> e3
e2 -> e1
e1 -> e2
e1 -> h
h -> e1
e4 -> e3
e4 -> e5
e5 -> e4
e5 -> e6
e6 -> e5
e6 -> e7
e7 -> e6
e7 -> e8
e8 -> e7
e8 -> NULL
old_tail -> h
sub_head -> e1 [color=red]
sub_tail -> e3 [color=red]
next_head -> e4
}
```
5. 將 `old_tail->next` 指向 `sub_tail` 指到的節點且 `sub_head->next` 指向 `next->head` 指到的節點,前者在這部份不會有任何改變,但在鏈結串列之間反轉後,需要上次反轉後的 tail 接上,就會有所功能
```graphviz
digraph {
rankdir=LR;
node[shape=record];
h [label="head"]
e1 [label="e"]
e2 [label="e"]
e3 [label="d"]
e4 [label="c"]
e5 [label="b"]
e6 [label="a"]
e7 [label="a"]
e8 [label="a"]
sub_head [shape=none, label="sub_head"]
sub_tail [shape=none, label="sub_tail"]
old_tail [shape=none, label="old_tail"]
next_head [shape=none, label="next_head"]
NULL [shape=none, label="NULL"]
h -> e3 [color=red]
e3 -> h
e3 -> e2
e2 -> e3
e2 -> e1
e1 -> e2
e1 -> e4 [color=red]
h -> e1
e4 -> e3
e4 -> e5
e5 -> e4
e5 -> e6
e6 -> e5
e6 -> e7
e7 -> e6
e7 -> e8
e8 -> e7
e8 -> NULL
old_tail -> h
sub_head -> e1
sub_tail -> e3
next_head -> e4
}
```
6. 更新所有指標並重複上述的步驟
7. 最後利用函式 `restructure_list` 將鏈結串列接好
:::
實際程式碼:
```c
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
if (!head || list_empty(head))
return;
int count = 0;
struct list_head *sub_head = head->next, *next_head = NULL,
*old_tail = head;
// cut the linked list to be singly-linked list
head->prev->next = NULL;
for (struct list_head *sub_tail = head->next; sub_tail;
sub_tail = sub_tail->next) {
if (++count == k) {
next_head = sub_tail->next;
sub_tail->next = old_tail;
q_reverse(old_tail);
// old node connects to the head of new list
old_tail->next = sub_tail;
// the new list connect to the next node
sub_head->next = next_head;
old_tail = sub_tail = sub_head;
sub_head = next_head;
count = 0;
}
}
/* restructure the doubly-linked list */
restructure_list(head);
}
```
其中函式 `restructure_list` 的實作如下,功能為將雙向鏈結串列的 `prev` 指標接好
```c
void restructure_list(struct list_head *head)
{
struct list_head *curr = head, *next = curr->next;
while (next) {
next->prev = curr;
curr = next;
next = next->next;
}
curr->next = head;
head->prev = curr;
}
```
### q_sort
函式功能: 以**遞增順序**來排序鏈結串列的節點
:::info
函式流程
函式 `q_sort` 的實作被主要拆為三個函式 `q_sort`, `mergesort` 及 `mergelist`
`q_sort`:
1. 首先將指向 `head` 的指標改成 `NULL`,目的在於把雙向鏈結串列的終點從 `head` 改為 `NULL`,變成單向鏈結串列
```c
head->prev->next = NULL;
```
2. 接著呼叫函式 `mergesort` 開始進行 merge sort
```c
head->next = mergesort(head->next);
```
3. 排序完後每個節點的 `prev` 會亂掉,因此需要走訪各個節點並且把所有 `prev` 接回來
```c
restructure_list(head);
```
`mergesort`: 參考[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)裡, merge sort 的實作方式並做修改
1. 使用「快慢指標」[Floyd’s Cycle detection](https://en.wikipedia.org/wiki/Cycle_detection) 演算法找出鏈結串列正中間的節點,並將鏈結串列切成 `left` 及 `right` 兩組鏈結串列
```c
struct list_head *fast = head, *slow = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
}
struct list_head *mid = slow;
slow->prev->next = NULL;
struct list_head *left = mergesort(head);
struct list_head *right = mergesort(mid);
```
2. 呼叫函式 `mergelist` 合併 `left` 及 `right`
```c
return mergelist(left, right);
```
`mergelist`: 參考 [21. Merge Two Sorted Lists](https://leetcode.com/problems/merge-two-sorted-lists/) ,並實作合併兩個鏈結串列
1. 利用指標的指標 `indirect` 指向指標 `res` ,並藉由修改指標完成鏈結串列合併的動作
```c
struct list_head *res = NULL;
struct list_head **indirect = &res;
```
2. 使用函式 `strcmp` 判斷資料的大小
```c
node = strcmp(list1_entry->value, list2_entry->value) < 0 ? &list1
```
:::
實際程式碼:
```c
/* Merge the two lists in a one sorted list. */
static struct list_head *mergelist(struct list_head *list1,
struct list_head *list2)
{
struct list_head *res = NULL;
struct list_head **indirect = &res;
for (struct list_head **node = NULL; list1 && list2;
*node = (*node)->next) {
element_t *list1_entry = list_entry(list1, element_t, list);
element_t *list2_entry = list_entry(list2, element_t, list);
node = strcmp(list1_entry->value, list2_entry->value) < 0 ? &list1
: &list2;
*indirect = *node;
indirect = &(*indirect)->next;
}
*indirect = (struct list_head *) ((size_t) list1 | (size_t) list2);
return res;
}
/*
* Divide the list into several nodes and merge to sorted list.
* No effect if q is NULL or empty.
*/
static struct list_head *mergesort(struct list_head *head)
{
if (!head || !head->next)
return head;
struct list_head *fast = head, *slow = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
}
struct list_head *mid = slow;
slow->prev->next = NULL;
struct list_head *left = mergesort(head);
struct list_head *right = mergesort(mid);
return mergelist(left, right);
}
/* Sort elements of queue in ascending order */
void q_sort(struct list_head *head)
{
if (!head || list_empty(head))
return;
// cut the linked list to be singly-linked list
head->prev->next = NULL;
head->next = mergesort(head->next);
/* restructure the doubly-linked list */
restructure_list(head);
}
```
### q_descend
函式功能: 對所有節點而言,移除所有在其之前且資料較小的節點
思考邏輯: 目標是把對每個節點之前,所有資料比其小的節點移除,換言之,其實可以利用雙向鏈結串列的特性,反向走訪所有的節點,並產生遞增的鏈結串列
實際程式碼:
```c
int q_descend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
if (!head || list_empty(head) || list_is_singular(head))
return 0;
for (struct list_head *curr = head->prev, *next = curr->prev; next != head;
next = curr->prev) {
element_t *curr_entry = list_entry(curr, element_t, list);
element_t *next_entry = list_entry(next, element_t, list);
// if current node is greater than next node
if (strcmp(curr_entry->value, next_entry->value) > 0) {
list_del(next);
q_release_element(next_entry);
} else
curr = next;
}
return q_size(head);
}
```
### q_merge
函式功能: 合併所有**已排序**的佇列,並合而為一個新的已排序佇列
:::info
函式流程
1. 利用指標 `merge` 暫時儲存已經合併的佇列
2. 走訪所有的佇列並一個一個和 `merge` 合併 (利用函式 `mergelist`) ,由於函式 `mergelist` 的輸入皆為單向鏈結串列,因此這裡需要先將原本的佇列變成單向鏈結串列,再進行合併
```c
head_entry->q->prev->next = NULL;
merge = mergelist(merge, head_entry->q->next);
```
3. 將合併完的佇列復原
```c
head_entry->q->next = head_entry->q;
```
4. 最後將合併的佇列接在 `chain` 的第一個元素上,並使用函式 `restructure_list` 將 `prev` 完整連接
```c
head_entry = list_entry(head->next, queue_contex_t, chain);
head_entry->q->next = merge;
/* restructure the doubly-linked list */
restructure_list(head_entry->q);
```
:::
實際程式碼:
```c
int q_merge(struct list_head *head)
{
// https://leetcode.com/problems/merge-k-sorted-lists/
if (!head || list_empty(head))
return 0;
struct list_head *merge = NULL;
queue_contex_t *head_entry = NULL;
list_for_each_entry (head_entry, head, chain) {
// cut the linked list to be singly-linked list
head_entry->q->prev->next = NULL;
merge = mergelist(merge, head_entry->q->next);
head_entry->q->next = head_entry->q;
}
head_entry = list_entry(head->next, queue_contex_t, chain);
head_entry->q->next = merge;
/* restructure the doubly-linked list */
restructure_list(head_entry->q);
return q_size(head_entry->q);
}
```
## 研讀 Linux 核心原始碼 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c)
原始碼的運作分析放在額外的筆記 [研讀 Linux 核心原始程式碼 `list_sort.c`](https://hackmd.io/@Risheng/list_sort)
### 引入 Linux 核心原始碼 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 到 lab0-c
完整的實作紀錄可參考 [commit](https://github.com/Risheng1128/lab0-c/commit/0368c53c4d81dbaac9c582db2e9f45b938c49dfd) ,以下為實作的流程
首先,將 Linux 核心原始檔案 [list_sort.c](https://github.com/torvalds/linux/blob/master/lib/list_sort.c) 及 [list_sort.h](https://github.com/torvalds/linux/blob/master/include/linux/list_sort.h) 的相關程式碼加進 lab0-c 並作修改,其中包含 list_sort 完整的程式碼及其函式定義,接著將以下 lab0-c 沒有的巨集函式加進 `list_sort.h`
- `likely()` (位於 [`include/linux/compiler.h`](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h))
- `unlikely()` (位於 [`include/linux/compiler.h`](https://github.com/torvalds/linux/blob/master/include/linux/compiler.h))
完成的 `list_sort.h` 實際程式碼如下所示:
```c
#pragma once
#ifndef likely
#define likely(x) __builtin_expect(!!(x), 1)
#endif
#ifndef unlikely
#define unlikely(x) __builtin_expect(!!(x), 0)
#endif
struct list_head;
__attribute__((nonnull(2, 3))) typedef int (*list_cmp_func_t)(
void *,
const struct list_head *,
const struct list_head *);
__attribute__((nonnull(2, 3))) void list_sort(void *priv,
struct list_head *head,
list_cmp_func_t cmp);
__attribute__((nonnull(2, 3))) int list_cmp(void *priv,
const struct list_head *list1,
const struct list_head *list2);
```
接著在 `Makefile` 新增要編譯出的目標檔案,這裡可由變數 `ENABLE_LINUX_SORT` 讓使用者決定是否要使用 `list_sort`
```
ENABLE_LINUX_SORT ?= 1
ifeq ($(ENABLE_LINUX_SORT), 1)
CFLAGS += -D FEATURE_LINUX_SORT=1
OBJS += list_sort.o
endif
```
在 `list_sort.c` 新增函式 `list_cmp` ,用來比較節點的資料
```c
/* List compare funciton */
__attribute__((nonnull(2, 3))) int list_cmp(void *priv,
const struct list_head *list1,
const struct list_head *list2)
{
// cppcheck-suppress nullPointer
element_t *list1_entry = list_entry(list1, element_t, list);
// cppcheck-suppress nullPointer
element_t *list2_entry = list_entry(list2, element_t, list);
return strcmp(list1_entry->value, list2_entry->value) <= 0 ? 0 : 1;
}
```
最後則是修改檔案 `qtest.c` 裡的函式 `do_sort` ,並搭配剛剛在 `Makefile` 裡設定的參數來決定要使用原本的 `q_sort` 或是新引入的 list_sort
```c
bool do_sort(int argc, char *argv[])
{
...
if (current && exception_setup(true))
#if FEATURE_LINUX_SORT
list_sort(NULL, current->q, list_cmp);
#else
q_sort(current->q);
#endif
...
}
```
### 比較自己實作的 merge sort 和 Linux 核心程式碼
首先建立一個用來量測排序時間的測試檔 `trace-time-measure.cmd` ,內容如下所示,其中隨機產生的資料數會改變
```
option fail 0
option malloc 0
new
ih RAND 100000
time
sort
time
```
接著修改以下的 `Makefile` 腳本並輸入命令 `make check` 來分別測試 `q_sort` 及 `list_sort`
```
check: qtest
./$< -v 3 -f traces/trace-time-measure.cmd
```
可以統計出兩者執行的時間,測試方式為對每個函式分別測試 3 次並紀錄其排序時間,同時資料總數從 100000 以公差為 100000 遞增到 500000
| 資料總數 | `q_sort` 第一次 (s) | `q_sort` 第二次 (s) | `q_sort` 第三次 (s) | `list_sort` 第一次 (s) | `list_sort` 第二次 (s) | `list_sort` 第三次 (s) |
| ------- | ----- | ----- | ----- | ----- | ----- | ----- |
| 100000 | 0.072 | 0.071 | 0.068 | 0.039 | 0.040 | 0.038 |
| 200000 | 0.190 | 0.222 | 0.200 | 0.114 | 0.120 | 0.114 |
| 300000 | 0.333 | 0.361 | 0.330 | 0.196 | 0.198 | 0.194 |
| 400000 | 0.488 | 0.528 | 0.517 | 0.286 | 0.284 | 0.301 |
| 500000 | 0.646 | 0.685 | 0.699 | 0.376 | 0.393 | 0.363 |
接著將以上的數據取平均後如下所示
| 資料總數 | `q_sort` 平均 (s) | `list_sort` 平均 (s) |
| ------- | ----- | ----- |
| 100000 | 0.070 | 0.039 |
| 200000 | 0.204 | 0.116 |
| 300000 | 0.341 | 0.196 |
| 400000 | 0.511 | 0.290 |
| 500000 | 0.676 | 0.377 |
最後使用 [gnuplot](https://en.wikipedia.org/wiki/Gnuplot) 畫圖,參考 [gnuplot 語法解說和示範](https://hackmd.io/@sysprog/gnu-linux-dev/https%3A%2F%2Fhackmd.io%2Fs%2FSkwp-alOg) ,以下為比較結果,可以發現 `list_sort` 的排序時間比起 `q_sort` 快了不少

接著利用 [perf](https://perf.wiki.kernel.org/index.php/Main_Page) 找到兩者的差異,安裝可以參考 [Linux 效能分析工具: Perf](http://wiki.csie.ncku.edu.tw/embedded/perf-tutorial)
輸入命令 `perf stat --repeat 5 -e cache-misses,cache-references,instructions,cycles ./qtest -f traces/trace-time-measure.cmd`
→ 程式會執行 5 次,並且自動平均,其中每次測試的資料數都設定為 500000 ,以下為 `q_sort` 及 `list_sort` 執行的結果
首先是 `q_sort` 的結果
```shell
Performance counter stats for './qtest -f traces/trace-time-measure.cmd' (5 runs):
2256,8287 cache-misses # 68.299 % of all cache refs ( +- 0.15% )
3288,3709 cache-references ( +- 0.36% )
22,6838,1411 instructions # 0.44 insn per cycle ( +- 0.00% )
50,7244,9200 cycles ( +- 0.51% )
1.18335 +- 0.00408 seconds time elapsed ( +- 0.34% )
```
接著是 `list_sort` 的結果
```
Performance counter stats for './qtest -f traces/trace-time-measure.cmd' (5 runs):
1887,5232 cache-misses # 72.800 % of all cache refs ( +- 0.22% )
2577,8636 cache-references ( +- 0.26% )
22,7975,0582 instructions # 0.71 insn per cycle ( +- 0.02% )
31,8624,7214 cycles ( +- 0.38% )
0.73931 +- 0.00590 seconds time elapsed ( +- 0.80% )
```
將輸出的結果做成表格,如下表所示
| | `q_sort` | `list_sort` |
| ---------------- | ------------ | ------------ |
| cycles | 50,7244,9200 | 31,8624,7214 |
| instructions | 22,6838,1411 | 22,7975,0582 |
| cache-references | 3288,3709 | 2577,8636 |
| cache-misses | 2256,8287 | 1887,5232 |
| insn per cycle | 0.44 | 0.71 |
可以發現 `list_sort` 發生 cache miss 的次數比 q_sort 來的少,可以對應到 [Linux 核心的鏈結串列排序](https://hackmd.io/@sysprog/linux2023-lab0/%2F%40sysprog%2Flinux2023-lab0-e) 裡提到「用 bottom up 實作 merge sort 對 cache 較友善,可避免大量的 [cache trashing](https://en.wikipedia.org/wiki/Thrashing_(computer_science))」
:::warning
探討 `list_sort.c` 的比較次數。
:notes: jserv
:::
## 運用 Valgrind 排除 qtest 實作的記憶體錯誤
輸入命令 `make valgrind` 對資料夾 `traces` 裡的所有測試資料使用 valgrind 進行檢查
:::spoiler 測試結果
```shell
+++ TESTING trace trace-01-ops:
# Test of insert_head and remove_head
--- trace-01-ops 5/5
+++ TESTING trace trace-02-ops:
# Test of insert_head, insert_tail, remove_head, remove_tail, and delete_mid
--- trace-02-ops 6/6
+++ TESTING trace trace-03-ops:
# Test of insert_head, insert_tail, remove_head, reverse and merge
--- trace-03-ops 6/6
+++ TESTING trace trace-04-ops:
# Test of insert_head, insert_tail, size, swap, and sort
--- trace-04-ops 6/6
+++ TESTING trace trace-05-ops:
# Test of insert_head, insert_tail, remove_head, reverse, size, swap, and sort
--- trace-05-ops 6/6
+++ TESTING trace trace-06-ops:
# Test of insert_head, insert_tail, delete duplicate, sort, descend and reverseK
--- trace-06-ops 6/6
+++ TESTING trace trace-07-string:
# Test of truncated strings
--- trace-07-string 6/6
+++ TESTING trace trace-08-robust:
# Test operations on empty queue
--- trace-08-robust 6/6
+++ TESTING trace trace-09-robust:
# Test remove_head with NULL argument
--- trace-09-robust 6/6
+++ TESTING trace trace-10-robust:
# Test operations on NULL queue
--- trace-10-robust 6/6
+++ TESTING trace trace-11-malloc:
# Test of malloc failure on insert_head
--- trace-11-malloc 6/6
+++ TESTING trace trace-12-malloc:
# Test of malloc failure on insert_tail
--- trace-12-malloc 6/6
+++ TESTING trace trace-13-malloc:
# Test of malloc failure on new
--- trace-13-malloc 6/6
+++ TESTING trace trace-14-perf:
# Test performance of insert_tail, reverse, and sort
--- trace-14-perf 6/6
+++ TESTING trace trace-15-perf:
# Test performance of sort with random and descending orders
# 10000: all correct sorting algorithms are expected pass
# 50000: sorting algorithms with O(n^2) time complexity are expected failed
# 100000: sorting algorithms with O(nlogn) time complexity are expected pass
--- trace-15-perf 6/6
+++ TESTING trace trace-16-perf:
# Test performance of insert_tail
--- trace-16-perf 6/6
+++ TESTING trace trace-17-complexity:
# Test if time complexity of q_insert_tail, q_insert_head, q_remove_tail, and q_remove_head is constant
Probably constant time
Probably constant time
Probably constant time
Probably constant time
--- trace-17-complexity 5/5
--- TOTAL 100/100
```
:::
由以上的測試結果,可以發現對於目前的實作, valgrind 沒有發出任何的警告
接著使用 [Massif](https://valgrind.org/docs/manual/ms-manual.html) 觀察記憶體使用情況,輸入命令 `valgrind -v --tool=massif ./qtest -f traces/trace-01-ops.cmd` 產生輸出檔 `massif.out.17672`
接著輸入命令 `massif-visualizer ./massif.out.17672` 讀取上述指令產生的輸出檔,以下為結果:

以 `traces/trace-01-ops.cmd` 為例,可以發現所有分配的記憶體在最後會完全被釋放
---
## 確保 qtest 提供的 `web` 命令得以搭配上述佇列實作使用
經過實驗證實,目前的實作會有以下的問題
:::warning
問題: 在輸入命令 `web` 並建立連線後,會有以下的問題
1. 當對伺服器端送出請求後,此時再從命令列透過 [stdio](https://man7.org/linux/man-pages/man3/stdio.3.html) 送出命令後,會產生一段很長的延遲,也就是後者的命令過了很久才會被處理
2. 對伺服器端送出請求後,會產生以下的錯誤訊息
> Unknown command 'favicon.ico'
:::
首先從第一個問題,可以得知上述提到的延遲是由於 lab0-c 內部的函式 [`read`](https://man7.org/linux/man-pages/man2/read.2.html) 壅塞所導致,主要是 I/O 正在處理來自客戶端或是來自命令列的命令,使其一無法馬上執行。因此這邊使用函式 [select](https://man7.org/linux/man-pages/man2/select.2.html) 對檔案描述子 (file descriptor) 進行管理,以下為 select 的 I/O 模型,屬於 [Multiplexing](https://en.wikipedia.org/wiki/Multiplexing) 模型,分成兩部份,第一部份為 select ,目的為等待準備好的資料,並回傳已經準備好的資料數,第二部份則是讀取準備好的資料。透過這樣的方法,可以一次管理多個檔案描述子。
> descriptor 翻譯為「描述子」,參見 [operator 為什麼翻譯為運算子](https://dev.to/codemee/kao-gu-operator-wei-shi-mo-fan-yi-wei-yun-suan-zi--1m7)
> :notes: jserv

而目前的目標則是**同時管理 stdin 及 socket 的資料**,首先可以參考原本管理 I/O 的實作,位於檔案 `console.c` 的函式 `run_console`
```c=
bool run_console(char *infile_name)
{
if (!push_file(infile_name)) {
report(1, "ERROR: Could not open source file '%s'", infile_name);
return false;
}
if (!has_infile) {
char *cmdline;
while (use_linenoise && (cmdline = linenoise(prompt))) {
interpret_cmd(cmdline);
line_history_add(cmdline); /* Add to the history. */
line_history_save(HISTORY_FILE); /* Save the history on disk. */
line_free(cmdline);
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);
}
} else {
while (!cmd_done())
cmd_select(0, NULL, NULL, NULL, NULL);
}
return err_cnt == 0;
}
```
將目光放在上述程式的變數 `use_linenoise` ,其功能是用來區別目前輸入為 stdin 或是 socket ,當輸入命令 `web` 時,根據函式 `do_while` 的內容,可以發現變數 `use_linenoise` 被設定為 `false` (下列程式碼第 12 行) ,同時也表示下次將讀取 socket 的資料
```c=
static bool do_web(int argc, char *argv[])
{
int port = 9999;
if (argc == 2) {
if (argv[1][0] >= '0' && argv[1][0] <= '9')
port = atoi(argv[1]);
}
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` 第 10 行的迴圈用來處理來自 stdin 的資料,而第 19 行則是處理來自 socket 的資料
接著修改函式 `run_console` 如下所示,移除變數 `use_linenoise` 並且由函式 `linenoise` 來同時管理兩者的資料
```c
bool run_console(char *infile_name)
{
if (!push_file(infile_name)) {
report(1, "ERROR: Could not open source file '%s'", infile_name);
return false;
}
if (!has_infile) {
char *cmdline;
while ((cmdline = linenoise(prompt))) {
interpret_cmd(cmdline);
line_history_add(cmdline); /* Add to the history. */
line_history_save(HISTORY_FILE); /* Save the history on disk. */
line_free(cmdline);
while (buf_stack && buf_stack->fd != STDIN_FILENO)
cmd_select(0, NULL, NULL, NULL, NULL);
has_infile = false;
}
} else {
while (!cmd_done())
cmd_select(0, NULL, NULL, NULL, NULL);
}
return err_cnt == 0;
}
```
接著引入上述提到的函式 `select` 到函式 `linenoise` 內部,主要修改位於檔案 `linenoise.c` 的核心函式 `line_edit` 。首先使用關鍵字 `extern` 取得 `console.c` 的變數 `web_fd` ,其為 socket 檔案描述子
```c
extern int web_fd;
fd_set read_set;
```
當然變數 `web_fd` 的宣告也要作修改
```diff
- static int web_fd;
+ int web_fd = -1;
```
接著在函式 `line_edit` 開始實作 select 的設定,可以參考 CS:APP 的第 12 章,前面提到要管理 stdin 及 socket ,因此設定的部份可以對應到下列程式的第 3 行及第 7 行,使用巨集函式 `FD_SET` 啟動 select 的監聽,其他的巨集函式可以參考 [select(2) — Linux manual page](https://man7.org/linux/man-pages/man2/select.2.html)
```c=
while (1) {
FD_ZERO(&read_set); /* clear read set */
FD_SET(l.ifd, &read_set); /* set stdin fd */
int fd = l.ifd + 1;
/* If web not ready listen */
if (web_fd != -1) {
FD_SET(web_fd, &read_set); /* set web fd */
fd = web_fd + 1;
}
...
}
select(fd, &read_set, NULL, NULL, NULL);
```
接著是讀取函式的部份,實作如下所示,有兩種情況 — socket 及 stdin ,前者主要是使用 `web_recv` 讀取資料,並且對客戶端回傳 [HTTP 狀態碼](https://developer.mozilla.org/zh-TW/docs/Web/HTTP/Status) ,後者則是維持原本的實作,使用函式 `read` 讀取來自 stdin 的資料
```c
if ((web_fd != -1) && FD_ISSET(web_fd, &read_set)) {
struct sockaddr_in clientaddr;
socklen_t clientlen = sizeof(clientaddr);
FD_CLR(web_fd, &read_set);
int web_connfd =
accept(web_fd, (struct sockaddr *) &clientaddr, &clientlen);
char *p = web_recv(web_connfd, &clientaddr);
int len = strlen(p);
for (int i = 0; i < len; i++)
line_edit_insert(&l, p[i]);
char *buffer = "HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n";
web_send(web_connfd, buffer);
free(p);
close(web_connfd);
} else if (FD_ISSET(l.ifd, &read_set)) {
signed char c;
int nread;
char seq[5];
FD_CLR(l.ifd, &read_set);
nread = read(l.ifd, &c, 1);
if (nread <= 0)
return l.len;
...
}
```
接著將 socket 設定為 non-blocking 模式,修改檔案 `web.c` 裡的函式 `web_open` ,如下所示
```diff
/* Create a socket descriptor */
- if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
+ if ((listenfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0)) < 0)
return -1;
+ /* set socket non-blocking */
+ int flags = fcntl(listenfd, F_GETFL);
+ fcntl(listenfd, F_SETFL, flags | O_NONBLOCK);
```
另外要特別注意,當 socket 設定為 non-blocking 模式時,此時的讀取函式 `read` 需要考慮到回傳值 `EAGAIN` 及 `EWOULDBLOCK` ,可參考 [read(2) — Linux manual page](https://man7.org/linux/man-pages/man2/read.2.html) ,以下為說明
> - **EAGAIN or EWOULDBLOCK**: The file descriptor fd refers to a socket and has been marked nonblocking (O_NONBLOCK), and the read would block. POSIX.1-2001 allows either error to be returned for this case, and does not require these constants to have the same value, so a portable application should check for both possibilities.
因此將函式 `rio_read` 進行修改
```diff
static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)
{
int cnt;
while (rp->count <= 0) { /* refill if buf is empty */
rp->count = read(rp->fd, rp->buf, sizeof(rp->buf));
if (rp->count < 0) {
+ // no data available yet
+ if (errno == EWOULDBLOCK || errno == EAGAIN)
+ rp->count = 0; /* and call read() again */
- if (errno != EINTR) /* interrupted by sig handler return */
+ else if (errno != EINTR) /* interrupted by sig handler return */
return -1;
} else if (rp->count == 0) { /* EOF */
return 0;
} else
rp->bufptr = rp->buf; /* reset buffer ptr */
}
...
}
```
:::warning
依循 [CONTRIBUTING.md](https://github.com/sysprog21/lab0-c/blob/master/CONTRIBUTING.md) 規範,在迴圈內讓 EOF 狀況及早脫離
:notes: jserv
:::
接著是目前的展示成果,主要展示當 stdin 已經先輸入資料但尚未處理,接著從另一個終端機透過 socket 輸入命令,並不會導致資料阻塞,完整實作可見 [commit 26a5dc](https://github.com/Risheng1128/lab0-c/commit/26a5dc3f49069fe97226faea129f4e2a6cfc7cc1)
:::info
Note: 由於目前讀取資料後的操作是透過函式 `line_edit_insert` 處理,因此會讓兩個 I/O 的資料拼接在一起
:::
{%youtube X_sJdTpZUdg %}
:::warning
TODO:
1. 處裡 "Unknown command 'favicon.ico'" 的問題
2. 確認 linenoise 對於 web 端輸入的命令也能展現歷史紀錄
:::
## 在 qtest 提供新的命令 `shuffle`