contributed by < willy-liu
>
element_t
- 雙向鏈結串列中的元素
queue_contex_t
- 佇列管理結構
:warning:
如果q->next == q->prev == q,代表該佇列是空的,否則應該要指向某個element_t的list
Commit 9ab5ddf
建立新的「空」佇列
一開始沒有特別留意 list.h
裡有哪些巨集可用,後來看過之後發現 INIT_LIST_HEAD
可以讓 q_new
的實作更簡潔、也更容易閱讀。
Commit 360896c
釋放佇列所佔用的記憶體
使用list_for_each_safe
來簡化原本的while loop,另外也把原本的if condition改成用q_release_element
。
:warning: 原本要使用list_for_each_entry_safe
,$ make test
可以通過,但下了commit就會過不了static analysis,錯誤訊息如下
然而q_free
裡根本沒有用到int
,我進到list_for_each_entry_safe
的巨集裡查看,如下
注意到#else的部分,迴圈條件sizeof(struct { int : -1; });
是一個錯誤的語法,會編譯錯誤,我認為這個#else本來就是要用來提醒programmer無法該環境使用此巨集,雖然該部分照理來說只要__LIST_HAVE_TYPEOF
為1不會被執行到,但那個int會被static alaysis檢測出來,無法通過。
再往前看__LIST_HAVE_TYPEOF
定義為
只要編譯器是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等指令)
Commit 03f6e38
計算目前佇列的節點總量
使用了list.h的巨集list_for_each
進行重構,提升整體可讀性。
Commit 51562f7
在佇列開頭 (head
) 插入新節點(LIFO 準則)
重構時發現list.h裡本來就有一個function跟原本插入節點的程式碼做一樣的事,因此把原本的替換掉,提高可讀性。
Commit 51562f7
在佇列尾端 (tail
) 插入新節點(FIFO 準則)
重構後想到,既然是雙向"環狀"的佇列,且佇列空的時候會有一個dummy head,prev和next都連到自己,那insert_tail不就只要把head移動到prev後再insert_head就好嗎,可讀性變佳的同時也少了大量重複程式碼。
Commit 1bf85d5
在佇列開頭 (head
) 移除節點
使用list_del將原本指標移動的部分包裝起來,另外*node
原本用來獲得element以及方便做指標的移動,用了list_del
後就可以把它inline進list_entry
。
Commit 1bf85d5
在佇列尾端 (tail
) 移除節點
和q_insert_tail一樣的想法,因為是雙向"環狀"的佇列,所以可以重用q_remove_head,但要注意的是有dummy head的關係,在remove head實際上是remove head->enxt,所以要傳入的應該是head->prev->prev,不然會刪錯。
Commit c45d606
參考此連結的解法,使用快慢指標刪除中心節點,在重構時改用q_release_element
移除element
Commit aa37570
使用list_for_each_safe
大幅改進了可讀性,要注意的是他只要有刪除,就要全刪,所以要用一個額外的變數last_duplicate
來刪除最後一個重複的節點,此外,在比較時也要注意safe有可能會跑到dummy head,會造成進行strcmp時發生segmentation fault。
Commit 41ea59b
看了list.h後,把原本移動指標的部分包裝近list_move裡,增加可讀性。
反向排列佇列,但不分配或釋放節點
有想過要使用list_move
修改,但是單純交換他們的prev跟next用到list_move好像就太多餘了。
只要三次的assignment不需要用六次。
Commit 224251b
原本寫了一個helper function,用來反轉區間內的節點順序,額外用了超過40行且很難看懂,重新畫了圖之後發現可以有更簡單的方式實作,如下:
若k=3,node當前為4,safe
為5,要把順序變為1->4->3->2->5的話,
這個方法中node->prev
始終要移動到safe前面,而執行的次數為k-1次,所以程式碼使用了一個巧妙的寫法,將判斷條件設為--count
就可以執行k-1次,且迴圈結束count
也會回到0。
Commit 527e966
以根據descend
來決定以升序/降序排序佇列節點
🔗 參考 Linked List Sort
原本使用selection sort,在研讀完merge sort後改以它實作。
用的是遞迴版的merge sort,有額外寫了兩個helper function merge
和 merge_sort
,前者負責將已排序的左右半合併,後者
實作流程:
head->prev->next
設為NULL
head->next
以及descend
傳入merge_sort
q_sort
,由於排序過程中,僅確保next
,指標是正確的,所以還需重連prev
指標Commit cc0c21b
第一版寫了一個helper function q_merge_two
,用於合併兩個已排序的list,但覺得兩兩合併太花時間了,特別是沒做優化時第一個list會越來越長,效率不佳。
第二版重構的方法:每次都從k個list中找到要的那個節點,然後將他移動到當前merged的最後。
在 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
命令,可以看到字元裝置的類型:
輸出範例:
c
(開頭的 c
代表 Character Device)5, 0
(主設備號(Major Number) 和 次設備號(Minor Number))
如果想查看所有字元裝置,可以執行:
類型 | 讀寫方式 | 典型用途 | 例子 |
---|---|---|---|
字元裝置(Character Device) | 以字節為單位讀寫(流式處理),不經過緩衝 | 需要即時回應的設備 | /dev/tty , /dev/random , /dev/snd/ |
區塊裝置(Block Device) | 以區塊(block)為單位讀寫,有緩衝機制 | 儲存設備(支援隨機存取) | /dev/sda , /dev/mmcblk0 |
cat
或 echo
讀寫字元裝置dd
讀取字元裝置stty
設定字元裝置(例如調整終端行為)字元裝置(Character Device)適用於即時讀寫、流式輸入輸出、不支援隨機存取的硬體,如終端機、滑鼠、聲音裝置等。它與區塊裝置(Block Device)不同,主要是用來處理小型、即時的數據流,而不是大規模的儲存讀寫。
Callback(回呼函式) 是一種將函式作為參數傳遞給另一個函式,並在特定時間點或條件滿足時執行該函式的機制。
這種技術常見於:
在 C 語言或其他語言中,回呼函式的核心概念是函式指標(Function Pointer),允許我們將函式的地址傳遞給其他函式,稍後再執行。
在這個例子中:
my_callback_function()
是一個普通函式。execute_callback()
接收一個函式指標,並在適當的時機執行該函式。(1)提高靈活性
(2)適用於異步處理(Asynchronous Processing)
(3)適用於系統開發(Kernel、驅動程式)
這裡 execute_callback()
可以接收不同的回呼函式,並將 value
作為參數傳遞。
qsort()
函式標準函式 qsort()
使用回呼函式來決定排序方式:
這裡 compare()
作為回呼函式,提供 qsort()
排序時的比較規則。
這裡 signal_handler()
是當 SIGINT
(Ctrl+C)被觸發時,系統會執行的回呼函式。
這種技巧在 C、C++、Python、JavaScript 等語言中都廣泛使用!
在 C++ 中,虛擬函式(Virtual Function) 和 多型(Polymorphism) 是物件導向程式設計(OOP)的核心概念,而 vtable(虛擬函式表) 則是其底層的實作機制之一。
在 C++ 中,如果一個基類的函式被標記為 virtual
,則在繼承該類的派生類別中,可以覆寫這個函式,而函式調用會根據物件的實際類型(runtime type)決定,而非變數的靜態類型(compile-time type)。
這裡 ptr->show();
會根據 ptr
指向的實際物件類型 Derived
來決定呼叫 Derived::show()
,這就是多型(Polymorphism)。
多型是一種在相同介面下能夠表現出不同行為的機制,C++ 支援編譯時多型(Compile-time Polymorphism) 和 執行時多型(Runtime Polymorphism)。
這種多型發生在編譯階段,像是函式重載(Function Overloading) 和 模板(Templates):
這種多型發生在程式執行階段(Runtime),是透過虛擬函式來實現的:
這裡 speak()
的行為會根據物件的實際類型決定,而非變數類型,這就是執行時多型的特點。
vtable(Virtual Table,虛擬函式表) 是 C++ 虛擬函式的底層實現方式,當一個類別內含至少一個虛擬函式時,編譯器會為這個類別生成一個vtable,它是一個函式指標的陣列,儲存該類別所有虛擬函式的地址。
假設有以下類別:
這時候,編譯器會產生以下 vtable
:
當程式執行 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
指令,進入低功耗狀態(例如睡眠模式)。這段程式碼會讓 MCU 進入低功耗狀態,直到發生中斷(例如按鍵、定時器觸發)。
WFE 指令的作用:
指令 | 觸發條件 | 應用場景 |
---|---|---|
WFI | 只有「中斷(Interrupt)」才能喚醒 CPU | 適合低功耗模式,直到有中斷發生 |
WFE | 中斷(Interrupt)或事件(Event) 都能喚醒 CPU | 適合多核心(SMP)系統,透過 SEV 來喚醒其他核心 |
WFE
,進入低功耗模式,等待事件發生。SEV
指令。這段程式碼會讓 CPU 進入低功耗模式,直到有事件(如 SEV
指令或中斷)發生。
SEV
(Set Event)是一個 ARM 指令,用來產生事件,通常配合 WFE
來實現核心間同步。
SEV
來喚醒另一個執行 WFE
的核心。SEV
指令來喚醒副核心(從 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 調度。
當 read()
嘗試讀取檔案時,如果沒有可用資料,進程會進入 睡眠狀態,直到 I/O 完成後被內核喚醒。
當進程收到訊號(如 SIGUSR1
),若它正在睡眠,則可以被喚醒。
kill
來喚醒進程執行後,在另一個終端執行:
這將發送 SIGUSR1
訊號,喚醒該進程。
當一個執行緒等待某個資源(如互斥鎖),它會進入 等待狀態(Blocked),直到其他執行緒釋放資源並喚醒它。
pthread_cond_wait()
當執行緒執行 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()
會喚醒所有等待特定條件的進程。這種機制常用於驅動程式,如當某個 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,而是在需要時才被喚醒,以提高系統效率並降低功耗。 🚀