---
tags: linux2023
---
# 2023q1 Homework1 (lab0)
contributed by < `visitorckw` >
## 開發環境
```bash
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
$ 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): 12
On-line CPU(s) list: 0-11
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 165
Model name: Intel(R) Core(TM) i5-10500 CPU @ 3.10GHz
Stepping: 3
CPU MHz: 3100.000
CPU max MHz: 4500.0000
CPU min MHz: 800.0000
BogoMIPS: 6199.99
Virtualization: VT-x
L1d cache: 192 KiB
L1i cache: 192 KiB
L2 cache: 1.5 MiB
L3 cache: 12 MiB
NUMA node0 CPU(s): 0-11
```
## 開發過程
> Current score: 83/100
### q_new
一開始的實作如下:
```c
struct list_head *q_new()
{
struct list_head *head =
(struct list_head *) malloc(sizeof(struct list_head));
if (!head)
return NULL;
INIT_LIST_HEAD(head);
return head;
}
```
後來想到可以寫得更精簡,當 malloc 配置記憶體成功後,才使用 `INIT_LIST_HEAD` 巨集。
```c
struct list_head *q_new()
{
struct list_head *head =
(struct list_head *) malloc(sizeof(struct list_head));
if (head)
INIT_LIST_HEAD(head);
return head;
}
```
### q_free
使用 `list_for_each_entry_safe` 巨集逐一釋放每個單元占用的空間。
```c
void q_free(struct list_head *l)
{
if (!l)
return;
element_t *entry;
element_t *safe;
list_for_each_entry_safe (entry, safe, l, list) {
free(entry->value);
free(entry);
}
free(l);
}
```
### q_insert_head
使用 malloc 函式去配置記憶體,若失敗就先用 free 函式釋放記憶體後 return false ,否則利用 list_add macro 來將新的節點加入到 list 之中。
原先採用 strcpy 函式進行字串的複製,但在 git commit 時會遇到以下錯誤:
```
Dangerous function detected in /home/student1/linux2023/lab0-c/queue.c
90: strcpy(sp, delNode->value);
```
因此定義了 strlcpy 巨集,在複製時需指定長度,透過改成 sprintf 函式來實作:
```c
#ifndef strlcpy
#define strlcpy(dst, src, sz) snprintf((dst), (sz), "%s", (src))
#endif
```
最後完整的 q_insert_head 函式如下:
```c
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *newNode = (element_t *) malloc(sizeof(element_t));
if (!newNode)
return false;
newNode->value = (char *) malloc(sizeof(char) * (strlen(s) + 1));
if (!newNode->value) {
free(newNode);
return false;
}
strlcpy(newNode->value, s, strlen(s) + 1);
list_add(&newNode->list, head);
return true;
}
```
### q_insert_tail
邏輯與 q_insert_head 函式相同。唯一的不同之處是原先使用 list_add 巨集來完成,這裡是要從尾端加入新節點,因此要改用 list_add_tail 巨集。
```c
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *newNode = (element_t *) malloc(sizeof(element_t));
if (!newNode)
return false;
newNode->value = (char *) malloc(sizeof(char) * (strlen(s) + 1));
if (!newNode->value) {
free(newNode);
return false;
}
strlcpy(newNode->value, s, strlen(s) + 1);
list_add_tail(&newNode->list, head);
return true;
}
```
### q_remove_head
先用 list_entry macro 抓出將要 remove 的節點,將節點的 value 字串的內容複製給 sp 之後,再用 list_del macro 刪除。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *delNode = list_entry(head->next, element_t, list);
size_t length = bufsize < strlen(delNode->value) + 1
? bufsize
: strlen(delNode->value) + 1;
strlcpy(sp, delNode->value, length);
list_del(head->next);
return delNode;
}
```
後來發現須要先檢查 sp 的值是否是 NULL,若是 NULL則不應該將字串複製給它。
```c
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *delNode = list_entry(head->next, element_t, list);
size_t length = bufsize < strlen(delNode->value) + 1
? bufsize
: strlen(delNode->value) + 1;
if (sp && bufsize)
strlcpy(sp, delNode->value, length);
list_del(head->next);
return delNode;
}
```
### q_remove_tail
跟 q_remove_head 的邏輯相同,差在要抓的節點是 head->prev。
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *delNode = list_entry(head->prev, element_t, list);
size_t length = bufsize < strlen(delNode->value) + 1
? bufsize
: strlen(delNode->value) + 1;
strlcpy(sp, delNode->value, length);
list_del(head->prev);
return delNode;
}
```
和 q_remove_head 一樣,後來加入了 sp 是否為 NULL 的檢查機制。
```c
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *delNode = list_entry(head->prev, element_t, list);
size_t length = bufsize < strlen(delNode->value) + 1
? bufsize
: strlen(delNode->value) + 1;
if (sp && bufsize)
strlcpy(sp, delNode->value, length);
list_del(head->prev);
return delNode;
}
```
### q_size
使用 list_for_each 巨集走訪整個 list 計算節點的數量。
```c=
int q_size(struct list_head *head)
{
if (!head)
return 0;
int ctr = 0;
struct list_head *node;
list_for_each (node, head)
++ctr;
return ctr;
}
```
### q_delete_mid
慢指標一次移動一格,快指標一次移動兩格。
因此當快指標走完整個 list 時,慢指標的位置會剛好只走了快指標的一半。
```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 *slow = head;
struct list_head *fast = head;
while (1) {
slow = slow->next;
if (fast->next == head || fast->next->next == head)
break;
fast = fast->next->next;
}
element_t *delNode = list_entry(slow, element_t, list);
list_del(slow);
free(delNode->value);
free(delNode);
return true;
}
```
### q_delete_dup
使用 list_for_each 巨集走訪整個鏈結串列。
額外用一個字串 str 來記錄當前節點的 value 字串內容。
並且再另外用一個 while 迴圈來找字串內容相同的元素,並使用 list_del 巨集對節點做 remove 。
- 待解決的問題: 在執行完 q_delete_dup 過後接著執行 q_free 會產生以下錯誤:
```
ERROR: There is no queue, but 3 blocks are still allocated
```
目前猜測是由於刪除時有發生 memory leak 的情形,需用 valgrind 等工具檢查錯誤產生的原因。
```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 true;
struct list_head *cur;
list_for_each (cur, head) {
if (cur->next == head)
break;
element_t *L = list_entry(cur, element_t, list);
element_t *R = list_entry(cur->next, element_t, list);
if (strcmp(L->value, R->value))
continue;
char *str = (char *) malloc(sizeof(char) * (strlen(L->value) + 1));
strlcpy(str, L->value, strlen(L->value) + 1);
struct list_head *prev = cur->prev;
while (prev->next != head) {
element_t *node = list_entry(prev->next, element_t, list);
if (strcmp(str, node->value))
break;
list_del(prev->next);
free(node->value);
free(node);
}
cur = prev;
}
return true;
}
```
### q_swap
迴圈每次將指標移動兩格,將自身與下一個節點的 value 交換。
交換採用三次 bitwise xor 的技巧。
```c
void q_swap(struct list_head *head)
{
// https://leetcode.com/problems/swap-nodes-in-pairs/
for (struct list_head *cur = head->next; (long int) cur ^ (long int) head;
cur = cur->next->next) {
if (cur->next == head)
return;
element_t *L = list_entry(cur, element_t, list);
element_t *R = list_entry(cur->next, element_t, list);
L->value = (char *) ((long int) L->value ^ (long int) R->value);
R->value = (char *) ((long int) L->value ^ (long int) R->value);
L->value = (char *) ((long int) L->value ^ (long int) R->value);
}
}
```
:::warning
撰寫更精簡的程式碼。
:notes: jserv
:::
修正後的程式碼如下,使用 flag 變數來記錄是否跟前一個節點交換 value 。
```c
void q_swap(struct list_head *head)
{
int flag = 0;
struct list_head *cur, *safe;
list_for_each_safe (cur, safe, head) {
if (flag & 1) {
element_t *L = list_entry(cur, element_t, list);
element_t *R = list_entry(cur->prev, element_t, list);
R->value = (char *) ((long int) L->value ^ (long int) R->value);
L->value = (char *) ((long int) L->value ^ (long int) R->value);
}
flag ^= 1;
}
}
```
### q_reverse
將所有節點的 next 和 prev 的值交換 ( 須包含 head )。
交換採用 3 次 bitwise xor 的技巧。
```c
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *cur = head;
do {
cur->prev =
(struct list_head *) ((long int) cur->prev ^ (long int) cur->next);
cur->next =
(struct list_head *) ((long int) cur->prev ^ (long int) cur->next);
cur->prev =
(struct list_head *) ((long int) cur->prev ^ (long int) cur->next);
cur = cur->prev;
} while ((long int) cur ^ (long int) head);
}
```
### q_reverseK
將每 k 個拆成一段獨立出來的 list 。
呼叫前面所實作好的 q_reverse 函式之後,再拼接回原本的鏈結串列中。
```c
void q_reverseK(struct list_head *head, int k)
{
// https://leetcode.com/problems/reverse-nodes-in-k-group/
struct list_head *ptr = head;
struct list_head *cur = head;
while (1) {
for (int i = 0; i < k; i++) {
cur = cur->next;
if (cur == head)
break;
}
if (cur == head)
break;
struct list_head *next = cur->next;
element_t ele;
struct list_head *dummy = &(ele.list);
dummy->next = ptr->next;
dummy->prev = cur;
ptr->next->prev = dummy;
cur->next = dummy;
q_reverse(dummy);
ptr->next = dummy->next;
dummy->next->prev = ptr;
dummy->prev->next = next;
next->prev = dummy->prev;
ptr = cur = dummy->prev;
}
}
```
### mergeTwoLists
由於實作 q_sort 以及 q_merge 兩個函式時都需要用到合併已排序連結串列的操作。因此額外實作本函式以利後續開發與維護。
合併兩個已經由小到大排序好的 list 合併。
採用 indirect pointer 技巧使程式碼更簡潔易讀。
```c
struct list_head *mergeTwoLists(struct list_head *L1, struct list_head *L2)
{
struct list_head **ptr = &L1;
struct list_head *ptr1 = L1->next;
struct list_head *ptr2 = L2->next;
while (ptr1 != L1 && ptr2 != L2) {
element_t *node1 = list_entry(ptr1, element_t, list);
element_t *node2 = list_entry(ptr2, element_t, list);
if (strcmp(node1->value, node2->value) < 0) {
(*ptr)->next = ptr1;
ptr1->prev = *ptr;
ptr1 = ptr1->next;
} else {
(*ptr)->next = ptr2;
ptr2->prev = *ptr;
ptr2 = ptr2->next;
}
ptr = &(*ptr)->next;
}
while (ptr1 != L1) {
(*ptr)->next = ptr1;
ptr1->prev = *ptr;
ptr1 = ptr1->next;
ptr = &(*ptr)->next;
}
while (ptr2 != L2) {
(*ptr)->next = ptr2;
ptr2->prev = *ptr;
ptr2 = ptr2->next;
ptr = &(*ptr)->next;
}
(*ptr)->next = L1;
L1->prev = *ptr;
return L1;
}
```
### q_sort
先利用和前面實作 q_delete_mid 所用到的快慢指針技巧找到將 list 拆分成兩半的位置。然後加入 dummy node 形成兩個獨立的 list。分別 sort 兩個list 之後,再將兩個排序好的 list 利用前面實作的 mergeTwoLists 函式合併。
```c
void q_sort(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *slow = head->next;
struct list_head *fast = head->next;
while (fast->next != head && fast->next->next != head) {
slow = slow->next;
fast = fast->next->next;
}
element_t ele;
struct list_head *dummy = &(ele.list);
struct list_head *mid = slow->next;
struct list_head *last = head->prev;
mid->prev->next = head;
head->prev = mid->prev;
dummy->next = mid;
dummy->prev = last;
last->next = dummy;
mid->prev = dummy;
q_sort(head);
q_sort(dummy);
mergeTwoLists(head, dummy);
}
```
### q_descend
類似於 monotonic stack 的概念,反向迭代整個 list ,並同時記錄當前所遇到最大的 value 。若當前的 value 比過去最大的 value 還要小則利用 list_del 函式來進行 remove 的動作。
```c
int q_descend(struct list_head *head)
{
// https://leetcode.com/problems/remove-nodes-from-linked-list/
char str[50000] = ""; // unsafe for possible larger length
struct list_head *next;
for (struct list_head *cur = head->prev; cur != head; cur = next) {
next = cur->prev;
element_t *node = list_entry(cur, element_t, list);
if (strcmp(node->value, str) < 0) {
list_del(cur);
free(node->value);
free(node);
} else
strlcpy(str, node->value, 50000);
}
return q_size(head);
}
```
### q_merge
採用頭尾兩兩合併的方式,不斷重複直到剩下一個 list 為止。
- 待解決的問題: 在執行 make test 指令時發現,由於 merge 過後會造成當前指向的 queue 被設為 null 進而導致後續的一串指令都失效。
```c
int q_merge(struct list_head *head)
{
// https://leetcode.com/problems/merge-k-sorted-lists/
if (!head || list_empty(head))
return 0;
if (list_is_singular(head))
return list_entry(head->next, queue_contex_t, chain)->size;
int queueSize = q_size(head);
while (queueSize > 1) {
struct list_head *node;
list_for_each (node, head) {
queue_contex_t *que1 = list_entry(node, queue_contex_t, chain);
queue_contex_t *que2 =
list_entry(head->prev, queue_contex_t, chain);
if (que1 == que2)
break;
que1->q = mergeTwoLists(que1->q, que2->q);
que2->q = NULL;
que2->size = 0;
list_del(head->prev);
}
queueSize = (queueSize + 1) / 2;
}
list_entry(head->next, queue_contex_t, chain)->size =
q_size(list_entry(head->next, queue_contex_t, chain)->q);
return list_entry(head->next, queue_contex_t, chain)->size;
}
```
### Valgrind 自動檢測
執行 ```make valgrind``` 過後,發現多數結果都如下:
```text
==616700== 2,049 bytes in 1 blocks are still reachable in loss record 46 of 47
==616700== at 0x483B7F3: malloc (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==616700== by 0x10C0C9: do_remove (qtest.c:373)
==616700== by 0x10C4B7: do_rh (qtest.c:463)
==616700== by 0x10E053: interpret_cmda (console.c:181)
==616700== by 0x10E623: interpret_cmd (console.c:201)
==616700== by 0x10EA60: cmd_select (console.c:610)
==616700== by 0x10F36B: run_console (console.c:705)
==616700== by 0x10D3E0: main (qtest.c:1276)
==616700==
```
代表檢測到程式結束時,仍然有著還沒被釋放的記憶體,但有指標依然指向它。可能是由於 globla 變數所導致的現象。
### q_shuffle
首先在 qtest.c 裡面增加 shuffle 命令:
```
ADD_COMMAND(shuffle, "Shuffle the queue with Fisher–Yates shuffle algorithm", "");
```
然後參考其他 `do_` 開頭的函式實作 `do_shuffle`
```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 sort on null queue");
error_check();
set_noallocate_mode(true);
if (exception_setup(true))
q_shuffle(current->q);
exception_cancel();
set_noallocate_mode(false);
q_show(3);
return !error_check();
}
```
另外由於 commit hook 不允許更動 queue.h 之中的程式碼,因此另外用一個標頭檔 shuffle.h 並在裡面增加 q_shuffle 函式的宣告
```c=
#ifndef SHUFFLE_H
#define SHUFFLE_H
void q_shuffle(struct list_head *head);
#endif
```
並在 qtest.c 之中引入此標頭檔。
---
### `lib/list_sort.c`
由於 list_sort.c 程式碼中所引入的標頭檔都是 linux 核心之中的標頭檔,因此需要自己做對應的修改才能順利的執行。
:::danger
改進漢語描述,注意作業書寫規範。
:notes: jserv
:::
1. 將 u8 改為 uint8_t
2. likely 以及 unlikely 巨集是定義在 linux/compiler.h 之中的巨集,因此需要在 list_sort.h 之中自己加入以下程式碼:
```c
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
```
3. 需要自己實作比較函式,如下:
```c
int cmp(void *_, const struct list_head *a, const struct list_head *b)
{
char *strA = list_entry(a, element_t, list)->value;
char *strB = list_entry(b, element_t, list)->value;
return strcmp(strA, strB);
}
```
### 論文閱讀 - Dude, is my code constant time?
* 本論文介紹了 dutect 工具,用於評估代碼在特定平台上是否以恆定時間運行,並且不需要對硬體有特別的要求。
* 方法步驟
1. Measure execution time
* 進行測量時,首先需要定義兩種不同的數據類別,一種是固定的數據類別,而另一種是隨機的數據類別。
* 現代 CPU 提供週期計數器(例如 x86 系列中的 TSC 寄存器,或 ARM 可用的 systick 外設),可用於準確地獲取執行時間測量。
* 為了最小化環境變化對結果的影響,在進行測量時,每次測量對應一個隨機選擇的輸入數據類別。類別分配和輸入準備任務在測量階段之前執行。
2. Apply post-processing
* 在進行統計分析前,應對每個單獨的測量進行一些處理。
* 典型的時序分佈對較長的執行時間呈正偏斜。這可能是由於測量數據本身的問題,或者是主進程被操作系統或其他外部因素干擾所致。因此可以捨棄大於固定閾值的測量值。
* 根據應用的統計檢驗方法,可以進行一些高階前處理。
3. Apply statistical test
* T 檢定: 可以使用 Welch’s t-test。需要注意的是,當 t 檢定與裁剪預處理結合使用時,不僅測試平均值是否相等,還測試更高階的統計矩,因為裁剪是一種非線性變換。
* 非參數檢定
* percentile 處理
percentile 是論文中步驟二所提及的 cropping 處理,目的是為了去除極值。