owned this note
owned this note
Published
Linked with GitHub
# 2022q1 Homework1 (lab0)
contributed by < `ibat10clw` >
> [lab0-c](https://github.com/ibat10clw/lab0-c)
###### tags: `linux2022`
## 實驗環境
```shell
$ gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 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.
$ lscpu
Architecture: x86_64
CPU op-mode(s): 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
Thread(s) per core: 2
Core(s) per socket: 4
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 60
Model name: Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz
Stepping: 3
CPU MHz: 1200.000
CPU max MHz: 4000.0000
CPU min MHz: 800.0000
BogoMIPS: 7183.16
Virtualization: VT-x
L1d cache: 128 KiB
L1i cache: 128 KiB
L2 cache: 1 MiB
L3 cache: 8 MiB
NUMA node0 CPU(s): 0-7
Vulnerability Itlb multihit: KVM: Mitigation: VMX disabled
Vulnerability L1tf: Mitigation; PTE Inversion; VMX conditional cach
e flushes, SMT vulnerable
Vulnerability Mds: Mitigation; Clear CPU buffers; SMT vulnerable
Vulnerability Meltdown: Mitigation; PTI
Vulnerability Spec store bypass: Mitigation; Speculative Store Bypass disabled v
ia prctl and seccomp
Vulnerability Spectre v1: Mitigation; usercopy/swapgs barriers and __user
pointer sanitization
Vulnerability Spectre v2: Mitigation; Full generic retpoline, IBPB condit
ional, IBRS_FW, STIBP conditional, RSB filling
Vulnerability Srbds: Mitigation; Microcode
Vulnerability Tsx async abort: Not affected
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtr
r pge mca cmov pat pse36 clflush dts acpi mmx f
xsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rd
tscp lm constant_tsc arch_perfmon pebs bts rep_
good nopl xtopology nonstop_tsc cpuid aperfmper
f pni pclmulqdq dtes64 monitor ds_cpl vmx smx e
st tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid sse4_
1 sse4_2 x2apic movbe popcnt tsc_deadline_timer
aes xsave avx f16c rdrand lahf_lm abm cpuid_fa
ult epb invpcid_single pti ssbd ibrs ibpb stibp
tpr_shadow vnmi flexpriority ept vpid ept_ad f
sgsbase tsc_adjust bmi1 avx2 smep bmi2 erms inv
pcid xsaveopt dtherm ida arat pln pts md_clear
flush_l1d
```
## 作業要求
> [lab0](https://hackmd.io/@sysprog/linux2022-lab0)
* 在 GitHub 上 fork [lab0-c](https://github.com/sysprog21/lab0-c)
* 依據上述指示著手修改 `queue.[ch]` 和連帶的檔案,測試後用 Git 管理各項修改,要能滿足 `$ make test` 自動評分系統的所有項目。
* q_new: 建立一個新的 queue 並將 head 的指標初始化
* q_free: 釋放目前 queue 中的所有元素
* q_insert_head: 在 head 插入一個新元素
* q_insert_tail: 在 tail 插入一個新元素
* q_remove_head: 移除 head 端的元素
* q_remove_tail: 移除 tail 端的元素
* q_size: 計算 queue 的長度
* q_delete_mid: 刪除 queue 中間的元素(⌊q_size / 2⌋)
* q_delete_dup: 刪除 queue 所有重複的元素
* q_swap: 將 queue 中相鄰的元素交換位置
* q_reverse: 將 queue 的順序反轉
* q_sort: 將 queue 中的元素以字典序做排序
## 開發紀錄
### q_new
最一開始沒有去判斷當 malloc 失敗的時候要有例外處理, 到了執行評分程式時才注意到, 後面如果有需要做 malloc 的部份也都需要檢查回傳的結果是否成功
INIT_LIST_HEAD 的作用是將 head->prev 和 head->next 指向自己
```c=
struct list_head *q_new()
{
// allocate a space of list_head
struct list_head *q = (struct list_head *) malloc(sizeof(struct list_head));
if (!q)
return NULL;
INIT_LIST_HEAD(q);
return q;
}
```
### q_free
根據[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#Linked-list-%E5%9C%A8-Linux-%E6%A0%B8%E5%BF%83%E5%8E%9F%E5%A7%8B%E7%A8%8B%E5%BC%8F%E7%A2%BC)中提到在 linux 中的 list 會把 list_head 嵌入到所需的結構體中, 但傳入的參數是 list_head, 因此需要透過 list_head 去找到 element_t 才能夠真正的去釋放記憶體
此外 element_t 中的 value 是一個指標, 也需要將其指向的記憶體空間收回
```c
void q_free(struct list_head *l) {
if (!l)
return;
element_t *previous, *current;
list_for_each_entry_safe (current, previous, l, list) {
free(current->value);
free(current);
}
free(l);
}
```
### q_insert_head
在插入的部份有兩個地方需要用到 malloc, 分別是 node 本身和 node 要存的value
由於 value 的部份是字串, 需要計算它的長度後再進行分配, 要是失敗的話則要先把前面 malloc 的node 釋放掉才能 return, 當 data 都準備好後只要將其接在 head 的next就好
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *node = (element_t *) malloc(sizeof(element_t));
if (!node)
return false;
unsigned int len = strlen(s) + 1;
node->value = malloc(sizeof(char) * len);
if (!node->value) {
free(node);
return false;
}
memcpy(node->value, s, len);
list_add(&node->list, head);
return true;
}
```
### q_insert_tail
由於是 circular doubly linked list, 因此能夠參考 insert head 的部份, 只有最後要改為將 data 接在 head 的 prev 就可以了
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *node = (element_t *) malloc(sizeof(element_t));
if (!node)
return false;
unsigned int len = strlen(s) + 1;
node->value = malloc(sizeof(char) * len);
if (!node->value) {
free(node);
return false;
}
memcpy(node->value, s, len);
list_add_tail(&node->list, head);
return true;
}
```
### q_remove_head
根據作業要求除了要將 node 移出 list 以外, 當傳入的 sp 不是空指標時要將 data 複製進去
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || head->next == head)
return NULL;
element_t *node = list_first_entry(head, element_t, list);
if (sp) {
strncpy(sp, node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(head->next);
return node;
}
```
### q_remove_tail
跟 remove_head 做比較, 差別只有移除的 node 從 head->next 改為 head->prev
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || head->next == head)
return NULL;
element_t *node = list_last_entry(head, element_t, list);
if (sp) {
strncpy(sp, node->value, bufsize - 1);
sp[bufsize - 1] = '\0';
}
list_del(head->prev);
return node;
}
```
### q_size
透過走訪整個 list 後來計算 list 的長度
```c
int q_size(struct list_head *head)
{
if (!head || head == head->next)
return 0;
int len = 0;
struct list_head *li;
list_for_each (li, head)
len++;
return len;
}
```
### q_delete_mid
這邊的關鍵在於如何找出位於中間的 node
我使用了 slow 和 fast 的指標來走訪 list, 兩個指標的移動速度剛好差一倍, 所以當 fast 繞了一圈後 slow恰好會在 list 中間, 找到之後將其刪除(除了移出 list 外, 記憶體也要收回)
```c
bool q_delete_mid(struct list_head *head)
{
if (!head || head->next == head)
return false;
struct list_head *fast = head;
struct list_head *slow = head;
do {
fast = fast->next->next;
slow = slow->next;
} while (fast->next != head && fast != head);
element_t *target = list_entry(slow, element_t, list);
list_del(slow);
free(target->value);
free(target);
return true;
}
```
### q_delete_dup
由於只會作用在 sorted list , 我用了一個 current 的指標, 並且會在每個 node 都向後看, 只要發現重複就將開始刪除, 直到 current->next 和 current 不相等後再把 current 刪除, 並且往更新過後的 list 的下一個 node 前進
```c
bool q_delete_dup(struct list_head *head)
{
if (!head || head->next == head)
return false;
struct list_head *current = head->next;
char tag;
while (current != head) {
element_t *node = list_entry(current, element_t, list);
while (current->next != head &&
!strcmp(node->value,
list_entry(current->next, element_t, list)->value)) {
tag = 1;
element_t *del_node = list_entry(current->next, element_t, list);
list_del(current->next);
free(del_node->value);
free(del_node);
}
if (tag) {
tag = 0;
element_t *del_node = list_entry(current, element_t, list);
struct list_head *tmp = current->next;
list_del(current);
free(del_node->value);
free(del_node);
current = tmp;
} else
current = current->next;
}
return true;
}
```
### q_swap
這裡採用比較直覺的作法, 交換兩個 node 共會影響 6 個 link 把他們全部轉向即可
由於 while 的判斷, 當 current 走回 head 的時候就會停止, 不過若 q_size 為偶數, 則最後一個 node 也會被調整, 最後的 if 是為了在此情形中調整 head 的 link
```c
void q_swap(struct list_head *head)
{
struct list_head *current = head->next, *previous = head, *tmp = NULL;
while (current != head && current->next != head) {
tmp = previous;
previous = current;
current = current->next;
previous->next = current->next;
current->next = previous;
current->prev = previous->prev;
previous->prev = current;
tmp->next = current;
previous->next->prev = previous;
current = previous->next;
}
if (current->next != head) {
previous->next = head;
head->prev = previous;
}
}
```
### q_reverse
由於 head 是不帶 data 的, 因此要反轉 list 只需要將 所有 node 的 next 和 prev 做交換即可
指標 tmp 用於輔助交換, 當交換完成後原先的 next 的方向會變成存在 prev 內, 因此透過 prev 來移動 current
```c
void q_reverse(struct list_head *head)
{
if (!head || head == head->next || list_is_singular(head))
return;
struct list_head *tmp;
struct list_head *current = head->next;
while (current != head) {
tmp = current->prev;
current->prev = current->next;
current->next = tmp;
current = current->prev;
}
tmp = head->prev;
head->prev = head->next;
head->next = tmp;
}
```
### q_sort
這邊採用了遞迴的 merge sort來實作, 並且在排序的過程中先忽視 prev 的存在, 同時將 circular 的結構破壞, 等待排序完畢後再透過 next 的順序去恢復成 doubly circular linked list
merge sort 共有兩大重點, 分別是:
1. 分割
在分割的部份我採用了 slow 和 fast 的指標來找尋 list 的中間節點, 找到後將中間節點的 next 指向 NULL, 直到所有 sub list 的 size 都小於等於 1 為止
1. 合併
合併的部份採用的方式是老師[你所不知道的 C 語言: linked list 和非連續記憶體](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)中介紹的 merge two sorted list 的方法
當 sorting 完成後, 把 prev 的 link 連回去, 然後把 last node 與 head 的連結也恢復後就完成了
```c
struct list_head *merge(struct list_head *l1, struct list_head *l2)
{
struct list_head *head = NULL, **ptr = &head, **node;
for (node = NULL; l1 && l2; *node = (*node)->next) {
node = strcmp(list_entry(l1, element_t, list)->value,
list_entry(l2, element_t, list)->value) < 0
? &l1
: &l2;
*ptr = *node;
ptr = &(*ptr)->next;
}
*ptr = l1 ? l1 : l2;
return head;
}
struct list_head *mergesort(struct list_head *head)
{
if (!head->next) {
return head;
}
struct list_head *fast = head;
struct list_head *slow = head;
while (fast->next && fast->next->next) {
fast = fast->next->next;
slow = slow->next;
}
struct list_head *mid = slow->next;
slow->next = NULL;
struct list_head *left = mergesort(head);
struct list_head *right = mergesort(mid);
return merge(left, right);
}
void q_sort(struct list_head *head)
{
if (!head || head->next == head || list_is_singular(head))
return;
struct list_head *tmp = head->next;
head->prev->next = NULL;
head->next = NULL;
struct list_head *sorted = mergesort(tmp);
struct list_head *current = sorted;
struct list_head *previous = 0;
while (current) {
previous = current;
current = current->next;
if (current)
current->prev = previous;
}
sorted->prev = head;
head->next = sorted;
head->prev = previous;
previous->next = head;
}
```
### 在 qtest 提供新的命令 shuffle
目前已經在 queue.c 中完成了 shuffle 的程式碼
先建立一個 newh 來輔助洗牌
每個回合會從原 list 中隨機選一個 node 移除, 並且 insert 在 newh 的 head
當所有元素都以新的順序被串在 newh 後, 之後再將 newh, first node, last node 的 link 串回原先的 head
便完成了洗牌的操作
```c
static bool q_shuffle(struct list_head *head)
{
if(!head || list_empty(head))
return false;
else if(list_is_singular(head))
return true;
int len = 0;
struct list_head *li;
struct list_head newh;
newh.next = &newh;
newh.prev = &newh;
list_for_each (li, head)
len++;
srand(time(NULL));
while (len) {
int node_num = (rand() % len) + 1;
li = head;
do {
li = li->next;
} while (--node_num);
struct list_head *tmp = li;
list_del(li);
list_add(tmp, &newh);
len--;
}
newh.next->prev = head;
newh.prev->next = head;
head->next = newh.next;
head->prev = newh.prev;
return true;
}
```
接下來要將功能與 qtest 做結合
在閱讀了作業要求中, 如何新增 do_hello 的方法後, 已經有成功新增 hello 命令
同時在 qtest.c 中找到了 do_show 的實作, 打算模仿此方法將 shuffle 的命令加入 qtest 中
最後我仿照了 do_dm 將 shuffle 的功能加入了 qtest
```c
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: Try to access null queue");
error_check();
bool ok = true;
if (exception_setup(true))
ok = q_shuffle(l_meta.l);
exception_cancel();
show_queue(3);
return ok && !error_check();
}
```
## TODO
### trace-17-complexity
我的程式碼在 github 的 CI 有時後會失敗, 但在自己的電腦上測試從 insert, remove, free 這三個部份做完後就一直是通過的狀態, 需要進一步釐清原因
### 部份程式碼改進
與觀摩區的作業比較後認為我的程式碼應該需要多使用 list.h 提供的 API 去改寫, 但有些 macro 還不是很理解, 等待理解後在將他們改進
## 參考資料
* [Reverse a doubly circular linked list](https://www.geeksforgeeks.org/reverse-a-doubly-circular-linked-list/)
*