contributed by < willy-liu
>
$ 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
element_t
- 雙向鏈結串列中的元素
queue_contex_t
- 佇列管理結構
建立新的「空」佇列
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
的實作更簡潔、也更容易閱讀。
釋放佇列所佔用的記憶體
- 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
。
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
的巨集裡查看,如下
#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
定義為
#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等指令)
+# 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 ."
計算目前佇列的節點總量
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
進行重構,提升整體可讀性。
在佇列開頭 (head
) 插入新節點(LIFO 準則)
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->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跟原本插入節點的程式碼做一樣的事,因此把原本的替換掉,提高可讀性。
在佇列尾端 (tail
) 插入新節點(FIFO 準則)
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就好嗎,可讀性變佳的同時也少了大量重複程式碼。
在佇列開頭 (head
) 移除節點
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
。
在佇列尾端 (tail
) 移除節點
if (!head || head->prev == head)
return NULL;
- struct list_head *node = head->prev;
- element_t *element = list_entry(node, element_t, list);
-
- head->prev = node->prev;
- node->prev->next = head;
-
- if (sp && bufsize > 0) {
- strncpy(sp, element->value, bufsize - 1);
- sp[bufsize - 1] = '\0';
- }
-
- return element;
+
+ return q_remove_head(head->prev->prev, sp, bufsize);
}
和q_insert_tail一樣的想法,因為是雙向的佇列,所以可以重用q_remove_head,但要注意的是有dummy head的關係,在remove head實際上是remove head->enxt,所以傳入時的head要->prev兩次,不然會刪錯。
移除佇列中間的節點
🔗 參考 LeetCode 2095: Delete the Middle Node of a Linked List
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;
參考此連結的解法,使用快慢指標刪除中心節點,在重構時把移除element的部分用了q_release_element
在已排序的佇列中,移除所有重複節點
🔗 參考 LeetCode 82: Remove Duplicates from Sorted List II
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。
交換佇列中相鄰的節點
🔗 參考 LeetCode 24: Swap Nodes in Pairs
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裡,增加可讀性。
反向排列佇列,但不分配或釋放節點
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不需要用六次。
每 k
個節點反轉一次
🔗 參考 LeetCode 25: Reverse Nodes in k-Group
+ 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行且很難看懂,重新畫了圖之後發現可以有更簡單的方式實作,如下:
若k=3,node當前為4,safe
為5,要把順序變為1->4->3->2->5的話,
這個方法中node->prev
始終要移動到safe前面,而執行的次數為k-1次,所以程式碼使用了一個巧妙的寫法,將判斷條件設為--count
就可以執行k-1次,且迴圈結束count
也會回到0。
以根據descend
來決定以升序/降序排序佇列節點
🔗 參考 Linked List Sort
void q_sort(struct list_head *head, bool descend)
{
if (!head || head->next == head || head->next->next == head)
return;
struct list_head *current = head->next->next;
struct list_head *pos;
while (current != head) {
pos = current->prev;
while (
pos != head &&
(descend
? strcmp(list_entry(pos, element_t, list)->value,
list_entry(current, element_t, list)->value) < 0
: strcmp(list_entry(pos, element_t, list)->value,
list_entry(current, element_t, list)->value) > 0)) {
pos = pos->prev;
}
// Remove current from the list
current->prev->next = current->next;
current->next->prev = current->prev;
// Insert current after pos
current->next = pos->next;
current->prev = pos;
pos->next->prev = current;
pos->next = current;
// Move to the next element
current = current->next;
}
}
依遞 減/增 順序移除節點
🔗 參考 LeetCode 2487: Remove Nodes From Linked List
/* 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);
}
合併多個排序佇列,形成新的排序佇列
🔗 參考 LeetCode 23: Merge k Sorted Lists
/* Merge all the queues into one sorted queue, which is in ascending/descending
* order */
static struct list_head *q_merge_two(struct list_head *head1,
struct list_head *head2)
{
struct list_head *current = head1;
struct list_head *current1 = head1->next;
struct list_head *current2 = head2->next;
while (current1 != head1 && current2 != head2) {
const element_t *element1 = list_entry(current1, element_t, list);
const element_t *element2 = list_entry(current2, element_t, list);
if (strcmp(element1->value, element2->value) < 0) {
current->next = current1;
current1->prev = current;
current1 = current1->next;
} else {
current->next = current2;
current2->prev = current;
current2 = current2->next;
}
current = current->next;
}
while (current1 != head1) {
current->next = current1;
current1->prev = current;
current1 = current1->next;
current = current->next;
}
while (current2 != head2) {
current->next = current2;
current2->prev = current;
current2 = current2->next;
current = current->next;
}
current->next = head1;
head1->prev = current;
return head1;
}
int q_merge(struct list_head *head, bool descend)
{
// https://leetcode.com/problems/merge-k-sorted-lists/
if (!head || head->next == head)
return 0;
struct list_head *current_queue = head->next;
struct list_head *next_queue = current_queue->next;
while (next_queue != head) {
queue_contex_t *current_context =
list_entry(current_queue, queue_contex_t, chain);
;
queue_contex_t *next_context =
list_entry(next_queue, queue_contex_t, chain);
;
struct list_head *merged_queue =
q_merge_two(current_context->q, next_context->q);
next_context->q->next = next_context->q;
next_context->q->prev = next_context->q;
current_context->q = merged_queue;
next_queue = next_queue->next;
}
return q_size(list_entry(current_queue, queue_contex_t, chain)->q);
}
在 Linux 和 Unix 系統中,Character Device(字元裝置) 是一種類型的裝置檔案,用來表示能夠以字元(byte stream)為單位進行 I/O 操作的硬體裝置。
seek()
來跳轉到檔案中的某個特定位置。/dev/tty
):負責處理鍵盤輸入和螢幕輸出。/dev/ttyS0
):像是 RS-232 串列埠,通常用於連接嵌入式設備或工業裝置。/dev/input/mice
):用來處理滑鼠輸入數據。/dev/snd/
):音效卡的輸入輸出介面,如 /dev/dsp
(舊式)、/dev/snd/
(ALSA 音效架構)。/dev/random
, /dev/urandom
):提供隨機數給應用程式使用。在 Linux 中,字元裝置會在 /dev/
目錄下,以特殊檔案的形式存在。使用 ls -l
命令,可以看到字元裝置的類型:
ls -l /dev/tty
輸出範例:
crw-rw-rw- 1 root tty 5, 0 Mar 10 12:00 /dev/tty
c
(開頭的 c
代表 Character Device)5, 0
(主設備號(Major Number) 和 次設備號(Minor Number))
如果想查看所有字元裝置,可以執行:
ls -l /dev | grep "^c"
類型 | 讀寫方式 | 典型用途 | 例子 |
---|---|---|---|
字元裝置(Character Device) | 以字節為單位讀寫(流式處理),不經過緩衝 | 需要即時回應的設備 | /dev/tty , /dev/random , /dev/snd/ |
區塊裝置(Block Device) | 以區塊(block)為單位讀寫,有緩衝機制 | 儲存設備(支援隨機存取) | /dev/sda , /dev/mmcblk0 |
cat
或 echo
讀寫字元裝置cat /dev/random # 讀取隨機數
echo "Hello" > /dev/tty # 在當前終端顯示 "Hello"
dd
讀取字元裝置dd if=/dev/random bs=1 count=10 # 從 /dev/random 讀取 10 個字節
stty
設定字元裝置(例如調整終端行為)stty -F /dev/ttyS0 speed 9600 # 設定序列埠速度為 9600 baud
字元裝置(Character Device)適用於即時讀寫、流式輸入輸出、不支援隨機存取的硬體,如終端機、滑鼠、聲音裝置等。它與區塊裝置(Block Device)不同,主要是用來處理小型、即時的數據流,而不是大規模的儲存讀寫。
Callback(回呼函式) 是一種將函式作為參數傳遞給另一個函式,並在特定時間點或條件滿足時執行該函式的機制。
這種技術常見於:
在 C 語言或其他語言中,回呼函式的核心概念是函式指標(Function Pointer),允許我們將函式的地址傳遞給其他函式,稍後再執行。
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()
接收一個函式指標,並在適當的時機執行該函式。(1)提高靈活性
(2)適用於異步處理(Asynchronous Processing)
(3)適用於系統開發(Kernel、驅動程式)
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
作為參數傳遞。
qsort()
函式標準函式 qsort()
使用回呼函式來決定排序方式:
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()
排序時的比較規則。
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)被觸發時,系統會執行的回呼函式。
這種技巧在 C、C++、Python、JavaScript 等語言中都廣泛使用!
在 C++ 中,虛擬函式(Virtual Function) 和 多型(Polymorphism) 是物件導向程式設計(OOP)的核心概念,而 vtable(虛擬函式表) 則是其底層的實作機制之一。
在 C++ 中,如果一個基類的函式被標記為 virtual
,則在繼承該類的派生類別中,可以覆寫這個函式,而函式調用會根據物件的實際類型(runtime type)決定,而非變數的靜態類型(compile-time type)。
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)。
多型是一種在相同介面下能夠表現出不同行為的機制,C++ 支援編譯時多型(Compile-time Polymorphism) 和 執行時多型(Runtime Polymorphism)。
這種多型發生在編譯階段,像是函式重載(Function Overloading) 和 模板(Templates):
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;
}
這種多型發生在程式執行階段(Runtime),是透過虛擬函式來實現的:
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()
的行為會根據物件的實際類型決定,而非變數類型,這就是執行時多型的特點。
vtable(Virtual Table,虛擬函式表) 是 C++ 虛擬函式的底層實現方式,當一個類別內含至少一個虛擬函式時,編譯器會為這個類別生成一個vtable,它是一個函式指標的陣列,儲存該類別所有虛擬函式的地址。
假設有以下類別:
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
:
lua
複製編輯
+--------+
| func1 | --> Base::func1
| func2 | --> Base::func2
+--------+
lua
複製編輯
+--------+
| func1 | --> Derived::func1 (覆寫)
| func2 | --> Base::func2 (繼承)
+--------+
當程式執行 Base* obj = new Derived();
時:
obj->func1();
會透過 vtable 找到 Derived::func1()
obj->func2();
會透過 vtable 找到 Base::func2()
使用虛擬函式雖然提供了多型,但也帶來了一些額外的開銷:
在效能關鍵的應用(如遊戲、嵌入式系統)中,過度使用虛擬函式可能會影響執行速度,因此在設計類別時需權衡取捨。
概念 | 作用 |
---|---|
虛擬函式(Virtual Function) | 允許子類別覆寫函式,並透過基類指標/參考在執行時動態決定要執行哪個函式。 |
多型(Polymorphism) | 讓不同類別的物件可以透過相同介面(基類)執行不同的行為。 |
vtable(虛擬函式表) | 編譯器用來實作虛擬函式的機制,是一個函式指標陣列,物件透過 vptr 指向該類別的 vtable 來動態決定函式呼叫。 |
在 C++ 中,當我們使用 virtual
關鍵字時,實際上就是讓編譯器為這個類別建立 vtable
,並透過 vptr
來動態解析函式調用,以達到執行時的多型(Runtime Polymorphism)。
快取記憶體(Cache) 是 CPU 內部或與 CPU 緊密相連的高速記憶體,用來加速 CPU 存取資料,減少訪問主記憶體(RAM)的頻率,提升系統效能。
現代 CPU 的快取一般分為多個層級:
當 CPU 需要存取某個資料時:
缺點:
IPI(處理器間中斷) 是一種特殊的 CPU 中斷,允許多核心處理器(SMP 系統)之間相互通知或同步。
smp_call_function()
就是用來發送 IPI 的機制之一。Page Fault(分頁錯誤) 發生在 CPU 嘗試存取不在記憶體(RAM)中的頁面時,由作業系統(OS)來處理並載入該頁面。
NULL
指標)。中斷(Interrupts) 是一種 CPU 打斷當前執行流程,處理特定事件的機制。
Enter
,產生鍵盤中斷(IRQ)。syscall
(系統呼叫)。int 0x80
(Linux 的舊式系統呼叫方式)。syscall
指令(現代 x86-64 使用)。重新排程(Rescheduling) 指的是作業系統根據 CPU 排程策略,將 CPU 執行權移交給不同的進程(Process)或執行緒(Thread)。
sleep()
sleep(5)
,OS 會將它從 CPU 移除,並在 5 秒後重新加入執行隊列。概念 | 作用 |
---|---|
Cache(快取) | 加速 CPU 存取記憶體,減少 RAM 存取次數。 |
IPI(處理器間中斷) | 讓多核 CPU 互相通訊,用於同步與工作分配。 |
Page Fault(分頁錯誤) | CPU 存取未載入記憶體的頁面,OS 需載入或終止進程。 |
Interrupts(中斷) | 暫停當前程式執行,以處理更高優先級的事件(如 I/O)。 |
Rescheduling(重新排程) | 根據 CPU 排程策略,將執行權交給不同的進程或執行緒。 |
這些機制共同協作,確保現代電腦系統能夠有效運作。 🚀
PMIC(Power Management Integrated Circuit,電源管理整合電路)是一種專門用來管理電子設備電源的整合電路(IC)。它負責調節、轉換、分配和監控電源,確保系統能夠高效、安全地運作。
PMIC 通常應用於:
PMIC 負責多種電源管理工作,以下是主要功能:
根據應用場景,PMIC 可以分為不同類型:
類型 | 主要功能 | 應用 |
---|---|---|
電池管理 PMIC | 負責鋰電池充放電、保護與監控 | 智慧手機、筆電、可攜設備 |
DC-DC 轉換 PMIC | 提供高效率電壓轉換(Buck、Boost 轉換器) | 物聯網、嵌入式設備 |
LDO 穩壓 PMIC | 提供低雜訊的線性穩壓輸出 | 高精度電子裝置(音訊、RF 產品) |
電源開關 PMIC | 控制多個電源輸入與輸出 | 汽車電子、伺服器 |
電流監測 PMIC | 監測功耗並提供 I²C/SPI 回報 | 伺服器、資料中心 |
在手機 SoC(如 Qualcomm Snapdragon、Apple A 系列處理器)中,PMIC 負責:
常見的手機 PMIC:
樹莓派使用 PMIC 來:
電動車(如 Tesla)內的 PMIC 負責:
特色 | 內容 |
---|---|
PMIC 作用 | 管理電源轉換、電池充電、電壓調節、保護機制等 |
主要功能 | DC-DC 轉換、LDO 穩壓、電池管理、過壓/過流保護、監測功耗 |
應用領域 | 智慧型手機、IoT、伺服器、電動車 |
常見 PMIC | Qualcomm PM8998、TI BQ25895、MxL7704 |
PMIC 在現代電子設備中至關重要,確保能源管理高效、安全且可靠,特別是在電池供電的設備(如手機、IoT 裝置)中更是不可或缺! 🚀
WFI(Wait For Interrupt) 和 WFE(Wait For Event) 是 ARM 架構中兩個低功耗指令,主要用來讓 CPU 進入低功耗狀態,等待**中斷(Interrupt)或事件(Event)**發生後再繼續執行。
這些指令常用於嵌入式系統、作業系統的省電模式、低功耗微控制器(MCU)等應用,可以顯著降低功耗並提升系統效能。
WFI 指令的作用:
sleep()
。WFI
指令,進入低功耗狀態(例如睡眠模式)。assembly
複製編輯
WFI ; CPU 進入低功耗模式,直到有中斷發生
c
複製編輯
#include "core_cm4.h" // Cortex-M4 CMSIS Headervoid enter_low_power_mode() {
__WFI(); // 讓 CPU 進入低功耗狀態,等待中斷
}
這段程式碼會讓 MCU 進入低功耗狀態,直到發生中斷(例如按鍵、定時器觸發)。
WFE 指令的作用:
指令 | 觸發條件 | 應用場景 |
---|---|---|
WFI | 只有「中斷(Interrupt)」才能喚醒 CPU | 適合低功耗模式,直到有中斷發生 |
WFE | 中斷(Interrupt)或事件(Event) 都能喚醒 CPU | 適合多核心(SMP)系統,透過 SEV 來喚醒其他核心 |
WFE
,進入低功耗模式,等待事件發生。SEV
指令。assembly
複製編輯
WFE ; CPU 進入低功耗狀態,直到發生事件
c
複製編輯
#include "core_cm4.h"void enter_event_waiting_mode() {
__WFE(); // 等待事件發生
}
這段程式碼會讓 CPU 進入低功耗模式,直到有事件(如 SEV
指令或中斷)發生。
SEV
(Set Event)是一個 ARM 指令,用來產生事件,通常配合 WFE
來實現核心間同步。
SEV
來喚醒另一個執行 WFE
的核心。SEV
指令來喚醒副核心(從 CPU)。assembly
複製編輯
SEV ; 發送事件,喚醒執行 WFE 的 CPU
c
複製編輯
#include "core_cm4.h"void trigger_event() {
__SEV(); // 觸發事件,讓執行 WFE 的 CPU 醒來
}
這段程式碼讓執行 WFE
的 CPU 退出低功耗狀態並繼續執行。
WFI 和 WFE 在 RTOS(即時作業系統) 或 Linux 內核 中有許多應用:
在 ARM 架構的 Linux 中,當 CPU 進入閒置狀態時,Linux 內核會自動執行 WFI
或 WFE
來降低功耗:
arch_cpu_idle()
內部會執行 WFI
,使 CPU 進入睡眠模式。在 FreeRTOS、Zephyr 等即時作業系統中:
WFI/WFE
,減少不必要的功耗。在 SMP(對稱多處理)系統 中:
WFE()
進入等待狀態。SEV()
喚醒等待的 CPU。指令 | 作用 | 何時喚醒 CPU | 應用 |
---|---|---|---|
WFI(Wait For Interrupt) | 進入低功耗模式,等待中斷 | 中斷發生時 | 省電模式、OS 低功耗管理 |
WFE(Wait For Event) | 進入低功耗模式,等待中斷或事件 | 中斷或 SEV 事件發生時 | 多核心同步、事件驅動系統 |
SEV(Set Event) | 觸發事件,讓 WFE 喚醒 |
發送事件後 | 核心間同步(SMP 系統) |
這些指令對於 低功耗系統設計、嵌入式系統、作業系統(OS)調度、多核心處理器 都非常重要。合理使用這些指令可以降低功耗並提升系統效率! 🚀
wake up process是什麼
Wake Up Process(喚醒進程) 是指 作業系統(OS)從「睡眠狀態(Sleeping)」或「等待狀態(Blocked)」中喚醒一個進程(Process)或執行緒(Thread),使其重新進入「可執行狀態(Runnable)」的過程。
這個機制在多工作業系統(如 Linux、Windows、RTOS)中至關重要,因為進程和執行緒在沒有事件發生時通常會進入休眠,以節省 CPU 資源。當某個事件(如 I/O 完成、訊號發生、中斷等)發生時,OS 會喚醒相應的進程,使其恢復執行。
在 Linux 內核(以及大多數作業系統)中,進程有幾種不同的狀態:
SIGSTOP
)。當一個進程處於 Sleeping(睡眠狀態),而某個事件發生時,OS 透過 Wake Up Process 機制將該進程從「睡眠狀態」轉為「可執行狀態」。
當進程在等待某個 I/O 操作完成時,它會進入 可中斷睡眠狀態(TASK_INTERRUPTIBLE)。當 I/O 完成時,驅動程式或內核會喚醒該進程,使其回到 可執行狀態(Runnable),等待 CPU 調度。
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 完成後被內核喚醒。
當進程收到訊號(如 SIGUSR1
),若它正在睡眠,則可以被喚醒。
kill
來喚醒進程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
複製編輯
kill -USR1 <進程 PID>
這將發送 SIGUSR1
訊號,喚醒該進程。
當一個執行緒等待某個資源(如互斥鎖),它會進入 等待狀態(Blocked),直到其他執行緒釋放資源並喚醒它。
pthread_cond_wait()
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()
被觸發。
wake_up()
在 Linux 內核中,wake_up()
是喚醒進程的主要函式,用來通知某個 等待隊列(Wait Queue) 上的進程,使其從 睡眠狀態 轉為 可執行狀態。
wake_up()
內部機制wake_up_process()
TASK_RUNNING
,使其可以被 CPU 排程。wake_up()
wake_up()
會喚醒所有等待特定條件的進程。c
複製編輯
void some_event_handler() {
wake_up(&some_wait_queue); // 喚醒在等待隊列上的進程
}
這種機制常用於驅動程式,如當某個 I/O 操作完成時,內核會調用 wake_up()
來喚醒等待的進程。
觸發條件 | 喚醒方法 | 應用場景 |
---|---|---|
I/O 完成 | 內核自動喚醒 | read() 、write() 等 |
訊號發送(Signal) | kill() , SIGUSR1 |
進程間通訊 |
條件變數(Condition Variable) | pthread_cond_signal() |
執行緒同步 |
計時器(Timer) | alarm() , usleep() |
定時喚醒進程 |
Linux 內核 wake_up() |
內核 API | 驅動程式、設備管理 |
Wake Up Process 是作業系統內部的重要機制,確保進程不會一直佔用 CPU,而是在需要時才被喚醒,以提高系統效率並降低功耗。 🚀