為什麼核心的 C 看起來怪怪的#
第一次讀 Linux 原始碼的人常常困惑:為什麼有 __init、likely()、container_of() 這些不在 K&R 課本裡的東西?因為核心是裸機級的 C:沒有 libc、沒有 malloc/free、沒有 exception。它必須在受限環境下榨出效能、表達多型、處理硬體記憶體模型。這一節整理最常見的慣用法。
container_of ── 沒有 OOP 也能多型#
這是整個核心最重要的巨集之一。給定一個結構體成員的指標,回推結構體本身的指標:
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) *__mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })用途:所有 Linux 的雙向鏈結串列、紅黑樹、kobject 都靠它做「侵入式容器」。
struct task_struct {
int pid;
char comm[16];
struct list_head tasks; // 嵌入鏈結串列節點
// ...
};
void scan_all_tasks(struct list_head *head) {
struct list_head *pos;
list_for_each(pos, head) {
struct task_struct *t = container_of(pos, struct task_struct, tasks);
printk("pid=%d comm=%s\n", t->pid, t->comm);
}
}對比一下傳統的「容器持有指標」設計,侵入式容器有兩個好處:
- 零額外記憶體配置:節點直接嵌在結構體裡,不需要
malloc(node_t) - 同一個物件可在多個容器裡:
task_struct可同時在 runqueue、tasks list、cgroup tree
C++ 用繼承做這件事,C 用 container_of + 嵌入欄位。
likely / unlikely ── 給編譯器的提示#
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)告訴編譯器某個分支在熱路徑上比較少走,幫助它做出更好的程式碼佈局(branch prediction、icache locality)。
if (unlikely(ptr == NULL)) {
return -EINVAL; // 錯誤路徑:移到函式尾部
}
// 正常路徑保持在 fall-through,cache 友善
不要亂用。大多數時候現代 CPU 的 branch predictor 已經做得夠好。只在有確切熱點資料支持時才加,否則會誤導編譯器、效能反而下降。Linux maintainers 對亂加
likely/unlikely的 patch 通常會打回票。
BUG_ON / WARN_ON / BUILD_BUG_ON#
| 巨集 | 行為 |
|---|---|
BUG_ON(cond) | 條件成立則觸發 panic,列印 stack trace |
WARN_ON(cond) | 條件成立則列印 stack trace 但繼續執行 |
BUILD_BUG_ON(cond) | 編譯期斷言,條件成立則編譯失敗 |
BUILD_BUG_ON_ZERO(e) | 表達式為非零則編譯失敗,可在巨集中嵌入 |
BUILD_BUG_ON(sizeof(struct foo) > PAGE_SIZE); // 編譯期保證
WARN_ON(in_interrupt()); // 執行期警告,不能在中斷脈絡
BUG_ON(!ptr); // 嚴重錯誤,崩潰
Linus 多次強調避免 BUG_ON:「核心崩潰永遠不是好的故障處理」。除非繼續執行會導致更大破壞(資料損毀),否則應該用
WARN_ON+ 回傳錯誤碼。
__init / __exit / __read_mostly ── section 標記#
static int __init my_module_init(void) { ... }
static void __exit my_module_exit(void) { ... }這些不是普通的標記,而是把函式/變數放到特殊的 ELF section:
| 標記 | section | 行為 |
|---|---|---|
__init | .init.text | 開機完成或模組載入後,記憶體被釋放 |
__exit | .exit.text | built-in 時直接被丟掉,模組才會用到 |
__initdata | .init.data | 初始化用資料,同樣可釋放 |
__read_mostly | .data..read_mostly | 把這個變數放到 read-mostly section,避免 false sharing |
__cacheline_aligned | aligned | 對齊到 cache line(通常 64 bytes) |
__percpu | per-CPU 區 | 每個 CPU 一份獨立的副本 |
把初始化程式碼集中到 .init.text,開機後可以歸還幾百 KB 記憶體 ── 嵌入式系統在意這個。
列舉錯誤碼:負數慣例#
核心函式回傳錯誤碼用負的 errno,正常回傳 0 或正值。配合三個輔助巨集:
#include <linux/err.h>
void *p = some_alloc(...);
if (IS_ERR(p))
return PTR_ERR(p); // 把 ERR_PTR 解碼成 -errno
return 0;
// 函式回傳 ERR_PTR:
return ERR_PTR(-ENOMEM);ERR_PTR(-12) 會把 -12 編碼成一個位於核心位址空間最後一頁的指標 ── 既能用 IS_ERR 偵測,又能用同一個指標型別回傳,避免「分兩個欄位回傳」的醜陋介面。
鏈結串列:struct list_head#
struct list_head {
struct list_head *next, *prev;
};整個核心就一種鏈結串列,沒有 template、沒有 generic,靠 container_of 解決型別問題。常用 API:
LIST_HEAD(my_list); // 宣告 + 初始化
INIT_LIST_HEAD(&head); // 動態初始化
list_add(&new->node, &head); // 加到頭
list_add_tail(&new->node, &head); // 加到尾
list_del(&entry->node); // 刪除
list_for_each(pos, &head) { ... } // 走訪指標
list_for_each_entry(t, &head, node) { ... } // 走訪結構體(包了 container_of)
list_for_each_entry_safe(t, tmp, &head, node) { ... } // 走訪過程中可刪除
_safe 版本在迴圈中保留 next 的副本,所以可以安全 list_del。沒有 _safe 而 list_del 會踩記憶體。
紅黑樹與其他資料結構#
| 結構 | 標頭檔 | 用途 |
|---|---|---|
list_head | linux/list.h | 雙向鏈結串列 |
hlist_head | linux/list.h | 單向 hash 鏈,head 較小 |
rb_node | linux/rbtree.h | 紅黑樹(CFS runqueue、ext4 extent) |
xarray | linux/xarray.h | 取代舊的 radix tree(page cache) |
idr / ida | linux/idr.h | ID 分配器 |
kfifo | linux/kfifo.h | lock-free single-producer FIFO |
llist | linux/llist.h | lock-free 鏈結串列 |
紅黑樹不像 list_head 那樣有大量便利巨集,需要自己寫插入時的比較函式:
struct mytree_node {
struct rb_node rb;
int key;
};
static int insert(struct rb_root *root, struct mytree_node *node) {
struct rb_node **link = &root->rb_node;
struct rb_node *parent = NULL;
while (*link) {
struct mytree_node *cur = rb_entry(*link, struct mytree_node, rb);
parent = *link;
if (node->key < cur->key)
link = &(*link)->rb_left;
else if (node->key > cur->key)
link = &(*link)->rb_right;
else
return -EEXIST;
}
rb_link_node(&node->rb, parent, link);
rb_insert_color(&node->rb, root);
return 0;
}per-CPU 變數#
核心常需要「每 CPU 一份的計數器」以避免鎖:
DEFINE_PER_CPU(int, my_counter);
void increment(void) {
this_cpu_inc(my_counter); // 對當前 CPU 的副本 +1,不需要鎖
}
int sum(void) {
int total = 0, cpu;
for_each_online_cpu(cpu)
total += per_cpu(my_counter, cpu);
return total;
}this_cpu_inc 在大多數架構上是單一 instruction(如 x86 inc),且因為只有當前 CPU 會寫入,不需要 atomic 操作。讀取時需要遍歷所有 CPU 加總 ── 用空間換時間。
RCU 讀取慣用法#
rcu_read_lock();
p = rcu_dereference(global_ptr);
if (p)
use(p);
rcu_read_unlock();
// 寫入端
old = global_ptr;
rcu_assign_pointer(global_ptr, new);
synchronize_rcu(); // 等所有讀者完成
kfree(old);詳見 08 同步管理 的 RCU 段落。rcu_dereference/rcu_assign_pointer 不是擺好看的,它們包含了 memory barrier,省略會導致現代亂序 CPU 上的隱性 race。
GFP flags ── 配置記憶體的脈絡#
kmalloc(size, flags) 的第二個參數決定配置失敗時的行為:
| flag | 何時用 |
|---|---|
GFP_KERNEL | 行程脈絡,可睡眠、可觸發 reclaim(最常見) |
GFP_ATOMIC | 中斷 / spinlock 內,不可睡眠(容易失敗,盡量避免) |
GFP_NOWAIT | 不睡眠也不喚醒 kswapd |
GFP_NOIO | 不能做 I/O 來釋放記憶體(檔案系統內部用) |
GFP_USER | 為使用者空間配置 |
GFP_DMA | 必須來自 DMA-able 的低位址區 |
選錯 flag 是核心新手最常見錯誤之一。在 spinlock 裡用 GFP_KERNEL ── 系統可能 deadlock。
巨集慣例:do-while-zero#
#define DO_THING(a, b) do { \
foo(a); \
bar(b); \
} while (0)為什麼要 do { ... } while (0)?確保多行巨集在 if/else 裡仍是單一語句:
if (cond)
DO_THING(x, y); // 展開後是一個合法的語句
else
other();如果只用 { ... },後面的分號會讓 else 變成孤兒。
小結#
核心 C 的「怪」幾乎都源自三個約束:
- 沒有 libc:所有便利函式自建(
kmalloc、memcpy_toio、copy_from_user) - 沒有例外:用負 errno +
IS_ERR/PTR_ERR表達錯誤 - 要榨效能:
likely、per-CPU、RCU、cacheline alignment、section 切割
讀過 linux/list.h、linux/err.h、linux/compiler.h、linux/kernel.h 這四個檔,你已經懂半個核心的「方言」。剩下的靠讀程式碼累積。