模块功能
1.什么是内核模块
Linux内核的整体结构非常庞大,其包含的组件也非常多,如何使用需要的组件呢:
方法一:把所有的组件都编译进内核文件,即zImage或bzImage。但这样会导致两个问题:一是生成的内核文件过大;二是要添加或删除某个组件,需要重新编译整个内核。
有没有一种机制能让内核文件(zImage或bzImage)本身并不包含某组件,而是在该组件需要被使用的时候,动态的添加到正在运行的内核中呢?
有,Linux提供了一种叫做"内核模块"的机制,就可以实现以上效果。
模块特点
内核模块具有如下特点:
(1)内核本身并不被编译进内核文件(zImage或bzImage);
(2)可以根据需要,在内核运行期间动态的安装或卸载;
范例(hello world):
#include <linux/module.h>
#include <linux/init.h>
static int __init hello_init()
{
printk("Hello World!\n");
return(0);
}
static void __exit hello_exit()
{
printk("<7>hello <0>exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
程序结构
1.模块安装函数(必需)
安装模块时被系统自动调用的函数,通过module_init宏来指定,如上hello world程序中module_init(hello_init);
为模块加载函数。
2.模块卸载函数(必需)
卸载模块时被系统自动调用的函数,通过module_exit宏来指定,如上hello world程序中module_exit(hello_exit);为模块卸载函数。
模块的编译
在Linux2.6下编译内核模块,通常使用makefile
内核文件由一个源文件构成,该如何编写makefile呢?下面是上述hello world源程序的makefile程序代码:
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
KDIR := /lib/modules/2.6.27.59/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers
endif
如果内核模块由多个源文件构成,该如何编写makefile。如一个内核模块由add.c和main.c两个源文件构成
add.c如下:
int add(int a, int b)
{
return a+b;
}
main.c如下:
#include <linux/module.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Davide Yuan");
MODULE_DESCRIPTION("Hello World Module!");
MODULE_ALIAS("A simplest module");
extern int add(int a, int b);
static int __init hello_init()
{
printk("Hello world!\n");
add(1, 2);
return 0;
}
static void __exit hello_exit()
{
printk("<7>hello <0>exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
则其makefile可以写为:
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
hello-objs := main.o add.o
else
KDIR := /lib/modules/2.6.27.59/build
all:
make -C $(KDIR) M=$(PWD) modules
clean:
rm -f *.ko *.o *.mod.o *.mod.c *.symvers
endif
安装与卸载内核模块
1.加载insmod(如:insmod hello.ko)
2.卸载rmmod(如:rmmod hello)
3.查看lsmod
4.加载modprobe(如:modprobe hello):modprobe如同insmod,也是加载一个模块到内核。他的不同之处在于他会根据文件/lib/modules/$<version>/modules.dep来查看要加载的模块,看他是否还依赖于其他模块,如果是,modprobe会首先找到这些模块,把他们先加载进内核。
模块可选信息
1.许可证申明:宏MODULE_LISENSE用来告知内核,该模块带有一个许可证,没有这样的说明,加载模块时内核会抱怨。有效的许可证有:"GPL"、"GPLv2"、"GPL and additional rights"、"Dual BSD/GPL"、"Dual MPL/GPL"和"Proprietary"。
2.作者申明(可选):如MODULE_AUTHOR("Simon li");
3.模块描述(可选):如MODULE_DESCRIPTION("Hello World Module");
4.模块版本(可选):如MODULE_VERTION("V1.0");
5.模块别名(可选):如MODULE_ALIAS("a simple module");
6.模块参数:通过宏module_param指定模块参数,模块参数用于在加载模块时传递参数给模块。module_param(name,type,perm);name是模块参数的名称type是这个参数的类型,perm是模块参数的访问权限。其中type常见值:bool,int,charp(字符串型)。perm常见值:S_IRUGO,任何用户都对/sys/module中出现的该参数具有读权限;S_IWUSR,允许root用户修改/sys/module中出现的该参数。例如:
int a = 3;
char *st;
module_param(a,int,S_IRUGO);
module_param(st,charp,S_IRUGO);
内核符号导出
/proc/kallsyms记录了内核中所有导出的符号的名字与地址。如下两个源文件hello.c、calculate.c:
hello.c如下:
#include <linux/module.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("David Yuan");
MODULE_DESCRIPTION("Hello World Module");
MODULE_ALIAS("A simplest module");
extern int add_integar(int a, int b);
extern int sub_integar(int a, int b);
static int __init hello_init()
{
int res = add_integar(1, 2);
return 0;
}
static void __exit hello_exit()
{
int res = sub_integar(2, 1);
}
module_init(hello_init);
module_exit(hello_exit);
calculate.c如下:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
int add_integar(int a, int b)
{
return a+b;
}
int sub_integar(int a, int b)
{
return a-b;
}
static int __init sym_init()
{
return 0;
}
static void __exit sym_exit()
{
}
module_init(sym_init);
module_exit(sym_exit);
EXPORT_SYMBOL(add_integar);
EXPORT_SYMBOL(sub_integar);
内核符号的导出使用:EXPORT_SYMBOL(符号名)、EXPORT_SYMBOL_GPL(符号名),其中EXPORT_SYMBOL_GPL只能用于包含GPL许可证的模块。
常见问题:版本不匹配
内核模块的版本由其所依赖的内核代码版本所决定,在加载内核模块时,insmod程序会将内核模块版本与当前正在运行的内核版本比较,如果不一致时,就会出现类似下面的错误:
insmod hello.ko
disagrees about version of symbol struct_module
insmod:error inserting 'hello.ko':-1 Invalid module format。
解决方法:
1.使用modprobe --force-modversion强行插入。
2.确保编译内核模块时,所依赖的内核代码版本等同于当前正在运行的内核。
注:可通过uname -r查看当前运行的内核版本。
2.4与2.6内核模块对比
详细参看网络资料。
总结——对比应用
对比应用程序,内核模块具有以下不同:应用程序是从头(main)到尾执行任务,执行结束后从内存中消失。内核模块则是先在内核中注册自己以便服务于将来的某个请求,然后他的初始化函数结束,此时模块仍然存在于内核中,直到卸载函数被调用,模块才从内核中消失。
内核打印
printk是内核中出现最频繁的函数之一,通过将printk与printf对比,将有助于理解。
相同点:打印信息。
不同点:printk在内核中使用,printf在应用程序中使用;printk允许根据严重程度,通过附加不同的"优先级"来对消息分类。
关于优先级,在<linux/kernel.h>中定义了8种记录级别。按照优先级递减的顺序分别是:
KERN_EMERG "<0>" :用于紧急消息,常常是那些崩溃前的消息。
KERN_ALERT "<1>":需要立刻行动的消息。
KERN_CRIT "<2>":严重情况。
KERN_ERR "<3>":错误情况。
KERN_WARNING "<4>":有问题的警告。
KERN_NOTICE "<5>":正常情况,但是仍然值得注意。
KERN_INFO "<6>":信息型消息。
KERN_DEBUG "<7>":用作调试消息。
没有指定优先级的printk默认使用DEFAULT_MESSAGE_LOGLEVEL优先级,他是一个在kernel/printk.c中定义的整数。
在2.6.29的内核中
#define DEFAULT_MESSAGE_LOGLEVEL。
控制台优先级配置:/proc/sys/kernel/printk 6 4 1 7
console_loglevel
Default_message_loglevel
Minimum_console_level
Default_console_loglevel
(完)
注:本文参看国嵌教育相关内容。