2025q1 Homework1 (lab0)
===
contributed by <`gg21-aping`>
{%hackmd NrmQUGbRQWemgwPfhzXj6g %}
### 開發環境
```
$ gcc --version
gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
Copyright (C) 2023 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
Address sizes: 39 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 4
On-line CPU(s) list: 0-3
Vendor ID: GenuineIntel
Model name: Intel(R) Core(TM) i5-4200U CPU @ 1.60GHz
CPU family: 6
Model: 69
Thread(s) per core: 2
Core(s) per socket: 2
Socket(s): 1
Stepping: 1
CPU(s) scaling MHz: 49%
CPU max MHz: 2600.0000
CPU min MHz: 800.0000
BogoMIPS: 4589.18
```
### Reviewed by `MikazkiHikari`
請確認是否有按照預期進行rebase;然後 queue.[ch]的部分寫的很詳盡,但其他的項目呢?
### 資料結構和實作方式
* `element_t`: the element to be linked in the queue, carrying a pointer to an array holding `string`.
* `queue_contex_t`: can be considered as the config of the queue structure, carrying the pointer to the head of the queue, the size, and the id.
### 佇列實作:修改 `queue.[ch]`
#### `queue_new()`
參考 Linux Kernel 實作佇列,使用 `INIT_LIST_HEAD()`, 將 `list_head` 的 `next` 和 `prev` 指標都指向結構本身。
#### `q_free()`
考量僅是將節點自佇列中移除,但不釋放其記憶體空間,可透過 `list_for_each_entry_safe()` 保存下個節點的指標,並逐一走訪佇列中各節點。
提交程式碼的時候遇到一些問題:
1. (resolved) 程式碼被判斷並沒有使用指定的 coding style。查 Linux 核心程式碼使用 `list_for_each_entry_safe()` 都沒有在函式名稱及 `(` 間插入一個空白。
```git
- list_for_each_entry_safe(e, next, head, list) {
+ list_for_each_entry_safe (e, next, head, list) {
q_release_element(e);
}
[!] queue.c does not follow the consistent coding style.
Make sure you indent as the following:
clang-format -i queue.c
```
感謝社團同學提出參考 Linux 核心程式碼對 `.clang-format` 的操作,新增 `SpaceBeforeParens` 並提出 [PR](https://github.com/sysprog21/lab0-c/pull/232)。
2. (resolved) `cppcheck` 觸發的 `unusedlabel` 警告。參考 [issue#211](https://github.com/sysprog21/lab0-c/pull/211#issuecomment-2690577631) 的討論,只能將 `cppcheck` 從 ubuntu 24.04 LTS 的[官方版本 2.13](https://launchpad.net/ubuntu/noble/+package/cppcheck) 再往上更新。[Commit e0c6b11](https://github.com/sysprog21/lab0-c/commit/e0c6b113c9c9404208dcf4dccf4d229016aa4778) 解決了 `unusedlabel` 警告,但依然會觸發 `uninitvar` 警告。因此提了 [issue#229](https://github.com/sysprog21/lab0-c/issues/229) 的討論。
```shell
Following files need to be cleaned up:
queue.c
Running static analysis...
queue.c:28:5: error: Uninitialized variable: next [uninitvar]
list_for_each_entry_safe (e, next, head, list) {
^
Fail to pass static analysis.
```
#### `q_insert_head/tail()`
使用 `strdup()` 複製字串 `s`,避免原始的 `s` 指向的記憶體區塊被釋放或更動後影響到 `e->value`。
> The `strdup()` function returns a pointer to a new string which is a
duplicate of the string s. Memory for the new string is obtained
with `malloc(3)`, and can be freed with `free(3)`.
`strdup()` 透過 `malloc(strlen(s) + 1)` 來動態分配記憶體空間,再透過 `strcopy()` 將原始字串的內容複製到新分配的記憶體。
##### todo:
* 在佇列中將節點插入頭或尾的操作基本相同,可以思考更有品味的程式碼。
#### `q_remove_head/tail()`
透過 `list_entry()` 系列操作可取得想要的節點資訊。注意 `strncpy(*dest, *src, n)` 在 destination 長度比 source 長度長的時候,會將剩餘空白的位置都自動補上終止符 `\0`,反之則 `dest` 指向的字串將不會有終止符。因此,為了避免呼叫者使用可能未正確終止的字串而導致的各種問題,做了一些防禦機制:
```c
strncpy(sp, e->value, bufsize - 1);
sp[bufsize - 1] = '\0';
```
> `unlink` 被認定是 american english,但 `unlinked` 或 `unlinks` 均不被 `aspell` 認定為 american english,`Functions` 也不被認定為 american english QQ
##### todo:
* 在佇列中移除頭或尾節點的操作基本相同,可以思考更有品味的程式碼。
#### `q_size()`
原本想透過 `container_of()` 直接使用 `queue_contx_t` 中的成員 `size`,但觀察 `qtest.c` 中對 `q_size()` 的呼叫,發現傳入的值有時是成員 `q`,有時是成員 `chain`。因此貿然的直接定位會無法取得正確的記憶體位置。因此仍是透過逐一走訪的方式計算。
#### `q_delete_mid()`
鏈結的中間節點可以透過兩次走訪鏈結取得,但透過 Hare and Tortoise Algorithm(快慢指標),我們可以只走訪鏈結一次,便快速定位到中間節點。僅需特別注意的是,此作業提供的結構為 circular doubly linked list,亦即指標不會指向 `NULL`,且單一節點便能取得其連結的前後節點。
注意 `list_del()` 並不會釋放記憶體空間,僅是將該節點從鏈結中移除。節點仍會存在,但其指標指向的位址不能被保證;若要再次將節點加入鏈結中,依然要先呼叫 `LIST_INIT_HEAD()`。
#### `q_delete_dup()`
在 `queue.h` 的註解中,並沒有提到所有呼叫此函式的鏈結都是有序的,但卻附上 leetcode 刪除有序鏈結中的重複節點。所以一開始會有點不太確定課程期待的寫法是什麼。若能保證是有序鏈結,那麼透過雙指標,就能有效率的完成刪除重複節點的行為。但若無法保證傳入的鏈結為有序,就需要透過例如雜湊表的方式,來達到最佳的效能。
參考數位同學的解法,推估該函式就是預設傳入的節點都為有序,因此先透過雙指標的方式完成此函式。逐一走訪佇列中各項元素,並透過 `strcmp` 做 `char` 的比較。
其中 `strcmp()` 規定傳入的字串必須是 null-ended strings(即字串必須是 `\0` 結尾),雖然在 `queue.h` 中並未明定,但若節點的 `value` 成員不存在,設定回傳 `false`。
```git
+ list_for_each_entry_safe(cur, next, head, list) {
+ if (&next->list == head)
+ break;
+
+ if (!cur->value || !next->value)
+ return false;
+
+ if (!strcmp(cur->value, next->value)) {
+ list_del(&cur->list);
+ q_release_element(cur);
+ }
+ }
```
後來發現我把題目理解成 `A -> B -> B -> C` 要變成 `A -> B -> C` 的意思,但其實應該要把 `B` 完全移除。所以又做了以下修正。透過變數 `should_delete` 紀錄最後一個重複節點,並進行刪除。
```git
element_t *curr = NULL, *next = NULL;
+ bool should_delete = false;
if (!head || list_empty(head))
return false;
list_for_each_entry_safe(curr, next, head, list) {
- if (&next->list == head)
- break;
+ bool duplicate =
+ (&next->list != head && !strcmp(curr->value, next->value));
- if (!curr->value || !next->value)
- return false;
-
- if (!strcmp(curr->value, next->value)) {
+ if (duplicate || should_delete) {
list_del(&curr->list);
q_release_element(curr);
+ should_delete = duplicate;
}
}
```
#### `q_swap()`
透過兩個指標,並使用 `list_move()` 可以快速的完成兩兩一組的交換操作。此時,`curr` 指向一組節點中的第一個節點,而 `next` 指向下一組節點中的第一個節點。
```c
curr = head->next;
while (curr != head && curr->next != head) {
next = curr->next->next;
list_move(curr->next, curr->prev);
curr = next;
}
```
更新:可透過呼叫 `q_reverseK(head, 2)` 來取代原本的 swap 操作。
#### `q_reverse()`
透過逐一走訪各節點並呼叫 `list_move()` 將各節點移至 `head` 節點後,即可完成整串鏈結的反轉。
#### `q_reverseK()`
透過三個指標 `start`、`end` 和 `iter` 來做鏈結的部分反轉。
```c
start = head;
while (size >= k) {
struct list_head *end = start->next, *iter = start->next;
for (i = 0; i < k; i++) {
list_move(iter, start);
iter = end->next;
}
start = end;
size -= k;
}
```
有特別注意變數宣告的位置,原本將 `end` 和 `iter` 一起宣告在整個函式區塊的最頂部 (beginning of the block);但考慮該二變數僅在迴圈中使用,應定義在最小作用域的程式碼區塊中的最頂部,因此做了修正。
#### `q_sort()`
考量到資料結構為鏈結串列,使用 merge sort 的方式對鏈結進行排序。概念如下:
* 透過快慢指針找到鏈結的中間點,透過 `list_cut_position()` 進行切分。
* 遵循 divide and conquer 的概念,透過遞迴手法不斷進行上述的切分行為。
* 呼叫 helper function `q_merge_two()` 進行排序。
* 參考 [yuto0226](https://hackmd.io/@yuto0226/linux2025-homework1) 同學的筆記,新增 `is_descend()` 函式來判斷排序順序。
#### `q_ascend/descend()`
題目要求將所有破壞升序、降序法則的節點刪除,即在升序的鏈結串列中,較小的元素不能在後面出現,降序則反。若從鏈結的起點開始往後走,判斷上會非常困難。但若從尾部開始往前走,則僅需紀錄一個 `extreme` 值,在升序排列中,遇到比 `extreme` 大的值一律刪除,在降序排列中則反。
因此我首先寫了 `list_for_each_entry_safe_reverse` 的 macro 來協助我完成安全的迴圈操作。
```C
/* Iterate through a list reversely */
#define list_for_each_entry_safe_reverse(entry, safe, head, member) \
for (entry = list_entry((head)->prev, typeof(*entry), member), \
safe = list_entry(entry->member.prev, typeof(*entry), member); \
&entry->member != (head); entry = safe, \
safe = list_entry(safe->member.prev, typeof(*entry), member))
```
並創建一個 `q_make_monotonic()` 的函式,透過對變數 `descend` 的控制,即可讓 `q_ascend()` 和 `q_descend()` 同時呼叫。
```C
/**
* q_make_monotonic() - Remove nodes to make queue monotonic
* @head: header of queue
* @descend: whether to filter for descending (true) or ascending (false)
*
* Return: the number of elements in queue after performing operation
*/
static int q_make_monotonic(struct list_head *head, bool descend)
{
element_t *curr, *next;
char *extreme = NULL;
if (!head || list_empty(head) || list_is_singular(head))
return q_size(head);
list_for_each_entry_safe_reverse(curr, next, head, list)
{
if (!extreme) {
extreme = curr->value;
continue;
}
if (is_descend(extreme, curr->value, descend)) {
list_del(&curr->list);
q_release_element(curr);
} else {
extreme = curr->value;
}
}
return q_size(head);
}
```
#### `q_merge()`
藉由之前完成 `q_sort()` 開發過的 `q_merge_two()`,用暴力解的方式,兩兩合併在一起。