---
tag: linux2022
---
# 2022q1 Homework1 (lab0)
contributed by < `jim12312321` >
### Reviewed by `kevinshieh0225`
- 在執行節點逐一刪除時,可以用 linux kernel api 中的 `list_for_each_safe`。
- 有一些我自己沒有注意到的實作細節,比如在移除節點時使用 `list_del_init`,可讓整體程式碼更安全。
- 可以在實作函式的改寫版本加上小標題,較容易追蹤文章結構
- 現在使用 `container_of` 應該不再需要 `cppcheck-suppress nullPointer` 了
- `q_delete_dup` 的規則判定有變,所有在原本佇列中發生重複的節點都要刪除,目前程式碼實作並不符合要求。
- 文件程式碼適時註解。
- 可實作看看迭代版本的 q_sort 並比較結果。
## 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 11.2.0-7ubuntu2) 11.2.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): 20
On-line CPU(s) list: 0-19
Thread(s) per core: 1
Core(s) per socket: 14
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 154
Model name: 12th Gen Intel(R) Core(TM) i7-12700H
Stepping: 3
CPU MHz: 2700.000
CPU max MHz: 6000.0000
CPU min MHz: 400.0000
BogoMIPS: 5376.00
Virtualization: VT-x
L1d cache: 336 KiB
L1i cache: 224 KiB
L2 cache: 8.8 MiB
NUMA node0 CPU(s): 0-19
```
## [作業要求](https://hackmd.io/@sysprog/linux2022-lab0)
詳細閱讀 [lab0](https://hackmd.io/@sysprog/linux2022-lab0) 說明,依據指示著手修改 queue.[ch] 和連帶的檔案,測試後用 Git 管理各項修改。
* `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`: 移走佇列中間的節點
* `q_delete_dup`: 在已經排序的狀況,移走佇列中具備重複內容的節點
* `q_swap`: 交換佇列中鄰近的節點
* `q_reverse`: 以反向順序重新排列鏈結串列,該函式不該配置或釋放任何鏈結串列節點,換言之,它只能重排已經存在的節點
* `q_sort`: 以==遞增順序==來排序鏈結串列的節點
## 針對佇列的操作
### q_new
建立空佇列,若沒有配置指定大小的記憶體,則回傳 NULL。
> commit 1be837
```cpp
struct list_head *q_new()
{
element_t *q = malloc(sizeof(element_t));
if (!q)
return NULL;
q->value = NULL;
q->list.next = &q->list;
q->list.prev = &q->list;
return &q->list;
}
```
> 「當每次提交一些東西時,Git 會產生一個快照,將所有檔案放置在一個物件記錄著,可以輸入 git log(歷史紀錄) 會看到提交後 40 個字元長 16 進位 的 SHA-1(Secure Hash Algorithm 1)雜湊演算碼,這可以做為提交後的識別碼,使用命令配合 commit 識別碼,一般只需要 6~8 個數字即可辨識。」 -- 摘自 [git](https://powerkaifu.github.io/2019/10/14/lesson-git/)
後來發現在 list.h 中有可以直接初始話佇列的函式 (INIT_LIST_HEAD) ,因此更新。
> commit 870612
```cpp
struct list_head *q_new()
{
element_t *q = malloc(sizeof(element_t));
if (q == NULL) {
return NULL;
}
INIT_LIST_HEAD(&q->list);
return &q->list;
}
```
### q_free
釋放佇列所佔用的記憶體
> 由於嘗試許久仍寫不出來,因此以下內容有去參考kevinshieh0225、laneser中q_free的寫法
>
> commit c26a4e
```cpp
void q_free(struct list_head *l)
{
if (!l)
return;
struct list_head *node = l->next;
while (node != l) {
// cppcheck-suppress nullPointer
element_t *e = container_of(node, element_t, list);
node = node->next;
q_release_element(e);
}
// cppcheck-suppress nullPointer
element_t *head = container_of(l, element_t, list);
free(head);
}
```
### q_insert_head
給定要插入的新字串,將新字串加入新節點並中在佇列**開頭**插入給定的新節點。
若新節點為 NULL 或沒有配置記憶體空間,則回傳 false 。若順利加入佇列**開頭**則回傳 true 。
> commit 870612
```cpp
bool q_insert_head(struct list_head *head, char *s)
{
element_t *q = malloc(sizeof(element_t));
if (!q) {
return false;
}
q->value = malloc(sizeof(s));
memset(q->value, '\0', strlen(q->value));
strncpy(q->value, s, strlen(s));
list_add(&q->list, head);
return true;
}
```
::: spoiler 踩坑紀錄
- 複製字串 (strnpy) 時用 strlen(s) 當作第三個參數 (ERROR: Corruption detected in block with address 0x555c8c87df10 when attempting to free it)
- 用設立一個變數計算字串字數取代 strlen
:::
> commit c26a4e
```cpp
bool q_insert_head(struct list_head *head, char *s)
{
element_t *q = malloc(sizeof(element_t));
if (q == NULL || head == NULL) {
return false;
}
int charsize = 0;
while (*(s + charsize))
charsize += 1;
q->value = malloc(charsize + 1);
if (q->value == NULL) {
free(q);
return false;
}
strncpy(q->value, s, charsize);
q->value[charsize] = '\0';
list_add(&q->list, head);
return true;
}
```
加入檢查 head 是否為 NULL 的判斷式讓程式更加 robust
> commit 58c193
```cpp
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *q = malloc(sizeof(element_t));
if (!q)
return false;
int charsize = 0;
while (*(s + charsize))
charsize += 1;
q->value = malloc(charsize + 1);
if (q->value == NULL) {
free(q);
return false;
}
strncpy(q->value, s, charsize);
q->value[charsize] = '\0';
list_add(&q->list, head);
return true;
}
```
### q_insert_tail
給定要插入的新字串,將新字串加入新節點並中在佇列**結尾**插入給定的新節點。
若新節點為 NULL 或沒有配置記憶體空間,則回傳 false 。若順利加入佇列**結尾**則回傳 true 。
> commit 870612
```cpp
bool q_insert_tail(struct list_head *head, char *s)
{
element_t *q = malloc(sizeof(element_t));
if (!q) {
return false;
}
q->value = malloc(sizeof(s));
memset(q->value, '\0', strlen(q->value));
strncpy(q->value, s, strlen(s));
list_add_tail(&q->list, head);
return true;
}
```
跟 q_insert_head 有一樣問題故修正。
> commit c26a4e
```cpp
bool q_insert_tail(struct list_head *head, char *s)
{
element_t *q = malloc(sizeof(element_t));
if (q == NULL || head == NULL) {
return false;
}
int charsize = 0;
while (*(s + charsize))
charsize += 1;
q->value = malloc(charsize + 1);
if (q->value == NULL) {
free(q);
return false;
}
strncpy(q->value, s, charsize);
q->value[charsize] = '\0';
list_add_tail(&q->list, head);
return true;
}
```
加入檢查 head 是否為 NULL 的判斷式讓程式更加 robust
> commit 58c193
```cpp
bool q_insert_tail(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *q = malloc(sizeof(element_t));
if (!q)
return false;
int charsize = 0;
while (*(s + charsize))
charsize += 1;
q->value = malloc(charsize + 1);
if (q->value == NULL) {
free(q);
return false;
}
strncpy(q->value, s, charsize);
q->value[charsize] = '\0';
list_add_tail(&q->list, head);
return true;
}
```
### q_remove_head
將佇列**開頭**的節點移除。
- 回傳 NULL (移除失敗)
- 佇列為空或 NULL
- 回傳被移除的節點 (移除成功)
- 還需將被移除的節點中儲存的值複製到 sp 中
:::spoiler 踩坑紀錄
- 沒有搞清楚 sp 是什麼,胡亂 malloc ,結果不意外的跳出錯誤訊息 (ERROR: Failed to store removed value)
- 查看 qtest.c 後發現 remove (在 qtest.c 中傳入 q_remove_head 的變數) 並且他已經配置好記憶體跟初始化內容,因此不需要多此一舉,去掉 malloc 後錯誤訊息即消失。
:::
> commit c26a4e
``` cpp
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (head == NULL || list_empty(head) != 0) {
return NULL;
}
if (sp == NULL) {
return NULL;
}
// cppcheck-suppress nullPointer
element_t *cur_e = list_entry(head->next, element_t, list);
list_del_init(head->next);
strncpy(sp, cur_e->value, bufsize - 1);
return cur_e;
}
```
將 sp 根據 bufsize 的大小規定,在尾端加上 '\0'
> commit fd5f90
``` cpp
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
if (!sp)
return NULL;
// cppcheck-suppress nullPointer
element_t *cur_e = list_entry(head->next, element_t, list);
list_del_init(head->next);
strncpy(sp, cur_e->value, bufsize - 1);
sp[bufsize - 1] = '\0';
return cur_e;
}
```
### q_remove_tail
將佇列**結尾**的節點移除。
- 回傳 NULL (移除失敗)
- 佇列為空或 NULL
- 回傳被移除的節點 (移除成功)
- 還需將被移除的節點中儲存的值複製到 sp 中
> commit c26a4e
``` cpp
element_t *q_remove_tail(struct list_head *head, char *sp, size_t bufsize)
{
if (head == NULL || list_empty(head) != 0) {
return NULL;
}
if (sp == NULL) {
return NULL;
}
// cppcheck-suppress nullPointer
element_t *cur_e = list_entry(head->prev, element_t, list);
list_del_init(head->prev);
strncpy(sp, cur_e->value, bufsize - 1);
return cur_e;
}
```
將 sp 根據 bufsize 的大小規定,在尾端加上 '\0'
> commit fd5f90
``` cpp
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
if (!sp)
return NULL;
// cppcheck-suppress nullPointer
element_t *cur_e = list_entry(head->prev, element_t, list);
list_del_init(head->prev);
strncpy(sp, cur_e->value, bufsize - 1);
sp[bufsize - 1] = '\0';
return cur_e;
}
```
### q_size
回傳佇列長度,若佇列是為 NULL 或空佇列則回傳 0。
> 以下程式碼源自老師的 [lab0 作業要求](https://hackmd.io/@sysprog/linux2022-lab0)
> commit 392750
```cpp
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
移除佇列中間的節點並將其佔用記憶體釋放。
- 回傳 true (移除成功)
- 移除並釋放中間的節點
- 回傳 false (移除失敗)
- 佇列為 NULL 或空佇列
> commit 16e285
```cpp
bool q_delete_mid(struct list_head *head)
{
if (head == NULL || list_empty(head) != 0) {
return false;
}
if (list_is_singular(head)) {
head = head->next;
// cppcheck-suppress nullPointer
element_t *e = container_of(head, element_t, list);
list_del(head);
q_release_element(e);
return true;
}
int del_n_index = 0;
struct list_head *node = head->next;
/* even nodes */
if (q_size(node) % 2 == 0) {
del_n_index = q_size(node) / 2;
} else { /* odd nodes */
del_n_index = (q_size(node) + 1) / 2 - 1;
}
while (del_n_index > 0) {
node = node->next;
del_n_index--;
}
// cppcheck-suppress nullPointer
element_t *e = container_of(node, element_t, list);
list_del(node);
q_release_element(e);
return true;
}
```
利用[你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)中提到的 fast/slow 指標找中間節點的方法重新改寫。
> commit 430645
```cpp=
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
if (list_is_singular(head)) {
head = head->next;
// cppcheck-suppress nullPointer
element_t *e = container_of(head, element_t, list);
list_del(head);
q_release_element(e);
return true;
}
struct list_head *fast = head, *slow = head;
while (true) {
if (fast->next == head || fast->next->next == head)
break;
fast = fast->next->next;
slow = slow->next;
}
slow = slow->next;
// cppcheck-suppress nullPointer
element_t *e = container_of(node, element_t, list);
list_del(node);
element_t *e = container_of(slow, element_t, list);
list_del(slow);
q_release_element(e);
return true;
}
```
### q_delete_dup
將佇列中存有重複的節點移除並釋放其記憶體空間。
> commit ec6546
```cpp
void q_delete_dup(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head *node, *del;
element_t *del_node;
node = head->next;
while (node != head) {
char *cstr, *nstr;
// cppcheck-suppress nullPointer
cstr = list_entry(node, element_t, list)->value;
if (node->next == head) {
break;
}
// cppcheck-suppress nullPointer
nstr = list_entry(node->next, element_t, list)->value;
if (strcmp(cstr, nstr) == 0) {
del = node;
node = node->next;
list_del_init(del);
// cppcheck-suppress nullPointer
del_node = list_entry(del, element_t, list);
q_release_element(del_node);
} else {
node = node->next;
}
}
return true;
}
```
### q_swap
將佇列中兩兩相鄰的節點互換。
> commit 0508d3
```cpp
void q_swap(struct list_head *head)
{
struct list_head *odd, *even;
odd = head->next;
even = odd->next;
while (true) {
// swap odd and even
odd->next = even->next;
odd->next->prev = odd;
even->prev = odd->prev;
even->prev->next = even;
odd->prev = even;
even->next = odd;
// end
odd = odd->next;
even = odd->next;
if (odd == head || even == head) {
break;
}
}
}
```
### q_reverse
反向順序重新排列鏈結串列,並且不能釋放或配置新的記憶體空間。
> commit 3cd169
```cpp
void q_reverse(struct list_head *head)
{
if (head == NULL || list_empty(head) != 0) {
return;
}
struct list_head *node, *temp;
for (node = head->next; node != head; node = node->prev) {
temp = node->next;
node->next = node->prev;
node->prev = temp;
}
temp = head->next;
head->next = head->prev;
head->prev = temp;
}
```
### q_sort
利用 merge sort q_delete_dup將佇列依照佇列節點中的字串遞增排序。
==以下程式優化太糟,無法通過效能測試,已更新較有效率的版本==
> commit 94ee22
```cpp
struct list_head *merge(struct list_head *left, struct list_head *right)
{
struct list_head *head, *cur;
// cppcheck-suppress nullPointer
char *lstr = list_entry(left, element_t, list)->value;
// cppcheck-suppress nullPointer
char *rstr = list_entry(right, element_t, list)->value;
int cmp = strcmp(lstr, rstr);
if (cmp > 0) { // left > right
head = right;
right = right->next;
right->prev = head->prev;
right->prev->next = right;
head->next = head;
head->prev = head;
} else { // left <= right
head = left;
left = left->next;
left->prev = head->prev;
left->prev->next = left;
head->next = head;
head->prev = head;
}
cur = head;
if (head == left) {
left = NULL;
} else if (head == right) {
right = NULL;
}
while (true) {
if (left == NULL) {
cur->next = right;
head->prev = right->prev;
right->prev->next = head;
right->prev = cur;
break;
} else if (right == NULL) {
cur->next = left;
head->prev = left->prev;
left->prev->next = head;
left->prev = cur;
break;
}
// cppcheck-suppress nullPointer
lstr = list_entry(left, element_t, list)->value;
// cppcheck-suppress nullPointer
rstr = list_entry(right, element_t, list)->value;
cmp = strcmp(lstr, rstr);
if (cmp > 0) { // left > right
cur->next = right;
if (q_size(right) == 0) {
right->prev = cur;
right = NULL;
} else {
right = right->next;
right->prev = right->prev->prev;
right->prev->next = right;
cur->next->prev = cur;
}
cur = cur->next;
cur->next = head;
head->prev = cur;
} else { // left <= right
cur->next = left;
if (q_size(left) == 0) {
left->prev = cur;
left = NULL;
} else {
left = left->next;
left->prev = left->prev->prev;
left->prev->next = left;
cur->next->prev = cur;
}
cur = cur->next;
cur->next = head;
head->prev = cur;
}
}
return head;
}
struct list_head *mergesort(struct list_head *head)
{
if (head->next == head) {
return head;
}
int mid_index = q_size(head) / 2;
struct list_head *mid, *temp = head;
while (mid_index > 0) {
temp = temp->next;
mid_index--;
}
mid = temp->next;
temp->next = head;
mid->prev = head->prev;
head->prev->next = mid;
head->prev = temp;
struct list_head *left = mergesort(head), *right = mergesort(mid);
return merge(left, right);
}
void q_sort(struct list_head *head)
{
if (head == NULL || list_empty(head) != 0 || list_is_singular(head) != 0) {
return;
}
struct list_head *node = head->next;
node->prev = head->prev;
node->prev->next = node;
head->prev = NULL;
head->next = NULL;
node = mergesort(node);
head->next = node;
head->prev = node->prev;
node->prev->next = head;
node->prev = head;
}
```
看完 [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)後,利用裡面老師示範的技巧改寫 mergesort
:::spoiler 解惑紀錄
在理解程式碼並實作的過程中,我對於 mergeTwoLists 中的這段程式碼不了解,經過查閱一些資料後,總算理解這樣做的用意,因而紀錄。(<s>以下內容為我個人理解,若有誤,歡迎題點。</s>)
> 不用在開發紀錄中說客套話。
> :notes: jserv
```cpp
*ptr = (struct list_head *) ((uintptr_t) left | (uintptr_t) right);
```
一開始我對這段程式碼存有幾點疑惑
- uintptr_t 是什麼?
- 為什麼這樣可以取代 if-else ?
- 為什麼這樣可以增進效能?
1. uintptr_t 是什麼?
根據 C99 中的規範,提供如 intptr_t,uintptr_t 讓任何可指向 void 的指標可以與之相互轉換且內容不變,因此便可以對指標所指向之地址進行操作 (e.g. void * 無法進行如++或--等數值操作) 。
> TODO: 翻閱 C 語言規格書
> :notes: jserv
參考資料:
<s>[intptr_t、uintptr_t数据类型的解析](https://blog.csdn.net/cs_zhanyb/article/details/16973379)</s>
> 避免看這種二手資訊,無助於你理解,而且屆時語言標準更動時,你無從掌握
> :notes: jserv
[C99 standard §7.18.1.4 (p.257)](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)
2. 為什麼這樣可以取代 if-else ?
因為 NULL 經過 uintptr_t 的轉換後就會變成 0 ,因此就可以透過 or 的操作,直接得到不是 NULL 的另一個指標。
3. 為什麼這樣可以增進效能?
跟處理器面對分支 (branch) 時的處理方式有關,若預測錯分支就會造成效能損耗。因此能減少分支的使用就**有可能**增進效能 (如果每次分支處理都猜對,那有沒有用 if-else 是沒有差別的)
> 以講義提供的範例程式碼來說,`if` 和 `else` 成立的機會約略是各 50%,於是分支預測器就很難總是猜對,你可以翻閱 CS:APP 第 3 章,得知如何分析產生的組合語言輸出,屆時你就會發現,最初的程式碼存在比較和分支跳躍等指令,反過來說,調整後的程式碼只要單純的 bitwise OR 操作,從而省下 CPU 週期。
> :notes: jserv
參考資料:
<s>
[代码里写很多if会影响效率吗?](https://www.zhihu.com/question/51107242)
[分支預測器 -wiki](https://zh.wikipedia.org/zh-tw/%E5%88%86%E6%94%AF%E9%A0%90%E6%B8%AC%E5%99%A8)
</s>
> 去翻閱計算機組織/結構的教科書,裡頭有 branch predictor 的說明,你需要「完整」的認識,而非從隻字片語中找線索,後者的難度比認真讀教科書還高,畢竟你變成「偵探」了
> :notes: jserv
:::
> commit b7014e
> commit 39f6ca
```cpp
struct list_head *mergeTwoLists(struct list_head *left, struct list_head *right)
{
struct list_head *head = NULL, **ptr = &head, **node;
for (node = NULL; left && right; *node = (*node)->next) {
// cppcheck-suppress nullPointer
node = (strcmp(list_entry(left, element_t, list)->value,
// cppcheck-suppress nullPointer
list_entry(right, element_t, list)->value) < 0)
? &left
: &right;
*ptr = *node;
ptr = &(*ptr)->next;
}
*ptr = (struct list_head *) ((uintptr_t) left | (uintptr_t) right);
return head;
}
struct list_head *mergesort(struct list_head *head)
{
if (!head->next)
return head;
struct list_head *fast = head, *slow = head, *mid;
while (true) {
if (!fast->next || !fast->next->next)
break;
fast = fast->next->next;
slow = slow->next;
}
mid = slow->next;
slow->next = NULL;
struct list_head *left = mergesort(head), *right = mergesort(mid);
return mergeTwoLists(left, right);
}
void q_sort(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head))
return;
struct list_head *node = head->next, *temp;
head->prev->next = NULL;
head->next = NULL;
node = mergesort(node);
temp = head;
temp->next = node;
while (temp->next) {
temp->next->prev = temp;
temp = temp->next;
}
temp->next = head;
head->prev = temp;
}
```
## 參考資訊
- 共筆格式參考:[共筆示範](https://hackmd.io/@cccccs100203/linux2020-lab0)
- 同學作業參考: [laneser](https://hackmd.io/@laneser/linux2022_lab0), [kevinshieh0225](https://hackmd.io/@Masamaloka/2022q1-hw1)
- 踩坑/解惑參考:
- [lab0-c](https://hackmd.io/@sysprog/linux2022-lab0)
- [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list)
- [sizeof 在語言層面的陷阱](http://blog.linux.org.tw/~jserv/archives/001876.html?fbclid=IwAR1leirIhqWK5Qm9VpCif08I3v6w0fZi5Y0W-EiSxjqFnVDWDd-p8F0dq_s)
- [C99 standard p.257](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1256.pdf)