# 2025q1 Homework1 ([lab0](https://github.com/willy-liu/lab0-c)) contributed by < `willy-liu` > {%hackmd NrmQUGbRQWemgwPfhzXj6g %} ## 開發環境 ```bash $ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 $ lscpu | grep -E "Architecture|CPU op-mode|Byte Order|Address sizes|CPU\(s\)|On-line CPU\(s\) list|Thread\(s\) per core|Core\(s\) per socket|Socket\(s\)|NUMA node\(s\)|Vendor ID|CPU family|Model:|Model name|Stepping|CPU MHz|CPU max MHz|CPU min MHz|BogoMIPS|Virtualization|L1d cache|L1i cache|L2 cache|L3 cache|NUMA node0 CPU\(s\)" 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): 12 On-line CPU(s) list: 0-11 Vendor ID: GenuineIntel Model name: Intel(R) Core(TM) i7-8700 CPU @ 3.20GHz CPU family: 6 Model: 158 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 Stepping: 10 CPU(s) scaling MHz: 25% CPU max MHz: 4600.0000 CPU min MHz: 800.0000 BogoMIPS: 6399.96 Virtualization: VT-x L1d cache: 192 KiB (6 instances) L1i cache: 192 KiB (6 instances) L2 cache: 1.5 MiB (6 instances) L3 cache: 12 MiB (1 instance) NUMA node(s): 1 NUMA node0 CPU(s): 0-11 ``` ## 針對佇列操作的程式碼實作 :::spoiler 觀念釐清 ### 結構、函式解釋 `element_t` - 雙向鏈結串列中的元素 - element_t 是一個雙向鏈結串列(doubly-linked list)的節點(element)。 - value 是指向字串的指標,需手動分配(malloc)並手動釋放(free)。 - list 是 struct list_head 類型,允許將 element_t 插入 Linux Kernel 風格的鏈結串列。 `queue_contex_t` - 佇列管理結構 - q:指向佇列的頭部,類型為 struct list_head*,用於管理佇列的起點。 - chain:允許多個佇列鏈結在一起,形成佇列的集合(類似多佇列管理)。 - size:紀錄佇列長度,方便計算操作的複雜度。 - id:唯一識別碼,讓每個佇列可被區分。 :warning: 如果q->next == q->prev == q,代表該佇列是空的,否則應該要指向某個element_t的list ::: ### q_new Commit [9ab5ddf](https://github.com/willy-liu/lab0-c/commit/9ab5ddfc4a036179d7d88c0db3889b8f77a75d84) 建立新的「空」佇列 ```diff struct list_head *head = malloc(sizeof(struct list_head)); - if (!head) - return NULL; - head->next = head; - head->prev = head; + if (head) { + INIT_LIST_HEAD(head); + } return head; ``` 一開始沒有特別留意 `list.h` 裡有哪些巨集可用,後來看過之後發現 `INIT_LIST_HEAD` 可以讓 `q_new` 的實作更簡潔、也更容易閱讀。 ### q_free Commit [360896c](https://github.com/willy-liu/lab0-c/commit/360896c88ed062f1f266e4ae85312aba34994641) 釋放佇列所佔用的記憶體 ```diff - struct list_head *current = head->next; - while (current != head) { - element_t *element = list_entry(current, element_t, list); - current = current->next; - if (element->value) { - free(element->value); - } - free(element); + struct list_head *entry, *safe; + list_for_each_safe (entry, safe, head) { + q_release_element(list_entry(entry, element_t, list)); } free(head); ``` 使用`list_for_each_safe`來簡化原本的while loop,另外也把原本的if condition改成用`q_release_element`。 :warning: 原本要使用`list_for_each_entry_safe`,`$ make test`可以通過,但下了commit就會過不了static analysis,錯誤訊息如下 ``` Running static analysis... queue.c:29:5: style: Label 'int' is not used. [unusedLabel] list_for_each_entry_safe (entry, safe, head, list) ^ Fail to pass static analysis. ``` 然而`q_free`裡根本沒有用到`int`,我進到`list_for_each_entry_safe`的巨集裡查看,如下 ```c #if __LIST_HAVE_TYPEOF #define list_for_each_entry_safe(entry, safe, head, member) \ for (entry = list_entry((head)->next, typeof(*entry), member), \ safe = list_entry(entry->member.next, typeof(*entry), member); \ &entry->member != (head); entry = safe, \ safe = list_entry(safe->member.next, typeof(*entry), member)) #else #define list_for_each_entry_safe(entry, safe, head, member) \ for (entry = safe = (void *) 1; sizeof(struct { int : -1; }); \ ++(entry), ++(safe)) #endif ``` 注意到#else的部分,迴圈條件`sizeof(struct { int : -1; });`是一個錯誤的語法,會編譯錯誤,我認為這個#else本來就是要用來提醒programmer無法該環境使用此巨集,雖然該部分照理來說只要`__LIST_HAVE_TYPEOF`為1不會被執行到,但那個int會被static alaysis檢測出來,無法通過。 再往前看`__LIST_HAVE_TYPEOF`定義為 ```c #if defined(__GNUC__) || defined(__clang__) || \ (defined(__STDC__) && defined(__STDC_VERSION__) && \ (__STDC_VERSION__ >= 202311L)) /* C23 ?*/ #define __LIST_HAVE_TYPEOF 1 #else #define __LIST_HAVE_TYPEOF 0 #endif ``` 只要編譯器是GNU、Clang且標準C版本大於C23就應該不會報錯。 我又到了`scripts/pre-commit.hook`查看,發現使用了`cppcheck`做static analysis,而`__GNUC__`、`__clang__`等是編譯器提供的巨集,所以在static analysis時他們當然就不存在,也就導致`__LIST_HAVE_TYPEOF`就會被define成0導致檢查出錯。 ✅解決方法 要在cppcheck用-d選項把編譯器參數傳進去,如下 (最後根據老師的pr review,使用bash function包起來再assign給CPPCHECK_OPTS,並且改使用`cc`來取得C Compiler,再進行STDC_VERSION等檢查,而不是直接用gcc/clang等指令) ```diff +# Generation of standard compliance for GCC/Clang +detect_cc_std() { + local STDC_VERSION="" + local EXTRA_DEFINES="" + if command -v cc >/dev/null 2>&1; then + if cc --version 2>/dev/null | grep -q "clang"; +then + STDC_VERSION=$(cc -dM -E -xc /dev/null | awk +'/__STDC_VERSION__/ {print $3}') + EXTRA_DEFINES="-D__clang__=1" + elif cc --version 2>/dev/null | grep -q "Free +Software Foundation"; then + STDC_VERSION=$(cc -dM -E -xc /dev/null | awk +'/__STDC_VERSION__/ {print $3}') + EXTRA_DEFINES="-D__GNUC__=1" + fi + fi + if [ -n "$STDC_VERSION" ]; then + EXTRA_DEFINES+=" -D__STDC__=1 -+D__STDC_VERSION__=${STDC_VERSION}" + fi + echo "$EXTRA_DEFINES" +} + CPPCHECK_OPTS="-I. --enable=all --error-exitcode=1" +CPPCHECK_OPTS+=" $detect_cc_std" CPPCHECK_OPTS+=" --force $(cppcheck_suppressions) $(cppcheck_build_unmatched)" CPPCHECK_OPTS+=" --cppcheck-build-dir=.out ." ``` ### q_size Commit [03f6e38](https://github.com/willy-liu/lab0-c/commit/03f6e38d534eb8a4306bd0e8e5475a7eba9df599) 計算目前佇列的節點總量 ```diff int count = 0; - struct list_head *current = head->next; - while (current != head) { + struct list_head *node; + + list_for_each (node, head) count++; - current = current->next; - } ``` 使用了list.h的巨集`list_for_each`進行重構,提升整體可讀性。 ### q_insert_head Commit [51562f7](https://github.com/willy-liu/lab0-c/commit/51562f75840ace85d40bca6222b47a5ed5f501cf) 在佇列開頭 (`head`) 插入新節點(LIFO 準則) ```c if (!head || !s) return false; element_t *new_element = malloc(sizeof(element_t)); if (!new_element) return false; new_element->value = strdup(s); if (!new_element->value) { free(new_element); return false; } ``` ```diff - new_element->list.next = head->next; - new_element->list.prev = head; - head->next->prev = &new_element->list; - head->next = &new_element->list; + list_add(&new_element->list, head); ``` 重構時發現list.h裡本來就有一個function跟原本插入節點的程式碼做一樣的事,因此把原本的替換掉,提高可讀性。 ### q_insert_tail Commit [51562f7](https://github.com/willy-liu/lab0-c/commit/51562f75840ace85d40bca6222b47a5ed5f501cf) 在佇列尾端 (`tail`) 插入新節點(FIFO 準則) ```diff bool q_insert_tail(struct list_head *head, char *s) { - if (!head || !s) - return false; - - element_t *new_element = malloc(sizeof(element_t)); - if (!new_element) - return false; - - new_element->value = strdup(s); - if (!new_element->value) { - free(new_element); - return false; - } - - new_element->list.next = head; - new_element->list.prev = head->prev; - head->prev->next = &new_element->list; - head->prev = &new_element->list; - - return true; + return q_insert_head(head->prev, s); } ``` 重構後想到,既然是雙向"環狀"的佇列,且佇列空的時候會有一個dummy head,prev和next都連到自己,那insert_tail不就只要把head移動到prev後再insert_head就好嗎,可讀性變佳的同時也少了大量重複程式碼。 ### q_remove_head Commit [1bf85d5](https://github.com/willy-liu/lab0-c/commit/1bf85d5a8ccf53223c9c6ba01dd41e79edcb2279) 在佇列開頭 (`head`) 移除節點 ```diff if (!head || head->next == head) return NULL; - struct list_head *node = head->next; - element_t *element = list_entry(node, element_t, list); + element_t *element = list_entry(head->next, element_t, list); - head->next = node->next; - node->next->prev = head; + list_del(&element->list); ``` 使用list_del將原本指標移動的部分包裝起來,另外`*node`原本用來獲得element以及方便做指標的移動,用了`list_del`後就可以把它inline進`list_entry`。 ### q_remove_tail Commit [1bf85d5](https://github.com/willy-liu/lab0-c/commit/1bf85d5a8ccf53223c9c6ba01dd41e79edcb2279) 在佇列尾端 (`tail`) 移除節點 ```C return q_remove_head(head->prev->prev, sp, bufsize); ``` 和q_insert_tail一樣的想法,因為是雙向"環狀"的佇列,所以可以重用q_remove_head,但要注意的是有dummy head的關係,在remove head實際上是remove head->enxt,所以要傳入的應該是head->prev->prev,不然會刪錯。 ### q_delete_mid Commit [c45d606](https://github.com/willy-liu/lab0-c/commit/c45d606a09b59ed123a3d00377d2e99e679d9f32) ```diff if (!head || head->next == head) return false; struct list_head *slow = head->next; struct list_head *fast = head->next; while (fast != head && fast->next != head) { slow = slow->next; fast = fast->next->next; } slow->prev->next = slow->next; slow->next->prev = slow->prev; - element_t *element = list_entry(slow, element_t, list); - free(element->value); - free(element); + q_release_element(list_entry(slow, element_t, list)); return true; ``` 參考此[連結](https://leetcode.com/problems/delete-the-middle-node-of-a-linked-list/solutions/1612140/one-pass-slow-and-fast)的解法,使用快慢指標刪除中心節點,在重構時改用`q_release_element`移除element ### q_delete_dup Commit [aa37570](https://github.com/willy-liu/lab0-c/commit/aa37570707bda75675d22fceeacafc03e60f90d7) ```diff if (!head || head->next == head) return false; - // q_sort(head, false); // 先排序 - - struct list_head *current = head->next; - bool has_duplicates = false; - - while (current != head && current->next != head) { - element_t *element = list_entry(current, element_t, list); - element_t *next_element = list_entry(current->next, element_t, list); - - if (strcmp(element->value, next_element->value) == 0) { - has_duplicates = true; - struct list_head *dup = current->next; - current->next = dup->next; - dup->next->prev = current; - free(next_element->value); - free(next_element); - } else { - if (has_duplicates) { - struct list_head *dup = current; - current = current->prev; - current->next = dup->next; - dup->next->prev = current; - free(element->value); - free(element); - has_duplicates = false; - } - current = current->next; - - if (has_duplicates) { - struct list_head *dup = current; - current = current->prev; - current->next = dup->next; - dup->next->prev = current; - element_t *element = list_entry(dup, element_t, list); - free(element->value); - free(element); - } - + struct list_head *node, *safe; + bool last_duplicate = false; + + list_for_each_safe (node, safe, head) { + + element_t *element = list_entry(node, element_t, list); + const element_t *element_safe = list_entry(safe, element_t, list); + + if (safe != head && strcmp(element->value, element_safe->value) == 0) { + last_duplicate = true; + list_del(node); + q_release_element(element); + } else if (last_duplicate) { + last_duplicate = false; + list_del(node); + q_release_element(element); } } + return true; } ``` 使用`list_for_each_safe`大幅改進了可讀性,要注意的是他只要有刪除,就要全刪,所以要用一個額外的變數`last_duplicate`來刪除最後一個重複的節點,此外,在比較時也要注意safe有可能會跑到dummy head,會造成進行strcmp時發生segmentation fault。 ### q_swap Commit [41ea59b](https://github.com/willy-liu/lab0-c/commit/41ea59b591226c77b142c3cffac63292ebf0ba78) ```diff if (!head || head->next == head) return; struct list_head *current = head->next; while (current != head && current->next != head) { - struct list_head *next = current->next; - current->prev->next = next; - next->prev = current->prev; - current->next = next->next; - next->next->prev = current; - next->next = current; - current->prev = next; // Swap current and next + list_move(current, current->next); // Move to the next pair current = current->next; } ``` 看了list.h後,把原本移動指標的部分包裝近list_move裡,增加可讀性。 ### q_reverse **反向排列**佇列,**但不分配或釋放節點** ```c void q_reverse(struct list_head *head) { if (!head || head->next == head) return; struct list_head *current = head; do { struct list_head *temp = current->next; current->next = current->prev; current->prev = temp; current = temp; } while (current != head); } ``` 有想過要使用`list_move`修改,但是單純交換他們的prev跟next用到list_move好像就太多餘了。 只要三次的assignment不需要用六次。 ### q_reverseK Commit [224251b](https://github.com/willy-liu/lab0-c/commit/224251b4164df3ee41c1c0a84d1dc5b23eab68e5) ```diff + struct list_head *node, *safe; + int count = 0; + list_for_each_safe(node, safe, head) { + count++; + if (count % k == 0) { + while (--count) { + list_move_tail(node->prev, safe); + } + } ``` 原本寫了一個helper function,用來反轉區間內的節點順序,額外用了超過40行且很難看懂,重新畫了圖之後發現可以有更簡單的方式實作,如下: ![image](https://hackmd.io/_uploads/ryclpc73Jx.png) 若k=3,node當前為4,`safe`為5,要把順序變為1->4->3->2->5的話, 這個方法中`node->prev`始終要移動到safe前面,而執行的次數為k-1次,所以程式碼使用了一個巧妙的寫法,將判斷條件設為`--count`就可以執行k-1次,且迴圈結束`count`也會回到0。 ### q_sort Commit [527e966](https://github.com/willy-liu/lab0-c/commit/527e9662c01eb23760cf7a4cc3304825afe79166) 以根據`descend`來決定以升序/降序排序佇列節點 🔗 參考 **[Linked List Sort](https://www.geeksforgeeks.org/merge-sort-for-linked-list/)** 原本使用selection sort,在研讀完merge sort後改以它實作。 用的是遞迴版的merge sort,有額外寫了兩個helper function `merge` 和 `merge_sort`,前者負責將已排序的左右半合併,後者 實作流程: 1. 先斷開環狀結構,把`head->prev->next`設為`NULL` 2. 把`head->next`以及`descend`傳入`merge_sort` 3. 使用快慢指標將list遞迴切成左右兩半,最後回傳排序並合併後的list。 4. 回到`q_sort`,由於排序過程中,僅確保`next`,指標是正確的,所以還需重連`prev`指標 ```C struct list_head *cur = head; while (cur->next) { // reconnect prev pointer cur->next->prev = cur; cur = cur->next; } ``` 5. 最後,再將環狀結構恢復。 ```C cur->next = head; head->prev = cur; ``` ### q_descend/q_ascend ```c /* Remove every node which has a node with a strictly less value anywhere to * the right side of it */ int q_ascend(struct list_head *head) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (!head || head->next == head) return 0; // From last element to first element struct list_head *cur = head->prev; const element_t *cur_elem = list_entry(cur, element_t, list); const char *min_val = cur_elem->value; // keep the last element cur = cur->prev; while (cur != head) { // Avoid the last element is removed struct list_head *temp = cur->prev; element_t *entry = list_entry(cur, element_t, list); if (strcmp(entry->value, min_val) > 0) { cur->prev->next = cur->next; cur->next->prev = cur->prev; free(entry->value); free(entry); } else { min_val = entry->value; } cur = temp; } return q_size(head); } /* Remove every node which has a node with a strictly greater value anywhere to * the right side of it */ int q_descend(struct list_head *head) { // https://leetcode.com/problems/remove-nodes-from-linked-list/ if (!head || head->next == head) return 0; // From last element to first element struct list_head *cur = head->prev; const element_t *cur_elem = list_entry(cur, element_t, list); const char *max_val = cur_elem->value; // keep the last element cur = cur->prev; while (cur != head) { // Avoid the last element is removed struct list_head *temp = cur->prev; element_t *entry = list_entry(cur, element_t, list); if (strcmp(entry->value, max_val) < 0) { cur->prev->next = cur->next; cur->next->prev = cur->prev; free(entry->value); free(entry); } else { max_val = entry->value; } cur = temp; } return q_size(head); } ``` ### q_merge Commit [cc0c21b](https://github.com/willy-liu/lab0-c/commit/cc0c21bed88b533d2b4d3113c27a60827b1ef72b) 第一版寫了一個helper function `q_merge_two` ,用於合併兩個已排序的list,但覺得兩兩合併太花時間了,特別是沒做優化時第一個list會越來越長,效率不佳。 第二版重構的方法:每次都從k個list中找到要的那個節點,然後將他移動到當前merged的最後。 ```C while (1) { queue_contex_t *entry, *best_queue = NULL; struct list_head *best_node = NULL; list_for_each_entry(entry, head, chain) { if (list_empty(entry->q)) continue; if (!best_node) { best_node = entry->q->next; best_queue = entry; } else if ((strcmp( list_entry(entry->q->next, element_t, list)->value, list_entry(best_node, element_t, list)->value) < 0) ^ descend) { best_node = entry->q->next; best_queue = entry; } } if (!best_node) break; if (list_is_singular(head)) { list_splice_tail_init(best_queue->q, &merged); best_queue->size = 0; break; } list_move_tail(best_node, &merged); best_queue->size--; } ``` ## Valgrind +自動測試程式 ## 研讀 Linux 核心原始程式碼的 lib/list_sort.c 並嘗試引入到 lab0-c 專案 ## 在 qtest 提供新的命令 shuffle ## 在 qtest 中執行 option entropy 1 並搭配 ih RAND 10 一類的命令,觀察亂數字串的分布,並運用「資訊與熵互補,資訊就是負熵」的觀念,設計一組新的命令或選項,得以在 qtest 切換不同的 PRNG 的實作 (可選定 Xorshift),搭配統計分析工具,比較亂數產生器 (如 Xorshift vs. 內建) 的品質 ## 研讀論文〈Dude, is my code constant time?〉,解釋本程式的 "simulation" 模式是如何透過以實驗而非理論分析,達到驗證時間複雜度,需要解釋 Student's t-distribution 及程式實作的原理。 ## Hw3 :::spoiler character device解釋 ### **Character Device(字元裝置)** 在 Linux 和 Unix 系統中,**Character Device(字元裝置)** 是一種類型的裝置檔案,用來表示能夠**以字元(byte stream)為單位進行 I/O 操作的硬體裝置**。 --- ### **字元裝置的特性** 1. **不使用緩衝區(Buffering)** - **即時讀寫(Streaming)**:資料是以字元(byte-by-byte)方式傳輸,而不是以固定大小的區塊(block)傳輸。 - **不支援隨機存取(Random Access)**:通常無法像區塊裝置(Block Device)一樣支援 `seek()` 來跳轉到檔案中的某個特定位置。 2. **範例裝置** - **終端機(TTY, `/dev/tty`)**:負責處理鍵盤輸入和螢幕輸出。 - **序列埠(Serial Port, `/dev/ttyS0`)**:像是 RS-232 串列埠,通常用於連接嵌入式設備或工業裝置。 - **滑鼠(Mouse, `/dev/input/mice`)**:用來處理滑鼠輸入數據。 - **聲音裝置(Sound, `/dev/snd/`)**:音效卡的輸入輸出介面,如 `/dev/dsp`(舊式)、`/dev/snd/`(ALSA 音效架構)。 - **隨機數產生器(Random, `/dev/random`, `/dev/urandom`)**:提供隨機數給應用程式使用。 --- ### **如何識別字元裝置** 在 Linux 中,字元裝置會在 `/dev/` 目錄下,以特殊檔案的形式存在。使用 `ls -l` 命令,可以看到字元裝置的類型: ```bash ls -l /dev/tty ``` 輸出範例: ```bash crw-rw-rw- 1 root tty 5, 0 Mar 10 12:00 /dev/tty ``` - `c`(開頭的 `c` 代表 **Character Device**) - `5, 0`(**主設備號(Major Number)** 和 **次設備號(Minor Number)**) - 主設備號:標識設備的類別 - 次設備號:標識同一類設備中的不同設備 如果想查看所有字元裝置,可以執行: ```bash ls -l /dev | grep "^c" ``` --- ### **字元裝置 vs. 區塊裝置** | 類型 | 讀寫方式 | 典型用途 | 例子 | | --- | --- | --- | --- | | **字元裝置(Character Device)** | 以**字節**為單位讀寫(流式處理),不經過緩衝 | 需要即時回應的設備 | `/dev/tty`, `/dev/random`, `/dev/snd/` | | **區塊裝置(Block Device)** | 以**區塊**(block)為單位讀寫,有緩衝機制 | 儲存設備(支援隨機存取) | `/dev/sda`, `/dev/mmcblk0` | --- ### **如何存取字元裝置** ### 1. 使用 `cat` 或 `echo` 讀寫字元裝置 ```bash cat /dev/random # 讀取隨機數 echo "Hello" > /dev/tty # 在當前終端顯示 "Hello" ``` ### 2. 使用 `dd` 讀取字元裝置 ```bash dd if=/dev/random bs=1 count=10 # 從 /dev/random 讀取 10 個字節 ``` ### 3. 使用 `stty` 設定字元裝置(例如調整終端行為) ```bash stty -F /dev/ttyS0 speed 9600 # 設定序列埠速度為 9600 baud ``` --- ### **結論** 字元裝置(Character Device)適用於**即時讀寫、流式輸入輸出、不支援隨機存取**的硬體,如終端機、滑鼠、聲音裝置等。它與區塊裝置(Block Device)不同,主要是用來處理小型、即時的數據流,而不是大規模的儲存讀寫。 ::: :::spoiler callback解釋 ### **Callback(回呼函式)是什麼?** **Callback(回呼函式)** 是一種**將函式作為參數傳遞給另一個函式**,並在特定時間點或條件滿足時執行該函式的機制。 這種技術常見於: - **異步編程(Asynchronous Programming)** - **事件驅動(Event-Driven)程式設計** - **系統回調(如內核驅動程式、信號處理等)** --- ## **1. Callback 的基本概念** 在 C 語言或其他語言中,**回呼函式的核心概念是函式指標(Function Pointer)**,允許我們將函式的地址傳遞給其他函式,稍後再執行。 ### **範例 1:基本 Callback(C語言)** ```c c 複製編輯 #include <stdio.h>// 定義一個函式作為回呼函式 void my_callback_function() { printf("這是回呼函式被執行!\n"); } // 接收函式指標作為參數 void execute_callback(void (*callback)()) { printf("開始執行 callback...\n"); callback(); // 呼叫回呼函式 printf("結束 callback\n"); } int main() { execute_callback(my_callback_function); return 0; } ``` ### **輸出結果** ``` 複製編輯 開始執行 callback... 這是回呼函式被執行! 結束 callback ``` 在這個例子中: - `my_callback_function()` 是一個普通函式。 - `execute_callback()` 接收一個函式指標,並在適當的時機執行該函式。 --- ## **2. 為什麼要使用 Callback?** **(1)提高靈活性** - 允許我們將**不同的函式**傳遞給相同的函式來執行,增強可重用性。 **(2)適用於異步處理(Asynchronous Processing)** - 適用於事件驅動架構,如 GUI 程式設計、非同步 I/O 操作。 **(3)適用於系統開發(Kernel、驅動程式)** - 內核函式可能會在某些條件滿足時呼叫驅動程式的回呼函式。 --- ## **3. 進階 Callback:傳遞參數** ```c c 複製編輯 #include <stdio.h>// 回呼函式定義,接受一個 int 參數 void my_callback_function(int num) { printf("回呼函式執行,數值為: %d\n", num); } // 主要函式,接受函式指標和參數 void execute_callback(void (*callback)(int), int value) { printf("開始執行 callback...\n"); callback(value); // 呼叫回呼函式,傳遞 value printf("結束 callback\n"); } int main() { execute_callback(my_callback_function, 42); return 0; } ``` ### **輸出結果** ``` makefile 複製編輯 開始執行 callback... 回呼函式執行,數值為: 42 結束 callback ``` 這裡 `execute_callback()` 可以接收不同的回呼函式,並將 `value` 作為參數傳遞。 --- ## **4. Callback 在實際應用中的例子** ### **(1)C 語言的 `qsort()` 函式** 標準函式 `qsort()` 使用回呼函式來決定排序方式: ```c c 複製編輯 #include <stdio.h>#include <stdlib.h>// 比較函式(回呼函式) int compare(const void *a, const void *b) { return (*(int*)a - *(int*)b); } int main() { int arr[] = {5, 2, 9, 1, 6}; int size = sizeof(arr) / sizeof(arr[0]); // 使用 qsort 排序,並傳遞 compare() 作為回呼函式 qsort(arr, size, sizeof(int), compare); // 印出排序後的數組 for (int i = 0; i < size; i++) { printf("%d ", arr[i]); } return 0; } ``` 這裡 `compare()` 作為回呼函式,提供 `qsort()` 排序時的比較規則。 --- ### **(2)Linux 信號處理(Signal Handling)** ```c c 複製編輯 #include <stdio.h>#include <signal.h>// 回呼函式,處理 SIGINT(Ctrl+C) void signal_handler(int signum) { printf("收到訊號 %d,程式終止。\n", signum); exit(0); } int main() { signal(SIGINT, signal_handler); // 設定回呼函式 while (1) {} // 無窮迴圈,等待訊號 return 0; } ``` 這裡 `signal_handler()` 是當 `SIGINT`(Ctrl+C)被觸發時,系統會執行的回呼函式。 --- ### **5. 結論** - **回呼函式(Callback)** 是一種**將函式指標作為參數**傳遞並稍後執行的技術。 - 常用於**異步處理、事件驅動系統、排序函式、信號處理、內核驅動**等領域。 - 透過**函式指標**,可以讓程式更加模組化、靈活且可擴展。 這種技巧在 **C、C++、Python、JavaScript** 等語言中都廣泛使用! ::: :::spoiler vtable(virtual table)->polymorphism vs virtual function ### **C++ 的 vtable(Virtual Table)、多型(Polymorphism)與虛擬函式(Virtual Function)** 在 C++ 中,**虛擬函式(Virtual Function)** 和 **多型(Polymorphism)** 是物件導向程式設計(OOP)的核心概念,而 **vtable(虛擬函式表)** 則是其底層的實作機制之一。 --- ## **1. 虛擬函式(Virtual Function)** 在 C++ 中,如果一個基類的函式被標記為 `virtual`,則在繼承該類的派生類別中,可以覆寫這個函式,而**函式調用會根據物件的實際類型(runtime type)決定**,而非變數的靜態類型(compile-time type)。 ### **範例:虛擬函式** ```cpp cpp 複製編輯 #include <iostream>class Base { public: virtual void show() { // 虛擬函式 std::cout << "Base 類別的 show()\n"; } }; class Derived : public Base { public: void show() override { // 覆寫基類函式 std::cout << "Derived 類別的 show()\n"; } }; int main() { Base* ptr = new Derived(); ptr->show(); // 這裡會執行 Derived::show() delete ptr; return 0; } ``` ### **輸出結果** ``` scss 複製編輯 Derived 類別的 show() ``` 這裡 `ptr->show();` 會根據 `ptr` 指向的實際物件類型 `Derived` 來決定呼叫 `Derived::show()`,這就是**多型(Polymorphism)**。 --- ## **2. 多型(Polymorphism)** 多型是一種**在相同介面下能夠表現出不同行為**的機制,C++ 支援**編譯時多型(Compile-time Polymorphism)** 和 **執行時多型(Runtime Polymorphism)**。 ### **(1)編譯時多型(Compile-time Polymorphism)** 這種多型發生在**編譯階段**,像是**函式重載(Function Overloading)** 和 **模板(Templates)**: ```cpp cpp 複製編輯 #include <iostream>// 函式重載 void show(int x) { std::cout << "整數: " << x << std::endl; } void show(double x) { std::cout << "浮點數: " << x << std::endl; } int main() { show(10); // 呼叫 show(int) show(3.14); // 呼叫 show(double) return 0; } ``` ### **(2)執行時多型(Runtime Polymorphism)** 這種多型發生在**程式執行階段(Runtime)**,是透過**虛擬函式**來實現的: ```cpp cpp 複製編輯 #include <iostream>class Animal { public: virtual void speak() { std::cout << "動物發出聲音\n"; } }; class Dog : public Animal { public: void speak() override { std::cout << "狗叫:汪汪!\n"; } }; int main() { Animal* pet = new Dog(); pet->speak(); // 執行 Dog::speak(),因為是虛擬函式 delete pet; return 0; } ``` 這裡 `speak()` 的行為會根據物件的**實際類型**決定,而非變數類型,這就是執行時多型的特點。 --- ## **3. vtable(虛擬函式表)** **vtable(Virtual Table,虛擬函式表)** 是 C++ 虛擬函式的底層實現方式,當一個類別內含**至少一個虛擬函式**時,編譯器會為這個類別生成一個**vtable**,它是一個函式指標的陣列,儲存該類別所有虛擬函式的地址。 ### **vtable 的工作方式** 1. **每個有虛擬函式的類別都有一個 vtable**(存放虛擬函式指標的陣列)。 2. **每個物件都有一個 vptr(虛擬函式表指標,Virtual Table Pointer)**,指向該類別的 vtable。 3. **當呼叫虛擬函式時,會透過 vptr 查找對應的函式地址,並執行正確的函式。** --- ### **vtable 的運作示意圖** 假設有以下類別: ```cpp cpp 複製編輯 class Base { public: virtual void func1() { std::cout << "Base::func1\n"; } virtual void func2() { std::cout << "Base::func2\n"; } }; class Derived : public Base { public: void func1() override { std::cout << "Derived::func1\n"; } }; ``` 這時候,編譯器會產生以下 `vtable`: ### **Base 類別的 vtable** ``` lua 複製編輯 +--------+ | func1 | --> Base::func1 | func2 | --> Base::func2 +--------+ ``` ### **Derived 類別的 vtable** ``` lua 複製編輯 +--------+ | func1 | --> Derived::func1 (覆寫) | func2 | --> Base::func2 (繼承) +--------+ ``` 當程式執行 `Base* obj = new Derived();` 時: - `obj->func1();` 會透過 vtable 找到 `Derived::func1()` - `obj->func2();` 會透過 vtable 找到 `Base::func2()` --- ## **4. vtable 與虛擬函式的代價** 使用虛擬函式雖然提供了多型,但也帶來了一些額外的開銷: - **函式呼叫透過 vtable,會有間接尋址的開銷**(比一般函式呼叫稍慢)。 - **每個有虛擬函式的類別會有額外的 vtable 開銷**。 - **每個物件都會有一個額外的 vptr 指標**(通常是 8 Bytes,在 64 位元系統上)。 在效能關鍵的應用(如遊戲、嵌入式系統)中,過度使用虛擬函式可能會影響執行速度,因此在設計類別時需權衡取捨。 --- ## **5. 總結** | 概念 | 作用 | | --- | --- | | **虛擬函式(Virtual Function)** | 允許子類別覆寫函式,並透過基類指標/參考在執行時動態決定要執行哪個函式。 | | **多型(Polymorphism)** | 讓不同類別的物件可以透過相同介面(基類)執行不同的行為。 | | **vtable(虛擬函式表)** | 編譯器用來實作虛擬函式的機制,是一個函式指標陣列,物件透過 vptr 指向該類別的 vtable 來動態決定函式呼叫。 | 在 C++ 中,當我們使用 `virtual` 關鍵字時,實際上就是讓編譯器為這個類別建立 `vtable`,並透過 `vptr` 來動態解析函式調用,以達到執行時的多型(Runtime Polymorphism)。 ::: :::spoiler cache, IPl (inter-processor interrupt), page fault, interrupts, rescheduling ## **1. Cache(快取記憶體)** **快取記憶體(Cache)** 是 CPU 內部或與 CPU 緊密相連的高速記憶體,用來**加速 CPU 存取資料**,減少訪問主記憶體(RAM)的頻率,提升系統效能。 ### **Cache 層級** 現代 CPU 的快取一般分為多個層級: - **L1 Cache**(Level 1):每個核心專屬,速度最快,容量最小(幾十 KB)。 - **L2 Cache**(Level 2):每個核心專屬或多個核心共享,容量較大(數百 KB)。 - **L3 Cache**(Level 3):多個核心共享,速度較慢,容量更大(幾 MB)。 ### **Cache 作用** 當 CPU 需要存取某個資料時: 1. **查詢 L1 Cache**,若命中(Cache Hit),則直接讀取。 2. 若未命中,則查詢 L2 Cache,若命中則讀取。 3. 若仍未命中,則查詢 L3 Cache,若還未命中,則從主記憶體(RAM)讀取(Cache Miss)。 4. 讀取到的資料會存入 Cache,以便下次存取時加速。 **缺點**: - 若資料未命中 Cache(Cache Miss),則需要存取較慢的 RAM,甚至更慢的磁碟(若涉及虛擬記憶體)。 --- ## **2. IPI(Inter-Processor Interrupt,處理器間中斷)** **IPI(處理器間中斷)** 是一種特殊的 **CPU 中斷**,允許多核心處理器(SMP 系統)之間相互通知或同步。 ### **IPI 的用途** 1. **喚醒處理器**:讓閒置的 CPU 核心開始工作。 2. **重新分配工作(Rescheduling)**:讓某些執行緒轉移到其他 CPU 核心執行。 3. **快取同步(Cache Coherency)**:確保多核心 CPU 間的 Cache 資料一致性。 4. **TLB(Translation Lookaside Buffer)同步**:當某核心更新分頁表(Page Table),需要通知其他核心刷新 TLB 快取。 ### **IPI 的運作** - 當一個 CPU 核心發送 IPI 給另一個核心時,目標核心的中斷控制器(APIC)會接收 IPI,然後觸發處理 IPI 的中斷服務程序(ISR)。 - Linux 內核的 `smp_call_function()` 就是用來發送 IPI 的機制之一。 --- ## **3. Page Fault(分頁錯誤)** **Page Fault(分頁錯誤)** 發生在 CPU 嘗試存取**不在記憶體(RAM)中的頁面**時,由作業系統(OS)來處理並載入該頁面。 ### **Page Fault 的種類** 1. **Minor Page Fault(次級分頁錯誤)** - 頁面已經在 RAM 內,但沒有映射到進程的地址空間(可能是因為換頁表未設置)。 - 例如,共享記憶體的頁面,可能需要 OS 建立適當的映射。 2. **Major Page Fault(主要分頁錯誤)** - 需要從磁碟(如 swap 空間或 mmap 檔案)載入缺失的頁面到記憶體。 - **代價較高,因為涉及磁碟 I/O**。 3. **Invalid Page Fault(無效分頁錯誤)** - 進程存取了**非法的記憶體區域**(例如存取 `NULL` 指標)。 - 這通常會導致 **Segmentation Fault(段錯誤,SIGSEGV)**。 ### **處理流程** 1. **CPU 產生 Page Fault** 2. **OS 查詢頁表(Page Table)** - **如果該頁面有效但尚未載入** → **從磁碟載入頁面**(可能會導致 Major Page Fault)。 - **如果該頁面無效** → **發送 SIGSEGV 訊號給進程,導致崩潰**(Segmentation Fault)。 --- ## **4. Interrupts(中斷)** **中斷(Interrupts)** 是一種 CPU **打斷當前執行流程,處理特定事件**的機制。 ### **中斷的類型** 1. **硬體中斷(Hardware Interrupts)** - 由外部設備觸發,如鍵盤、滑鼠、磁碟 I/O、網路請求。 - 例如: - 鍵盤按下 `Enter`,產生鍵盤中斷(IRQ)。 - 磁碟完成 I/O,通知 CPU 讀取資料。 2. **軟體中斷(Software Interrupts)** - 由軟體觸發,如 `syscall`(系統呼叫)。 - 例如: - `int 0x80`(Linux 的舊式系統呼叫方式)。 - `syscall` 指令(現代 x86-64 使用)。 3. **異常(Exceptions)** - 由 CPU 自行觸發,如: - 除零錯誤(Divide by Zero)。 - Page Fault(分頁錯誤)。 - 違反存取(Segmentation Fault)。 ### **中斷處理流程** 1. **CPU 儲存當前執行狀態** 2. **跳轉到對應的中斷處理函式(Interrupt Service Routine, ISR)** 3. **ISR 完成後,恢復原本的執行狀態,繼續執行程式** --- ## **5. Rescheduling(重新排程)** **重新排程(Rescheduling)** 指的是作業系統**根據 CPU 排程策略,將 CPU 執行權移交給不同的進程(Process)或執行緒(Thread)**。 ### **Rescheduling 何時發生?** 1. **時間片(Time Slice)結束** - 在**多工系統**中,每個進程獲得的 CPU 時間有限,當時間片用完時,OS 會執行 **上下文切換(Context Switch)**,讓其他進程執行。 2. **進程進入等待狀態(Blocked)** - 例如,當進程等待 I/O 操作完成時,OS 會將 CPU 分配給其他可執行的進程。 3. **進程被手動 `sleep()`** - 如果進程執行 `sleep(5)`,OS 會將它從 CPU 移除,並在 5 秒後重新加入執行隊列。 4. **高優先權的進程需要執行** - 如果有**更高優先權的進程**準備好執行,OS 可能會搶佔當前進程,強制重新排程。 ### **Rescheduling 的影響** - **提升系統效能**:讓 CPU 資源更有效率地分配。 - **支援多工(Multitasking)**:讓多個應用程式同時執行。 - **影響延遲(Latency)**:如果排程策略不佳,可能會造成程式的回應延遲。 --- ## **結論** | 概念 | 作用 | | --- | --- | | **Cache(快取)** | 加速 CPU 存取記憶體,減少 RAM 存取次數。 | | **IPI(處理器間中斷)** | 讓多核 CPU 互相通訊,用於同步與工作分配。 | | **Page Fault(分頁錯誤)** | CPU 存取未載入記憶體的頁面,OS 需載入或終止進程。 | | **Interrupts(中斷)** | 暫停當前程式執行,以處理更高優先級的事件(如 I/O)。 | | **Rescheduling(重新排程)** | 根據 CPU 排程策略,將執行權交給不同的進程或執行緒。 | 這些機制共同協作,確保現代電腦系統能夠有效運作。 🚀 ::: :::spoiler PMIC(power management integrated circuit ### **PMIC(Power Management Integrated Circuit,電源管理整合電路)** ### **1. PMIC 是什麼?** PMIC(**Power Management Integrated Circuit,電源管理整合電路**)是一種專門用來管理電子設備電源的整合電路(IC)。它負責**調節、轉換、分配和監控電源**,確保系統能夠高效、安全地運作。 PMIC 通常應用於: - **智慧型手機、平板電腦、筆記型電腦** - **嵌入式系統(IoT、MCU、DSP)** - **電動車(EV)、可攜式裝置(如穿戴設備)** - **伺服器與網路設備** --- ### **2. PMIC 的主要功能** PMIC 負責多種電源管理工作,以下是主要功能: ### **(1)電壓調節(Voltage Regulation)** - 透過 **DC-DC 轉換器** 或 **LDO(低壓降穩壓器)** 來穩定供應電壓。 - 例如,將 5V USB 轉換為 3.3V 或 1.8V 供應 MCU、FPGA、SoC 使用。 ### **(2)電源切換(Power Switching)** - 控制不同電源輸出的開關,確保系統能夠根據需求分配電能,並支援多電壓系統。 ### **(3)電池管理(Battery Management)** - 監控電池電壓、電流、溫度,確保安全充電與放電。 - **支援充電控制(Battery Charging)**,如 **鋰離子電池(Li-Ion)管理**。 - 例如 **BQ24195** 是一款常見的 PMIC,專門管理鋰電池充放電。 ### **(4)能耗管理(Power Efficiency)** - 透過低功耗模式(Low Power Mode)、睡眠模式(Sleep Mode)來降低能耗,延長電池壽命。 - 常用於 **行動裝置、IoT 設備**,減少功耗並提升效能。 ### **(5)過壓/過流/過熱保護(Protection Circuits)** - 防止電源突波、短路、過熱等情況損壞設備。 - 內建 **過壓(OVP)、過流(OCP)、過溫(OTP)保護機制**。 ### **(6)時鐘管理(Clock Management)** - 某些 PMIC 內建 **時鐘產生器(Clock Generator)**,提供時脈給 MCU 或處理器。 ### **(7)電源狀態監測(Power Monitoring)** - 透過 I²C、SPI 或 GPIO 介面,回報電壓、電流等數據給主機處理器(如 SoC)。 - 幫助系統在電源異常時進行適當應對,如警報或安全關機。 --- ### **3. PMIC 的種類** 根據應用場景,PMIC 可以分為不同類型: | 類型 | 主要功能 | 應用 | | --- | --- | --- | | **電池管理 PMIC** | 負責鋰電池充放電、保護與監控 | 智慧手機、筆電、可攜設備 | | **DC-DC 轉換 PMIC** | 提供高效率電壓轉換(Buck、Boost 轉換器) | 物聯網、嵌入式設備 | | **LDO 穩壓 PMIC** | 提供低雜訊的線性穩壓輸出 | 高精度電子裝置(音訊、RF 產品) | | **電源開關 PMIC** | 控制多個電源輸入與輸出 | 汽車電子、伺服器 | | **電流監測 PMIC** | 監測功耗並提供 I²C/SPI 回報 | 伺服器、資料中心 | --- ### **4. PMIC 應用範例** ### **(1)智慧型手機中的 PMIC** 在手機 SoC(如 Qualcomm Snapdragon、Apple A 系列處理器)中,PMIC 負責: - 提供 **1.8V / 3.3V** 給處理器、記憶體、顯示器。 - 管理 **USB 充電(5V to 4.2V 鋰電池)**。 - 監控溫度,避免過熱。 常見的手機 PMIC: - **Qualcomm PM8998(Snapdragon)** - **TI BQ25895(快充電池管理)** ### **(2)樹莓派(Raspberry Pi)的 PMIC** 樹莓派使用 PMIC 來: - 將 **5V USB 電源轉換為 3.3V、1.8V**。 - 透過 **I²C 介面監測電壓與溫度**。 - 例如 **MaxLinear MxL7704** 是 Raspberry Pi 4 的 PMIC。 ### **(3)電動車(EV)中的 PMIC** 電動車(如 Tesla)內的 PMIC 負責: - 管理 **高壓電池(400V-800V)轉換為低壓 12V/5V**。 - 提供電源給 **ADAS(自動駕駛系統)、ECU(車輛控制單元)**。 - 例如 **Texas Instruments TPS65987D** 是一款支援 USB-C 電源傳輸(PD)的 PMIC。 --- ### **5. 總結** | 特色 | 內容 | | --- | --- | | **PMIC 作用** | 管理電源轉換、電池充電、電壓調節、保護機制等 | | **主要功能** | DC-DC 轉換、LDO 穩壓、電池管理、過壓/過流保護、監測功耗 | | **應用領域** | 智慧型手機、IoT、伺服器、電動車 | | **常見 PMIC** | Qualcomm PM8998、TI BQ25895、MxL7704 | PMIC 在現代電子設備中至關重要,確保能源管理高效、安全且可靠,特別是在電池供電的設備(如手機、IoT 裝置)中更是不可或缺! 🚀 ::: :::spoiler WFI/WFE(wait for interrupt/event) ## **WFI / WFE(Wait For Interrupt / Wait For Event)** **WFI(Wait For Interrupt)** 和 **WFE(Wait For Event)** 是 **ARM 架構**中兩個低功耗指令,主要用來讓 CPU 進入低功耗狀態,等待**中斷(Interrupt)**或**事件(Event)**發生後再繼續執行。 這些指令常用於**嵌入式系統、作業系統的省電模式、低功耗微控制器(MCU)等應用**,可以顯著降低功耗並提升系統效能。 --- ## **1. WFI(Wait For Interrupt)** **WFI 指令的作用**: - **讓 CPU 進入低功耗模式,直到接收到中斷(Interrupt)時才繼續執行**。 - 適用於處理器**等待外部或內部中斷發生**,類似於 `sleep()`。 - 一旦中斷發生,CPU 會恢復執行。 ### **WFI 工作流程** 1. CPU 執行 `WFI` 指令,進入低功耗狀態(例如睡眠模式)。 2. CPU 暫停執行,並等待中斷(如 Timer IRQ、GPIO IRQ)。 3. 一旦中斷發生,CPU **退出低功耗狀態**,繼續執行後續指令。 ### **WFI 的優勢** - 減少 CPU 的功耗,因為在等待中斷時 CPU **停止執行指令**,降低電力消耗。 - 適用於 **需要中斷觸發的應用**,如定時喚醒、事件驅動系統。 ### **WFI 使用範例(C / ARM Assembly)** ### **ARM 組語** ``` assembly 複製編輯 WFI ; CPU 進入低功耗模式,直到有中斷發生 ``` ### **C 語言(CMSIS for Cortex-M)** ```c c 複製編輯 #include "core_cm4.h" // Cortex-M4 CMSIS Headervoid enter_low_power_mode() { __WFI(); // 讓 CPU 進入低功耗狀態,等待中斷 } ``` 這段程式碼會讓 MCU 進入低功耗狀態,直到發生中斷(例如按鍵、定時器觸發)。 --- ## **2. WFE(Wait For Event)** **WFE 指令的作用**: - **讓 CPU 進入低功耗模式,直到有「事件(Event)」發生**。 - 事件可以來自**中斷(Interrupt)或顯式事件通知(SEV 指令)**。 - 適用於 **多核(SMP)系統**,其中一個核心可以喚醒另一個核心。 ### **WFE 與 WFI 的不同** | 指令 | 觸發條件 | 應用場景 | | --- | --- | --- | | **WFI** | 只有「中斷(Interrupt)」才能喚醒 CPU | 適合低功耗模式,直到有中斷發生 | | **WFE** | **中斷(Interrupt)或事件(Event)** 都能喚醒 CPU | 適合多核心(SMP)系統,透過 `SEV` 來喚醒其他核心 | ### **WFE 工作流程** 1. CPU 執行 `WFE`,進入低功耗模式,等待事件發生。 2. **事件來源**: - 硬體中斷(如 Timer IRQ)。 - 其他 CPU 核心執行 `SEV` 指令。 3. 一旦有事件發生,CPU 會**退出低功耗狀態**並繼續執行。 ### **WFE 的優勢** - 可用於 **多處理器(Multi-Core)架構**,允許一個 CPU 喚醒另一個 CPU。 - 支援等待中斷與等待事件,使得應用更靈活。 ### **WFE 使用範例** ### **ARM 組語** ``` assembly 複製編輯 WFE ; CPU 進入低功耗狀態,直到發生事件 ``` ### **C 語言(CMSIS for Cortex-M)** ```c c 複製編輯 #include "core_cm4.h"void enter_event_waiting_mode() { __WFE(); // 等待事件發生 } ``` 這段程式碼會讓 CPU 進入低功耗模式,直到有事件(如 `SEV` 指令或中斷)發生。 --- ## **3. SEV(Set Event)—— 配合 WFE 使用** `SEV`(Set Event)是一個 ARM 指令,用來**產生事件**,通常配合 `WFE` 來實現核心間同步。 ### **SEV 使用場景** - **多核心(SMP)系統**:一個核心可以使用 `SEV` 來喚醒另一個執行 `WFE` 的核心。 - **主從架構(Master-Slave)**:主核心(主 CPU)發送 `SEV` 指令來喚醒副核心(從 CPU)。 ### **SEV 使用範例** ### **ARM 組語** ``` assembly 複製編輯 SEV ; 發送事件,喚醒執行 WFE 的 CPU ``` ### **C 語言** ```c c 複製編輯 #include "core_cm4.h"void trigger_event() { __SEV(); // 觸發事件,讓執行 WFE 的 CPU 醒來 } ``` 這段程式碼讓執行 `WFE` 的 CPU **退出低功耗狀態**並繼續執行。 --- ## **4. WFI/WFE 在作業系統(OS)中的應用** WFI 和 WFE 在 **RTOS(即時作業系統)** 或 **Linux 內核** 中有許多應用: ### **(1)Linux 低功耗模式** 在 ARM 架構的 Linux 中,當 CPU 進入閒置狀態時,Linux 內核會自動執行 `WFI` 或 `WFE` 來降低功耗: - `arch_cpu_idle()` 內部會執行 `WFI`,使 CPU 進入睡眠模式。 - 當有新的排程或 IRQ 發生時,CPU 會被喚醒。 ### **(2)RTOS 任務調度** 在 **FreeRTOS、Zephyr** 等即時作業系統中: - 當任務進入 **阻塞狀態(Blocked)**,CPU 會執行 `WFI/WFE`,減少不必要的功耗。 - 當任務準備好執行時,系統透過中斷來喚醒 CPU。 ### **(3)多核心同步** 在 **SMP(對稱多處理)系統** 中: - 一個 CPU 使用 `WFE()` 進入等待狀態。 - 另一個 CPU 在準備好時,透過 `SEV()` 喚醒等待的 CPU。 --- ## **5. 總結** | 指令 | 作用 | 何時喚醒 CPU | 應用 | | --- | --- | --- | --- | | **WFI(Wait For Interrupt)** | 進入低功耗模式,等待中斷 | **中斷發生時** | 省電模式、OS 低功耗管理 | | **WFE(Wait For Event)** | 進入低功耗模式,等待中斷或事件 | **中斷或 SEV 事件發生時** | 多核心同步、事件驅動系統 | | **SEV(Set Event)** | 觸發事件,讓 `WFE` 喚醒 | **發送事件後** | 核心間同步(SMP 系統) | 這些指令對於 **低功耗系統設計、嵌入式系統、作業系統(OS)調度、多核心處理器** 都非常重要。合理使用這些指令可以降低功耗並提升系統效率! 🚀 ### 你說: wake up process是什麼 ### ChatGPT 說: ## **Wake Up Process(喚醒進程)** **Wake Up Process(喚醒進程)** 是指 **作業系統(OS)從「睡眠狀態(Sleeping)」或「等待狀態(Blocked)」中喚醒一個進程(Process)或執行緒(Thread),使其重新進入「可執行狀態(Runnable)」的過程**。 這個機制在多工作業系統(如 Linux、Windows、RTOS)中至關重要,因為進程和執行緒在沒有事件發生時通常會進入休眠,以節省 CPU 資源。當某個事件(如 I/O 完成、訊號發生、中斷等)發生時,OS 會喚醒相應的進程,使其恢復執行。 --- ## **1. 進程的狀態(Process States)** 在 Linux 內核(以及大多數作業系統)中,進程有幾種不同的狀態: 1. **Running(執行中)**:進程正在 CPU 上運行。 2. **Runnable(可執行)**:進程已準備好執行,但尚未獲得 CPU 時間。 3. **Sleeping(睡眠中)** - **Interruptible Sleep(可中斷睡眠)**:進程在等待某個事件(如 I/O 完成),可以被訊號(Signal)或中斷(Interrupt)喚醒。 - **Uninterruptible Sleep(不可中斷睡眠)**:進程等待某些關鍵資源,通常不會被打斷(如等待磁碟 I/O)。 4. **Stopped(停止)**:進程被暫停(如 `SIGSTOP`)。 5. **Zombie(殭屍)**:進程已終止,但仍佔據進程表條目,等待父進程回收。 當一個進程處於 **Sleeping(睡眠狀態)**,而某個事件發生時,OS **透過 Wake Up Process 機制將該進程從「睡眠狀態」轉為「可執行狀態」**。 --- ## **2. Wake Up Process 何時發生?** ### **(1)I/O 完成時** 當進程在等待某個 I/O 操作完成時,它會進入 **可中斷睡眠狀態(TASK_INTERRUPTIBLE)**。當 I/O 完成時,驅動程式或內核會喚醒該進程,使其回到 **可執行狀態(Runnable)**,等待 CPU 調度。 ### **範例:進程等待 I/O** ```c c 複製編輯 #include <stdio.h>#include <unistd.h>#include <fcntl.h>int main() { int fd = open("test.txt", O_RDONLY); if (fd < 0) { perror("open failed"); return 1; } char buf[100]; printf("進程進入睡眠,等待 I/O...\n"); ssize_t n = read(fd, buf, sizeof(buf)); // 進程會進入睡眠,直到 I/O 完成 printf("I/O 完成,進程被喚醒,讀取到 %zd 個字節。\n", n); close(fd); return 0; } ``` 當 `read()` 嘗試讀取檔案時,如果沒有可用資料,進程會進入 **睡眠狀態**,直到 I/O 完成後被內核喚醒。 --- ### **(2)訊號(Signal)觸發** 當進程收到訊號(如 `SIGUSR1`),若它正在睡眠,則可以被喚醒。 ### **範例:使用 `kill` 來喚醒進程** ```c c 複製編輯 #include <stdio.h>#include <signal.h>#include <unistd.h>void signal_handler(int sig) { printf("收到訊號 %d,進程被喚醒!\n", sig); } int main() { signal(SIGUSR1, signal_handler); printf("進程進入睡眠狀態...\n"); pause(); // 進程進入睡眠,直到收到訊號 printf("進程恢復執行。\n"); return 0; } ``` 執行後,在另一個終端執行: ```bash bash 複製編輯 kill -USR1 <進程 PID> ``` 這將發送 `SIGUSR1` 訊號,喚醒該進程。 --- ### **(3)互斥鎖(Mutex)與條件變數(Condition Variable)** 當一個執行緒等待某個資源(如互斥鎖),它會進入 **等待狀態(Blocked)**,直到其他執行緒釋放資源並喚醒它。 ### **範例:使用 `pthread_cond_wait()`** ```c c 複製編輯 #include <stdio.h>#include <pthread.h>#include <unistd.h>pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void* thread_func(void* arg) { pthread_mutex_lock(&lock); printf("執行緒進入睡眠,等待條件變數...\n"); pthread_cond_wait(&cond, &lock); // 進入等待狀態 printf("執行緒被喚醒,繼續執行!\n"); pthread_mutex_unlock(&lock); return NULL; } int main() { pthread_t t; pthread_create(&t, NULL, thread_func, NULL); sleep(2); // 模擬某個事件發生 printf("主執行緒發送信號,喚醒子執行緒!\n"); pthread_cond_signal(&cond); // 喚醒等待中的執行緒 pthread_join(t, NULL); return 0; } ``` 當執行緒執行 `pthread_cond_wait()` 時,它會進入睡眠,直到 `pthread_cond_signal()` 被觸發。 --- ## **3. Linux 內核中的 `wake_up()`** 在 Linux 內核中,`wake_up()` 是喚醒進程的主要函式,用來通知某個 **等待隊列(Wait Queue)** 上的進程,使其從 **睡眠狀態** 轉為 **可執行狀態**。 ### **`wake_up()` 內部機制** 1. **`wake_up_process()`** - 直接將進程的狀態設為 `TASK_RUNNING`,使其可以被 CPU 排程。 2. **`wake_up()`** - `wake_up()` 會喚醒所有等待特定條件的進程。 ### **內核範例** ```c c 複製編輯 void some_event_handler() { wake_up(&some_wait_queue); // 喚醒在等待隊列上的進程 } ``` 這種機制常用於驅動程式,如當某個 I/O 操作完成時,內核會調用 `wake_up()` 來喚醒等待的進程。 --- ## **4. 總結** | 觸發條件 | 喚醒方法 | 應用場景 | | --- | --- | --- | | **I/O 完成** | 內核自動喚醒 | `read()`、`write()` 等 | | **訊號發送(Signal)** | `kill()`, `SIGUSR1` | 進程間通訊 | | **條件變數(Condition Variable)** | `pthread_cond_signal()` | 執行緒同步 | | **計時器(Timer)** | `alarm()`, `usleep()` | 定時喚醒進程 | | **Linux 內核 `wake_up()`** | 內核 API | 驅動程式、設備管理 | **Wake Up Process** 是作業系統內部的重要機制,確保進程不會一直佔用 CPU,而是在需要時才被喚醒,以提高系統效率並降低功耗。 🚀 :::