文章目錄
- lib/kfifo.c 內核FIFO實現(Kernel FIFO Implementation) 高效的無鎖字節流緩沖區
- 歷史與背景
- 這項技術是為了解決什么特定問題而誕生的?
- 它的發展經歷了哪些重要的里程碑或版本迭代?
- 目前該技術的社區活躍度和主流應用情況如何?
- 核心原理與設計
- 它的核心工作原理是什么?
- 它的主要優勢體現在哪些方面?
- 它存在哪些已知的劣勢、局限性或在特定場景下的不適用性?
- 使用場景
- 在哪些具體的業務或技術場景下,它是首選解決方案?請舉例說明。
- 是否有不推薦使用該技術的場景?為什么?
- 對比分析
- 請將其 與 其他相似技術 進行詳細對比。
- include/linux/kfifo.h
- KFIFO: 內核高性能無鎖循環緩沖區
- 宏和結構體詳解
- KFIFO 狀態檢查宏
- `kfifo_is_empty`
- `kfifo_is_empty_spinlocked` 和 `kfifo_is_empty_spinlocked_noirqsave`
- `kfifo_is_full`
- kfifo_out: 從KFIFO中獲取數據
- 宏定義詳解
- lib/kfifo.c
- KFIFO 面向記錄的數據出隊核心實現
- `kfifo_copy_out`: 從循環緩沖區中復制數據
- `kfifo_out_copy_r`: "窺探"并復制一條記錄的數據
- `__kfifo_out_r`: 完成一次面向記錄的出隊操作
- `__kfifo_out`: 核心數據出隊函數
- `__kfifo_out_peek`: 窺探并復制數據
- `__kfifo_peek_n`: 窺探記錄頭以獲取記錄長度
lib/kfifo.c 內核FIFO實現(Kernel FIFO Implementation) 高效的無鎖字節流緩沖區
https://github.com/wdfk-prog/linux-study
歷史與背景
這項技術是為了解決什么特定問題而誕生的?
lib/kfifo.c
中的kfifo(Kernel First-In-First-Out)是為了在Linux內核中提供一個通用、高效、線程安全的先進先出字節流緩沖區(FIFO)而誕生的。在它出現之前,許多內核子系統和驅動程序都需要在生產者(寫入數據方)和消費者(讀取數據方)之間傳遞數據,并且它們都各自實現了自己的環形緩沖區(Ring Buffer)邏輯。
這導致了幾個突出問題:
- 代碼重復:大量驅動中存在功能相似但實現各異的環形緩沖區代碼,造成了代碼冗余和維護困難。
- 容易出錯:環形緩沖區的邊界條件處理,特別是索引的回繞(wrap-around)邏輯,是常見的bug來源。手寫實現很容易在多線程并發場景下出現問題。
- 缺乏標準化:沒有一個標準的接口,使得代碼的可讀性和可重用性都很差。
- 性能不佳:一些簡單的實現可能使用了不必要的鎖,或者沒有充分利用CPU架構的特性來優化性能。
kfifo的誕生就是為了用一個經過充分測試、高度優化和標準化的實現來取代所有這些分散的、臨時的解決方案。
它的發展經歷了哪些重要的里程碑或版本迭代?
kfifo由Stefani Dogcu在2004年左右引入內核,其設計從一開始就非常精巧,后續的發展主要集中在API的完善和優化上:
- 初始設計:kfifo的核心設計從一開始就奠定了其高效的基礎:
- 大小必須是2的冪:這是kfifo最重要的設計約束。它允許使用按位與操作(
index & (size - 1)
)來代替昂貴的模運算(index % size
)來實現索引的回繞,極大地提高了性能。 - 索引永不回繞:讀寫索引(
in
和out
)被定義為無限增長的無符號整數。這巧妙地避免了在多線程場景下因讀寫指針同時回繞到0而產生的混淆(空還是滿?),是實現無鎖操作的關鍵。
- 大小必須是2的冪:這是kfifo最重要的設計約束。它允許使用按位與操作(
- API的演進:
- 最初的API比較基礎,主要有
kfifo_put()
和kfifo_get()
。 - 后來增加了
kfifo_in()
和kfifo_out()
等函數,它們允許生產者/消費者直接在kfifo的內存空間上進行操作,避免了一次不必要的數據拷貝(即從用戶提供的緩沖區拷貝到kfifo)。 - 引入了動態分配kfifo的函數(
kfifo_alloc
)和靜態定義的宏(DEFINE_KFIFO
),使其使用更加靈活。 - 為了支持需要傳遞離散記錄(records)而非字節流的場景,內核還引入了
kfifo_rec
變體。
- 最初的API比較基礎,主要有
目前該技術的社區活躍度和主流應用情況如何?
kfifo是Linux內核中一個非常基礎、穩定且被極其廣泛使用的組件。它已經成為在內核中實現生產者-消費者數據傳遞的標準工具。其應用遍布內核的各個角落:
- 字符設備驅動:TTY(終端)和串口驅動使用kfifo來緩沖用戶輸入和程序輸出。
- USB驅動:用于緩沖USB設備發來的數據。
- 網絡驅動:在某些場景下用于緩沖數據包。
- 工業I/O(IIO)子系統:用于緩沖來自傳感器的數據。
核心原理與設計
它的核心工作原理是什么?
kfifo是一個經過精心設計的環形緩沖區,其高效和線程安全的特性主要源于以下幾個核心設計點:
- 2的冪大小:如上所述,強制kfifo的大小必須是2的冪。這使得索引的回繞操作可以通過位運算
index & (size - 1)
來完成,效率極高。 - 無限增長的索引:
in
(寫入)和out
(讀出)索引被定義為無符號整數(unsigned int
),它們只會單調遞增,永遠不會被顯式地重置為0。當索引的值超過其最大表示范圍時,它會自動利用無符號整數的溢出特性從0開始繼續增長。 - 計算已用/可用空間:kfifo中已存儲的數據量總是等于
in - out
。可用的空閑空間總是size - (in - out)
。由于索引是無限增長的,這個簡單的減法就避免了傳統環形緩沖區中需要比較in
和out
指針大小來判斷緩沖區是空是滿的復雜邏輯。 - 內存屏障(Memory Barriers):這是實現**單生產者/單消費者(SPSC)**場景下無鎖操作的關鍵。
- 在
kfifo_put()
(生產者)中,首先將數據拷貝到緩沖區,然后才更新in
索引。這兩步操作之間需要一個寫內存屏障(smp_wmb()
)。這確保了對數據的寫入操作一定先于in
索引的更新對其他CPU可見。 - 在
kfifo_get()
(消費者)中,首先讀取in
索引的值,然后才從緩沖區拷貝數據,最后更新out
索引。在讀取in
索引和拷貝數據之間需要一個讀內存屏障(smp_rmb()
)。這確保了消費者能看到生產者更新后的in
索引,然后才去讀取相應的數據。 - 正是這種精巧的屏障使用和操作順序,保證了在SPSC場景下,生產者和消費者可以安全地并發訪問kfifo而無需任何鎖。
- 在
它的主要優勢體現在哪些方面?
- 極高的性能:基于2的冪大小和位運算,以及在SPSC場景下的無鎖設計,使其數據傳輸的開銷極小。
- 線程安全:為最常見的SPSC場景提供了開箱即用的無鎖安全保證。對于多生產者或多消費者(MPMC)的場景,kfifo也明確要求用戶必須在外部使用鎖(如自旋鎖)來保護訪問,提供了清晰的安全模型。
- 代碼簡潔:將復雜的環形緩沖區邏輯封裝成簡單易用的API,極大地簡化了驅動程序的開發。
- 健壯性:作為一個通用的內核組件,它經過了社區的廣泛使用和嚴格測試,非常可靠。
它存在哪些已知的劣勢、局限性或在特定場景下的不適用性?
- 大小限制:大小必須是2的冪,這可能會導致一些內存的浪費。例如,如果你需要一個33KB的緩沖區,你必須分配一個64KB的kfifo。
- 字節流導向:標準的kfifo是為字節流設計的。它不保留消息邊界。如果生產者寫入了兩次數據,消費者一次性讀取可能會將兩次寫入的數據合并在一起。對于需要保持消息邊界的場景,需要使用
kfifo_rec
或在應用層自己處理分包。 - 無鎖限制:其無鎖特性僅適用于單生產者、單消費者的場景。只要存在多個生產者或多個消費者,就必須由調用者負責加鎖。
使用場景
在哪些具體的業務或技術場景下,它是首選解決方案?請舉例說明。
在內核中,任何需要在兩個執行單元(如中斷上下文和進程上下文,或者兩個內核線程)之間傳遞字節流數據的生產者-消費者模型中,kfifo都是首選。
- 例一:串口驅動
當串口硬件收到數據時,會觸發一個中斷。串口驅動的中斷處理函數(生產者)需要快速地將收到的字節存放到一個緩沖區中,然后退出中斷。內核中的一個內核線程(消費者)會從這個緩沖區中讀取數據,并將其傳遞給上層的TTY子系統。這個“中斷上下文生產,線程上下文消費”的場景是典型的SPSC模型,使用kfifo可以實現無鎖、高效的數據緩沖。 - 例二:用戶空間I/O緩沖
一個字符設備驅動在處理用戶空間的write()
系統調用時,可以將用戶傳入的數據放入一個kfifo(生產者是進程上下文)。設備的硬件發送邏輯(可能是由一個工作隊列或內核線程驅動)則從kfifo中取出數據并發送(消費者是另一個內核上下文)。
是否有不推薦使用該技術的場景?為什么?
- 需要保持消息邊界:如果生產者寫入的是一個個獨立的數據包,且消費者需要以同樣的數據包單位來讀取,那么標準的kfifo不適用,因為它會合并字節。此時應考慮使用
kfifo_rec
或專門的消息隊列。 - 數據量巨大且非2的冪:如果需要一個非常大的緩沖區(如幾MB),且大小需求恰好不是2的冪(如3MB),那么使用kfifo導致的內存浪費可能會比較顯著。在這種情況下,可能需要評估是否值得為了性能而接受這種空間開銷。
- 需要復雜隊列操作:如果需要的不僅僅是FIFO,還需要如優先級、亂序處理等復雜隊列功能,那么kfifo不適用。應選擇更高級的隊列數據結構。
對比分析
請將其 與 其他相似技術 進行詳細對比。
特性 | kfifo | 手動實現的環形緩沖區 | 鏈表 (Linked List) |
---|---|---|---|
核心功能 | 高效、線程安全的字節流FIFO | 字節流FIFO | 離散對象(節點)的序列 |
實現方式 | 連續的內存塊,2的冪大小,無限增長的無符號索引 | 連續的內存塊,手動處理索引回繞 | 離散的內存節點,通過指針連接 |
性能 | 非常高。無鎖(SPSC),位運算代替模運算。 | 可變。性能取決于實現質量,通常不如kfifo優化。 | 較低。每個節點都需要單獨的內存分配/釋放,緩存局部性差。 |
內存占用 | 固定。一次性分配連續內存。大小為2的冪可能導致浪費。 | 固定。一次性分配連續內存。 | 動態。根據元素數量變化,但每個節點有額外的指針開銷。 |
線程安全 | 內置SPSC無鎖。MPMC需外部加鎖。 | 需手動實現。非常容易出錯。 | 需手動加鎖。 |
數據模型 | 字節流。不保留消息邊界。 | 字節流。 | 離散記錄。每個節點是一個獨立的消息。 |
典型用途 | 內核驅動中的生產者-消費者數據緩沖(串口,USB)。 | (應被kfifo取代) | 管理離散的對象列表,如任務隊列、設備列表。 |
include/linux/kfifo.h
KFIFO: 內核高性能無鎖循環緩沖區
此代碼片段來自Linux內核的<linux/kfifo.h>
頭文件, 它定義了**KFIFO (內核FIFO)**的數據結構和初始化宏。KFIFO是一個高度優化的、通用的、固定大小的循環緩沖區(circular buffer)實現。
它的核心原理有兩個:
- 強制容量為2的冪: KFIFO要求其容量必須是2的冪(2, 4, 8, 16, …)。這個看似嚴格的限制是為了一個巨大的性能優勢: 它可以使用**位掩碼(bitmask)來代替昂貴的模運算(modulo)**來實現索引的回繞(wrap-around)。例如, 對于一個大小為16的緩沖區, 其掩碼為15(二進制
1111
)。當索引增加到16時,16 & 15
的結果是0, 實現了從末尾到開頭的回繞, 這比16 % 16
的計算速度快得多。 - 生產者/消費者模型解耦: KFIFO的設計旨在安全高效地在兩個不同的執行上下文之間傳遞數據, 最經典的場景就是中斷處理程序(生產者)和進程上下文(消費者)。通過精心設計的內存屏障和原子操作(在
kfifo_in
/kfifo_out
函數中實現), KFIFO可以在單生產者/單消費者的場景下實現**無鎖(lock-free)**操作, 極大地降低了系統開銷和中斷延遲。
此代碼片段本身專注于KFIFO的數據結構定義和靜態初始化。它通過一系列復雜的C預處理器宏, 提供了兩種主要的FIFO聲明方式:
DECLARE_KFIFO
: 用于在編譯時聲明一個緩沖區內嵌的FIFO。FIFO的管理結構和其實際的數據存儲區是一塊連續的內存。這適用于大小在編譯時就已確定的場景。DECLARE_KFIFO_PTR
: 用于聲明一個緩沖區外置的FIFO。FIFO的管理結構本身不包含數據存儲區, 數據區需要稍后通過kfifo_alloc
或kfifo_init
動態分配并關聯。這適用于大小在運行時才能確定的場景。
在STM32H750這樣的單核系統中, KFIFO的價值依然巨大:
- 性能: 位掩碼操作帶來的效率提升對于資源受限的MCU至關重要。
- 中斷處理: 它是處理異步數據流的理想工具。例如, UART或SPI的接收中斷可以將收到的數據快速推入一個KFIFO, 而主循環或一個低優先級任務可以安全地從這個KFIFO中取出數據進行處理。這避免了在中斷上下文中進行耗時操作, 保證了系統的實時響應能力。
- 并發安全: 即使在單核系統上, 如果內核是搶占式的, 一個任務也可能在操作KFIFO的中間被搶占。KFIFO內部的實現(雖然在此代碼片段中未完全展示)確保了這種并發訪問的安全性。
宏和結構體詳解
/* 前向聲明一個分散-聚集列表結構體, 此處未使用但kfifo可能與其他內核API交互. */
struct scatterlist;/** __kfifo: KFIFO最核心的、與類型無關的管理結構體.* 它包含了操作一個循環緩沖區所需的所有元數據.*/
struct __kfifo {unsigned int in; /* "放入"索引: 下一個數據要寫入的位置. */unsigned int out; /* "取出"索引: 下一個數據要讀取的位置. */unsigned int mask; /* 容量掩碼: 值等于 (容量 - 1). 用于通過位與(&)操作實現索引回繞. */unsigned int esize; /* 元素大小: FIFO中單個元素占用的字節數. */void *data; /* 數據指針: 指向實際存儲數據的緩沖區的起始地址. */
};/** __STRUCT_KFIFO_COMMON: 一個通用的宏, 定義了所有KFIFO結構體都包含的聯合(union).* union允許多個成員共享同一塊內存. 這是一種C語言的技巧, 用于實現類型安全和方便的訪問.*/
#define __STRUCT_KFIFO_COMMON(datatype, recsize, ptrtype) \union { \struct __kfifo kfifo; /* 可以將這塊內存解釋為核心管理結構. */ \datatype *type; /* 可以將其解釋為指向"元素類型"的指針, 用于編譯時類型檢查. */ \const datatype *const_type;/* ...const版本. */ \char (*rectype)[recsize]; /* 可以將其解釋為指向"記錄類型"的數組的指針, 用于面向記錄的FIFO. */ \ptrtype *ptr; /* 可以將其解釋為指向"通用指針類型"的指針. */ \ptrtype const *ptr_const; \}/** __STRUCT_KFIFO: 定義一個"緩沖區內嵌"的KFIFO結構體.* @type: FIFO中存儲的數據的類型.* @size: FIFO的容量, 必須是2的冪.* @recsize: (用于面向記錄的FIFO)記錄的大小.* @ptrtype: (用于面向記錄的FIFO)通用指針類型.*/
#define __STRUCT_KFIFO(type, size, recsize, ptrtype) \
{ \__STRUCT_KFIFO_COMMON(type, recsize, ptrtype); \/** buf[...]: 這就是內嵌的數據緩沖區.* ((size < 2) || (size & (size - 1))) ? -1 : size : 這是一個編譯時斷言.* (size & (size - 1)) 是一個判斷size是否為2的冪的技巧. 如果不是, 結果不為0.* 如果size小于2或不是2的冪, 整個表達式會變成 buf[-1], 這是一個無效的數組大小, 會導致編譯錯誤.* 這就強制了使用者必須提供一個有效的、2的冪的容量.*/ \type buf[((size < 2) || (size & (size - 1))) ? -1 : size]; \
}/* STRUCT_KFIFO: __STRUCT_KFIFO的一個簡化版宏, 用于聲明一個標準的、面向元素類型的內嵌式FIFO. */
#define STRUCT_KFIFO(type, size) \struct __STRUCT_KFIFO(type, size, 0, type)/** __STRUCT_KFIFO_PTR: 定義一個"緩沖區外置"的KFIFO結構體.* 注意buf的大小為0. 這是一個C語言的"柔性數組成員"或"零長度數組"技巧.* 它表示數據緩沖區不在這里, 需要在別處動態分配.*/
#define __STRUCT_KFIFO_PTR(type, recsize, ptrtype) \
{ \__STRUCT_KFIFO_COMMON(type, recsize, ptrtype); \type buf[0]; \
}/* STRUCT_KFIFO_PTR: __STRUCT_KFIFO_PTR的一個簡化版宏. */
#define STRUCT_KFIFO_PTR(type) \struct __STRUCT_KFIFO_PTR(type, 0, type)/* 為了兼容舊代碼, 定義了一個名為 struct kfifo 的標準動態FIFO類型. */
struct kfifo __STRUCT_KFIFO_PTR(unsigned char, 0, void);/* -- 面向記錄的FIFO的特殊類型定義, 此處不深入 -- */
#define STRUCT_KFIFO_REC_1(size) \struct __STRUCT_KFIFO(unsigned char, size, 1, void)
#define STRUCT_KFIFO_REC_2(size) \struct __STRUCT_KFIFO(unsigned char, size, 2, void)
struct kfifo_rec_ptr_1 __STRUCT_KFIFO_PTR(unsigned char, 1, void);
struct kfifo_rec_ptr_2 __STRUCT_KFIFO_PTR(unsigned char, 2, void);/** __is_kfifo_ptr: 一個輔助宏, 用于在編譯時判斷一個KFIFO是內嵌式還是指針式.* 它比較fifo結構體的大小和標準的指針式KFIFO結構體的大小. 如果相等, 說明它是指針式的.*/
#define __is_kfifo_ptr(fifo) \(sizeof(*fifo) == sizeof(STRUCT_KFIFO_PTR(typeof(*(fifo)->type))))/*** DECLARE_KFIFO_PTR: 用戶接口宏, 用于聲明一個指針式的KFIFO變量.* @fifo: 變量名.* @type: 元素類型.*/
#define DECLARE_KFIFO_PTR(fifo, type) STRUCT_KFIFO_PTR(type) fifo/*** DECLARE_KFIFO: 用戶接口宏, 用于聲明一個內嵌式的KFIFO變量.* @fifo: 變量名.* @type: 元素類型.* @size: 容量, 必須是2的冪.*/
#define DECLARE_KFIFO(fifo, type, size) STRUCT_KFIFO(type, size) fifo/*** INIT_KFIFO: 用戶接口宏, 用于初始化一個由DECLARE_KFIFO聲明的FIFO.* @fifo: 目標FIFO變量名.*/
#define INIT_KFIFO(fifo) \
(void)({ \/* ({...}) 是一個GCC擴展, 稱為"語句表達式", 允許多行代碼的宏表現得像一個單一表達式. */ \/* typeof(&(fifo)) __tmp = &(fifo); 獲取一個指向用戶fifo結構體的、類型正確的臨時指針. */ \typeof(&(fifo)) __tmp = &(fifo); \/* struct __kfifo *__kfifo = &__tmp->kfifo; 從聯合體中獲取核心管理結構的指針. */ \struct __kfifo *__kfifo = &__tmp->kfifo; \/* 初始化輸入和輸出索引為0. */ \__kfifo->in = 0; \__kfifo->out = 0; \/** 初始化掩碼:* 如果是外置式FIFO, 此處mask為0, 因為容量未知.* 如果是內嵌式FIFO, mask = 容量 - 1. ARRAY_SIZE(__tmp->buf)獲取內嵌數組的大小.*/ \__kfifo->mask = __is_kfifo_ptr(__tmp) ? 0 : ARRAY_SIZE(__tmp->buf) - 1;\/* 初始化元素大小. */ \__kfifo->esize = sizeof(*__tmp->buf); \/** 初始化數據指針:* 如果是外置式FIFO, data指針為NULL, 等待后續kfifo_init/alloc.* 如果是內嵌式FIFO, data指針指向自己的buf成員.*/ \__kfifo->data = __is_kfifo_ptr(__tmp) ? NULL : __tmp->buf; \
})
KFIFO 狀態檢查宏
此代碼片段繼續展示了Linux內核 KFIFO (內核FIFO) 的接口, 這次是一組用于檢查KFIFO狀態 (是否為空或已滿) 的宏。這些宏是KFIFO生產者/消費者模型中進行流量控制和狀態判斷的基礎。它們被設計得既高效又安全, 提供了不同級別的并發保護。
其核心原理非常直觀:
- 空 (Empty): 當"放入"索引(
in
)與"取出"索引(out
)相等時, 緩沖區為空。 - 滿 (Full): 當緩沖區中已存儲的元素數量等于其總容量時, 緩沖區為滿。
然而, 這些宏的實現細節體現了內核編程的高度技巧性, 特別是在類型安全和并發控制方面。
+ 1
的目的不是為了計算地址, 而是一個編譯時(compile-time)的 “詭計”, 用來強制進行類型檢查, 確保傳入宏的 fifo 是一個指向完整結構體(complete type)的指針, 而不是一個 void * 或者指向一個不完整類型(incomplete type)的指針。
kfifo_is_empty
這是最基礎的、非線程安全的空狀態檢查宏。
原理與工作流程:
它直接比較in
和out
索引。如果相等, 則FIFO為空。
/*** kfifo_is_empty - 如果fifo為空則返回true* @fifo: 要使用的fifo的地址*/
#define kfifo_is_empty(fifo) \
({ \/** ({...}) 是一個GCC的"語句表達式"擴展. 它允許多行代碼的宏表現得像一個單一表達式,* 并且可以安全地返回值, 避免了傳統宏的許多副作用.*/ \/** typeof((fifo) + 1) __tmpq = (fifo);* 這是一個類型安全的宏技巧. `typeof`獲取fifo指針的正確類型,* 并創建一個臨時指針變量`__tmpq`. 這可以防止在宏內部多次對`fifo`求值,* 避免了當傳入`fifo++`這類表達式時產生的意外行為.*/ \typeof((fifo) + 1) __tmpq = (fifo); \/** 核心邏輯: 直接比較in和out索引.* 比較的結果(true或false)是這個語句表達式的最終值, 也就是宏的返回值.*/ \__tmpq->kfifo.in == __tmpq->kfifo.out; \
})
適用場景: 只能在可以確定沒有其他執行緒(無論是任務還是中斷)會同時修改FIFO的上下文中使用, 或者由調用者自己提供外部鎖定。
kfifo_is_empty_spinlocked
和 kfifo_is_empty_spinlocked_noirqsave
這兩個宏提供了線程安全的空狀態檢查。它們是實際并發編程中必須使用的版本。
原理與工作流程:
它們都通過在執行kfifo_is_empty
檢查前后獲取和釋放一個自旋鎖(spinlock
)來保證操作的原子性。
kfifo_is_empty_spinlocked
:
這是最常用、最安全的版本, 特別是用于中斷上下文與進程上下文之間的同步。
/*** kfifo_is_empty_spinlocked - 使用自旋鎖, 返回fifo是否為空* @fifo: 要使用的fifo的地址* @lock: 用于鎖定的自旋鎖*/
#define kfifo_is_empty_spinlocked(fifo, lock) \
({ \unsigned long __flags; \bool __ret; \/** spin_lock_irqsave: 1. 保存當前的中斷狀態到__flags. 2. 禁用本地中斷. 3. 獲取自旋鎖.* 這可以防止來自其他任務的搶占, 以及來自本地中斷處理程序的并發訪問.*/ \spin_lock_irqsave(lock, __flags); \/* 在持有鎖的情況下, 調用非安全的版本進行檢查. */ \__ret = kfifo_is_empty(fifo); \/** spin_unlock_irqrestore: 1. 釋放自旋鎖. 2. 恢復之前保存的中斷狀態.*/ \spin_unlock_irqrestore(lock, __flags); \/* 返回檢查結果. */ \__ret; \
})
適用場景: 在STM32H750這類系統中, 當一個中斷處理程序作為生產者向FIFO寫入數據, 而一個內核任務(進程上下文)作為消費者讀取數據時, 消費者在檢查FIFO是否為空時必須使用這個宏。
kfifo_is_empty_spinlocked_noirqsave
:
這是一個輕量級的版本, 它不處理中斷狀態。
/*** kfifo_is_empty_spinlocked_noirqsave - 使用自旋鎖返回fifo是否為空,* 不禁用中斷.* @fifo: 要使用的fifo的地址* @lock: 用于鎖定的自旋鎖*/
#define kfifo_is_empty_spinlocked_noirqsave(fifo, lock) \
({ \bool __ret; \/** spin_lock: 只獲取自旋鎖, 在單核系統上, 這主要起到禁用內核搶占的作用.* 它不會禁用硬件中斷.*/ \spin_lock(lock); \__ret = kfifo_is_empty(fifo); \/* spin_unlock: 只釋放自旋鎖, 重新啟用內核搶占. */ \spin_unlock(lock); \__ret; \
})
適用場景: 用于兩個內核任務之間的同步, 或者可以確定與FIFO交互的代碼路徑都不會在中斷上下文中執行的場景。在單核系統上, 這足以防止任務間的并發沖突。我們之前分析的lineinfo_watch_poll
函數就使用了這個版本, 因為它是在進程上下文中被調用, 而其生產者(中斷處理程序)在推入數據后會使用spin_lock_irqsave
版本的鎖, 這種"非對稱"的鎖使用是正確且高效的。
kfifo_is_full
這是最基礎的、非線程安全的滿狀態檢查宏。
原理與工作流程:
它通過kfifo_len()
宏(此處未顯示, 但其作用是計算in - out
)來獲取當前FIFO中的元素數量, 然后將該數量與FIFO的容量(mask + 1
)進行比較。為了效率, 它直接與mask
比較。
/*** kfifo_is_full - 如果fifo已滿則返回true* @fifo: 要使用的fifo的地址*/
#define kfifo_is_full(fifo) \
({ \/* 同樣使用類型安全的臨時變量技巧. */ \typeof((fifo) + 1) __tmpq = (fifo); \/** kfifo_len(__tmpq) 計算出當前FIFO中的元素數量 (in - out).* __tmpq->kfifo.mask 是 (容量 - 1).* 當FIFO滿時, len == mask + 1, 所以 len > mask.* 這個比較避免了計算 "mask + 1".*/ \kfifo_len(__tmpq) > __tmpq->kfifo.mask; \
})
適用場景: 由生產者在嘗試向FIFO寫入數據之前調用, 以防止數據溢出和丟失。同樣, 這個基礎版本需要調用者自己處理鎖定。
kfifo_out: 從KFIFO中獲取數據
此宏是Linux內核 KFIFO (內核FIFO) 接口中負責從緩沖區取出數據的核心消費者API。它的根本原理是以一種高效且在特定條件下無需外部加鎖的方式, 從循環緩沖區中復制出指定數量的元素。
這個宏的設計集成了多項內核編程的最佳實踐, 以實現高性能、類型安全和并發安全。
核心工作原理 (內部函數 __kfifo_out
所做的事情):
- 計算可用長度: 它首先讀取
in
和out
索引, 計算出當前FIFO中可用元素的數量 (len = in - out
)。 - 確定復制數量: 它取用戶請求的數量
n
和FIFO中實際可用的數量len
之間的最小值, 作為本次要復制的元素數量。 - 分段復制: 由于數據在循環緩沖區中可能被分割在兩段 (一部分在數組末尾, 一部分在數組開頭),
__kfifo_out
會智能地處理這種情況。它可能會執行一次或兩次memcpy
操作, 將數據從FIFO的內部data
緩沖區復制到用戶提供的buf
中。 - 更新
out
索引: 在所有數據被安全地復制出去之后, 它會原子地更新out
索引, 將其增加實際復制出去的元素數量。這個操作順序是至關重要的。 - 無鎖(Lock-Free)操作: 在單生產者/單消費者的場景下,
kfifo_out
是無鎖的。這是因為:- 只有消費者(調用
kfifo_out
的代碼)會修改out
索引。 - 只有生產者(調用
kfifo_in
的代碼)會修改in
索引。 __kfifo_out
內部使用了內存屏障(memory barrier, 如smp_rmb()
), 確保對in
索引的讀取發生在我們開始復制數據之前, 并且對out
索引的更新發生在我們完成復制之后。這可以防止編譯器或CPU對操作進行不安全的亂序優化, 保證了生產者和消費者之間看到的數據狀態是一致的。
- 只有消費者(調用
宏定義詳解
kfifo_out
本身是一個復雜的宏, 它封裝了所有必要的類型檢查和邏輯分派。
/*** kfifo_out - 從fifo中獲取數據* @fifo: 要使用的fifo的地址* @buf: 指向存儲數據的緩沖區的指針* @n: 最多要獲取的元素的數量** 此宏從fifo中獲取一些數據, 并返回被復制的元素的數量.** 注意: 在只有一個并發讀取者和一個并發寫入者的情況下,* 使用此宏不需要額外的加鎖.*/
#define kfifo_out(fifo, buf, n) \
/** 步驟4: 將整個宏的返回值包裹在一個帶有 __must_check 屬性的輔助函數中.* 這會強制調用kfifo_out的代碼必須檢查其返回值(即實際取出的元素數量), 否則編譯器會發出警告.* 這是一個增強代碼健壯性的重要措施.*/ \
__kfifo_uint_must_check_helper( \
({ \/* 步驟1: 使用標準的類型安全技巧創建臨時變量. */ \typeof((fifo) + 1) __tmp = (fifo); \/* 為目標緩沖區buf也創建一個類型正確的臨時指針, 確保其類型與FIFO元素類型兼容. */ \typeof(__tmp->ptr) __buf = (buf); \unsigned long __n = (n); \/** 步驟2: 檢查是否為"面向記錄"的FIFO.* sizeof(*__tmp->rectype) 會得到記錄的大小(如果不是記錄式FIFO, 則為0).*/ \const size_t __recsize = sizeof(*__tmp->rectype); \/* 獲取指向核心管理結構的指針. */ \struct __kfifo *__kfifo = &__tmp->kfifo; \/** 步驟3: 邏輯分派.* 使用三元運算符, 根據 __recsize 是否為0, 來決定調用哪個底層工作函數.*/ \(__recsize) ?\__kfifo_out_r(__kfifo, __buf, __n, __recsize) : /* 如果是記錄式, 調用__kfifo_out_r */ \__kfifo_out(__kfifo, __buf, __n); /* 否則, 調用標準的__kfifo_out */ \
}) \
)/** 這是一個非常簡單的內聯函數, 它的唯一目的就是帶上 __must_check 屬性.* 它直接返回傳入的值.*/
static inline unsigned int __must_check
__kfifo_uint_must_check_helper(unsigned int val)
{return val;
}
lib/kfifo.c
KFIFO 面向記錄的數據出隊核心實現
此代碼片段展示了Linux內核 KFIFO (內核FIFO) 中用于面向記錄(record-based)的數據出隊(dequeue)操作的底層核心實現。與簡單的、逐元素(element)的FIFO不同, 面向記錄的FIFO用于處理可變長度的數據包或消息。它的核心原理是: 在存入每條可變長度的數據記錄之前, 先存入一個固定大小的頭部, 這個頭部記錄了緊隨其后的數據記錄的長度。
這組函數協同工作, 實現了一個安全、分層的記錄出隊過程:
__kfifo_out_r
(API層): 這是外部調用的頂層函數。它負責完成一次完整的"記錄出隊"事務: 檢查非空, 調用下一層來復制數據, 最后更新out
指針以宣告記錄已被消耗。kfifo_out_copy_r
(邏輯層/ framing層): 它負責處理"記錄"的邏輯。它首先"窺探"(peek)記錄的頭部以獲取其長度, 然后調用物理層來復制記錄的數據體, 但它不會更新out
指針。kfifo_copy_out
(物理層/ copy層): 這是最底層的內存復制引擎。它不關心"記錄"的概念, 只負責高效、安全地將指定長度的字節從循環緩沖區中復制出來, 并正確處理地址回繞(wrap-around)的情況。
kfifo_copy_out
: 從循環緩沖區中復制數據
這是最底層的物理復制函數。
原理與工作流程:
/** kfifo_copy_out: 從fifo內部緩沖區復制數據到外部.* @fifo: 指向核心 __kfifo 管理結構的指針.* @dst: 目標緩沖區的地址.* @len: 要復制的元素的數量.* @off: 起始復制點, 以元素為單位的邏輯偏移量.*/
static void kfifo_copy_out(struct __kfifo *fifo, void *dst,unsigned int len, unsigned int off)
{unsigned int size = fifo->mask + 1; // FIFO的總容量 (元素數)unsigned int esize = fifo->esize; // 每個元素的大小 (字節)unsigned int l;/* 使用位掩碼將邏輯偏移量轉換為緩沖區內的實際偏移量, 處理回繞. */off &= fifo->mask;/* 如果元素大小不是1字節, 將所有單位從"元素"轉換為"字節". */if (esize != 1) {off *= esize;size *= esize;len *= esize;}/** 計算第一段要復制的長度.* l = min(要復制的總長度, 從當前偏移量到物理緩沖區末尾的長度)*/l = min(len, size - off);/* 復制第一段 (從off到緩沖區末尾). */memcpy(dst, fifo->data + off, l);/** 復制第二段 (從緩沖區開頭到剩余部分).* 如果數據沒有回繞 (len <= l), 那么 len - l 等于 0, 第二個memcpy不會執行任何操作.*/memcpy(dst + l, fifo->data, len - l);/** 關鍵: 內存屏障.* smp_wmb() (Write Memory Barrier) 確保在它之前的所有內存寫入操作(即上面的memcpy),* 必須在它之后的任何內存操作(特別是調用者對fifo->out索引的更新)對其他CPU可見之前完成.* 這可以防止一種競態條件: 消費者還沒完成數據復制, 生產者就看到了更新后的out索引,* 誤認為空間已空閑并覆蓋了正在被復制的數據.* 在單核系統上, 它主要用來阻止編譯器的亂序優化, 保證操作的邏輯順序.*/smp_wmb();
}
kfifo_out_copy_r
: "窺探"并復制一條記錄的數據
這個函數負責讀取記錄的頭部, 并據此復制記錄的數據體。
原理與工作流程:
/** kfifo_out_copy_r: 窺探下一條記錄的長度, 并復制其數據.* @fifo: 指向核心 __kfifo 管理結構的指針.* @buf: 目標緩沖區.* @len: 目標緩沖區的最大容量.* @recsize: 記錄頭部的長度 (1或2字節).* @n: [輸出參數] 用于返回實際記錄數據的長度.* @return: 實際復制到buf中的字節數.*/
static unsigned int kfifo_out_copy_r(struct __kfifo *fifo,void *buf, unsigned int len, size_t recsize, unsigned int *n)
{/* 調用一個內部函數(此處未顯示)來窺探FIFO, 讀取記錄頭, 獲取記錄數據的長度. */*n = __kfifo_peek_n(fifo, recsize);/* 如果用戶的緩沖區大小len小于記錄的實際長度*n, 則只復制len個字節. */if (len > *n)len = *n;/** 調用物理復制函數.* 復制的起始點是 fifo->out + recsize, 即跳過記錄頭, 從實際數據開始復制.*/kfifo_copy_out(fifo, buf, len, fifo->out + recsize);/* 返回實際復制的字節數. */return len;
}
__kfifo_out_r
: 完成一次面向記錄的出隊操作
這是導出的API函數, 它將上述兩個函數的功能組合起來, 完成一次完整的記錄出隊。
原理與工作流程:
/** __kfifo_out_r: 從面向記錄的fifo中獲取一條記錄.* @return: 復制到buf中的數據字節數.*/
unsigned int __kfifo_out_r(struct __kfifo *fifo, void *buf,unsigned int len, size_t recsize)
{unsigned int n;/* 如果FIFO為空, 直接返回0. */if (fifo->in == fifo->out)return 0;/** 步驟1: 調用邏輯層函數, 它會窺探記錄長度, 并將數據復制到buf中.* 執行后, len 變量包含了實際復制的字節數, n 變量包含了記錄的完整數據長度.*/len = kfifo_out_copy_r(fifo, buf, len, recsize, &n);/** 步驟2: 更新 'out' 索引.* 將 'out' 索引向前移動整個記錄的長度, 包括記錄數據(n)和記錄頭(recsize).* 這個更新操作是在數據復制完成之后才執行的, 這是保證安全的關鍵.*/fifo->out += n + recsize;/* 返回實際復制到用戶緩沖區的字節數. */return len;
}
EXPORT_SYMBOL(__kfifo_out_r);
__kfifo_out
: 核心數據出隊函數
這是標準的、用于**面向元素(element-based)**的FIFO的數據出隊函數。它的作用是從FIFO中復制出指定數量的元素, 并更新FIFO的狀態以表示這些元素已被消耗。
原理與工作流程:
它將"出隊"這個動作優雅地分解為兩個獨立的步驟:
- 窺探與復制 (
__kfifo_out_peek
): 它首先調用__kfifo_out_peek
。這個函數負責計算實際可復制的元素數量, 并執行從KFIFO內部緩沖區到用戶目標緩沖區的內存復制(memcpy
), 但它不會修改FIFO的任何狀態指針。 - 消耗與更新 (
fifo->out += len
): 在數據被安全地復制出去之后,__kfifo_out
才執行最關鍵的一步: 將out
索引向前移動實際復制出去的元素數量(len
)。這個更新操作宣告了緩沖區中的這部分空間現在可以被生產者重新使用。
這種兩步分離的設計是KFIFO在單生產者/單消費者場景下實現無鎖(lock-free)的關鍵。
/** __kfifo_out: 從fifo中獲取數據并更新索引.* @return: 實際獲取的元素數量.*/
unsigned int __kfifo_out(struct __kfifo *fifo,void *buf, unsigned int len)
{/* 步驟1: 調用peek函數, 它會計算可復制的數量, 并執行內存復制. */len = __kfifo_out_peek(fifo, buf, len);/** 步驟2: 更新out索引, 將其增加已復制的元素數量.* 這個操作是在數據復制完成之后才執行的, 保證了操作的安全性.*/fifo->out += len;return len;
}
EXPORT_SYMBOL(__kfifo_out);
__kfifo_out_peek
: 窺探并復制數據
此函數的作用是"窺探"FIFO中的數據——即復制數據, 但不更新out
索引。這允許消費者在不消耗數據的情況下檢查隊列頭部的內容。
原理與工作流程:
/** __kfifo_out_peek: 從fifo中窺探數據 (復制但不消耗).* @return: 實際窺探并復制的元素數量.*/
unsigned int __kfifo_out_peek(struct __kfifo *fifo,void *buf, unsigned int len)
{unsigned int l;/* 計算當前FIFO中的元素數量. */l = fifo->in - fifo->out;/* 取用戶請求數量'len'和實際可用數量'l'中的較小者. */if (len > l)len = l;/* 調用底層復制引擎kfifo_copy_out(在之前的分析中), 執行內存復制. */kfifo_copy_out(fifo, buf, len, fifo->out);return len;
}
EXPORT_SYMBOL(__kfifo_out_peek);
__kfifo_peek_n
: 窺探記錄頭以獲取記錄長度
這是一個內部輔助函數, 專門用于面向記錄(record-based)的FIFO。它的唯一作用是窺探下一條記錄的頭部, 并解析出該記錄的數據體長度。
原理與工作流程:
它從fifo->out
的當前位置讀取1個或2個字節, 并將它們組合成一個代表長度的整數。
/* __KFIFO_PEEK: 一個安全的宏, 用于讀取內部數據緩沖區中的一個元素. */
#define __KFIFO_PEEK(data, out, mask) \((data)[(out) & (mask)])/** __kfifo_peek_n: 窺探下一條記錄的長度.* @recsize: 記錄頭的大小 (1或2字節).* @return: 下一條記錄的數據體長度.*/
static unsigned int __kfifo_peek_n(struct __kfifo *fifo, size_t recsize)
{unsigned int l;unsigned int mask = fifo->mask;unsigned char *data = fifo->data;/* 讀取長度的第一個字節. */l = __KFIFO_PEEK(data, fifo->out, mask);/* 如果記錄頭是2字節(recsize=2), 則讀取第二個字節并組合成一個16位數. */if (--recsize)l |= __KFIFO_PEEK(data, fifo->out + 1, mask) << 8;return l;
}