一.LINUX Kernel Module
LINUX Kernel是组件模式的,所谓组件模式是指:LINUX Kernel在运行时,允许“代码”动态的插入或者移出Kernel。
所谓模块是指:相关的一些子程序,数据、入口点和出口点共同组合成的一个单一的二进制映像,也就是一个可装载的Kernel目标文件。
模块的支持,使得系统可以拥有一个最小的内核映像,并且通过模块的方式支持一些可选的特征和驱动程序。
模块可动态的插入Kernel和从Kernel中移除,提供了一种调试内核程序的简便方法。模块的加载方式分为两种:静态加载和动态加载。
静态加载是将模块直接编译入内核,若模块需要修改和升级,我们就得重新编译整个内核,并且必须重新烧写内核,工作量加大。
动态加载是需要时,加载入内核,不需要时,从内核卸载。不需对内核进行编译和烧写,就可方便的对模块进行修改和升级,大大减小了工作量,也省去了我们很多的麻烦.
二.实践
1>.Hello World:
模块的开发就像写一个应用程序,它有自己的入口点,出口点,生命周期.
/*
*hello.c Hello,World! As a Kernel Module
*/
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
/*
*hello_init the init function , called when the module is loaded.
*Return zero if successfully loaded, nozero otherwise.
*/
static int hello_init(void)
{
printk(KERN_ALERT" Hello,World!\n");
return 0;
}
/*
*hello_exit the exit function ,called when the module is removed.
*/
static void hello_exit(void)
{
printk(KERN_ALERT"Good,Bye!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("HuG");
注释:
module_init()和module_exit()都是宏。
module_init()把hello_init()函数注册为这个模块的入口点。当模块被装载时,内核调用hello_init()函数。
module_init()的任务是把它的唯一参数作为相应模块的初始化函数。
初始化函数必须有如下的形式:int my_init(void)
由于初始化函数不会被外部的代码直接调用,所以,不必export这个函数。所以将初始化函数标志为static会更加的合理。
初始化函数的返回值:如果初始化成功,返回0;否则返回非零。
此处的初始化函数仅仅是打印出一句话。实际开发的模块中,初始化函数一般完成的工作是:注册资源,为数据结构分配内存等等。
同理,module_exit()是把hello_exit()函数注册为这个模块的出口点。当模块从内核中移除时,内核调用这个函数。
退出函数在返回之前,必须清除模块所占的资源,确保硬件处于一致状态等等。
退出函数必须有如下形式:void my_exit(void);同上,将函数标志为static会更合理。
注意:若采取静态加载的方式,将模块编译进内核,则内核启动时,调用static int my_init(void);但是退出函数static void my_exit(void);不会包含在内核的映像内,它也不会被调用。因为静态的加载方式,是将模块当作内核的一部分编译进内核中,所以代码永远不会从内核中删除。
宏MODULE_LICENSE()用于指定这个文件的版权许可。
MODULE_AUTHOR()用于指定本文件的作者。宏的值完全是为了提供说明信息。
2>.building Modules
在编译模块,让模块开始工作之前,我们必须确定模块的源码放置位置:
有两种方式:
一、把模块的源码增加到内核源码的一个合适的地方,即可以把文件作为一个"patch”,最终也可以将代码合到官方的源码树内。
二、在源码树之外维护和编译模块。
2.1>.在源码树内添加
我们通常的选择是将模块放入源码树之内,作为LINUX的一部分。这样模块可以生存在内核的源码树内。
如果,我们的模块是一个和USB相关的一个驱动,我们可将其放入drivers/usb/目录下。我们进入drivers/usb/gadget/目录下,我们发现gadget/目录下有很多驱动程序,都是些和USB相关的驱动。因此,我们将模块放在此目录下。
新建目录drivers/usb/gadget/hhtest/,将模块的所有文件放于此目录下(test.c+Makefile+Kconfig)
A>.test.c见hello world程序;
B>.Makefile中需写入:
obj-$(CONFIG_USB_GADGET_TEST) += test.o
C>.Kconfig写入:
config USB_GADGET_TEST
tristate "Hello Driver added by hh" //在menuconfig界面显示的配置选项名称
default n
help
test for adding driver to menuconfig.
/×××××××××××××××××××××格式说明××××××××××××××××××××××××××××××
config USB_GADGET_TEST
tristate "Gadget Netmeeting support"
default n
help
If you say Y here,netmeeting driver will be compiles into the kernel.You can also say M here and the driver will be built as a module named netmeeting.ko
If unsure , say N.
第一行定义了配置选项。事先已经假设存在有CONFIG_前缀,因此用不着我们写。
第二行说明了这个配置选项是三态的,即有三种选择方式。第一种是选择Y,表示相对应的程序编译到Kernel之内;第二种选择是M,表示相对应的程序编译成模块;第三种选择是N,表示不编译相对应的程序。
如果没有编译成模块这个选项,可以用bool代替tristate。指令tristate后带引号的文本是配置选项名,用于各种配置实用程序的选项显示。
第三行为这个配置选项指定一个默认值,这里的默认值是选择n。即:不编译相对应的程序。
第四行help指令表示其后面是帮助文本。有助于用户和开发人员理解相应的程序和建立自己的内核。
还有一些其它的指令。
//Kconfig文件用于衔接整个Kernel源码树;
×××××××××××××××××××××××××××××××××××××××××××××××××××/
D>.在Makefile中添加:obj-$(CONFIG_USB_GADGET_TEST) += hhtest/
//添加目的:使build系统能够沿着源码树往下找到hhtest/子目录
E>.其次要在一个已经存在的Kconfig文件中添加如下一行(一般在新建目录的父目录的Kconfig添加):
source "drivers/usb/gadget/hhtest/Kconfig"
source命令的作用是,让后面带的文件或者目录下的文件生效。相当于,让文件执行一下,让修改生效。省去了重启电脑的麻烦。
由于我们的Kconfig是新建的,所以,它并没有生效。
所以,我们在一个已经存在的Kconfig文件中,调用source命令 , 让我们新建的Kconfig文件生效。
==================================================================================================================
源码树内添加方法{多文件}:
如果新加模块包含多个文件,则可以将Makefile(gadget/hhtest/下)写为:
obj-$(CONFIG_USB_GADGET_ONLINE) += test.o
test-objs :=one.o two.o three.o four.o
这样,test.ko由one.c,two.c,three.c,four.c四个文件编译链接而成。
如果,想要为这些文件指定额外的gcc编译选项,在Makefile文件中添加类似如下的一行:
EXTRA_CFLAGS += -ONLY_TEST
如果我们将模块的所有文件放置在目录gadget/下,则将hhtest/目录下的Makefile中的内容,添加到目录gadget/目录下的Makefile中即可;
===================================================================================================================
2.2>.置于源码树之外
如果将模块源码置于源码树之外,那么在自己的源码目录下创建Makefile文件,并且添加:
obj-m := test.o
这样会把netmeeting.c编译成netmeeting.ko。如果有多个源文件,那么在Makefile文件中添加:
obj-m := test.o
test-objs :=one.o two.o three.o four.o
放置于源码树之外和放置于源码树之内,主要区别在于build过程。
放置于源码树之外,编译模块时,需要使用命令make去找到内核源码文件和基本的Makefile文件。如下所示:
make -C /kernel/source/location SUBDIRS=$PWD modules
/keinel/source/location是已经配置过的内核源码树位置。
3、安装模块(installing Modules)
编译后的模块要放在目录/lib/modules/version/kernel/下。一般是在Makefile中添加:
modules_install:
cp netmeeting.ko /lib/modules/version/kernel/
.PHONY:
modules_install
此步操作,需要root权限。
4.生成模块依赖(Generating Modules Dependencies)
LINUX模块实用工具能够理解模块间的依赖性。
也就是说如果模块chen依赖于模块sbb,那么在装载模块chen时,模块sbb会自动的装载。
也就是LINUX下常说的依赖性编译:文件A的编译依赖于B,B的编译依赖于文件C。
在root权限下,运行如下命令来建立模块间的依赖信息。
sudo depmod 或 sudo depmod -n //-n:show process
----------------------------------------------------------------------------------------------------------------------------------------
该命令详细的使用方法和规则,可通过下面命令来查看帮助:
depmod --help
----------------------------------------------------------------------------------------------------------------------------------------
模块间的依赖信息存放在文件/lib/modules/version/modules.dep中.
----------------------------------------------------------------------------------------------------------------------------------------
5、装载模块(loading Modules)
用命令insmod装载模块是最简单的一种方法。它请求内核装载指定的模块。
命令insmod不会检查模块间的依赖关系,也不会执行是否有错误的检查。
加载模块:
insmod (模块名)
卸载模块:
rmmod (模块名)
这两个工具很简单,实用,但是缺乏智能性。
××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××
而实用工具modprobe提供了依赖性关系的解决方案,智能的错误检查和报告等。装载模块时,是我们的首选。
root权限下,执行命令:
加载模块:
modprobe (模块名) (模块参数) //模块参数见下一节6.
modprobe命令,不仅试图装载写在其后的模块,还试图装载它依赖的所有模块。因此,它是首选。
卸载模块:
modprobe r (模块名)
这里的modprobe可以移除多个模块,还可以移除它依赖的并且不在使用中的其它模块。
6、模块参数(Module Parameters)
对于如何向模块传递参数,LINUX Kernel提供了一个简单的框架。其允许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,在驱动程序中,参数的用法如同全局变量。这些模块参数也能够在sysfs中显示出来(sysfs暂不清楚是什么)。结果,有许多办法来创建和管理模块参数。
通过宏module_param()定义一个模块参数:
module_param(name,type,perm);
参数解释:
name:既是用户看到的参数名,也是模块内接受参数的变量。
type:表示参数的数据类型,是下列之一:byte,short,ushort,int,uint,long,ulong,charp,bool,invbool。
参数类型分别是:a byte, a short integer, an unsigned short integer, an integer, an unsigned integer,
a long integer, an unsigned long integer, a pointer to a char, a Boolean, a Boolean whose value is
inverted from what the user specifies.
The byte type is stored in a single char and the Boolean types are stored in variables of type int.
The rest are stored in the corresponding primitive C types.
perm:指定了在sysfs中相应文件的访问权限。
访问权限用通常的八进制格式来表示或者通常的S_Ifoo定义。
八进制格式的使用和操作系统下是一致的。
0755:表示所有制是读写、执行的权限。所在组是读和执行的权限。其他用户是读和执行的权限。
S_Ifoo下:例如:
S_IRUGO|S_IWUSR(表示其它用户具有读权限,用户具有写权限)。用0表示完全关闭在sysfs中相对应的项。
因为宏是不能声明变量的,所以,在使用宏之前,必须先声明变量。典型的用法如下:
static unsigned int use_acm = 0;
module_param(use_acm,uint,S_IRUGO);
这些变量的声明是放在模块源文件的开头部分。即use_acm是全局变量。
我们可以使用宏module_param_named()使模块源文件内部的变量名与外部的参数名有不同的名字。
可以理解为,给变量名加了个引用。
module_param_named(name,variable,type,perm);
name:外部可见的参数名
variable:源文件内部的全局变量
例如:
static unsigned int max_test = 9;
module_param_named(maximum,max_test,int,0);
如果模块参数是一个字符串时,通常使用charp类型定义这个模块参数。内核复制用户提供的字符串到内存,并且相对应的变量指向这个字符串。例如:
static char *name;
module_param(name,charp,0);
另一种方法是通过宏
module_param_string()让内核把字符串直接复制到程序中的字符数组内。
module_param_string(name,string,len,perm);
name:外部的参数名
string:内部的变量名
len:以string命名的buffer大小(len可以小于buffer的大小,但是没有意义)
perm:sysfs的访问权限(或者perm为零,表示完全关闭相对应的sysfs项)。
以上的都是只能传递一个参数给模块。如果要给模块传递多个参数,可以通过宏
module_param_array(name,type,nump,perm);
name:既是外部模块的参数名又是程序内部的变量名。name数组必须静态分配。
type:是数据类型。
perm:sysfs的访问权限。
nump:是一个指针。其值表示有多少个参数存放在数组name中。
例如:
static int finish[MAX_FISH];
static int nr_fish;
module_param_array(fish,int,&nr_fish,0444);
我们可以通过宏module_param_array_named(name,array,type,nump,perm);
参数的意义和宏module_param_named()是一样的。
最后,用宏MODULE_PARM_DESC()对参数进行说明:
static unsigned short size = 1;
module_param(size,ushort,0644);
MODULE_PARM_DESC(size,"The size in inches of the fishing pole"\
"connected to this computer");
使用这些宏,需要包含头文件:<linux/moduleparam.h>
===================================================================================================================
7.输出符号(Exported Symbols)
当装载模块的时候,模块是动态的链接入内核之中。
然而,动态链接的二进制代码只能调用外部函数,所以外部函数必须明确的输出,才能被模块调用。
在内核中,通过EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()来达到目的。
输出的函数,可以被其它的模块调用。
没有输出的函数,不能被其它模块调用。
模块比核心内核映像代码具有更严格的链接和调用规则。因为所有核心源文件链接成一个单一的作为基础的映像,因此在内核中核心代码可以调用任何非静态的接口。
当然,输出符号也必须是非静态属性。
一套输出的内核符号称之为输出的内核接口,也称之为Kernel API。
当函数声明时,用EXPORT_SYMBOL()把函数输出。
例如:
int usb_gadget_register_driver(struct usb_gadget_driver *driver)
{
.......
}
EXPORT_SYMBOL(usb_gadget_register_driver);
这样,任何模块都可以调用函数usb_gadget_register_driver(),只要在源文件中包含了声明这个函数的头文件,或者extern这个函数的声明(这点同C语言)。
若你希望你的接口让只遵守GPL的模块调用。那么通过MODULE_LICENSE()的使用,内核链接器能够保证做到这一点。
EXPORT_SYMBOL_GPL(usb_gadget_register_driver);只允许标有GPL许可证的模块访问函数usb_gadget_register_driver()。
如果你的代码配置为模块方式,那么必须确保:源文件中使用的所有接口必须是已经输出的符号,否则导致在装载时链接错误。