為什麼有核心模組#
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_SYMBOL 與 extern 關係,產生 /lib/modules/$(uname -r)/modules.dep。modprobe(不是 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_filesystem、misc_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可能原因:
- 引用計數沒歸零(有東西還在用)
- 模組註冊了某個資源但沒在
__exit釋放 - Workqueue / timer / kthread 還在跑
開發時常用 rmmod -f(force)── 不要這樣做,會讓核心狀態不一致,幾乎一定會 panic。正確做法是修 __exit 函式,確保所有資源被釋放。
小結#
核心模組是進入核心開發的最低門檻:
- 寫一個
.c+Makefile module_init/module_exit註冊進入離開點MODULE_LICENSE("GPL")不要忘module_param讓參數可調EXPORT_SYMBOL/EXPORT_SYMBOL_GPL給其他模組用- 開發用
insmod,部署用modprobe
下一章我們把模組做成「會出現在 /dev/ 下的字元裝置」,讓 userspace 用 read/write/ioctl 跟它互動。