為什麼核心的 C 看起來怪怪的#

第一次讀 Linux 原始碼的人常常困惑:為什麼有 __initlikely()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);
    }
}

對比一下傳統的「容器持有指標」設計,侵入式容器有兩個好處:

  1. 零額外記憶體配置:節點直接嵌在結構體裡,不需要 malloc(node_t)
  2. 同一個物件可在多個容器裡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.textbuilt-in 時直接被丟掉,模組才會用到
__initdata.init.data初始化用資料,同樣可釋放
__read_mostly.data..read_mostly把這個變數放到 read-mostly section,避免 false sharing
__cacheline_alignedaligned對齊到 cache line(通常 64 bytes)
__percpuper-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。沒有 _safelist_del 會踩記憶體。

紅黑樹與其他資料結構#

結構標頭檔用途
list_headlinux/list.h雙向鏈結串列
hlist_headlinux/list.h單向 hash 鏈,head 較小
rb_nodelinux/rbtree.h紅黑樹(CFS runqueue、ext4 extent)
xarraylinux/xarray.h取代舊的 radix tree(page cache)
idr / idalinux/idr.hID 分配器
kfifolinux/kfifo.hlock-free single-producer FIFO
llistlinux/llist.hlock-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 的「怪」幾乎都源自三個約束:

  1. 沒有 libc:所有便利函式自建(kmallocmemcpy_toiocopy_from_user
  2. 沒有例外:用負 errno + IS_ERR/PTR_ERR 表達錯誤
  3. 要榨效能likely、per-CPU、RCU、cacheline alignment、section 切割

讀過 linux/list.hlinux/err.hlinux/compiler.hlinux/kernel.h 這四個檔,你已經懂半個核心的「方言」。剩下的靠讀程式碼累積。