---
tags: linux2022
---
# 2022q1 Homework1 (lab0)
contributed by < [ShienF](https://github.com/ShienF/lab0-c) >
## 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
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: 158
Model name: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
Stepping: 9
CPU MHz: 2800.000
CPU max MHz: 3800.0000
CPU min MHz: 800.0000
BogoMIPS: 5599.85
Virtualization: VT-x
L1d cache: 128 KiB
L1i cache: 128 KiB
L2 cache: 1 MiB
L3 cache: 6 MiB
NUMA node0 CPU(s): 0-7
```
## 作業要求
> [lab0](https://hackmd.io/@sysprog/linux2022-lab0)
## 開發前準備
### Makefile
`##` 指的是我另補充的註解
```cmake=
CC = gcc
## kind of macro in c, but use with this: $()
CFLAGS = -O1 -g -Wall -Werror -Idudect -I.
## CFLAGS:Extra flags to give to the C compiler
GIT_HOOKS := .git/hooks/applied
## := will be assigned immediately while = will be eassigned in the end
DUT_DIR := dudect
all: $(GIT_HOOKS) qtest
tid := 0
# Control test case option of valgrind
ifeq ("$(tid)","0")
TCASE :=
else
TCASE := -t $(tid)
endif
# Control the build verbosity
ifeq ("$(VERBOSE)","1")
Q :=
VECHO = @true
else
Q := @
VECHO = @printf
endif
## @ don't show the command
# Enable sanitizer(s) or not
ifeq ("$(SANITIZER)","1")
# https://github.com/google/sanitizers/wiki/AddressSanitizerFlags
CFLAGS += -fsanitize=address -fno-omit-frame-pointer -fno-common
LDFLAGS += -fsanitize=address
## LDFLAGS: Extra flags to give to compilers when they are supposed to invoke the linker,\
## ‘ld’, such as -L.
## += means concatenate
endif
$(GIT_HOOKS):
@scripts/install-git-hooks
@echo
## echo means show in the termianl; @ means don't show this "echo" command
OBJS := qtest.o report.o console.o harness.o queue.o \
random.o dudect/constant.o dudect/fixture.o dudect/ttest.o \
linenoise.o
deps := $(OBJS:%.o=.%.o.d)
qtest: $(OBJS)
$(VECHO) " LD\t$@\n"
$(Q)$(CC) $(LDFLAGS) -o $@ $^ -lm
## $@ means the file name of the target of the rule.
## $^ means the names of all the prerequisites, with spaces between them.
## $< means the name of the first prerequisite.
%.o: %.c
@mkdir -p .$(DUT_DIR)
$(VECHO) " CC\t$@\n"
$(Q)$(CC) -o $@ $(CFLAGS) -c -MMD -MF .$@.d $<
## -MMD means avoid to create header file
## % means every string
## target: dependencies
## to do the commands
check: qtest
./$< -v 3 -f traces/trace-eg.cmd
test: qtest scripts/driver.py
scripts/driver.py -c
valgrind_existence:
@which valgrind 2>&1 > /dev/null || (echo "FATAL: valgrind not found"; exit 1)
valgrind: valgrind_existence
# Explicitly disable sanitizer(s)
$(MAKE) clean SANITIZER=0 qtest
$(eval patched_file := $(shell mktemp /tmp/qtest.XXXXXX))
cp qtest $(patched_file)
chmod u+x $(patched_file)
sed -i "s/alarm/isnan/g" $(patched_file)
scripts/driver.py -p $(patched_file) --valgrind $(TCASE)
@echo
@echo "Test with specific case by running command:"
@echo "scripts/driver.py -p $(patched_file) --valgrind -t <tid>"
## shell means execute at shell
clean:
rm -f $(OBJS) $(deps) *~ qtest /tmp/qtest.*
rm -rf .$(DUT_DIR)
rm -rf *.dSYM
(cd traces; rm -f *~)
-include $(deps)
```
## 開發過程
### q_new
因為要創立一個新的 queue ,所以要配置一個空間給串起這個 queue 的 `struct list_head`。
在配置空間後要注意是否為 NULL(無法配置記憶體),若非 NULL 則要對此 `struct list_head` 初始化,否則若下一步為刪除 queue 有可能會造成不如預期的結果。
```c
/*
* Create empty queue.
* Return NULL if could not allocate space.
*/
struct list_head *q_new()
{
struct list_head *head;
head = malloc(sizeof(struct list_head));
if (!head) {
free(head);
return NULL;
} else {
INIT_LIST_HEAD(head);
return head;
}
}
```
### q_free
在釋放 queue 的記憶體空間時,原本的想法為運用 `list.h` 裡的 `list_for_each_safe` 迭代 queue 裡每個 `struct list_head` 的地址,但參考了 [eric88525](https://hackmd.io/@eric88525/linux2022-lab0),透過`list_for_each_entry_safe`迭代每個 queue 的主體: `element_t` 的地址,再釋放掉才是正確的,否則會漏釋放 `element_t` 除了 `struct list_head` 的其他成員,如 `char *`。
另外在宣告 `element_t *` 時,是不需要再對其配置記憶體空間的,因為此指標只是用來存取迭代時的指標。宣告後也注意要初始化,避免造成不如預期的結果。
最後還釐清了一件事:`free`了指標,意思是指標指向的內容被清掉,指標本身仍存在。
```c
/* Free all storage used by queue */
void q_free(struct list_head *l)
{
if (!l)
return;
element_t *node = NULL, *safe = NULL;
list_for_each_entry_safe (node, safe, l, list) {
q_release_element(node);
}
free(l);
}
```
### q_insert_head & q_insert_tail
在原有的 queue 裡插入新的元素時,因為傳進此 function 的參數為 string,因此需要配置一個queue主體:`element_t` 的記憶體空間,再把此 string assign 給此 queue(`element_t` 裡的 `char *value`)。
一開始的作法並未配置string的空間,得到以下 error。
:::danger
ERROR: Need to allocate separate string for each queue element
:::
參考了 [Chao-Shun-Cheng](https://hackmd.io/@Chao-Shun-Cheng/linux2022-lab0),配置了字串長度的記憶體空間,最後利用 `list.h` 裡的 `list_add` 將新的 `element_t->list` 加入原有的 `head` 裡。
`strlen` 並不含`'\0'`,因此在配置記憶體空間時需多加一。
第三個 `if` 要注意若 `element->value` 未配置成功,前面配置的 `element_t` 也需釋放。
`q_insert_head` 及 `q_insert_tail`可以透過`list_add` 及 `list_add_tail` 來區別。
```c
/*
* Attempt to insert element at head of queue.
* Return true if successful.
* Return false if q is NULL or could not allocate space.
* Argument s points to the string to be stored.
* The function must explicitly allocate space and copy the string into it.
*/
bool q_insert_head(struct list_head *head, char *s)
{
if (!head)
return false;
element_t *element = malloc(sizeof(element_t));
if (!element)
return false;
int len = strlen(s);
element->value = malloc(sizeof(char) * len + 1);
if (!element->value) {
free(element);
return false;
}
strncpy(element->value, s, len + 1);
list_add(&element->list, head);
return true;
}
```
### q_remove_head & q_remove_tail
刪除 queue 裡第一個元素,並回傳此第一個元素。利用 `list.h` 裡的 `list_first_entry` 將第一個 `element_t` 的地址取出,用來之後取出其成員 `char *value` 及 function 的回傳值。
再運用`list.h` 裡的 `list_del` 把 `head->next` ,注意根據[你所不知道的 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)其中的==Linked list 在 Linux 核心原始程式碼==, `head` 本身並不為 `element_t`。所以要移除的元素為 `head->next`。
另外應盡量避免 `strcpy` 改用 `strncpy` 較為安全。 `strncpy` 的數量應包含 `'\0'`。`sp`的結尾應含有 `'\0'`。
`q_remove_head` 及 `q_remove_tail`可以透過`list_first_entry`,`list_last_entry`,以及 `list_del(head->next)`,`list_del(head->prev)` 來區別。
```c
/*
* Attempt to remove element from head of queue.
* Return target element.
* Return NULL if queue is NULL or empty.
* If sp is non-NULL and an element is removed, copy the removed string to *sp
* (up to a maximum of bufsize-1 characters, plus a null terminator.)
*
* NOTE: "remove" is different from "delete"
* The space used by the list element and the string should not be freed.
* The only thing "remove" need to do is unlink it.
*
* REF:
* https://english.stackexchange.com/questions/52508/difference-between-delete-and-remove
*/
element_t *q_remove_head(struct list_head *head, char *sp, size_t bufsize)
{
if (!head || list_empty(head))
return NULL;
element_t *target = list_first_entry (head, element_t, list);
list_del(head->next);
if (sp) {
strncpy(sp, target->value, bufsize);
sp[bufsize - 1] = '\0';
}
return target;
}
```
### q_release_element
```c
/*
* WARN: This is for external usage, don't modify it
* Attempt to release element.
*/
void q_release_element(element_t *e)
{
free(e->value);
free(e);
}
```
### q_size
運用`list.h` 裡的 `list_for_each` 迭代 queue 裡的`struct head_list`,迭代次數即為 queue 裡的元素個數。
```c
/*
* Return number of elements in queue.
* Return 0 if q is NULL or empty
*/
int q_size(struct list_head *head)
{
if (!head || list_empty(head))
return 0;
int len = 0;
struct list_head *li;
list_for_each (li, head)
len++;
return len;
}
```
### q_delete_mid
參考[你所不知道的 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)其中的==案例探討: Leetcode 2095. Delete the Middle Node of a Linked List==,利用指標的指標 `**indir` 來簡化程式碼。首先將 `**indir` 初始化為指向 `head->next` 的指標,利用快慢指標的概念,快指標走兩步,慢指標的指標走一步,當快指標走到 `head` 時(不含 `head` 的元素個數為偶數時),或快指標走到的元素下一元素為 `head` 時(不含 `head` 的元素個數為奇數時),慢指標的指標即為指向 mid 的指標的指標,因為快指標走的步數為慢指標的指標的兩倍。
注意 queue 裡的元素都是由 `head->next` 為起始。
為了避免上述==案例探討==中提到 heap-use-after-free 的問題,迴圈結束後另外宣告一個指標來接慢指標,再運用`list.h` 裡的 `list_del` 將 mid 的前後指標互相接上,再使用 `q_release_element` 釋放其記憶體空間。
```c
/*
* Delete the middle node in list.
* The middle node of a linked list of size n is the
* ⌊n / 2⌋th node from the start using 0-based indexing.
* If there're six element, the third member should be return.
* Return true if successful.
* Return false if list is NULL or empty.
*/
bool q_delete_mid(struct list_head *head)
{
if (!head || list_empty(head))
return false;
struct list_head **indir = &head->next;
for (struct list_head *fast = head->next;
fast != head && fast->next != head; fast = fast->next->next)
indir = &(*indir)->next;
struct list_head *del = *indir;
list_del(del);
element_t *del_element = list_entry (del, element_t, list);
q_release_element(del_element);
return true;
}
```
### q_delete_dup
跟 [LeetCode 82. Remove Duplicates from Sorted List II](https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/) 相同,要刪掉 ascending list 中重複的元素,運用`list.h` 裡的 `list_for_each_entry_safe` 迭代,除了每個 `element_t` 的地址之外,還可以先得之下一個 `element_t` 的地址,如此可以檢查當前及下個元素的字串是否相同。
另外利用變數 `check` 確認在上一個迴圈是否有重複的字串,若有則當前的元素也必須刪除,因為此當前元素即為上一個迴圈的下一個 `element_t`。
第一個 `if` 若未判斷下個 `element_t` 是否為 `head` 則會出現錯誤,參考 [laneser](https://hackmd.io/@laneser/linux2022_lab0),新增了此條件。
```c
/*
* Delete all nodes that have duplicate string,
* leaving only distinct strings from the original list.
* Return true if successful.
* Return false if list is NULL.
*
* Note: this function always be called after sorting, in other words,
* list is guaranteed to be sorted in ascending order.
* // https://leetcode.com/problems/remove-duplicates-from-sorted-list-ii/
*/
bool q_delete_dup(struct list_head *head)
{
if (!head)
return false;
element_t *element, *next_element;
bool check = 0;
list_for_each_entry_safe (element, next_element, head, list) {
if (&next_element->list != head &&
strcmp(next_element->value, element->value) == 0) {
list_del(&element->list);
q_release_element(element);
check = 1;
} else if (check) {
list_del(&element->list);
q_release_element(element);
check = 0;
}
}
return true;
}
```
### q_swap
思考一陣子想不出解法,於是參考 [cwl0429](https://hackmd.io/yJclgTuVRzeVCjHH9FtUVw#q_free):在迴圈裡透過`list.h` 裡的 `list_move` 將現在這個元素 `cur` 移動到下一個元素 `cur->next` 的後面,就可以達到兩兩交換的結果。
另外由於兩兩指標交換,上ㄧ次迭代時的`cur` 位置已經向後移一個,上一次迭代結束後又向後移一個,總共後移兩次,如此剛好能達成被交換過的 pair 不會再被交換。
```c
/*
* Attempt to swap every two adjacent nodes.
// https://leetcode.com/problems/swap-nodes-in-pairs/
*/
void q_swap(struct list_head *head)
{
if (!head)
return;
struct list_head *cur;
for (cur = head->next; cur != head && cur->next != head; cur = cur->next)
list_move(cur, cur->next);
}
```
### q_reverse
運用`list.h` 裡的 `list_for_each_safe` 迭代 queue 裡的 `strurct head_list` 的地址,將每個 `strurct head_list` 的 next 指標及 prev 指標互換,迭代完成後,再處理 queue 的 `head` 的 next 指標及 prev 指標。
```c
/*
* Reverse elements in queue
* No effect if q is NULL or empty
* This function should not allocate or free any list elements
* (e.g., by calling q_insert_head, q_insert_tail, or q_remove_head).
* It should rearrange the existing ones.
*/
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *node, *next;
list_for_each_safe (node, next, head) {
node->next = node->prev;
node->prev = next;
}
head->next = head->prev;
head->prev = next;
}
```
##### 二更
```c
void q_reverse(struct list_head *head)
{
if (!head || list_empty(head))
return;
struct list_head *node, *next;
list_for_each_safe (node, next, head) {
list_move (node, head);
}
}
```
### q_sort
依作業要求時間複雜度必須為 $O(nlogn)$,所以參考了你所不知道的 [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)其中的==Merge Sort 的實作==,將 sorting 分成三個 function:`merge_two_lists`, `merge_sort`, `q_sort`
* `merge_two_lists`
目的為將兩個串列依據順序合併,用兩個指標分別指著兩串列開頭,比較指標的內容,將較小的放到新的串列裡,該指標移至下一個位置。
運用指標的指標技巧 `struct list_head **indir`,指向新串列的下一個可用節點,就不需要配置額外的記憶體。而 `struct list_head **temp` 可以用來指向 L1, L2 的目前節點,程式碼更加簡化。
特別注意的是因為迭代的是 `struct list_head` 的地址,但要比較的值其實是鑲嵌在 `struct list_head` 的 `element_t` 的成員,因此需要運用`list.h` 裡的 `list_entry` 求出 `element_t` 的地址。
當迴圈迭代完成,最終會剩下一個節點尚未被加入新串列,採 bit-wsie `|` 找出非空的節點且能加速運算。
```c
struct list_head *merge_two_lists(struct list_head *L1, struct list_head *L2)
{
struct list_head *head = NULL;
struct list_head **indir = &head, **temp = NULL;
element_t *L1_ele, *L2_ele;
for (; L1 && L2; *temp = (*temp)->next) {
L1_ele = list_entry (L1, element_t, list);
L2_ele = list_entry (L2, element_t, list);
temp = strcmp(L1_ele->value, L2_ele->value) < 0 ? &L1 : &L2;
*indir = *temp;
indir = &(*indir)->next;
}
*indir = (struct list_head *) ((uintptr_t) L1 | (uintptr_t) L2);
return head;
}
```
* `merge_sort`
採遞迴方式,在傳入的一條串列中,利用快慢指標方式找出第一個與中間節點,再讓 `merge_two_lists` 將第一個節點開頭的串列及中間節點開頭的串列,合併成一個有序的串列。終止條件為當串列本身是空,或只有一個節點時。
需要注意的是原本的串列是 circular linked-list,而在 `merge_two_lists` 以及此函式裡在迭代兩個串列時的中止條件為走到尾巴,所以在找到第一個節點開頭及中間節點開頭的串列後,要把第一個節點的串列尾巴指向 `NULL` ,否則串列的尾巴會是串列開頭。這裡參考了 [cwl0429](https://hackmd.io/yJclgTuVRzeVCjHH9FtUVw#q_free)。
```c
struct list_head *merge_sort(struct list_head *head)
{
if (!head || !head->next)
return head;
struct list_head *slow = head, *fast;
for (fast = head->next; fast && fast->next; fast = fast->next->next)
slow = slow->next;
struct list_head *left, *right;
right = slow->next;
slow->next = NULL;
left = merge_sort(head);
right = merge_sort(right);
return merge_two_lists(left, right);
}
```
* 補充:當 n 為 2 的冪,此法遞迴呼叫次數為 2n-1 次(2 * 切幾刀 + 1)。
* `q_sort`
首先將串列的尾巴指向 `NULL` ,如同上個函式說明,為了符合`merge_two_lists` 以及 `merge_sort` 裡迴圈的終止條件。
由於 `head` 並未存 queue 的內容,所以由 `head->next` 為開頭的串列傳到 `merge_sort` 裡排序。
排序完成後因為前面的函式僅更新 `next` 的單向指標,必須再迭代一次排序後的串列,將 `prev` 指標更新,也需將 `head` 接到串列尾巴,讓串列成為 doubly circular linked-list,以維持 `struct list_head` 的型態。
##### 二更
補上第 20 行,以免漏接了一個指標。
```c=
/*
* Sort elements of queue in ascending order
* No effect if q is NULL or empty. In addition, if q has only one
* element, do nothing.
*/
void q_sort(struct list_head *head)
{
if (!head || list_empty(head) || list_is_singular(head)) {
return;
}
head->prev->next = NULL;
head->next = merge_sort(head->next);
struct list_head *tmp = NULL, *prev = head;
for (tmp = head->next; tmp->next != NULL; tmp = tmp->next) {
tmp->prev = prev;
prev = prev->next;
}
tmp->prev = prev;
tmp->next = head;
head->prev = tmp;
}
```