Linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。
与开发的内核核心子系统不同,模块开发更接近编写新的应用程序,因为至少要在模块文件中具有入口点和出口点。
下面是hello_world内核模块
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static int hello_init(void)
{
printk(KERN_ALERT "I bear a charmed life.\n");
return 0;
}
static void hello_exit(void)
{
printk(KERN_ALERT "Out,out,brief candle !\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Shakespeare");
hello_init()函数是模块的入口点,它通过module_init()注册到系统中,在模块装载时被调用。调用module_init()实际上不是真正的函数调用,而是一个宏调用,它唯一的参数便是模块的初始化函数。模块的所有初始化函数必须符合下面的形式:
int my_init(void);
因为init函数通常不会被外部函数直接调用,所以不必导出该函数,故它可被标记为static类型。init函数会返回一个int型数据,如果初始化顺利完成,那么它的返回值为0,失败的话,返回一个非0值。
hello_exit()函数是模块的出口函数,它由module_exit()注册到系统,在模块从内存卸载时,内核便会调用hello_exit()。在退出函数返回后,模块就被卸载了。
退出函数必须符号以下形式:
void my_exit(void);
MODULE_LICENSE()宏用于指定模块的版权,如果载入非GPL模块到系统内存,则会在内核中设置被污染标志。MODULE_AUTHOR()宏指定代码作者,完全是用作信息记录目的。
1 构建模块
在2.6内核中,由于采用了新的kbuild构建系统,现在构建模块相比从前更加容易,构建过程中的第一步是决定在哪里管理模块代码,你可以把模块源码加入到内核源代码数中,也可以在内核源码树外维护呵呵构建模块源码。
放在内核源代码树中
最理想的情况莫过于模块正式成为Linux内核的一部分,这样就会被存放在内核源代码树中。
首先你要清楚你的模块应该在内核源代码树中何处。设备驱动程序存放在内核源码树根目录drivers/的子目录下,在drivers内部,设备驱动文件被进一步细分。如字符设备存在于drivers/char/目录下,而块设备存放在drivers/block/目录下,USB设备则存放在drivers/usb/目录下。
假设你有一个字符设备,而且希望放在drivers/char/目录下,那么你要注意,在该目录下同时会存在大量的C源代码文件和其他目录。所以对于仅仅只有一两个源文件的设备驱动程序,可以直接存放在该目录下。如果驱动程序包含许多源文件和其他辅助文件,那么可以创建一个新子目录。
假设你想创建自己代码的子目录,你的驱动程序是一个钓鱼杆和计算机的接口,名为Fish Master XL 2000 Titanium,那么你应在drivers/char/目录下建立一个名为fishing的子目录。现在你需要项drivers/char/下的Makefile文件中添加一行。编辑drivers/char/Makefile加入:
obj-m += fishing/
这行编译指令告诉模块构建系统在编译模块时需要进入fishing/子目录中。更可能发生的情况是,你的驱动程序的编译取决于一个特殊配置选项,比如,可能的CONFIG_FISHING_POLE。如果这样,你需要用下面的指针替代刚才那条指令:
obj-$(CONFIG_FISHING_POLE) += fishing/
最后,在drivers/char/fishing/下,需要添加一个新的Makefile文件,其中需要有下面这样:
obj-m += fishing.o
此刻构建系统运行就将会进入fishing/目录下,并且将fishing.c编译为fishing.ko模块,虽然拓展名是.o,但是模块被编译后的拓展名是.ko。 要是你的钓鱼杆驱动程序编译时有编译选项,那么你可能需要这么来做:
obj-$(CONFIG_FISHING_POLE) += fishing.o
如果喜欢把源文件置于drivers/char/目录下,并且不建立新目录。那么你要做的便是将前面提到的行(也就是原来处于drivers/char/fishing/下你自己的Makefile中的)都加入到drivers/char/Makefile中。
开始编译吧,运行内核构建过程,如果模块编译取决于配置选项,比如有CONFIG_FISHING_POLE约束,那么在编译前首先要确保选项被允许。
放在内核代码外
如果你喜欢脱离内核源代码树来维护和构建你的模块,那么你要做的就是在你自己的源代码树目录建立一个Makefile文件,它只需要一行指令:
obj-m := fishing.o
就可以把fishing.c编译成fishing.ko。
模块在内核内或内核外构建的最大区别 在于构建过程。当你的模块在内核源代码树外时,你必须告诉make如何找到内核源代码文件和基础的Makefile文件。
make -C /kernel/sourece/location SUBDIRS=$PWD modules
/kernel/source/location是你以配置的内核源码树。
2 安装模块
编译后的模块将被装入到目录/lib/modules/version/kernel/下。比如,如果使用的是2.6.10内核,而且你将你的模块源代码直接放在drivers/char/下,那么编译后的钓鱼杆驱动程序的存放路径是:/lib/modules/2.6.10/kernel/drivers/char/fishing.ko
下面的构建命令用来安装编译的模块到合适的目录下:
make modules_install
3 产生模块依赖性
Linux模块之间存在依赖性,也就是说钓鱼模块依赖鱼饵模块,那么当载入钓鱼模块时,鱼饵模块会被自动载入,这里需要的依赖信息必须事先生成。若想产生内核依赖关系的信息,root用户可运行命令:
depmod
为了执行更快的更新操作,可以只为新模块生成依赖信息,而不是生成所有的依赖关系,这时root用户可运行命令:
depmod -A
模块依赖关系信息存放在/lib/modules/version/modules.dep文件中
4 载入模块
载入模块最简单的方法是通过insmod命令,这是个功能很有限的命令,它的作用就是请求内核载入你指定的模块。insmod程序不执行任何依赖性分析或进一步的错误检查,它用法简单,以root运行命令:
insmod module
需要载入的模块名称由参数module指定,比如装载钓鱼杆模块,那你就执行命令:
insmod fishing
卸载一个模块,你可使用rmmod命令,同样用root身份执行:
rmmod module
比如,rmmod fishing将卸载钓鱼杆模块。
系统为我们提供了一个更先进的工具modprobe,它提供了模块依赖性分析,错误智能检查,错误报告以及许多其他功能和选项,推荐用这个命令:
modprobe module [module parameters]
module指定需要载入的模块名称,后面的参数将在模块加载时传入内核。modprobe命令不但会加载指定的模块,而且会自动加载任何它所依赖的有关模块。所以说它是加载模块的最佳技术。
modprobe命令也可用来从内核中卸载模块,当然这也需要root身份运行。
modprobe -r modules
参数modules指定一个或多个需要卸载的模块,与rmmod命令不同,modprobe也会卸载给定模块所依赖的相关模块,前提是这些相关模块没有被使用。
5 管理配置选项
2.6内核中新引入了kbuid系统,加入一个新配置选项是很容易的。你所需做的全部就是向Kconfig文件中添加一项,用于对应内核源码树。对驱动程序而言,Kconfig通常和源码处于同一目录。如果钓鱼杆驱动程序子drivers/char/下,那么你便会发现drivers/char/Konfig同时存在。 如果你建立了一个新子目录,而且也希望Kconfig文件存在于该目录中的话,那么必须在一个已存在的Kconfig文件中将它引入:
source "drivers/char/fishing/Kconfig"
可以很方便地在Kconfig文件中加入一个配置选项,请看钓鱼杆模块的选项如下所示: 配置选项第一行定义了该选项所代表的配置文件,注意CONFIG_前缀不需要写上。 第二行声明选项类型为tristate,也就是说被编译进内核(Y),也可作为模块编译(M),或干脆不编译它(N)。如果编译选项代表的是一个系统功能,而不是一个模块,那么编译选项将用bool代替tristate,这说明它不允许被编译成模块。处于指令之后的引号内文件为该选项指定了名称。 第三行指定了该选项的默认选择,这里默认操作是不编译它。
help指令是为该选项提供帮助文档。
除了上述选项外,还存在其他选项。
6 模块参数
Linux允许驱动程序声明参数,从而用户可以在系统启动或者模块装载时再指定参数值,这些参数对于你的驱动程序属于全局变量,模块参数同时也将出现在sysfs文件系统中。 定义一个模块参数可通过宏module_param()完成:
module_param(name,type,perm);
参数name既是用户可见的参数名,也是模块中存放模块参数的变量名。参数type则存放参数的类型,最后一个参数perm指定了模块在sysfs文件系统下对应文件的权限,该值可以是八进制格式,比如0666,或是S_Ifoo的定义形式,比如S_IRUGO|S_IWUSR,如果该值为0,则表示禁止所有的sysfs项。
上面的宏并没有定义变量,你必须在使用该宏前进行变量定义。通常使用类似下面的语句完成定义。
static int allow_live_bait = 1;
module_param(allow_live_bait,bool,0666);
有可能模块的外部参数名称不同于它对应的内部变量名称,这是就该使用宏module_param_named()定义了:
module_param_named(name,variable,type,perm);
参数name是外部可见的参数名称,参数variable是参数对应的内部全局变量名称。比如:
static unsigned int max_text = DEFAULT_MAX_LINE_TEST;
module_param_named(maximum_line_test,max_test,int ,0)
其他宏:
module_param_string(name,string,len,name);
module_param_array(name,type,nump,perm);
module_param_array_named(name,array,type,nump,perm);
上述所有宏被定义在linux/moduleparam.h文件中。
7 导出符号表
模块被载入后,就会动态连接到内核。注意,它与用户空间中的动态连接库类型,只有当被显式导出后的外部函数,才可以被动态库调用。在内核中,导出内核内核函数需要使用特殊的指令: EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()。
导出的内核函数可以被模块调用,而未导出的函数模块则无法使用。导出的内核符号表被看做是导出的内核接口,甚至称为内核API。
导出符号相当简单,在声明函数后紧跟上EXPORT_SYMBOL()指令就搞定了,比如:
int get_priate_beard_color(void)
{
return pirate->beard->color;
}
EXPORT_SYMBOL(get_priate_beard_color);
假定get_priate_beard_color同时也定义在一个可访问的文件中,那么任何模块现在都可以访问它。
有一些开发者希望自己的接口仅仅对GPL兼容的模块可见,内核连接器使用MODULE_LICENSE()宏可满足这个要求,如果你希望先前的函数仅仅对标记为GPL协议的模块可见,那么你就需要用:
EXPORT_SYMBOL_GPL(get_priate_beard_color);
如果你的代码被设置为模块,那么就必须确保它被编译为模块时所用的全部接口已被导出,否则就会产生连接错误(而且模块不能成功编译)。
|