1. Hello World 模块
构造2.6.x内核的模块,必须在自己的系统中配置并构造好内核树。"hello world"示例模块代码如下:
module_init和module_exit使用了内核的特殊宏来表示hello_init和hello_exit所扮演的角色。特殊宏MODULE_LICENSE用来告诉内核,该模块采用自由许可证。模块能够调用printk是因为在insmode函数装入模块后,模块就连接到了内核,因而可以访问内核的公用符号。当然我们也可以使用rmmod工具来卸载该模块。
[wzhwho@local~]#insmod ./hello.ko
Hello, world
[wzhwho@local~]#insmod ./hello.ko
Goodbye, world
2. 核心模块与应用程序
1) 应用程序从头到尾执行单个任务;而模块需要预先注册自己,以便服务将来某个请求,然后它的初始化函数就立即结束。
2) 应用程序在退出时,可以不管资源的释放或者其他的清除工作;但模块的退出函数必须仔细撤销初始化函数所做的一切
3) 应用程序可以调用它并未定义的函数,因为连接过程会解析外部引用而使用适当的函数库;模块仅仅被连接到内核,他能调用的函数仅仅是又内核导出的函数,而不存在任何可链接的函数库。还有printk缺乏对浮点数的支持,也就是说内核代码不支持浮点运算。
4) 应用程序的段错误是无害的;模块就不能出现段错误。
5) 应用程序在虚拟内存中布局,并具有一块很大的栈空间(栈是用来保存函数调用历史以及当前活动函数中的自动变量);但内核具有非常小的栈,它可能和一个4096字节大小的页一样大。所以在内核空间,使用大的结构应该在调用时动态分配该结构。
模块化代码在内核空间运行,用于扩展内核的功能。Linux内核代码(包括驱动程序代码)必须是可重入的,它必须能够同时运行在多个上下文中。在2.6中,内核代码(几乎)始终不能假定在给定的代码段中能够独占处理器。
3. 编译模块
构造2.6.x内核的模块,必须在自己的系统中配置并构造好内核树。下面是由GNU makefile的扩展语法编写规则。
obj-m := hello.o
该句说明有一个模块需要从目标文件hello.o中构造,而从该目标文件中构造的模块名称为hello.ko。
如果要构造的模块名称为module.ko,是由两个源文件生成,则正确的makefile编写如下:
obj-m := module.o
module-objs := file1.o file2.o
为了让这种类型的makefile正常工作,必须在大的内核构造系统环境中调用它们。如果内核源代码树保存在~/kernel-2.6.18目录中,则用来构造模块的make命令应该是(在包含模块源代码和makefile的目录中键入):
make -C ~//kernel-2.6.18 M='pwd' modules
上述命令是首先改变目录到-C选项指定的位置(即内核源代码目录),其中保存有内核的顶层makefile文件。M=选项让该makefile在构造modules目标之前返回到模块源代码目录。然后,modules目标指向obj-m变量中设定的模块。
下面提供一种方法使得内核树之外的模块构造更加容易。如下
ifneq ($(KERNELRELEASE))
obj-m := hello.o
else
KERNELDIR ?= /lib/moduels/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $( KERNELDIR) M=$( PWD) modules
endif
4. 装载和卸载模块
工具insmod将模块的代码和数据装入内核,然后使用内核的符号表解析模块中任何未解析的符号。与连接器不同,内核不会修改模块的磁盘文件,而仅仅是修改内存中的副本。工具insmode定义在kernel/module.c中的一个系统调用,名字都有sys_前缀。
工具modprobe,与insmod的区别在于,它会考虑要装载的模块是否引用了一些当前内核不存在的符号。如果有这类引用,modprobe会在当前搜索路径中查找定义了这些符号的其他模块。如果modprobe找到了这些模块,它会同时将这些模块装载到内核。
工具rmmod从内核中移除模块。如果内核认为模块仍然在使用状态(某个程序正打开由该模块导出的设备文件),或者内核被配置为禁止移除模块,则无法移除模块。
工具lsmod列出当前装载到内核中的所有模块。通过读取/proc/modules虚拟文件来获得这些信息
5. 版本依赖与平台依赖
在构造过程中,可以将自己的模块和当前内核树中的一个文件(vermagic.o)链接;该目标文件包含有关内核的信息,包括目标内核版本、编译器版本和一些重要配置变量的设置。在试图装载模块时,这些信息可用来检测模块和正在运行的内核的兼容性。
6. 内核符号表
内核符号表中包含了所有全局内核项(函数和变量)的地址,这是实现模块化驱动程序所必需的。如果一个模块需要其它模块导出符号,则应该使用下面的宏:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
_GPL版本使得要到导出的模块只能被GPL许可证下的模块使用。符号必须在模块文件的全局部分导出,不能在函数中导出。导出的符号扩展为特殊的变量,而该变量必须是全局的。该变量将在模块可执行文件的特殊部分(一个ELF段)中保存,在装载时,内核通过这个段寻找模块导出的变量。
7. 初始化和关闭
7.1. 初始化
模块的初始化函数负责注册模块所需要的任何设施。它应该被声明为static,因为这种函数在特定文件之外没有其他意义。初始化标记(__init)是对内核的一种暗示,表明该函数仅在初始化期间使用。在模块被装载后,模块装载器就会将初始化函数扔掉,释放出内存。__init和__initdata是可选的,而__devinit和__devinidata,只有在内核未被配置为支持热插拔设备的情况下,才会被内核翻译。
7.2. 清除函数
每个重要的模块都需要一个清除函数,用来释放模块在系统占有的资源。清除函数没有返回值,__exit修饰词标记该代码仅用于模块卸载(编译器将该函数放在特殊的ELF字段)。
7.3. 初始化过程中的错误处理
如果在注册设施时遇到任何错误,首先要判断模块是否可以继续初始化。通常,在某个注册失败后可以通过降低功能来继续运作。如果在发生了某个特定的错误后无法继续装载模块,则要将之前的任何注册工作都撤销掉。模块的清除函数需要安装注册地相反顺序撤销所有设施。在出错的时候使用goto语句,可以提高效率(因为不使用goto语句,就得记录设施注册成功与否的状态,在调用清除函数,往往需要更多的代码和CPU时间)。
7.4. 模块参数
由于系统的不同,模块需要参数发生变化。模块的参数可以在运行insmod或modprobe命令装载模块时赋值,而modprobe还可以从它的配置文件(/etc/modprobe.conf)中读取参数值。我们给hello world模块添加两个参数:一个是整数值,其名称为howmany;另一个是字符串,名称为whom。在装载模块时,将向whom问候howmany次。执行命令如下:
[wzhwho@local~]#insmod hello howmany=10 whom="wzhwho"
当然在insmod改变模块参数之前,模块必须让这些参数对insmod命令可见。参数必须使用module_param宏来声明,在<moduleparam.h>中定义。module_param需要三个参数:变量名称、类型和用户sysfs入口项的访问许可掩码。在hello.c中如下:
static char *whom= "wzhwho";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
模块装载器耶支持数组参数,在提供数组值时,用逗号划分各数组成员。要声明数组参数如下:
module_param(name, type, num,perm);
module_param中的最后一个成员是访问许可值,在<linux/stat.h>中定义。如果perm被设置为0,就不会有对应的sysfs入口项;否则模块参数会在/sys/module中出现,并设给定的访问许可。如果对参数使用S_IRUGO,则任何人可读取参数,但不能修改;S_IRUGO| S_IWUSR允许root用户修改该参数。