笔记来自:从 2.4 到 2.6:Linux 内核可装载模块机制的改变对设备驱动的影响 作者:周婷,软件工程师,S3 Graphics 上海研发中心,工作方向: 视频解码。email: [email protected]。
推荐一本书:《 LINUX内核模块编程指南》
在linux根目录下查找一个名叫“kernel”的目录find / -name kernel -type d .解释-name文件名 -type 为文件的类型,这里文件的类型是一个目录(directory)
内核版本的获取:当设备驱动需要同时支持不同版本内核时,在编译阶段,内核模块需要知道当前使用的内核源码的版本,从而使用相应的内核 API。2.4 与 2.6 内核下,源码头文件 linux/version.h 中有定义
清单1:判断内核版本的代码段。 #include <linux/version.h> #if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 0) #define LINUX26 #endif #ifdef LINUX26 /*code in 2.6 kernel*/ #else /*code in 2.4 kernel */ #endif
内核模块机制的改变:在2.6内核中,内核模块必须调用宏module_init 与module_exit() 去注册初始化与退出函数。在2.4 内核中,如果初始化函数命名为init_module()、退出函数命名为cleanup_module(),可以不必使用module_init 与module_exit 宏。推荐使用module_init 与module_exit宏,使代码在2.4与2.6内核中都能工作。
2.4内核中,模块的编译只需内核源码头文件;需要在包含linux/modules.h之前定义MODULE;编译、连接后生成的内核模块后缀为.o。 2.6内核中,模块的编译需要配置过的内核源码;编译、连接后生成的内核模块后缀为.ko;编译过程首先会到内核源码目录下,读取顶层的Makefile文件,然后再返回模块源码所在目录
清单2:2.4 内核模块的Makefile模板 #Makefile2.4 KVER=$(shell uname -r) KDIR=/lib/modules/$(KVER)/build OBJS=mymodule.o CFLAGS=-D__KERNEL__ -I$(KDIR)/include -DMODULE -D__KERNEL_SYSCALLS__ -DEXPORT_SYMTAB -O2 -fomit-frame-pointer -Wall -DMODVERSIONS -include $(KDIR)/include/linux/modversions.h all: $(OBJS) mymodule.o: file1.o file2.o ld -r -o $@ $^ clean: rm -f *.o
清单3:2.6 内核模块的Makefile模板 # Makefile2.6 ifneq ($(KERNELRELEASE),) #kbuild syntax. dependency relationshsip of files and target modules are listed here. mymodule-objs := file1.o file2.o obj-m := mymodule.o else PWD := $(shell pwd) KVER ?= $(shell uname -r) KDIR := /lib/modules/$(KVER)/build all: $(MAKE) -C $(KDIR) M=$(PWD) clean: rm -rf .*.cmd *.o *.mod.c *.ko .tmp_versions endif
清单4: 可同时在2.4 与 2.6 内核下工作的Makefile #Makefile for 2.4 & 2.6 VERS26=$(findstring 2.6,$(shell uname -r)) MAKEDIR?=$(shell pwd) ifeq ($(VERS26),2.6) include $(MAKEDIR)/Makefile2.6 else include $(MAKEDIR)/Makefile2.4 endif模块的初始化与退出:在2.6内核中,内核模块必须调用宏module_init 与module_exit() 去注册初始化与退出函数。在2.4 内核中,如果初始化函数命名为init_module()、退出函数命名为cleanup_module(),可以不必使用module_init 与module_exit 宏。推荐使用module_init 与module_exit宏,使代码在2.4与2.6内核中都能工作。
清单5:适用于2.4与2.6内核的模块的初始化与退出模板 #include <linux/module.h> /* Needed by all modules */ #include <linux/init.h> /* Needed for init&exit macros */ static int mod_init_func(void) { /*code here*/ return 0; } static void mod_exit_func(void) { /*code here*/ } module_init(mod_init_func); module_exit(mod_exit_func);模块使用计数:从设备使用的角度出发,当需要打开、开始使用某个设备时,使用try_module_get(dev->owner)去增加管理此设备的owner模块的使用计数;当关闭、不再使用此设备时,使用module_put(dev->owner)减少对管理此设备的owner模块的使用计数。这样,当设备在使用时,管理此设备的模块就不能被卸载;只有设备不再使用时模块才能被卸载。2.6内核下,对于为具体设备写驱动的开发人员而言,基本无需使用try_module_get与module_put,因为此时开发人员所写的驱动通常为支持某具体设备的owner模块,对此设备owner模块的计数管理由内核里更底层的代码如总线驱动或是此类设备共用的核心模块来实现,从而简化了设备驱动开发。
try_module_get 与module_put的引入与使用与2.6内核下的设备模型密切相关。模块是用来管理硬件设备的,2.6 内核为不同类型的设备定义了struct module *owner 域,用来指向管理此设备的模块。如字符设备的定义:
struct cdev { struct kobject kobj; struct module *owner; struct file_operations *ops; struct list_head list; dev_t dev; unsigned int count; };
模块输出的内核符号:模块加载后,2.4内核下可通过 /proc/ksyms、 2.6 内核下可通过/proc/kallsyms查看模块输出的内核符号
2.4 内核下,缺省情况时模块中的非静态全局变量及函数在模块加载后会输出到内核空间。 2.6 内核下,缺省情况时模块中的非静态全局变量及函数在模块加载后不会输出到内核空间,需要显式调用宏EXPORT_SYMBOL才能输出
清单6: 同时支持2.4与2.6的输出内核符号代码段 #include <linux/module.h> #ifndef LINUX26 EXPORT_NO_SYMBOLS; #endif EXPORT_SYMBOL(var); EXPORT_SYMBOL(func);模块的命令行输入参数:
2.4内核下,linux/module.h中定义有宏MODULE_PARM(var,type) 用于向模块传递命令行参数。var为接受参数值的变量名,type为采取如下格式的字符串[min[-max]]{b,h,i,l,s}。min及max用于表示当参数为数组类型时,允许输入的数组元素的个数范围;b:byte;h:short;i:int;l:long;s:string。
2.6内核下,在头文件linux/moduleparam.h里定义了如下宏:
module_param(name, type, perm) module_param_array(name, type, nump, perm)type 类型可以是byte、short,、ushort、 int、 uint、long、ulong、charp, bool or invbool, 不再采用2.4内核中的字符串形式,而且在模块编译时会将此处申明的type与变量定义的类型进行比较,判断是否一致。
perm表示此参数在sysfs文件系统中所对应的文件节点的属性。2.6内核使用sysfs文件系统,这是一个建立在内存中比proc更强大的文件系统。sysfs文件系统可以动态、实时,有组织层次地反应当前系统中的硬件、驱动等状态。当perm为0时,表示此参数不存在sysfs文件系统下对应的文件节点。 模块被加载后,在/sys/module/ 目录下将出现以此模块名命名的目录。如果此模块存在perm不为0的命令行参数,在此模块的目录下将出现parameters目录,包含一系列以参数名命名的文件节点,这些文件的权限值等于perm,文件的内容为参数的值。
nump 为保存输入的数组元素个数的变量的指针。当不需保存实际输入的数组元素个数时,可以设为NULL。从2.6.0至2.6.10 版本,须将变量名赋给nump;从2.6.10 版本开始,须将变量的引用赋给nump,这更易为开发人员理解。加载模块时,使用逗号分隔输入的数组元素。
清单7: 适用于2.4与2.6内核的模块输入参数模板 #include <linux/module.h> #ifdef LINUX26 #include <linux/moduleparam.h> #endif int debug = 0; char *mode = "800x600"; int tuner[4] = {1, 1, 1, 1}; #ifdef LINUX26 int tuner_c = 1; #endif #ifdef LINUX26 MODULE_PARM(debug, "i"); MODULE_PARM(mode, "s"); MODULE_PARM(tuner,"1-4i"); #else module_param(debug, int, 0644); module_param(mode, charp, 0644); #if LINUX_VERSION_CODE >= KERNEL_VERSION(2, 6, 10) module_param_array(tuner, int, &tuner_c, 0644); #else module_param_array(tuner, int, tuner_c, 0644); #endif #endif模块编译生成后,加载模块时可以输入:
modprobe my_module mode=1024x768 debug=1 tuner=22,33在linux/moduleparam.h还定义有:
module_param_array_named(name, array, type, nump, perm) module_param_call(name, set, get, arg, perm) module_param_named(name, value, type, perm)读者可以参阅linux/moduleparam.h查看这些宏的详细描述,有一点需注意,在2.6内核里,module_param这一系列宏使用的都是小写名字。
模块的许可证声明
从2.4.10版本内核开始,模块必须通过MODULE_LICENSE宏声明此模块的许可证,否则在加载此模块时,会收到内核被污染"kernel tainted" 的警告。从linux/module.h文件中可以看到,被内核接受的有意义的许可证有 "GPL","GPL v2","GPL and additional rights","Dual BSD/GPL","Dual MPL/GPL","Proprietary"。
在同时支持2.4与2.6内核的设备驱动中,模块可按如下方式声明自己的许可证。
#if LINUX_VERSION_CODE >= KERNEL_VERSION(2,4,10) MODULE_LICENSE("GPL"); #endif |
小结
此外,2.6内核里还有一些模块机制的改变,不常为驱动开发人员用到。如加载内核模块的接口request_module在2.4 下为request_module(const char * module_name);在2.6内核下为request_module(const char *fmt, ...)。在2.6 内核下,驱动开发人员可以通过调用
request_module("msp3400"); request_module("char-major-%d-%d", MAJOR(dev), MINOR(dev)); |
这种更灵活的方式加载其它内核模块。
2.6内核在linux/module.h中还提供了MODULE_ALIAS(alias)宏,模块可以通过调用此宏为自己定义一或若干个别称。而在2.4内核下,用户只能在/etc/modules.conf中为模块定义别称。
通过以上比较可以看到,从2.4到2.6内核,可装载模块管理机制的改变使设备驱动的开发变得更加简洁、灵活、健壮。