0 问题:某项目中,关于一种调用设备驱动程序,出现异常时,驱动设备无法正常退出(lsmod 显示驱动设备被占用、无法rmmod 退出),也无法继续使用的问题。
1 linux 模块
内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块(Loadable Kernel Module,LKM),我们简称为模块。模块具有以下特点:
模块本身不被编译入内核映像,从而控制了内核的大小 。
模块一旦被加载,它就和内核中的其他部分一样 。
模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功能。
2 编译内核模块
hello 模块
#include <linux/module.h> //所有模块都需要的头文件
#include <linux/init.h> // init&exit相关宏
#include <linux/kernel.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("xxx");
MODULE_DESCRIPTION("hello world module");
static int __init hello_init(void)
{
printk(KERN_WARNING "hello world.\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_WARNING "hello exit!\n");
}
module_init(hello_init);
module_exit(hello_exit);
Makefile
ifneq ($(KERNELRELEASE),)
obj-m :=hello.o
else
KDIR :=/lib/modules/$(shell uname -r)/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers *.order
endif
使用
make
sudo insmod hello.ko
sudo rmmod hello
dmesg
[10611.395806] hello world.
[10644.832158] hello exit!
KERNELRELEASE
这个 makefile 在一次典型的建立中要被读 2 次。
KERNELRELEASE是在内核源码的顶层Makefile中定义的一个变量,在第一次(刚开始)读取执行此Makefile时,KERNELRELEASE没有被定义(为空),所以make将读取执行else之后的内容。如果make的目标是clean,直接执行clean操作,然后结束。当make的目标为all时,-C(KDIR)指明跳转到内核源码目录下读取那里的Makefile,M=(PWD)表明然后返回到当前目录继续读入、执行当前的Makefile。当从内核源码目录返回时,KERNELRELEASE已被被定义,kbuild也被启动去解析kbuild语法的语句,make将继续读取else之前的内容(指 obj-m :=hello.o)。else之前的内容为kbuild语法的语句,指明模块源码中各文件的依赖关系,以及要生成的目标模块名。
以上介绍了内核模块的概念以及内核模块的编译。
3 强制卸载内核模块
发现一篇非常好的文章,对照无法强制卸载内核模块的各个情况,相应进行了介绍。
原文链接:Linux强制卸载内核模块(由于驱动异常导致rmmod不能卸载)
对照文章,本人遇到的驱动被占用问题属于以下这种类型。
引用计数在有其它模块或者内核本身引用的时候不为0,要卸载就要等待它们不引用为止 | used | 需要通过外部修正的方式, 将驱动的引用计数置 0 即可 |
(1)现象
rmmod: ERROR: Module XXX is in use
(2)其本质就是模块的模块的引用计数不为 0
, 要解决此类问题, 只需要将模块的引用计数强制置为 0
即可.
-
查找到
none_exit
模块的内核模块结构struct moudle
, 可以通过find_module
函数查找到, 也可以参照find_module
函数实现. -
重置模块的引用计数
// 清除驱动的引用计数 for_each_possible_cpu(cpu) { local_set((local_t*)per_cpu_ptr(&(mod->refcnt), cpu), 0); //local_set(__module_ref_addr(mod, cpu), 0); //per_cpu_ptr(mod->refptr, cpu)->decs; //module_put(mod); } atomic_set(&mod->refcnt, 1);
4 强制卸载驱动源码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/cpumask.h>
#include <linux/list.h>
#include <asm-generic/local.h>
#include <linux/platform_device.h>
#include <linux/kallsyms.h>
#include <linux/sched.h>
/*
* 加载模块的时候, 传递字符串到模块的一个全局字符数组里面
*
* module_param_string(name, string, len, perm)
*
* @name 在加载模块时,参数的名字
* @string 模块内部的字符数组的名字
* @len 模块内部的字符数组的大小
* #perm 访问权限
*
* */
static char *modname = NULL;
module_param(modname, charp, 0644);
MODULE_PARM_DESC(modname, "The name of module you want do clean or delete...\n");
//#define CONFIG_REPLACE_EXIT_FUNCTION
#ifdef CONFIG_REPLACE_EXIT_FUNCTION
// 此处为外部注册的待卸载模块的exit函数
// 用于替代模块原来的exit函数
// 注意--此函数由于需要被待删除模块引用, 因此不能声明为static
/* static */ void force_replace_exit_module_function(void)
{
/
// 此处完善待卸载驱动的 exit/cleanup 函数
/
printk("module %s exit SUCCESS...\n", modname);
// platform_device_unregister((struct platform_device*)(*(int*)symbol_addr));
}
#endif // CONFIG_REPLACE_EXIT_FUNCTION
static int force_cleanup_module(char *del_mod_name)
{
struct module *mod = NULL, *relate = NULL;
int cpu;
#ifdef CONFIG_REPLACE_EXIT_FUNCTION
void *origin_exit_addr = NULL;
#endif
/
// 找到待删除模块的内核module信息
/
#if 0
// 方法一, 遍历内核模块树list_mod查询
struct module *list_mod = NULL;
/* 遍历模块列表, 查找 del_mod_name 模块 */
list_for_each_entry(list_mod, THIS_MODULE->list.prev, list)
{
if (strcmp(list_mod->name, del_mod_name) == 0)
{
mod = list_mod;
}
}
/* 如果未找到 del_mod_name 则直接退出 */
if(mod == NULL)
{
printk("[%s] module %s not found\n", THIS_MODULE->name, modname);
return -1;
}
#endif
// 方法二, 通过find_mod函数查找
if((mod = find_module(del_mod_name)) == NULL)
{
printk("[%s] module %s not found\n", THIS_MODULE->name, del_mod_name);
return -1;
}
else
{
printk("[before] name:%s, state:%d, refcnt:%u\n",
mod->name ,mod->state, module_refcount(mod));
}
/
// 如果有其他驱动依赖于当前驱动, 则不能强制卸载, 立刻退出
/
/* 如果有其他模块依赖于 del_mod */
if (!list_empty(&mod->source_list))
{
/* 打印出所有依赖target的模块名 */
list_for_each_entry(relate, &mod->source_list, source_list)
{
printk("[relate]:%s\n", relate->name);
}
}
else
{
printk("No modules depond on %s...\n", del_mod_name);
}
/
// 清除驱动的状态和引用计数
/
// 修正驱动的状态为LIVE
mod->state = MODULE_STATE_LIVE;
// 清除驱动的引用计数
for_each_possible_cpu(cpu)
{
local_set((local_t*)per_cpu_ptr(&(mod->refcnt), cpu), 0);
//local_set(__module_ref_addr(mod, cpu), 0);
//per_cpu_ptr(mod->refptr, cpu)->decs;
//module_put(mod);
}
atomic_set(&mod->refcnt, 1);
#ifdef CONFIG_REPLACE_EXIT_FUNCTION
/
// 重新注册驱动的exit函数
/
origin_exit_addr = mod->exit;
if (origin_exit_addr == NULL)
{
printk("module %s don't have exit function...\n", mod->name);
}
else
{
printk("module %s exit function address %p\n", mod->name, origin_exit_addr);
}
mod->exit = force_replace_exit_module_function;
printk("replace module %s exit function address (%p -=> %p)\n", mod->name, origin_exit_addr, mod->exit);
#endif
printk("[after] name:%s, state:%d, refcnt:%u\n",
mod->name, mod->state, module_refcount(mod));
return 0;
}
static int __init force_rmmod_init(void)
{
return force_cleanup_module(modname);
}
static void __exit force_rmmod_exit(void)
{
printk("=======name : %s, state : %d EXIT=======\n", THIS_MODULE->name, THIS_MODULE->state);
}
module_init(force_rmmod_init);
module_exit(force_rmmod_exit);
MODULE_LICENSE("GPL");
5 Makefile
MODULE_NAME := force_rmmod
#MODCFLAGS:=-O2 -Wall -DMODULE -D__KERNEL__ -DLINUX -std=c99
#EXTRA_CFLAGS += $(MODULE_FLAGS) $(CFG_INC) $(CFG_INC)
EXTRA_CFLAGS += -g -std=gnu99 -Wfatal-errors
ifneq ($(KERNELRELEASE),) # kernelspace
obj-m := $(MODULE_NAME).o
else # userspace
CURRENT_PATH ?= $(shell pwd)
LINUX_KERNEL ?= $(shell uname -r)
LINUX_KERNEL_PATH ?= /lib/modules/$(LINUX_KERNEL)/build
CURRENT_PATH := $(shell pwd)
modules:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
modules_install:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules_install
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
rm -f modules.order Module.symvers Module.markers
.PHNOY:
modules modules_install clean
endif
6 使用方法
# 通过 `modname` 制定待卸载驱动的信息
sudo insmod force_rmmod.ko modname=卸载驱动名称
sudo rmmod 卸载驱动名称