2. 内核模块基础:module_init / module_exit、LICENSE、作者信息、编译 Makefile

在编写 proc 文件系统读写回调之前,必须掌握 Linux 内核模块的基本骨架。内核模块是动态加载到内核空间的代码段,它通过 module_init 和 module_exit 宏声明入口与出口函数,通过 MODULE_LICENSE 声明许可证,通过 MODULE_AUTHOR 和 MODULE_DESCRIPTION 提供元信息。本章将拆解这些要素,并给出标准 Makefile 模板。

2.1 模块入口与出口:module_init / module_exit

每个内核模块必须且只能有一个入口函数和一个出口函数。入口函数在模块加载时执行,完成资源申请、proc 文件创建、数据结构初始化等操作;出口函数在模块卸载时执行,负责资源释放、proc 文件移除、注销回调等清理工作。

宏 / 函数 作用 调用时机 返回值
module_init(func) 声明模块入口函数 insmod 加载时 0 成功,负值失败
module_exit(func) 声明模块出口函数 rmmod 卸载时 void
入口函数名 通常为 xxx_init — int
出口函数名 通常为 xxx_exit — void
关键点:入口函数返回非 0 值时,内核认为模块加载失败,不会保留模块在内存中。出口函数不应返回错误,所有清理逻辑必须在出口函数内完成。

2.1.1 标准入口/出口函数模板

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

static int __init my_proc_init(void)
{
    pr_info("my_proc: module loaded\n");
    /* 在这里创建 proc 文件、分配内存、注册回调 */
    return 0;
}

static void __exit my_proc_exit(void)
{
    pr_info("my_proc: module unloaded\n");
    /* 在这里移除 proc 文件、释放内存、注销回调 */
}

module_init(my_proc_init);
module_exit(my_proc_exit);
提示:__init 和 __exit 是内核属性标记,告诉编译器将函数放入特定内存段。加载完成后 __init 函数占用的内存可以被回收。

2.2 模块元信息:LICENSE、AUTHOR、DESCRIPTION

Linux 内核要求每个模块明确声明许可证,否则内核会认为模块是“污染”的(tainted)。MODULE_LICENSE 必须使用内核认可的字符串,常见值如下:

许可证字符串 含义 是否污染内核
"GPL" GNU General Public License v2 否
"GPL v2" GPL v2 专用 否
"Dual BSD/GPL" BSD 或 GPL 双许可 否
"Proprietary" 专有许可证 是

除许可证外,建议提供作者和描述信息,便于 modinfo 命令查看:

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <your.email@example.com>");
MODULE_DESCRIPTION("Proc file system read/write example module");
MODULE_VERSION("1.0");
警告:如果模块使用了 GPL-only 符号(如 proc_create 系列函数),但声明了非 GPL 许可证,内核会拒绝加载该模块,并打印 Unknown symbol 错误。

2.3 模块编译:标准 Makefile 模板

内核模块不能使用用户空间的 gcc 直接编译,必须借助内核构建系统(Kbuild)。以下 Makefile 适用于单文件模块:

obj-m := my_proc.o

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

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

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

如果模块包含多个源文件,使用 模块名-objs 语法:

obj-m := my_proc.o
my_proc-objs := main.o proc_ops.o utils.o

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

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

clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean
变量 含义 示例
obj-m 需要编译成模块的目标文件 obj-m := my_proc.o
KDIR 内核源码树路径 /lib/modules/$(uname -r)/build
M=$(PWD) 告诉 Kbuild 在模块源码目录构建 M=$(shell pwd)
modules Kbuild 目标,生成 .ko 文件 $(MAKE) -C $(KDIR) M=$(PWD) modules

2.4 模块加载与卸载流程 SVG

下图展示了从 insmod 到 rmmod 的完整生命周期,以及 module_init / module_exit 在其中的位置:

模块加载与卸载生命周期 insmod rmmod module_init() module_exit() proc_create() proc_remove() 用户空间触发 insmod 调用内核 sys_init_module 系统调用 用户空间触发 rmmod 调用内核 sys_delete_module 系统调用

2.5 编译与加载验证步骤

  1. 准备源码:将上述模块代码保存为 my_proc.c,Makefile 保存为 Makefile。
  2. 执行编译:在源码目录运行 make,生成 my_proc.ko。
  3. 加载模块:sudo insmod my_proc.ko,使用 dmesg | tail 查看入口打印。
  4. 查看模块信息:modinfo my_proc.ko 显示许可证、作者、描述。
  5. 卸载模块:sudo rmmod my_proc,再次查看 dmesg 确认出口打印。
常见错误排查:
  • “Unknown symbol”:检查 MODULE_LICENSE 是否与使用的内核符号兼容。
  • “Invalid module format”:内核版本与编译头不匹配,确认 KDIR 指向当前运行内核。
  • “Permission denied”:加载/卸载需要 root 权限,使用 sudo。

2.6 本章小结