---
tags: linux2022
---
# 2022q1 Homework1 (lab0)
contributed by < [nckusaniel](https://github.com/nckusaniel) >
## 實驗環境
```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
型號: 126
Model name: Intel(R) Core(TM) i5-1035G1 CPU @ 1.00GHz
製程: 5
CPU MHz: 1200.000
CPU max MHz: 3600.0000
CPU min MHz: 400.0000
BogoMIPS: 2380.80
虛擬: VT-x
L1d 快取: 192 KiB
L1i 快取: 128 KiB
L2 快取: 2 MiB
L3 快取: 6 MiB
NUMA node0 CPU(s): 0-7
```
## [作業要求](https://hackmd.io/@sysprog/linux2022-lab0)
* `q_new` : 建立新的「空」佇列
* `q_free` : 釋放佇列所佔用的記憶體
* `q_insert_head` : 在佇列開頭 (head) 插入 (insert) 給定的新節點 (以 LIFO 準則)
* `q_insert_tail` : 在佇列尾端 (tail) 插入 (insert) 給定的新節點 (以 FIFO 準則)
* `q_remove_head` : 在佇列開頭 (head) 移去 (remove) 給定的節點
* `q_release_element` : 釋放特定節點的記憶體空間
* `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_sort` : 以遞增順序來排序鏈結串列的節點
## 開發過程
### 實做queue.c
關鍵結構
1.
```c=
typedef struct {
char *value;
struct list_head list;
} element_t;
```
2.
```c=
struct list_head {
struct list_head *prev;
struct list_head *next;
};
```
### q_new
1.給node配置一個空間
2.檢查空間是否足夠
3.利用INIT_LIST_HEAD將node初始化
```c=
struct list_head *q_new()
{
struct list_head *node = malloc(sizeof(struct list_head));
if (!node) {
return NULL;
}
INIT_LIST_HEAD(node);
return node;
}
```
### q_free
1.宣告兩個element_t指標(enrty在前,safe在後)
2.判斷指標l是否為空的
3.利用list_for_each_entry_safe,探索l串列之element_t結構並利用q_release_element 釋放entry
4.釋放l
```c=
void q_free(struct list_head *l)
{
element_t *entry, *safe;
if (!l)
return;
list_for_each_entry_safe (entry, safe, l, list)
q_release_element(entry);
free(l);
}
```
### q_insert_head
1.先判斷head是否為空
2.配置空間給q作插入,並判斷q否存在
3.配置空間給value,並判斷value是否存在並將s複製過去
4.利用list_add作插入
```c=
bool q_insert_head(struct list_head *head, char *s)
{
//檢查head
if (head == NULL)
return false;
element_t *n = malloc(sizeof(element_t));
if (!n) {
return false;
}
n->value = malloc(strlen(s) + 1);
if (!n->value) {
free(n);
return false;
}
strncpy(n->value, s, strlen(s) + 1);
list_add(&n->list, head);
return true;
}
```
一開始不知道要配置空間給value,直接使用q->value=s,導致錯誤的發生,查詢後知道了要先malloc空間給value,再使用strcpy執行陣列的複製
commit時發現因為使用了會導致buffer overflow的函式strcpy,在研讀[https://security.web.cern.ch/recommendations/en/codetools/c.shtml] 後改用**strlcpy**,而後又發現c99目前無法支援strlcpy,因此改用**strncpy**
而後發現trace 03會不通過,因此回頭細看q_insert_head是否有錯誤並參考其他同學的code後發現了幾項問題
1.q->value 必須要配置空間,並判斷是否存在,不存在的話執行strncpy會有溢位產生
#### 最重要的!!
我希望算出s的大小而後配置給n->value因此寫
```c=
n->value = malloc(**sizeof**(strlen(s) + 1));
//發現不管怎麼測試都會錯誤
```
發現
1.sizeof(s),算出的s是char*型態的位元組,而非s中有幾個元素
2.sizeof(strlen(s)+1),其中sizeof不是函數,因此不能在裡面放入strlen
3.strlen會回傳size_t而sizeof(size_t)會算出size_t的位元組量,與所求s的大小不相同而導致錯誤?
改寫
```c=
n->value = malloc(strlen(s) + 1);
```
刪除sizeof,直接malloc s的字串大小
:::warning
### Todo
- [ ] 使用strdup 來簡化程式碼
- [ ] 去確認原先程式碼錯誤,是否如上述發現中鎖描述
:::
### q_insert_tail
原理同 q_insert_head,但使用list_add_tail
```c=
bool q_insert_tail(struct list_head *head, char *s)
{
if (head == NULL)
return false;
element_t *n = malloc(sizeof(element_t));
if (!n) {
return false;
}
n->value = malloc(strlen(s) + 1);
if (!n->value) {
free(n);
return false;
}
strncpy(n->value, s, strlen(s) + 1);
list_add_tail(&n->list, head);
return true;
}
```
### q_remove_head
1.判斷queue中是否有元素
2.宣告target指向第一個元素
3.移除target
4.將target的資料給sp
```c=
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
//判斷queue是否有元素
if (!head || list_empty(head))
return NULL;
// target指向第一個點
element_t *target = list_entry(head->next, element_t, list);
//移除taget
list_del_init(head->next);
// target的value 非空且被移除(初始化)就將value的資料給sp
if (sp != NULL) {
strncpy(sp, target->value, bufsize - 1);
// sp[bufsize]="\0";
}
return target;
}
```
發現錯誤,改進
1.判斷queue中是否有元素用list_empty來改進
2.使用strnspy
### q_remove_tail
1.道理同remove_head
```c=
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
//判斷queue是否有元素
if (!head || list_empty(head))
return NULL;
// target指向最後一個點(head->prev)
element_t *target = list_entry(head->prev, element_t, list);
//移除taget
list_del_init(head->prev);
// target的value 非空且被移除(初始化)就將value的資料給sp
if (sp != NULL) {
strncpy(sp, target->value, bufsize - 1);
}
return target;
}
```
### q_release_element
1事先預設好了
```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;
}
```
### q_delete_mid
1.先判斷是否為空
2.使用慢指標去追蹤,快指標到終點時,慢指標會指到中間
3.把slow 刪除並釋放
```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 *fast, *slow;
for (fast = slow = head->next; fast->next != head && fast != head;
slow = slow->next, fast = fast->next->next) {
}
//找到中間點(慢指標),移除後釋放
element_t *mid = list_entry(slow, element_t, list);
list_del_init(slow);
q_release_element(mid);
return true;
}
```
### q_delete_dup
思路
1.先判斷head是否為空
2.使用變數count,目的是希望當目前節點跟下一個節點不同時,去判斷目前節點是否要被刪除
3.宣告兩個指標first,second指向list_head,利用list_for_each_safe探索queue
4.將兩個指標分別利用list_entry取得element_t的address,目的是取用value
5.第一個if中,利用strcmp來判斷first 跟second之中的字串是否相等,若相等就刪除並釋放,且將count+1
6.若字串不相等,必須要利用count來判斷,目前節點的字串之前是否曾經釋放過,是的話目前節點也必須釋放
#### 疑問: 原本執行時一直跑出Segmentation fault occurred. You dereferenced a NULL or invalid pointer。之後觀察其他同學的code之後發現在
```c=
if ( 0 == strcmp(entry->value, safe->value))
```
#### 加入second != head 就可以解決此問題。猜想是因為當second=head時,執行element_t *safe = list_entry(second, element_t, list); 且因為head沒有被嵌入至任何struct之中,所以在取用其safe的value時會發生記憶體上的錯誤。
```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;
}
//計算這個node殺了幾次
int count = 0;
// 宣告兩個指向現在跟下一個list
struct list_head *first, *second;
//探訪所有node
list_for_each_safe (first, second, head) {
// list轉換成struct
element_t *entry = list_entry(first, element_t, list);
element_t *safe = list_entry(second, element_t, list);
//如果now跟next的字串內容相同,殺了first,並釋放,注意seocnd不能等於head沒value會錯誤
if (second != head && 0 == strcmp(entry->value, safe->value)) {
list_del(first);
q_release_element(entry);
//砍了一次
count++;
//字串跟下一個點不相同,判斷之前有沒有砍過
} else {
if (count > 0) {
list_del(first);
q_release_element(entry);
count = 0;
}
}
}
return true;
}
```
q_swap
1.檢查head是否存在
2.宣告兩個指標now以及next1,分別指向目前節點,及下一點
3.for迴圈實做,list_del_init(now)使next1跑到now前面,再利用list_add(now,next1)將now加入next1後面,使它們位子交換
```c=
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
if (!head || list_empty(head))
return;
struct list_head *now, *next1;
for (now = head->next, next1 = now->next; now->next != head && now != head;
now = now->next, next1 = now->next) {
list_del_init(now);
list_add(now, next1);
}
}
```
這題卡了很久,有以下幾點要注意
1.參考[kdnvt的寫法](https://hackmd.io/@5ux520T8Sd-OoYfdxZR55Q/SyzDKJ9y9#q_swap)才知道只要先刪除,在插入就可以使兩個點的位子交換了
2.如果queue中有奇數個資料,則最後一項不需要交換
3.在利用list_del_init(now)之後next1會跑到前面
#### q_reverse
1.用類似交換排序的方式來實做,但是移出比較大小
單純就是把資料放在最右邊
```c=
void q_reverse(struct list_head *head)
{
struct list_head *node, *first, *second;
// n紀錄總共多少點
int n = 0;
int j;
list_for_each (node, head)
n++;
//第一層for迴圈代表已經放了幾個點到最後面執行n-1次即可
for (int i = 1; i < n; i++) {
//第二層執行左右交換,並且每次可以根據i來減少交換次數
for (first = head->next, second = first->next, j = n; i < j;
j--, second = first->next) {
//刪除first加入其後面
list_del_init(first);
list_add(first, second);
}
}
}
```
#### 效率太差作更正
思路
1.探索每一個點,將其一個一個插入到最前面
```c=
void q_reverse(struct list_head *head)
{
//要考慮head為null的情況
if (!head || list_empty(head)) {
return;
}
struct list_head *node, *safe;
list_for_each_safe (node, safe, head) {
list_move(node,head);
}
}
```
:::info
原本用打算依序將節點移動到最後面用list_move_tail實做,但是終止條件有點難寫
- [ ] 嘗試用list_move_tail實做
:::
q_sort
1.交換排序實做
```c=
void q_sort(struct list_head *head)
{
struct list_head *node, *first, *second;
// n紀錄總共多少點
int n = 0;
int j;
list_for_each (node, head)
n++;
//第一層for迴圈代表已經放了幾個點到最後面執行n-1次即可
for (int i = 1; i < n; i++) {
//第二層執行左右交換,並且每次可以根據i來減少交換次數
for (first = head->next, second = first->next, j = n; i < j;
j--, second = first->next) {
//利用element取value
element_t *current = list_entry(first, element_t, list);
element_t *current_next = list_entry(second, element_t, list);
// value大的放後面
if (strlen(current->value) > strlen(current_next->value)) {
list_del_init(first);
list_add(first, second);
}
}
}
}
```
qsort
1.參考[你所不知道的 C 語言: linked list 和非連續記憶體](/YA7RMqd6RE2UL8OmmjU9PQ)之中的quicksort實做
```c=
void q_sort(struct list_head *head)
{
struct list_head list_less, list_greater;
element_t *pivot;
element_t *item = NULL, *is = NULL;
if (!head || list_empty(head) || list_is_singular(head))
return;
INIT_LIST_HEAD(&list_less);
INIT_LIST_HEAD(&list_greater);
pivot = list_first_entry(head, element_t, list);
list_del(&pivot->list);
list_for_each_entry_safe (item, is, head, list) {
if (strcmp(item->value, pivot->value) < 0)
list_move_tail(&item->list, &list_less);
else
list_move(&item->list, &list_greater);
}
q_sort(&list_less);
q_sort(&list_greater);
list_add(&pivot->list, head);
list_splice(&list_less, head);
list_splice_tail(&list_greater, head);
}
```
執行後發現trace-14以及trace-15過不了,因為執行時間太久,必須要用複雜度更低的方法,因此在研讀[你所不知道的 C 語言: linked list 和非連續記憶體](/YA7RMqd6RE2UL8OmmjU9PQ)決定使用merge sort來實做
### 利用merge sort實做
參考[Risheng1128 ](https://hackmd.io/@Risheng/linux2022-lab0#q_sort)來糾正我原本的錯誤
思路
1. 分成三個funtion qsort,merge_cut,merge_twolist。
2.利用qsort呼叫merge_cut來做list的分解,再利用merge_twolist來合併
### qsort
1.注意邊界條件
```c=
if (!head || list_empty(head) || list_is_singular(head))
return;
```
2.因為circular doubly linked list,有pre以及next兩個指標,並且head以及tail相接,在執行上會比較複雜,因此將circular doubly linked list先改成singular linked list。
並且在執行完merge_cut之後再進行link的調整
```c=
head->prev->next = NULL;
head->next = merge_cut(head->next);
```
:::info
起初我的想法是直接head= merge_cut(head);
而後發現把head也丟進merge_cut以及merge_twolist之中,會有更多的邊界條件要處理,因為list會一直被切割,並且只有某些list有head。因此使用head->next = merge_cut(head->next); 來解決head的傳入問題
:::
最後會發現link list 所有的prev都沒有指向正確的地方,並且首尾沒有相接所以執行以下
```c=
struct list_head *now = head, *next = head->next;
while (next) {
next->prev = now;
now = next;
next = next->next;
}
now->next = head;
head->prev = now;
}
```
#### merge_cut
#### 先討論邊界條件
```c=
if (!node1 || !node1->next) {
return node1;
}
```
:::warning
這邊是傳入原本List中的head->next,並且tail->next已經被改成NULL,因此不能使用list_empty(node1),原先使用list_empty(node1)導致一直出錯,之後利用GDB檢查之後才發線問題所在
:::
#### 利用快慢指標來將list進行分割,並遞迴呼叫merge_twolist
1.mid是後面串列的開頭,所以有fast->next; fast = fast->next->next兩個結束循環的可能性
2.將slow的next設為NULL,來確保遞迴呼叫時所有tail的next都是NULL,來避免邊界條件
```c=
struct list_head *slow = node1, *fast = slow->next;
for (; fast && fast->next; fast = fast->next->next)
slow = slow->next;
struct list_head *mid = slow->next;
slow->next = NULL;
struct list_head *left = merge_cut(node1);
struct list_head *right = merge_cut(mid);
return merge_twolist(left, right);
```
#### merge_twolist
1.利用[指標的指標],來實作兩個list的合併
2.因為*ptr = &head,所以最初*ptr就是代表head
3.而if之中的*ptr則是指node中的next指標
4.if之中只有改變next指標,並沒有改變pre指標,所以qsort 之中才要去將所有prev指標做調整
```c=
struct list_head *head = NULL;
struct list_head **ptr = &head;
while (list1 && list2) {
//比大小<0代表l1小
if (strcmp(list_entry(list1, element_t, list)->value,
list_entry(list2, element_t, list)->value) < 0) {
*ptr = list1;
list1 = list1->next;
} else {
*ptr = list2;
list2 = list2->next;
}
ptr = &(*ptr)->next;
}
```
最後當l1或l2為NULL時(也就是ptr所停在的地方),我們要探討到底是將l1連接到l2抑或是l2連接到l1。
**將list1及list2做OR運算,並且轉換型態存給ptr**
```c=
*ptr = (struct list_head *) ((uintptr_t) list1 | (uintptr_t) list2);
```
#### 完整程式碼
```c=
struct list_head *merge_twolist(struct list_head *list1,
struct list_head *list2)
{
struct list_head *head = NULL;
struct list_head **ptr = &head;
while (list1 && list2) {
//比大小<0代表l1小
if (strcmp(list_entry(list1, element_t, list)->value,
list_entry(list2, element_t, list)->value) < 0) {
*ptr = list1;
list1 = list1->next;
} else {
*ptr = list2;
list2 = list2->next;
}
ptr = &(*ptr)->next;
}
// l1和l2作or運算並型態轉換存回ptr
*ptr = (struct list_head *) ((uintptr_t) list1 | (uintptr_t) list2);
return head;
}
//將資料切割(divide
struct list_head *merge_cut(struct list_head *node1)
{
//邊界條件
if (!node1 || !node1->next) {
return node1;
}
struct list_head *slow = node1, *fast = slow->next;
for (; fast && fast->next; fast = fast->next->next)
slow = slow->next;
// mid是新串列開頭
struct list_head *mid = slow->next;
slow->next = NULL;
struct list_head *left = merge_cut(node1);
struct list_head *right = merge_cut(mid);
return merge_twolist(left, right);
}
void q_sort(struct list_head *head)
{
//邊界條件
if (!head || list_empty(head) || list_is_singular(head))
return;
//首先要將傳進來的head改變成單向的linklist
head->prev->next = NULL;
head->next = merge_cut(head->next);
struct list_head *now = head, *next = head->next;
while (next) {
next->prev = now;
now = next;
next = next->next;
}
now->next = head;
head->prev = now;
}
```
### 代辦事項
- [ ] 研讀 Linux 核心原始程式碼的 lib/list_sort.c 並嘗試引入到 lab0-c 專案,比較你自己實作的 merge sort 和 Linux 核心程式碼之間效能落差
- [ ] 在 qtest 提供新的命令 shuffle,允許藉由 Fisher–Yates shuffle 演算法,對佇列中所有節點進行洗牌 (shuffle) 操作
- [ ] 在 qtest 提供新的命令 web,提供 web 伺服器功能,注意: web 伺服器運作過程中,qtest 仍可接受其他命令,可依據前述說明,嘗試整合 tiny-web-server
- [ ] 運用 Valgrind 排除 qtest 實作的記憶體錯誤,並透過 Massif 視覺化 “simulation” 過程中的記憶體使用量,需要設計對應的實驗
- [ ] 解釋 select 系統呼叫在本程式的使用方式,並分析 console.c 的實作,說明其中運用 CS:APP RIO 套件 的原理和考量點。可對照參考 CS:APP 第 10 章重點提示
- [ ] 研讀論文〈Dude, is my code constant time?〉,解釋本程式的 “simulation” 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 Student’s t-distribution 及程式實作的原理。注意:現有實作存在若干致命缺陷,請討論並提出解決方案;
- [ ] 指出現有程式的缺陷 (提示: 和 RIO 套件 有關) 或者可改進之處 (例如更全面地使用 Linux 核心風格的鏈結串列 API),嘗試實作並提交 pull request