一、问题现象:
在insmod时调用的init函数代码执行过程中出现oops,导致rmmod卸载失败,此时不得不重启目标板?
二、下面是《精通linux设备驱动程序开发》中模拟鼠标的输入设备驱动的内核模块vms.c代码:
#include <linux/fs.h>
#include <asm/uaccess.h>
#include <linux/input.h>
#include <linux/platform_device.h>
#include <linux/pci.h>
#include <linux/module.h>
static struct platform_device *vms_dev;
static struct input_dev *vms_input_dev;
static ssize_t write_vms(struct device *dev, struct device_attribute *attr, const char *buffer, size_t count)
{
int x, y;
sscanf(buffer, "%d%d", &x, &y);
printk("(x,y)=(%d,%d)\n", x, y);
input_report_rel(vms_input_dev, REL_X, x);
input_report_rel(vms_input_dev, REL_Y, y);
input_sync(vms_input_dev);
return count;
}
DEVICE_ATTR(coordinates, 0644, NULL, write_vms);
static struct attribute *vms_attrs[] = {
&dev_attr_coordinates.attr,
NULL
};
static struct attribute_group vms_attr_group = {
.attrs = vms_attrs,
};
static int __init vms_init(void)
{
int err;
printk("vms_init===========\n");
vms_dev = platform_device_register_simple("vms", -1, NULL, 0);
if (IS_ERR(vms_dev)) {
printk("############################platform_device_register_simple failed\n");
return PTR_ERR(vms_dev);
}
printk("vms_dev = 0x%x\n", vms_dev);
err = sysfs_create_group(&vms_dev->dev.kobj, &vms_attr_group);
if (err) {
printk("==============================sysfs_create_group failed\n");
return -1;
}
printk("vms_init++++++++++\n");
vms_input_dev = input_allocate_device();
if (!vms_input_dev) {
printk("Bad input_allocate_device()\n");
return -1;
}
//vms_input_dev->name = "Vms_device";
strcpy(vms_input_dev->name, "Vms test"); // oops!,程序退出
set_bit(EV_REL, vms_input_dev->evbit);
set_bit(REL_X, vms_input_dev->relbit);
set_bit(REL_Y, vms_input_dev->relbit);
input_register_device(vms_input_dev);
//strcpy(vms_input_dev->name, "Vms test");
printk("Virtual Mouse Driver Initialized.\n");
return 0;
}
static void __exit vms_cleanup(void)
{
input_unregister_device(vms_input_dev);
sysfs_remove_group(&vms_dev->dev.kobj, &vms_attr_group);
platform_device_unregister(vms_dev);
printk("Virtual Mouse Driver Exited.\n");
return;
}
module_init(vms_init);
module_exit(vms_cleanup);
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Xumin");
insmod ./vms.ko后通过dmesg看到的信息:
然后sudo rmmod vms模块,会发现卸载不了:
我们知道,rmmod是调用sys_delete_module函数进行删除模块的,下面是其具体实现的的解析:
所以需要通过编写专门用于卸载vms内核模块的内核模块force_rmmod,下面是force_rmmod.c的源代码:
#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>
static void force(void)
{
int symbol_addr;
printk("XXXXXX, force!\n");
symbol_addr = kallsyms_lookup_name("vms_dev");
platform_device_unregister((struct platform_device*)(*(int*)symbol_addr));
}
static int __init force_rmmod_init(void)
{
struct module *mod, *relate;
int cpu;
int symbol_addr;
symbol_addr = kallsyms_lookup_name("vms_dev");
printk("YYYYY, symbol_addr:0x%x\n", symbol_addr);
printk("[module init] name:%s, state:%d\n", THIS_MODULE->name, THIS_MODULE->state);
list_for_each_entry(mod, THIS_MODULE->list.prev, list)
{
if (strcmp(mod->name, "vms") == 0) {
printk("[vms]:name:%s, state:%d, refcnt:%u\n",
mod->name ,mod->state, module_refcount(mod));
if (!list_empty(&mod->source_list)) {
list_for_each_entry(relate, &mod->source_list, source_list)
printk("[relate]:%s\n", relate->name);
} else {
printk("used by NULL...\n");
}
mod->state = 0;
mod->exit = force;
for_each_possible_cpu(cpu)
local_set((local_t*)per_cpu_ptr(mod->refptr, cpu), 0);
//local_set(__module_ref_addr(mod, cpu), 0);
//per_cpu_ptr(mod->refptr, cpu)->decs;
//module_put(mod);
printk("[after]:name:%s, state:%d, refcnt:%u\n",
mod->name, mod->state, module_refcount(mod));
}
}
return 0;
}
static void __exit force_rmmod_exit(void)
{
printk("[module exit] name:%s, state:%d\n", THIS_MODULE->name, THIS_MODULE->state);
}
module_init(force_rmmod_init);
module_exit(force_rmmod_exit);
MODULE_LICENSE("GPL");
通过安装force_rmmod.ko后,会发现vms模块目前的引用计数为1,且状态处于1,通过上面对sys_delete_module函数的理解得知,删除一个模块,需要将模块状态置为0,且引用计数置为0。
下面是模块的基本知识:
extern struct module __this_module;
#define THIS_MODULE (&__this_module);
enum module_state{
MODULE_STATE_LIVE; // 模块存活,0
MODULE_STATE_COMING; // 正在加载模块,1
MODULE_STATE_GOING; // 正在卸载模块,2
};
struct module {
enum module_state state; // 模块状态
/* Member of list of modules */
struct list_head list; // 内核模块链表
/* Unique handle for this module */
char name[MODULE_NAME_LEN]; //模块名称
...
#ifdef CONFIG_MODULE_UNLOAD
/* What modules depend on me? */
struct list_head modules_which_use_me;
/* Who is waiting for us to be unloaded */
struct task_struct *waiter;
/* Destruction function. */
void (*exit) (void);
#ifdef CONFIG_SMP
char *refptr;
#else
local_t ref;
#endif
#endif
...
};
static inline local_t *__module_ref_addr(struct module *mod, int cpu)
{#ifdef CONFIG_SMP
return (local_t *) (mod->refptr + per_cpu_offset(cpu));
#else
return &mod->ref;
#endif
}
但若仅将设置模块的引用计数和状态为0,还是会出现sys_delete_module因调用模块exit的函数(即vms_cleanup函数)而出现宕机的问题(因为程序出现oops,导致input_register_device(vms_input_dev); 根本没有执行,而该exit函数却调用了input_unregister_device(vms_input_dev); ,从而导致宕机)。为了解决这个问题,需要把exit换成一个能成功执行的函数。但问题依然没能完全解决,虽然此时可以rmmod vms成功,执行lsmod也查知vms在模块链表中已经删除,但vms模块因为在oops之前已经执行过 vms_dev = platform_device_register_simple("vms", -1, NULL, 0); 仍然会导致后续再执行insmod ./vms.ko时失败,因为该注册函数对应的注销函数没有被调用到。vms_dev是一个内核变量符号,通过sudo cat /proc/kallsyms | grep vms_dev 可以得知vms_dev的地址。(注意,在ubuntu下,必须使用root权限才能获取到符号地址!)然后在force_rmmod.c的exit函数中将这个vms_dev注销掉。但这种做很麻烦,每次都要手动去获取vms_dev的地址。其实内核可以通过kallsyms_lookup_name函数获取到vms_dev的地址,见force_rmmod的exit代码。
在执行insmod ./force_rmmod.ko之后,便可以随心所欲地安装和卸载vms.ko了。
三、问题解决思路
1、首先弄懂内核模块为啥无法删除? rmmod是通过sys_delete_module来卸载模块的,删除内核模块的必要条件是该模块的引用计数为0,且状态为“MODULE_STATE_LIVE; // 模块存活,0”,然后才调用模块的exit函数。如果执行exit时失败,甚至系统宕机,我们需要分析模块的init函数,仔细分析其流程,设计我们自己的exit函数。
2、执行cat /proc/kallsyms | grep vms_dev,结果发现其符号地址为0。原来是ubuntu的安全机制处理的结果,需要root权限才能查看地址。其对应的内核中获取符号地址的方法是:kallsyms_lookup_name函数。
3、程序出现rmmod失败的原因一是卸载的模块被其他模块引用,或者模块的初始化代码出现Bug,虽没有其他模块引用,但其模块引用计数也是1。所以需要仔细排查模块初始化代码中是否有异常退出的情况发生。
四、force_rmmod.c对应的Makefile(vms.c对应的Makefile类似)
KVERS = $(shell uname -r)
obj-m := force_rmmod.o
all: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(shell pwd) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean