為什麼有核心模組#

Linux 是 monolithic kernel,所有子系統共享一個位址空間 ── 但不代表全部功能都得編進 vmlinuz。Loadable Kernel Module(LKM) 讓你在系統運行時動態插入/卸載功能:驅動、檔案系統、網路協定、加密演算法都可以做成模組。

好處:

  • 核心映像更小(嵌入式系統重要)
  • 不用重啟就可以換驅動
  • 開發迭代快(編譯模組比重編核心快得多)

代價:

  • 模組與核心共享位址空間,沒有任何隔離
  • 模組崩潰 = 核心崩潰
  • 模組必須對應核心版本(無 ABI 穩定性保證)

第一個模組:Hello World#

// hello.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, kernel!\n");
    return 0;   // 0 = 成功;非 0 = 載入失敗
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Andrew");
MODULE_DESCRIPTION("A trivial example module");
MODULE_VERSION("0.1");

對應的 Makefile(必須叫這個名字、必須是 tab 不是空格):

obj-m := hello.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

default:
	$(MAKE) -C $(KDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean

操作:

make                      # 產出 hello.ko
sudo insmod ./hello.ko    # 載入
dmesg | tail              # 看到 "Hello, kernel!"
lsmod | grep hello        # 確認已載入
sudo rmmod hello          # 卸載

重要 API 拆解#

module_init / module_exit#

註冊模組的進入點/離開點。回傳 0 才表示載入成功,回傳負值會讓 insmod 失敗並列印 errno。

static int __init my_init(void) {
    if (something_wrong)
        return -ENODEV;     // 模組不會被視為載入
    return 0;
}

__init 標記告訴連結器把這段程式碼放進 .init.text section,模組成功載入後整段程式碼會被釋放。所以絕對不能在 __init 函式以外的地方呼叫被 __init 標記的函式 ── 它的記憶體已經沒了。

MODULE_LICENSE#

License 字串涵義
"GPL"GPL v2 或更新版
"GPL v2"僅 GPL v2
"GPL and additional rights"GPL + 附加授權
"Dual BSD/GPL"雙重授權
"Dual MIT/GPL"雙重授權
"Proprietary"私有 ── 載入後核心會被標記為 “tainted”

這不是擺好看的:標為 GPL 的模組才能呼叫 EXPORT_SYMBOL_GPL 匯出的核心 API。寫成 Proprietary 會被禁止連結 GPL-only 符號(如 kthread_create_on_node、許多 mm 內部 API)。

缺少 MODULE_LICENSE 也會被視為 proprietary,dmesg 會印「module taints kernel」。

模組參數#

允許 insmod 時帶參數:

static int howmany = 1;
static char *whom = "world";

module_param(howmany, int, S_IRUGO);
module_param(whom,    charp, S_IRUGO);

MODULE_PARM_DESC(howmany, "How many times to greet");
MODULE_PARM_DESC(whom,    "Whom to greet");

static int __init hello_init(void)
{
    int i;
    for (i = 0; i < howmany; i++)
        printk(KERN_INFO "Hello, %s!\n", whom);
    return 0;
}
sudo insmod ./hello.ko howmany=3 whom=Andrew
# Hello, Andrew!
# Hello, Andrew!
# Hello, Andrew!

module_param(name, type, perm) 的第三個參數是 /sys/module/<modname>/parameters/<name> 的權限:

  • S_IRUGO(0444):所有人可讀,不可寫
  • S_IRUGO | S_IWUSR(0644):root 可在執行時寫入
  • 0:不在 sysfs 暴露

如果允許執行時修改(S_IWUSR),可以這樣:

echo 5 | sudo tee /sys/module/hello/parameters/howmany

陣列參數:

static int values[10];
static int n_values;
module_param_array(values, int, &n_values, S_IRUGO);

符號匯出(EXPORT_SYMBOL)#

模組想呼叫核心或別的模組的函式 ── 那個函式必須被匯出

// 在 mod_a.c
int my_useful_function(int x) {
    return x * 2;
}
EXPORT_SYMBOL(my_useful_function);
// 或限制只給 GPL 模組用:
EXPORT_SYMBOL_GPL(my_useful_function);
// 在 mod_b.c
extern int my_useful_function(int x);

static int __init b_init(void) {
    printk("got %d\n", my_useful_function(21));
    return 0;
}

載入順序 a ➡️ b。卸載順序 b ➡️ a。如果 b 有引用 a 的符號,rmmod a 會失敗。

EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL

  • EXPORT_SYMBOL:任何模組都能用
  • EXPORT_SYMBOL_GPL:只有 MODULE_LICENSE("GPL")(或相容字串)的模組能連結

核心多數新 API 都用 _GPL 變體。這是 Linus 與社群表達「核心 API 不歡迎被閉源產品使用」的方式。

模組依賴與載入順序#

depmod 會掃描所有模組,分析 EXPORT_SYMBOLextern 關係,產生 /lib/modules/$(uname -r)/modules.depmodprobe(不是 insmod!)會根據這個依賴圖自動載入所有需要的前置模組

sudo modprobe hello       # 自動處理依賴
sudo modprobe -r hello    # 卸載

對比 insmod 是「直接載入這個 .ko,不管依賴」── 開發階段用 insmod,正式部署用 modprobe

自動載入機制#

當核心偵測到某個硬體或請求某個檔案系統,會嘗試自動 modprobe 對應模組。靠的是模組告訴核心「我支援哪些東西」:

static const struct pci_device_id my_ids[] = {
    { PCI_DEVICE(0x1234, 0x5678) },
    { },
};
MODULE_DEVICE_TABLE(pci, my_ids);

MODULE_DEVICE_TABLE 會把資訊寫進 .ko 的特殊 section,depmod 取出後產生 modules.alias。當核心看到 PCI device id 1234:5678,就會 modprobe 這個模組。

USB、PCI、I2C、platform device 都有對應的 device table 機制。

模組生命週期與引用計數#

模組可以是「正在被使用」狀態 ── 例如某個檔案系統模組正在被 mount,這時 rmmod 會被拒絕。靠 try_module_get / module_put 維護引用計數,多數情況下子系統會替你做(例如 register_filesystemmisc_register)。

lsmod
# Module                  Size  Used by
# nf_conntrack          172032  4 nf_nat,nf_conntrack_netlink,...

Used by 那欄就是引用計數 + 反向依賴。

核心日誌等級#

printk 的等級(從重到輕):

KERN_EMERG    "<0>"   // 系統不可用
KERN_ALERT    "<1>"   // 需要立即處理
KERN_CRIT     "<2>"   // 嚴重狀況
KERN_ERR      "<3>"   // 錯誤
KERN_WARNING  "<4>"   // 警告
KERN_NOTICE   "<5>"   // 正常但需要關注
KERN_INFO     "<6>"   // 資訊
KERN_DEBUG    "<7>"   // 除錯
printk(KERN_INFO "value=%d\n", x);
// 等同:
pr_info("value=%d\n", x);     // 推薦寫法
pr_err("oops: %d\n", err);
pr_debug("trace point\n");    // 預設不輸出,需要動態啟用

pr_debug 配合 CONFIG_DYNAMIC_DEBUG,可以在執行期透過 /sys/kernel/debug/dynamic_debug/control 開關,不用重編模組。

開發實務#

編譯依賴#

模組必須對應準確的核心版本/lib/modules/$(uname -r)/build 是個 symlink,指向你跑的這顆核心的 source / headers。如果系統沒裝 linux-headers-$(uname -r),編譯會失敗。

簽章與 secure boot#

近代 Linux 在啟用 Secure Boot 時,會強制要求模組有有效簽章(CONFIG_MODULE_SIG_FORCE)。自製模組要嘛簽章、要嘛關閉強制。/proc/sys/kernel/tainted 第 13 bit 標示「載入了未簽章模組」。

卸載失敗的常見原因#

sudo rmmod hello
# rmmod: ERROR: Module hello is in use

可能原因:

  1. 引用計數沒歸零(有東西還在用)
  2. 模組註冊了某個資源但沒在 __exit 釋放
  3. Workqueue / timer / kthread 還在跑

開發時常用 rmmod -f(force)── 不要這樣做,會讓核心狀態不一致,幾乎一定會 panic。正確做法是修 __exit 函式,確保所有資源被釋放。

小結#

核心模組是進入核心開發的最低門檻:

  1. 寫一個 .c + Makefile
  2. module_init / module_exit 註冊進入離開點
  3. MODULE_LICENSE("GPL") 不要忘
  4. module_param 讓參數可調
  5. EXPORT_SYMBOL / EXPORT_SYMBOL_GPL 給其他模組用
  6. 開發用 insmod,部署用 modprobe

下一章我們把模組做成「會出現在 /dev/ 下的字元裝置」,讓 userspace 用 read/write/ioctl 跟它互動。