Linux内核模块引用计数始终为0问题求助
你遇到的模块引用计数始终为0的问题,核心是**struct file_operations的.owner字段自动维护引用计数的逻辑,仅在通过标准字符设备注册接口(register_chrdev/cdev_add)关联fops时生效,proc_create接口不会自动处理该引用计数逻辑**。
很多开发者误以为只要设置了.owner = THIS_MODULE,所有场景下打开文件都会自动给模块加引用,实际上procfs是虚拟文件系统,它的内部文件打开流程默认不会触发try_module_get对owner的计数操作,所以哪怕用户态持有打开的fd、甚至做了mmap映射,模块的use count还是0,rmmod能直接卸载触发空指针崩溃。
另外还有一个容易遗漏的点:mmap建立的用户态页映射,就算fd关闭了,只要映射没解除,用户态访问映射内存还是会调用模块里的fault回调等逻辑,这部分也需要额外持有模块引用,不然fd关了之后计数掉0,还是能被卸载。
不要依赖fops的owner自动计数,手动在open、mmap回调里维护模块引用计数,在release、munmap对应的vma关闭逻辑里释放计数,确保只要有用户态持有fd或者持有mmap映射,模块引用计数就不为0,无法被卸载。
具体需要实现三个核心逻辑:
- 实现
.open回调:打开文件时调用try_module_get(THIS_MODULE),给模块计数+1 - 实现
.release回调:关闭文件时调用module_put(THIS_MODULE),给模块计数-1 - 实现
.mmap回调时,在完成remap_pfn_range等映射操作前额外调用一次try_module_get(THIS_MODULE),同时给映射的vm_operations_struct设置.close回调,在munmap触发vma关闭时调用module_put(THIS_MODULE),避免mmap后用户态关闭fd但映射还存在时计数不准的问题。
module.c
#include <linux/fs.h> #include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/mm.h> static struct proc_dir_entry *proc_file; static void test_vma_close(struct vm_area_struct *vma) { // mmap映射解除时,释放之前持有的模块引用 module_put(THIS_MODULE); } static const struct vm_operations_struct test_vm_ops = { .close = test_vma_close, }; static int test_open(struct inode *inode, struct file *file) { // 打开文件时增加模块引用 try_module_get(THIS_MODULE); return 0; } static int test_release(struct inode *inode, struct file *file) { // 关闭文件描述符时释放模块引用 module_put(THIS_MODULE); return 0; } static int test_mmap(struct file *file, struct vm_area_struct *vma) { // 建立mmap映射时额外增加一次模块引用,防止fd关闭后映射仍存在时模块被卸载 try_module_get(THIS_MODULE); vma->vm_ops = &test_vm_ops; // 此处替换为实际业务的mmap逻辑,比如调用remap_pfn_range映射内核内存到用户态 // remap_pfn_range(vma, vma->vm_start, pfn, vma->vm_end - vma->vm_start, vma->vm_page_prot); return 0; } static const struct file_operations test_file_ops = { .owner = THIS_MODULE, .open = test_open, .release = test_release, .mmap = test_mmap }; static int __init initialize(void) { int error = 0; proc_file = proc_create(THIS_MODULE->name, 0444, NULL, &test_file_ops); if (!proc_file) { error = -EIO; } return error; } static void __exit teardown(void) { proc_remove(proc_file); } module_init(initialize); module_exit(teardown); MODULE_LICENSE("GPL");
Makefile
obj-m += test.o test-objs := module.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
注意:原Makefile里使用
SUBDIRS=是旧版本内核的写法,新版本内核直接用M=指定模块目录即可,不需要额外加EXTRA_CFLAGS引用用户态头文件,内核模块编译会自动使用内核源码树的头文件,添加用户态头文件路径反而会引发编译冲突。
- 加载模块后,用户态打开/proc下对应的模块文件,执行
lsmod可以看到模块引用计数为1,此时执行rmmod会直接返回Module is in use无法卸载 - 对文件做mmap映射后就算关闭fd,只要映射没通过
munmap解除,模块引用计数始终保持为1,无法被卸载 - 关闭fd、解除所有mmap映射后,引用计数回到0,才能正常rmmod卸载,不会触发系统崩溃。
内容的提问来源于stack exchange,提问作者Anton

