说明:只供学习交流
一,什么是内核模块
Linux内核的整体结构非常庞大,其包含的组件也非常多,如何使用需要的组件呢:
方法一:把所有的组件都编译进内核文件,即:zImage或bzImage,但这样会导致两个问题:一是生成的内核文件过大;二是如果要添加或删除某个组件,需要重新编译整个内核。
有没有一种机制能让内核文件(zImage或bzImage)本身并不包含某组件,而是在该组件需要被使用的时候,动态地添加到正在运行的内核中呢?
有,Linux提供了一种叫做“内核模块”的机制,就可以实现以上效果。
内核模块具有如下特点:
(1)可以根据需求,在内核运行期间动态的安装或卸载。
(2)内核模块是具有独立功能的程序。它可以被单独编译,但是不能单独运行,它的运行必须被链接到内核作为内核的一部分在内核空间中运行。
(3):模块编程和内核版本密切相连,因为不同的内核版本中某些函数的函数名会有变化。因此模块编程也可以说是内核编程。
(4):模块本身不被编译进内核映像,从而控制了内核的大小;模块一旦加载,就和内核中其他部分完全一样。
一个简单的范例:
#include <linux/init.h>
#include <linux/module.h>
static int hello_init(void)
{
printk(KERN_WARNING"Hello, world !\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_INFO "Goodbye, world\n");
}
module_init(hello_init);
module_exit(hello_exit);
说明:代码中__init和__exit是Linux内核的宏定义,使系统在初始化或卸载完成后释放掉该函数,并释放其占用的内存。
1、模块加载函数(必需)
安装模块时被系统自动调用的函数,通过module_init宏来指定。
2、模块卸载函数(必需)
卸载模块时被系统自动调用的函数,通过module_exit宏来指定。
二,模块的安装与卸载
加载:insmod(insmodhello.ko)
卸载:rmmod(rmmodhello)
查看:lsmod
加载:modprobe(modprobe hello)
modprobe如同insmod, 也是加载一个模块到内核。它的不同之处在于它会根据文件
/lib/modules/<$version>/modules.dep
来查看要加载的模块,看它是否还依赖于其他模块,如
果是,modprobe会首先找到这些模块, 把它们先加载到内核。
三,内核打印函数printk
Printk和printf最大的区别是printk可以通过附加不同日志级别(loglevel),或者说消息优先级,可让printk根据这些级别所表示的严重程度对消息进行分类。在头文件<linux/kernel.h>中定义了8种可用的日志级别字符串,对应的文件内容如下:
#define KERN_EMERG "<0>" /*紧急事件消息,系统崩溃之前提示,表示系统不可用 */
#define KERN_ALERT "<1>" /*报告消息,表示必须立即采取措施 */
#define KERN_CRIT "<2>" /*临界条件,通常涉及严重的硬件或软件操作失败*/
#define KERN_ERR "<3>" /*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/
#define KERN_WARNING "<4>" /*警告条件,对可能出现问题的情况进行警告*/
#define KERN_NOTICE "<5>" /*正常但又重要的条件,用于提醒。常用于与安全相关的*/
#define KERN_INFO "<6>" /*提示信息,如驱动程序启动时,打印硬件信息*/
#define KERN_DEBUG "<7>" /*调试级别信息*/
数值越小优先级越高。在/proc/sys/kernel/printk中记录了与printk打印相关的几个数值,如在RHEL5中这个文件内容是:
可见该文件包含了4个整数值,从左到右分别是
(1):当前控制台日记级别:优先级小于此值时,printk才能打印到终端(纯字符界面)。
(2):未明确指定日记级别的默认消息日志级别。
(3):最小充许设置的控制台日志级别数值。
(4)系统引导时默认的日志级别:限定系统启动时控制台打印情况。
注意:在Xwindows图形环境下,虚拟终端不会打印printk信息(除非声明为最高优先级KERN_EMERG),这些信息的输出进到了内核回环缓冲区或日志文件中。
如果系统同时运行klogd和syslogd这两个日志服务程序,printk的信息会追加到/var/log/messages日志文件中。如果klogd没有运行,则这些消息不会传递到用户空间,这时只能查看/proc/kmesg文件(里面是内核回环缓冲区的消息),该文件不能像普通文件一样的打开,需使用dmesg命令查看。
四,模块可选信息
(1):许可证申明(MODULE_LICENSE)。
宏MODULE_LICENSE用来告知内核,该模块带有一个许可证,没有这样的说明,加载模块时内核会抱怨。有效的许可证有"GPL“、"GPL v2"、"GPL and additional rights"、"Dual
BSD/GPL"、"Dual MPL/GPL"和"Proprietary"。
(2)作者申明(可选)
MODULE_AUTHOR(“Simon Li");
(3)模块描述(可选)
MODULE_DESCRIPTION("Hello WorldModule");
(4)模块版本(可选)
MODULE_VERSION("V1.0");
(5)模块别名(可选)
MODULE_ALIAS("a simple module");
(五)模块参数
模块参数传递的概念:
对于如何向模块传递参数,LinuxKernel提供了一个简单的框架,其充许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,在驱动程序里,参数的用法如同全局变量。
(1)module_param
通过宏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);
如果模块内部的变量名和外部参数的变量名不同,则使用
module_param_named(name, variable, type,perm);
其中name为外部可见的参数名。
Variable为源文件内部的全局变量名。其他参数同module_param。
注意:这类宏函数并不会声明变量,因此在使用宏之前,必须声明变量,另外参数变量还必须是全局的,放在模块源文件开头。
(二):module_param_array
module_param_array(name, type, nump, perm)宏定义一个模块参数数组,并让内核在模块插入时把命令行的以逗号分开的参数序列传入该模块参数数组中。如果内部的数组名与外部的数组名不同可以使用:
module_param_array_named(name, array, type,nump,perm)。
参数说明:name:模块参数数组,即是外部模块的参数名又是程序内部的变量名,该数组必须静态分配。
type:表示参数的数据类型。
nump:一个整形数,其值表示有多少个参数存放在数组name中,NULL表示不关心用户提供的个数。
perm:sysfs的访问权限。
注意:如果name表示字符串数组时,其中的字符串不能包含逗号,否则一个字符串会被解析成两个。
范例:
Static int fish[MAX_FISH];
Static int nr_fish;
//最终传递数组的个数存在nr_fish中
Module_param_array(fish, int, &nr_fish,0444);
(3)module_param_string
module_param_string(name, string, len, perm)定义一个模块字符串参数,并让内核在模块插入时把命令行的字符串参数直接复制到程序中的字符数组内。
参数说明:name:外部可见的参数名。
string:内部的变量名。
len:以string命名的buffer大小(可以小于buffer的大小,但是没有意义)。
perm:sysfs的访问权限(或者perm为0,表示完全关闭相对应的sysfs项)。
范例:
static char species[BUF_LEN];
module_param_string(specifies, species,BUF_LEN, 0);
注意:以上宏函数的perm表示的权限值不能包含让普通用户也用写权限,否则编译报错。这点可参考linux/moduleparam.h中__module_param_call()宏的定义。
例子:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("param test");
MODULE_AUTHOR("tanghui");
static char *str_var = "Hello Linux";
static int a = 1;
static int int_array[6];
static char string[100];
int narr;
module_param(a, int , 0644); //权限不能为0666,一般用户不能有写权限,否则编译出错
module_param(str_var, charp, 0644);
module_param_array(int_array, int, &narr, 0644);
module_param_string(str, string, 100, 0644);
static int __init Module_param_init(void)
{
printk(KERN_WARNING "Param Test!\n");
printk("a = %d\n", a);
printk("%s\n", str_var);
int i = 0;
for (i = 0; i < narr; i++)
{
printk("%d\t", int_array[i]);
}
printk("\n%s\n", string);
printk("\n");
return (0);
}
static void __exit Module_param_exit(void)
{
printk(KERN_WARNING "Bye!\n");
}
module_init(Module_param_init);
module_exit(Module_param_exit);
运行结果:
提示:这里我们还需要掌握一个知识:前面提到过,函数module_param(name, type, perm)的第3个参数perm表示在sysfs中建立的对应文件的权限,事实上当插入模块param.ko后,内核会在/sys/module/中建立param/目录。而由于模块param中通过param中通过module_param等方法声明了4个参数a ,int_array, str, ptr,所以内核还会在/sys/module/param/目录下产生parameters/目录,并生成对应的文件a, int_array, str, ptr,而module_param函数的第三个参数perm正是指定这3个文件的访问权限,通过写这3个文件可以修改对应的参数,而通过读它们则可以获得其当前值。如果perm值填0,内核将不会产生对应参数文件。
六,内核符号导出
(1)内核符号的概念:
在编程中,一个符号(symbol)是一个程序的创建块:它是一个变量名或一个函数名。正如你自己编制的程序一样,内核具有各种符号也是不应该感到惊奇的。当然,区别在于内核是一个非常复杂的代码块,并且含有许多许多的全局符号。
(2)存放内核符号的文件
两个文件是用作符号表的:
/proc/kallsyms和内核顶层目录下的System.map文件。内核符号表的格式形式如命令:nm –n vmlinux的输出,其中vmlinux是内核镜像文件。
/proc/kallsyms是一个“proc文件”,是在内核启动时创建的。实际上它并不是一个真实的文件;它只是内核数据的简单表示形式。
System.map是文件系统上的一个真实文件。随着每次内核的编译,就会产生一个新的System.map文件,并且需要用该文件取代原来的文件。另外,System.map存放的内核符号不会因为插入模块而更新,只会因为重新编译内核而改变,/proc/kallsyms则包含了模块的符号列表。
(三)内核符号导出的方法
内核符号的导出使用:
EXPORT_SYMBOL(符号名)
EXPORT_SYMBOL_GPL(符号名)
其中EXPORT_SYMBOL_GPL只能用于包含GPL许可证的模块。
范例:
Calculate.c
1 #include <linux/module.h>
2 #include <linux/init.h>
3 #include <linux/kernel.h>
4
5 MODULE_LICENSE("GPL");
6 MODULE_AUTHOR("tang hui");
7 MODULE_DESCRIPTION("Module_export Test");
8
9 int add_integar(int a, int b)
10 {
11 return (a + b);
12 }
13
14 int sub_integar(int a, int b)
15 {
16 return (a - b);
17 }
18
19 static int __init sym_init(void)
20 {
21 return (0);
22 }
23
24 static void __exit sym_exit(void)
25 {
26
27 }
28
29 module_init(sym_init);
30 module_exit(sym_exit);
31 EXPORT_SYMBOL(add_integar);
32 EXPORT_SYMBOL(sub_integar);
33
Hello.c
1 #include <linux/kernel.h>
2 #include <linux/module.h>
3 #include <linux/init.h>
4
5 MODULE_LICENSE("GPL");
6 MODULE_AUTHOR("tang hui");
7 MODULE_DESCRIPTION("Module_export Test");
8
9 extern int add_integar(int a, int b);
10 extern int sub_integar(int a, int b);
11
12 static int __init hello_init(void)
13 {
14 int ret = add_integar(1, 2);
15
16 printk(KERN_WARNING "hello init, ret = %d\n", ret);
17
18 return (0);
19 }
20
21 static void __exit hello_exit(void)
22 {
23 int res = sub_integar(2, 1);
24
25 printk(KERN_WARNING "hello exit, res = %d\n", res);
26 }
28 module_init(hello_init);
29 module_exit(hello_exit);
Makefile:
Makefile:
1 ifneq ($(KERNELRELEASE),)
2
3 obj-m := calculate.o hello.o
4
5 else
6
7 KDIR := /lib/modules/2.6.29/build
8
9 all:
10 make -C $(KDIR) M=$(PWD) modules
11
12 clean:
13 rm -f *.ko *.o *.mod.o *.mod.c *.order *.symvers
14
15 endif
运行结果:
注意:模块加载的顺序,hello.ko依赖于calculate.ko,所有要先加载calculate.ko,在加载hello.ko。
(七)内核模块开发常见的问题
(1)版本不匹配
解决方法:
1、使用modprobe --force-modversion强行插入
2、确保编译内核模块时,所依赖的内核代码版本等同于当前正在运行的内核。
内核模块版本信息可以通过命令modinfo查看:modinfo *.ko
好了,到此为止,模块的基础知识我已经介绍完了,之所以把这些知识整理成文档,一则为了以后好复习,二则想把自己所学与大家分享。我会持续更新我的博客,从Linux命令,到Linux应用开发,Linux驱动,Linux系统移植,Uboot开发与移植,项目的制作等等。希望大家多多关注与支持。