1 Linux 内核模块简介
Linux 内核是一个十分庞大的系统,如何能够为其瘦身,订制适合自己应用场景的 linux 系统,这就需要了解模块技术。
linux 内核与各模块的关系分为两种:
- 把所有需要的功能都直接编译进入内核,但这会带来两个问题,一是生成的内核很大,从而导致内核占用内存也很大;二是如果内核需要新增或者删除功能的话,必须要重新编译内核。
- 编出的内核本身不需要包含所有功能,而这些功能在需要使用时动态加载到内核,如此动态加载的功能单元就叫做模块。
Linux 提供的这种机制被称为模块,模块具有这样的特点:
- 模块本身不被编译进入内核,从而控制了内核的大小。
- 模块一旦被加载,它就和内核中的其它部分完全一样。
2 模块样例
以下是一个样例模块 hello.ko 样例:
/*
*a simple kernel module: hello
*
*Licensed under GPLv2 or later
*/
#include
#include
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World enter.\n");
return 0;
}
module_init(hello_init);
static void __exit hello_exit(void)
{
printk(KERN_INFO "Hello world exit.\n");
}
module_exit(hello_exit);
MODULE_AUTHOR("penghuster ");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("A simple Hello World Module.");
MODULE_ALIAS("a simplest module.");
编译此模块的方法可以分为两种方式:
2.1 与内核编译同步捆绑进行
在内核的相关功能单元(如 drivers)下创建 hello 文件夹,然后根据内核Kconfig 和 Makefile的编写规则,编写 hello 模块的 Kconfig 和 Makefile 文件。
drivers/hello/Kconfig:
#
# Hello driver configuration
#
#menu "HELLO driver"
comment "hello driver"
config CONFIG_HELLO
bool "support HELLO"
#endmenu
drivers/hello/Makefile:
#drivers/test/test/Makefile
#
#Makefile for the TEST CPU
#
obj-$(CONFIG_HELLO) += hello.o
另外在drivers/Kconfig文件中添加行 source "drivers/hello/Kconfig"
在drivers/Makefile文件中添加 obj-y += hello/
注意:hello/Makefile也可直接写为
obj-m += hello.o
或obj-y += hello.o
从而可以省略 Kconfig的相关工作。Kconfig是为了make menuconfig
配置而添加。
2.2 独立编译模块
需要进行独立模块编译前,需要确认 /lib/modules/
目录下是否已经安装你需要进行模块编译的内核版本。如果没有,请安装对应版本的 linux-image 和 Linux-image-extra 包,具体命令如下:
sudo apt-get install --reinstall linux-image-3.13.0-34-generic
sudo apt-get install --reinstall linux-image-extra-3.13.0-34-generic`
然后编写如下Makefile:
##此处表示编写在本机上使用的,交叉编译器进行交叉编译,此处也需要改变。
KVERS = $(shell uname -r)
obj-m += hello.o
#Specify flags for the module compilation
#EXTRA_CFLAGS = -g -o0
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(PWD) clean
2.3 内核模块的相关操作命令
insmod ./hello.ko
---加载模块
rmmod hello
---卸载模块
lsmod / cat /proc/modules
---查看所有模块
/sys/module/hello
下执行 tree -a
---查看模块的详细组成文件及其信息
/lib/modules/
---文件查看模块依赖
modinfo hello.ko
---查看模块信息
modprobe ./hello.ko
---加载该模块的同时,加载该模块的所有依赖模块
modprobe -r hello
---卸载该模块,同时卸载其依赖模块
3 Linux 内核模块的程序结构
3.1 模块加载函数
初始化函数原型
statitc int __init initialization_function(void)
{
/* 初始化代码*/
}
module_init(initialization_function);
内核中也可以主动调用以下函数来灵活加载模块
request_module(module_name);
在 Linux ,所有标识为 __init 的函数如果直接编译进入内核,成为内核镜像的一部分,在连接的时候都会放到 .init.text 段区。
注意:很明显模块通过 insmod 加载,并没有编入到内核镜像中,两者是通过MODULE宏变量来实现是否编入内核。
#ifdef MODULE
#define module_init(initfn) static inline initcall_t __inittest(void)
{ return initfn; }
#else
#define module_init(x) __initcall(x);
#endif
#define __init __attribute__((__section__(".init.text")))
所有的 __init 函数在区段 .initcall.init 中保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些 __init 函数,并在初始化完成后,释放 init区段(包括 .init.text / .initcall.init)的内存.
除函数外数据也可以定义为 __initdata. 只有初始化阶段需要的函数.
3.2 模块卸载函数
函数原型
static void __exit cleanup_function(void)
{
//释放代码
}
module_exit(cleanup_function);
同样也存在 __exitdata 只在退出阶段可用的数据。
3.3 模块参数
我们可以使用 "module_param(param_name, param_type, r/w)" 为模块定义一个参数。例如:
static char *book_name = "dissecting Linux device Driver";
module_param(book_name, charp, S_IRUGO);
static int book_num = 4000;
module_param(book_num, int, S_IRUGO);
其参数类型可以是 byte, short, ushort, int, uint, long, ulong, charp(char *), bool 或 invbool(布尔的反)。
模块参数的传递,用户可以在加载模块时使用形式为 insmod(/modprbe) module_name param_name = praram_value
,如果模块被内置,则使用 bootloader 通过在 bootargs 里设置 “模块名.参数名 = 值” 值得形式传递给内核。
另外也可使用 “module_param_array(array_name, array_type, array_length, r/w)” 来传递数组参数。运行 insmod 或 modprobe 时,应该使用逗号分隔开数组元素。
模块加载成功后,在 /sys/module/ 目录下会出现以此模块名命名的目录。当参数的读写属性为 0 时,标示在 sysfs 文件系统下,不存在对应的文件节点;当模块存在 “参数读写属性” 不为 0 的命令行参数时, 此模块目录下将出现 parameters 目录,此目录下将出现与参数同名的文件,其文件的读写属性与参数一致。
3.4 导出符号
Linux 的 “/proc/kallsyms” 文件对应着内核符号表,它记录了符号以及符号所在的内存地址。
模块可以使用如下宏导出符号到内核符号中:
EXPORT_SYMBOL(symbol_name);
EXPORT_SYMBOL_GPL(symbol_name);
导出的符号可以被其它模块使用,只需要使用前申明下即可。EXPORT_SYMBOL_GPL 只适用于包含 GPL 许可权的模块。
3.5 模块声明与描述
Linux 内核模块的一般信息声明
MODULE_AUTHOR("penghuster ");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("A simple Hello World Module.");
MODULE_ALIAS("a simplest module.");
MODULE_DEVICE_TABLE(table_info);
对于 USB和PCI等设备,通常会创建一个 MODULE_DEVICE_TABLE,已表明该驱动模块所支持的设备。
3.6 模块的使用计数
Linux 2.4 内核中,模块自身通过 MOD_INC_USE_COUNT、MOD_DEC_USE_COUNT 宏来管理自己被使用的计数。
Linux 2.6 以后内核提供了模块计数管理接口 try_module_get(&module)
则增加使用计数函数 和 module_put(&module)
减少使用计数函数,从而取代 linux 2.4 的方式。只有当模块使用计数为 0 时,方能够卸载模块。