子系統程式碼依賴分布於通用標頭檔、架構覆寫實作、產生檔案與核心實作目錄中的基礎設施。以下筆記整理數個不屬於單一裝置或協定子系統的機制:診斷、錯誤值指標、分支提示與可修補分支、單次複製存取,以及 SLUB 配置路徑。
診斷與錯誤回報 #
核心診斷使用依嚴重性區分的 pr_*() 巨集,以及具備裝置上下文的 dev_*() 巨集:
if (revision < minimum_revision) {
dev_err(dev, "unsupported revision %#x\n", revision);
return -ENODEV;
}
pr_debug("queue depth: %u\n", depth);dev_err() 包含裝置上下文;不存在裝置時,適合使用 pr_err()。dynamic debug 可以啟用選定的 pr_debug() 和 dev_dbg() 呼叫位置,而不會增加永久性的日誌雜訊。pr_warn_ratelimited() 等速率限制變體可防止重複發生的狀況持續填滿日誌。
將預期的失敗以負錯誤號碼回傳。WARN_ON() 和 WARN_ON_ONCE() 會回報遭到違反的不變條件並繼續執行,因此呼叫端仍必須處理由此產生的狀態。BUG() 會終止目前的執行路徑,並可能使系統不穩定;核心指引將其限制在無法繼續執行的情況。需要診斷上下文但不需要宣告條件時,dump_stack() 會記錄呼叫鏈。
錯誤值指標 #
原本回傳指標的函式可以使用 ERR_PTR() 編碼負錯誤號碼:
struct object *object_create(unsigned long flags)
{
if (flags & ~OBJECT_VALID_FLAGS)
return ERR_PTR(-EINVAL);
/* Allocate and initialize the object. */
}
struct object *object = object_create(flags);
if (IS_ERR(object))
return PTR_ERR(object);include/linux/err.h 定義 ERR_PTR()、PTR_ERR()、IS_ERR()、IS_ERR_OR_NULL() 和 PTR_ERR_OR_ZERO()。目前的通用程式碼會保留到 -MAX_ERRNO 的錯誤值,其中 MAX_ERRNO 為 4095,但呼叫端必須將此表示視為不透明。函式契約必須區分有效指標、NULL 與錯誤指標;只測試 !ptr 會漏掉編碼後的錯誤。
分支預測、前綴與架構掛鉤 #
停用分支剖析時,通用的 likely(x) 和 unlikely(x) 巨集會使用 __builtin_expect():
if (unlikely(!object))
return -ENOMEM;此提示會將預期值傳達給編譯器。它可能影響區塊配置,以及哪條邊成為直落路徑,但原始碼並不要求產生 x86 分支提示前綴。現代編譯器與處理器的行為使特定前綴成為實作細節,因此在提出指令層級的主張前,應檢查產生的物件檔:
objdump -drS path/to/object.o啟用 CONFIG_TRACE_BRANCH_PROFILING 後,include/linux/compiler.h 會透過 __branch_check__() 展開這些巨集。展開結果會在專用區段記錄檔案、函式與行號資料,並呼叫 ftrace_likely_update();而 likely_notrace() 會保留未插樁形式。分支命中診斷依賴產生的中繼資料,而不是在每個分支處手寫日誌呼叫。
Static key 解決的是不同問題。static_branch_likely() 及相關 API 描述變更不頻繁、但會在熱路徑上執行的條件。通用 jump-label 程式碼會呼叫 arch_static_branch() 等架構掛鉤。在 x86 上,arch/x86/include/asm/jump_label.h 會產生修補位置和 __jump_table 項目;執行時修補會將 NOP 變更為跳轉,或反向變更。因此,停用路徑可以避免反覆評估的載入與條件分支。
不要用 static key 取代任意可變布林值。更新會修補可執行文字,因此刻意設計為比評估最佳化後的讀取路徑成本更高。
通用介面與架構覆寫 #
核心標頭檔通常公開穩定的通用 API,而 <asm/...> 提供架構實作。常見安排如下:
- 子系統包含通用 Linux 標頭檔;
- 該標頭檔透過
arch/<arch>/include/asm包含由架構選定的標頭檔; - 架構標頭檔實作掛鉤,或包含
asm-generic後備實作。
範例包括屏障、原子操作、自旋鎖、jump label 和單次複製存取輔助函式。應同時搜尋公開巨集與其架構掛鉤;停留在包裝器可能會掩蓋真正重要的指令序列。
產生的標頭檔與表格構成另一層。Kconfig 產生設定標頭檔,系統呼叫與偏移機制產生 ABI 相依的常數或表格,而連結器會收集用於 initcall、追蹤中繼資料、jump label 與匯出符號的特殊命名區段。include/generated/ 中的產生物是建置結果的證據,而不是權威來源。編輯之前,應先定位產生它的指令碼、輸入表格、Kconfig 符號或連結器指令碼。
單次複製存取 #
READ_ONCE() 與 WRITE_ONCE() 透過架構和通用 rwonce 標頭檔定義。它們會防止編譯器合併、憑空產生或重新擷取被標記的存取,並保留適當單次複製操作之間對編譯器可見的順序:
if (READ_ONCE(state->ready))
consume(READ_ONCE(state->value));它們本身不提供 CPU 間排序、互斥或原子的讀取-修改-寫入操作。發佈資料通常需要 smp_store_release() 和 smp_load_acquire() 等 release/acquire 操作,或使用鎖。應根據並行協定選擇原語,而不是僅為了讓競態偵測器停止警告就加入 READ_ONCE()。
include/asm-generic/rwonce.h 中的通用原始碼記錄兩個核心使用情境:同一 CPU 上程序程式碼與中斷/NMI 程式碼之間的通訊,以及其排序由明確屏障或原子指令提供的存取。
追蹤 SLUB 配置路徑 #
SLUB 主要實作於 mm/slub.c,共用 slab 介面位於 mm/slab.h,公開配置 API 位於 <linux/slab.h>。呼叫端透過如下介面要求物件:
object = kmem_cache_alloc_node(cache, GFP_KERNEL, node);
if (!object)
return -ENOMEM;
/* Use object. */
kmem_cache_free(cache, object);公開呼叫會經過內嵌函式、配置掛鉤、偵錯或 sanitizer 插樁,以及快速或慢速路徑。確切內部細節取決於 SLUB 偵錯和 KASAN 等設定符號。目前的 mm/slub.c 描述低開銷的每 CPU 快取、在可用時透過 compare-and-exchange 進行的近似無鎖慢速路徑釋放,以及對部分 slab 集區的集中管理。不應將來自其他核心版本的歷史註解當作目前的演算法規格。
配置旗標是呼叫路徑契約的一部分。GFP_KERNEL 允許回收和睡眠;原子上下文需要適當的非睡眠配置策略。節點選擇是受政策和可用性限制的 NUMA 偏好,並不保證配置永遠不會回退。
原始碼位置工作流程 #
面對不熟悉的內部機制時,請沿著編譯後的路徑追蹤:
- 從呼叫位置開始,記錄本地
Makefile和 Kconfig 中的建置條件。 - 使用
git grep搜尋公開符號,然後追蹤巨集別名、靜態內嵌函式、函式指標和架構掛鉤。 - 檢查產生的設定與目標架構,因為原始碼中的非作用中分支可能看起來像權威實作。
- 如果問題涉及前處理或指令產生,使用
make path/to/file.i或.s。 - 使用
nm、readelf或objdump檢查連結後的物件;只有在識別相關執行時路徑後,才使用 ftrace、tracepoint 或perf probe。 - 與相同核心修訂版本的文件和原始碼比較。
原始碼路徑是有用的座標,但不是永久性的 API 承諾。內部結構和輔助函式名稱可能在版本間變更,因此當精確配置很重要時,文章和偵錯筆記應連結至有文件記錄的介面或具版本號的原始碼修訂版本。
評論
在 GitHub 上閱讀或評論(發表評論需要登入)