Linux内核的功能有两种方式加载到内核中:
模块的特点:
模块主要由以下几部分组成:
主要完成模块的一些初始化工作,如申请内存资源、申请设备号、注册设备等,其形式如下:
static int __init xxxx_init(void)
{
...
return 0;
}
module_init(xxxx_init);
完成模块加载函数的代码编写后,需要使用宏module_init(xxx_init);
注册该模块加载函数。
调用时机:
当使用insmod
或modprobe
命令加载内核模块时,内核会调用该模块加载函数。
在代码中加载其他模块的方式:
request_module(const char *fmt, ...);
主要完成模块的一些去初始化工作,如释放申请的内存资源、设备号等,其形式如下:
static void __exit xxxx_exit(void)
{
....
}
module_exit(xxxx_exit);
完成模块卸载函数的代码编写后,需要使用宏module_exit(xxxx_exit);
注册该模块卸载函数。
调用时机:
当使用rmmod
或modprobe
命令卸载内核模块时,内核会调用该模块卸载函数。
许可证声明描述内核模块的许可权限,属必须添加的内容。如果不声明许可证,内核加载模块时会提示被污染警告(Kernel Tainted)。
可接受的许可证有:“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”、“Proprietary”。
模块许可证声明使用如下宏:
MODULE_LICENSE("GPL");
加载模块时,可通过模块参数方式向模块中的变量传递参数,该变量本身对应模块内部的全局变量。
可使用如下方式为模块定义一个参数:
//可参考`linux/moduleparam.h`
module_param(参数名, 参数类型, 参数读/写权限);
module_param_array(数组名, 数组类型, 数组长, 参数读/写权限);
参数名:模块内部的全局变量
参数类型:
参数读/写权限:(参考linux/stat.h
)
例如:
static char *name = "hello, world!!!";
module_param(name, charp, S_IRUGO);
int array_num[10];
int array_len;
module_param_array(array, int, &array_len, S_IRUGO);
在加载内核模块时,可通过如下方式向模块传递参数:
insmod/modprobe 模块名 参数名=参数值
insmod/modprobe 模块名 数组名=元素1,元素2,元素3 //元素之间以逗号分隔,不能再加空格
在加载内核模块时,若不向模块传递参数,则参数将使用默认值。
如果模块被编译进内核,也就无法通过insmod/modprobe
方式加载模块和传递参数。但可以修改bootloader,在bootargs里设置“模块名.参数名=值”的形式向该内置模块传递参数。
当模块被加载后,会在/sys/module
目录下出现以此模块命名的目录。
当“参数读写权限”为0时,则在sysfs
文件系统下不存在对应的文件节点。
当“参数读写权限”不为0时,则在sysfs
文件系统下的/sys/module/parameters
目录会出现一系列以参数名命令的文件节点。这些文件权限就是参数的读写权限,而文件内容就是参数的值。
/proc/kallsyms
文件对应内核符号表,记录了符号以及符号所在的内存地址。
模块可使用如下宏将其符号(全局变量或函数)导出到内核符号表中,供其他模块使用。其他模块使用这些符号前,只需先声明一下即可。
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名); //只适用于包含GPL许可权的模块
MODULE_AUTHOR(); //声明作者信息
MODULE_DESCRIPTION(); //声明描述信息,如功能
MODULE_VERSION(); //声明版本信息
MODULE_DEVICE_TABLE(); //声明设备表,如USB、PCI等设备驱动,通常会创建这个表说明支持的设备
MODULE_ALIAS(); //给模块取别名
Linux2.6
以后的内核提供了模块计数管理接口,来配合设备模型。
内核为不同类型的设备定义了struct module *owner
域,用来指向管理此设备的模块。
当开始使用某个设备时,内核调用try_module_get
增加管理此设备的模块的使用计数。
当不再使用此设备时,内核使用module_put
减少对管理此设备的管理模块的使用计数。
以此达到效果:当设备正在使用时,管理此设备的模块不能被卸载;当设备没有在使用时,模块才允许被卸载。
int try_module_get(struct module *module); //增加模块使用计数
void module_put(struct module *module); //减少模块使用计数
Linux2.6
以后的内核,计数管理由内核更底层的代码实现,开发人员所写驱动很少需要亲自调用计数管理接口。
编写一个简单的Makefile
完成模块的编译。
# 获取内核版本信息
KVERS = $(shell uname -r)
# 内核模块,模块名对应源文件名
obj-m += modulename.o
# 若一个模块包含多个源文件,需添加如下变量
modulename-objs := file1.o file2.o
# 模块编译的特殊标志,可选
#EXTRA_CFLAGS = -g -O0
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
该Makefile
文件需跟源代码位于同一级目录,运行make
命令编译生成模块,运行make clean
命令清除编译产物。
insmod module_name.ko
:加载模块,但不会自动解析该模块所依赖的其他模块,也不会加载依赖的其他模块。
modprobe module_name.ko
:加载模块,自动解析和加载该模块所依赖的其他模块。通过解析modules.dep
文件获取依赖关系。
rmmod module_name
:卸载模块。
modprobe -r module_name
:卸载模块及其依赖的模块。
lsmod
:打印系统中已加载的所有模块以及模块间的依赖关系。通过读取和解析/proc/modules
实现。
modinfo module_name.ko
:打印模块的信息,如模块作者、模块说明、模块许可证等。
/proc/modules
:存放内核已加载模块的信息,如模块名、设备号等。
/sys/module/module_name
:存放内核已加载模块的信息,一个目录对应一个模块。
/sys/module/module_name/parameters
:存放模块中以参数名命名的文件节点。
/lib/modules/
:存放模块之间的依赖关系,由整体编译内核时由depmod
工具生成。
/proc/kallsyms
:内核符号表,记录所有的内核符号及符号所在的内存地址。
模块源代码:
#include
#include
#include
static char *hello_name = "hello world";
module_param(hello_name, charp, S_IRUGO);
int hello_year = 1997;
module_param(hello_year, int, S_IRUGO);
unsigned int hello_array[10] = {12, 23, 34, 45, 56, 67, 78, 89, 90, 100};
unsigned int hello_array_len;
module_param_array(hello_array, uint, &hello_array_len, S_IRUGO);
static int hello_init(void)
{
int i = 0;
printk(KERN_INFO "call hello_init\n");
printk(KERN_INFO "hello_name:%s\n", hello_name);
printk(KERN_INFO "hello_year:%d\n", hello_year);
for (i = 0; i < sizeof(hello_array) / sizeof(hello_array[0]); i++)
{
printk(KERN_INFO "hello_array[%d]:%u\n", i, hello_array[i]);
}
return 0;
}
module_init(hello_init);
static void hello_exit(void)
{
printk(KERN_INFO "call hello_exit\n");
}
module_exit(hello_exit);
MODULE_AUTHOR("Handsome <[email protected]>");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("a simple test module");
MODULE_ALIAS("simple_test");
Makefile
文件:
KVERS = $(shell uname -r)
obj-m := hello.o
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
操作步骤:
make
编译生成hello.ko
sudo bash
进入root
insmod hello.ko
加载模块,查看打印rmmod hello
卸载模块,查看打印insmod hello.ko hello_name="welcome to heaven!!!" hello_year=2060 hello_array=11,22,33,44,55
加载模块并传递参数,查看打印lsmod
查看已加载模块中是否有本模块cat /proc/modules
查看已加载模块中是否有本模块cat /proc/kallsyms
查看内核符号表中是否有本模块的符号,如函数名、变量名。modinfo hello.ko
查看本模块的信息/sys/modules/hello/parameters
中是否有hello_name
、hello_year
、hello_array
这三个参数对应的文件并查看文件内容。rmmod hello
再次卸载模块