分类: LINUX
2006-11-23 02:17:36
本章以下部分介绍如何写一个完整的模块,skull(Simple Kernel Utility for Loading
Locations)。
先介绍如何写一个makefile来编译内核模块。
首先需要在包含头文件之前,要在预处理器中定义符号__KERNEL__ 。如前所述,若没有这个符号,则在内核头文件中的大部分内核相关的内容就无法使用。
另一个重要的符号是MODULE,需要在包含
若是为一个SMP机器编译,还需要定义__SMP__。在2.2内核中,“multiprocessor or
uniprocessor”选项用来恰当配置相关内容,因此,在模块中使用如下行可完成相应工作:
# include
# ifdef CONFIG_SMP
# define
__SMP__
# endif
头文件中的许多函数都声明为inline类型,因而模块编写者必须给编译器指定-O选项(指定该选项后gcc才可扩展inline函数;另外gcc还能同时接受-g和-O用于调试使用了inline函数的代码)。
另外还需要确认编译器是否与你的目标内核相匹配,可参考内核源码树中的Documentation/Changes文件(内核和编译器是同时开发的)。
最后,为防止意外错误,最好使用-Wall ( all warnings )编译选项,并且最好解决你代码中的所有警报信息。关于代码风格,可参见Documentation/CodingStyle。
以上所介绍的所有的定义和选项最好都放在make命令使用的CFLAGS变量中。
当模块由几个不同的源代码文件组成时,还需要在makefile中使用ld –r命令来将它们连接起来。这样就输出了一个包含各个源程序信息的目标文件(.o文件)。-r选项表示“可重定位”(relocatable),即表明输出的 .o文件没有嵌入绝对地址,使可重定位的。
下面是编译一个由两个源文件组成的模块的makefile的例子。
KERNELDIR=/usr/src/linux
include $(KERNELDIR)/.config
CFLAGS=-D__KERNEL__ -DMODULE –I$(KERNELDIR)/include
\
-O –Wall
ifdef CONFIG_SMP
CFLAGS+= -D__SMP__ -DSMP
endif
all: skull.o
skull.o: skull_init.o skull_clean.o
$(LD) –r $^ -o $@
clean:
rm –f *.o *~ core
这里使用了.c到.o文件的隐含规则,会自动调用当前设置的的活或默认的编译器$(CC)以及选项$(CFLAGS)来完成。
加载:
编译好后,就可用insmod命令把它加载到内核。insmod将模块中未解析的(unresolved)符号link到当前运行着的内核的符号表中。后面将会看到,用insmod加载模块时,可通过附加命令行选项来对模块进行配置。
内核对insmod的支持依赖于kernel/modules.c中定义的几个系统调用。见书。
查看内核源代码可知,所有的系统调用的名字都有sys_前缀,这是区别其他函数的标志。
Version Dependency(版本相关性)
对于要连接的每个不同版本的内核,你的模块都要相应地编译一次。每个模块定义了一个符号叫作__module_kernel_version,使用insmod加载模块时,insmod会与当前内核进行匹配,详细见Chapter 11(该描述适用于2.2以及以上内核版本)。
当包含了
若要为某一特定版本的内核编译模块,必须在makefile中包含该内核相关的头文件(例如,通过声明一个不同的KERNELDIR来实现)。本书中所有示例模块都使用KERNELDIR变量来指出正确的内核源代码,它可以设为环境变量,或在make的命令行中传递。
当请求加载一个模块时,insmod按其特定的搜索路径查找目标文件.o,即在/lib/modules下的版本相关的目录(version-dependent directories)下搜索。注意:老版本内核中例如2.0第一个查找目录是当前目录,不过出于安全原因,已被禁止。因此,加载非搜索路径下的模块module.o时,需要给出模块的正确路径。
UTS_RELEASE——可用字符串来描述内核版本,如”2.3.48”。
LINUX_VERSION_CODE——用二进制数表示内核版本,一个字节代表内核发布数字的一个部分。例如,the code for 2.3.48就是131888(亦即0x020330)。
KERNEL_VERSION(major, minor, release)——用来从individual numbers即(major, minor, release)创建一个版本号。例如,KERNEL_VERSION(2,3,48)扩展为131888。
version.h已经包含在module.h中,因而不必特地包含version.h。另一方面,你也可以去掉module.h对version.h的包含,只要事先声明__NO_VERSION__即可。
处理版本兼容性问题的最好办法是将其限定在一个特定头文件中;在我们的示例代码中包含了sysdep.h,用来将所有兼容性问题隐藏到适当的宏定义中。
第一个版本依赖性问题是驱动的”make
install”规则,不同内核版本中模块的安装目录不同,这在version.h中定义。所有的makefile都包含下面的代码,来自Rules.make:
VERSIONFILE=$(INCLUDEDIR)/linux/version.h
VERSION=$(shell awk –F\” ‘/REL/{print
$$2}’ $(VERSIONFILE))
INSTALLDIR=/lib/modules/$(VERSION)/misc
我们选择将所有驱动都安装到misc目录,这既是附加杂乱模块的正确选择,也是避免应付/lib/modules下的目录结构发生变更时的情况的好办法。新老modutils工具包都使用misc这个目录。
使用上面定义的INSTALLDIR,makefile的安装规则如下:
install:
install –d $(INSTALLDIR)
install –c $(OBJS) $(INSTALLDIR)
每个计算机平台有自己的特性。
应用程序员必须将自己的代码与预先编译好的库相链接并遵循一定的参数传递规则,而内核开发人员则不同,他们可以将一些处理器的寄存器用于特定的角色。此外,内核代码可以针对某一CPU系列的某一款特定处理器进行优化,以发挥最佳性能。而应用程序通常以二进制代码的形式发布。
模块化的代码在编译时需要采用与编译内核时所用的相同的编译选项(即保留相同的用于特殊用途的寄存器并采用相同的优化),以与内核能相互协作。考虑到这一点,顶层的Rules.make包含了一个平台相关的文件来对makefile补充所需的额外定义。所有这些平台相关的文件都以Makefile.platform的形式命名,并依据当前的内核配置为make变量指派合适的值。
如此设计的makefile的另一个有趣的特点是完全支持交叉编译。需要交叉编译时,只需要用另一套工具(例如arm-linux-gcc, arm-linux-ld)来代替当前的编译工具(gcc, ld等)。所用的前缀(如arm-linux)定义为$(CROSS_COMPILE),可在make命令行或环境变量中定义。(注意:SPARC架构的处理器的处理比较特殊)。
前面提到insmod利用公开的内核符号表来解析模块中未定义的符号。
内核符号表中包含了全局的内核项目(用于实现模块化驱动程序的函数和变量)的地址。公开的内核符号表可从文件/proc/ksyms中以文本形式读取。
一旦模块加载成功,任何模块中发布的符号(any symbol exported by the module)就成为内核符号表的一部分,它就会出现在文件/proc/ksyms中或ksyms命令的输出。
模块堆叠:
另外,新的模块可以使用你的模块所发布的符号,你也可以在其它模块的上层堆叠新的模块(stack new modules on top of other modules)。在主流的内核源代码中也使用了这种模块堆叠的方法:msdos文件系统依赖于fat模块开放出来的符号,各个输入USB设备模块堆叠于usbcore和input模块之上。
模块堆叠在复杂的工程中非常有用。如果以设备驱动的形式实现一个新的抽象,它可以提供一个设备相关的插接口(plug)。例如,video-for-linux
set of dirvers就分割为一个通用模块,更底层的硬件相关的设备驱动使用这个模块发布的符号。根据你的设置,你可以为你所安装的硬件加载一个通用视频模块和一个硬件相关的模块。
对并口和大量相关设备的支持也是用相同的方式来处理的,USB内核子系统也一样。并口子系统的模块堆叠如Figure 2-2所示,箭头表示模块(通过函数和数据结构)与内核编程接口的通信。
使用堆叠的模块时,需要了解modprobe工具。modprobe与insmod的功能大致相同,不过它还能自动加载你要加载的模块所需要的任何其它模块。因此,一个modprobe命令有时能代替好几次insmod调用(不过需要注意:modprobe只在所安装的模块目录树中进行查找,如果要加载其它目录例如当前目录下的模块,仍需使用insmod命令)。
层次化的模块化大大简化了各个层次,有助于缩短开发周期。
通常情况下,一个模块不必发布它的任何符号就可实现其功能。但是你可能需要发布这些符号以供其它模块使用。你可能还需要包含一些特定指令来避免发布全部的non-static符号,因为大部分版本的modutils工具缺省情况下会发布所有的符号。
Linux内核头文件提供了一个便捷的方法来管理你的符号的能见度(visibility),从而减少名字空间污染和实现适当的信息隐藏。(本节描述的机制适用于2.1.18以及后续版本,2.0内核则完全不同)。
如果你的模块不发布(export)任何符号时,可以在源文件中的模块初始化函数(init_module)中放置一个宏:
EXPORT_NO_SYMBOLS;
如果你想发布模块中符号的一个子集,则首先要定义预处理宏EXPORT_SYMTAB。这个宏必须在包含module.h之前定义。通常是在编译时在Makefile中用-D编译选项来定义它。……见书……
如前所述,init_module为模块注册其所提供的所有可供应用程序所访问和使用的功能(facility)。
模块可以注册多种类型的facilities,对于每个功能,都有一个相关的内核函数来完成对其的注册。传递给内核注册函数的参数通常是一个指向一个数据结构的指针,这个数据结构描述了新功能(facility)和新功能所注册的名字。这个数据结构通常内嵌了指向模块函数的指针,这样就可以调用模块所提供函数。
若在注册时发生了错误,必须要撤消失败前所有已完成的注册动作。
Linux不为每个模块保留它所注册的功能,因此当init_module在某处失败时,模块应有自我回收能力。若没能注销已注册的内容,内核将处于不稳定的状态:你将无法通过重新加载模块来再次注册你的功能,因为它们已经注册并可能处于繁忙状态,并且你无法注销它们因为你需要在上次注册时所用的同一指针,而要找到这相同的地址几乎是不可能的。恢复这种情况很繁琐,通常需要reboot,并对你的模块作出修正。
有时错误恢复最好使用goto语句来处理。我讨厌使用goto,但依我看来,这是它唯一的用武之地。在内核中,goto经常如下使用以处理错误。
下面的代码(使用假想的注册和注销函数)可在初始化在任何时刻失败时都正确执行:
int init_module(void)
{
int err;
/* registration takes a pointer and a name
*/
err = register_this(ptr1, “skull”);
if (err) goto
fail_this;
err = register_that(ptr2, “skull”);
if (err) goto
fail_that;
err = register_those(ptr3,”skull”);
if (err) goto fail_those;
return 0; /* success */
fail_those: unregister_that(ptr2, “skull”);
fail_that: unregister_this(ptr1, “skull”);
fail_this: return err; /*
propagate the error */
}
这段代码用来注册功能模块,其中goto语句用来在失败时注销此前已注册的功能。代码中的返回值err是一个错误码。在Linux内核中,错误码是一个负数集,定义在
很明显,cleanup_module要取消所有init_module中完成的注册。注意,各个功能在注销顺序上与注册时相反,这是很有趣的,可以参考init_module中使用goto语句对错误的处理,这样就可以通过跳转来依次注销失败前所注册的功能。
void cleanup_module(void)
{
unregister_those(ptr3, “skull”);
unregister_that(ptr2, “skull”);
unregister_this(ptr1, “skull”);
return;
}
若不使用goto,另一个选择是跟踪已成功注册的功能并在失败时调用cleanup_module。cleanup函数仅解除成功完成的注册。但这种方法需要更多的代码和更多的CPU时间,因此,使用goto语句仍是最好的错误恢复工具。
如果init和cleanup要处理的事情远比注册几个功能要复杂,则使用goto方法将变得很难管理,因为所有的cleanup代码都要在init_module中重复,甚至几个标签相混杂。因此,有时候另一种代码框架就更为成功了。
这种方法就是一旦发生错误,就在init_module内部调用cleanup_module函数。这时,cleanup函数就必须在注销一个功能之前检查各个项目(功能)的状态(即功能是否已成功注册)。最简单的代码形式看起来如下:
struct something *item1;
struct somethingelse *item2; //要注册的facility对应的数据结构指针;
int stuff_ok;
void cleanup_module(void)
{
if (item1)
release_thing(item1);
if (item2)
release_thing2(item2);
if (stuff_ok)
unregister_stuff();
return;
}
int init_module(void)
{
int err = -ENOMEM;
item1 = allocate_thing(arguments);
item2 =
allocate_thing2(arguments2); //为数据结构分配资源;
if (!item2 ||!item2)
goto fail; //若其中之一资源分配失败,转向fail调用cleanup_module;
err = register_stuff(item1,
item2); //注册;
if (!err)
stuff_ok = 1;
else
goto fail; //若注册失败,则转向fail调用cleanup_module;
return 0; /* success */
fail:
cleanup_module();
return err;
}
系统为每个模块保留了一个使用计数来确定模块是否能被安全卸载。设备繁忙时驱动模块是不能卸载的。
在现代内核中,系统能自动跟踪模块的使用计数,所使用的机制后一章介绍。不过,有时候仍需要对使用计数进行人工校正。特别是要与老的内核相兼容时,必须使用人工维护使用计数的方式。
通过3个宏来维护使用计数:
MOD_INC_USE_COUNT
为当前模块增加使用计数。
MOD_DEC_USE_COUNT
减小使用计数。
MOD_IN_USE
计数值非0时为真(true)。
这些宏定义在
注意在cleanup_module中不必检查MOD_IN_USE,因为内核在调用cleanup函数前就由sys_delete_module系统调用(在kernel/module.c中定义)事先完成了这项检查。
对模块使用计数进行合理管理对系统的稳定性至关重要。记住内核可以决定在任何时候卸载模块。在模块中作任何事情之前都必须都必须调用MOD_INC_USE_COUNT。
若忘了更新模块的使用计数,则无法卸载模块。(在开发期间很可能发生这种情况,例如,若进程因为你的驱动程序引用了NULL指针而终止,驱动程序就不可能去关闭设备,使用计数也就无法恢复到0。一种方法是在调试期间完全不用使用计数,将MOD_INC_USE_COUNT和MOD_DEC_USE_COUNT定义为空操作即no-ops;另一方法是利用其它方法将计数强制复位为0,在Chapter 5 “Using the ioctl Argument”中介绍)。
使用计数的当前值可以在/proc/modules中每一项的第3个域中找到。该文件显示了当前加载到系统中的模块,每一项对应一个模块。其中的域包括模块名、模块使用的内存的字节数和当前使用计数。如下是一个/proc/modules样例:
parport_pc 7604 1 (autoclean)
lp 4800 0 (unused)
parport 8084 1 [parport_probe
parport_pc lp]
lockd 33256 1 (autoclean)
sunrpc 56612 1 (autoclean) [lockd]
ds 6252 1
i82365 22304 1
pcmcia_core 41280 0 [ds i82365]
并口模块以堆叠方式加载,如前述和Figure
2-2。(autoclean)标签标识该模块由kmod或kerneld管理(见Chapter 11)。还有其它一些标志。在Linux 2.0中,第二个域(即大小-size)是用页面为单位表示的(大部分平台的页面大小为4KB)。
使用rmmod来卸载模块。由于不用链接,因而比加载简单。该命令进行delete_module系统调用,如果使用计数为0则这个系统调用会进一步调用模块中的cleanup_module函数,否则返回一个错误。
cleanup_module函数管理模块所注册的各个项目。只有模块发布的符号是自动删除的。
现在我们知道,内核调用init_module来初始化一个新加载的模块,并在移除模块之前调用cleanup_module。而在现代内核中,这两个函数通常有不一样的名字。例如在2.4内核中,可以为模块的init和cleanup函数起任意的名字,而不必再必须使用init_module()和cleanup_module()的名字。这可以通过宏module_init()和module_exit()实现(这些宏在linux/init.h中定义)。宏的用法如下两行(通常在源文件的末尾,这样就保证了函数的定义在宏的使用之前):
module_init(my_init);
module_exit(my_cleanup);
这样作的好处是内核中各模块的初始化和清除函数都可以有独一无二的名字,便于调试。
另外,从2.2内核开始,引入了__init和__exit两个宏。例如:
static int __init my_init(void){
……
}
static void __exit my_cleanup(void){
……
}
__init使得初始化函数在完成模块的初始化之后就被废弃,并且其占用的内存也被回收。__exit则忽略“清理收尾”的函数。注意,这两个宏只对编译进内核的模块起作用,对动态加载的模块不起任何作用也没有任何影响。很容易理解,编译进内核的模块是没有清理收尾工作的,而动态加载的模块却需要自己完成这些工作。
__init的使用(还有用于数据成员的__initdata)可以减少内核对内存的占用量。使用它是无害的,即使用于动态模块中虽然没有现时作用,但对将来是一个好的潜在的增强。
一个模块要完成其特定功能,必须要使用相应的系统资源,例如内存、I/O端口、外部存储器、中断线(interrupt line)和DMA通道。
编写内核代码与编写应用程序一样需要管理内存的分配。使用kmalloc或kfree来获取或释放内存空间,并通常带优先级GFP_KERNEL或GFP_USER;GFP(get
free page)。内存分配详见第7章。
通常各个设备需要分派特定的独占端口,因而必须确保各驱动程序要使用的端口或其他系统资源未被其他设备占用。
典型驱动程序的工作在大多数时候都是对I/O口和I/O
memory(统称I/O区)进行读写,包括设备的初始化以及正常工作时。必须保证一个设备的驱动以独占的方式访问其I/O区域。
避免设备间I/O冲突的主要方法是使用请求/释放机制来管理I/O资源。注意这个机制是一个协助系统管理资源的软机制,而与硬件特性无关(硬件可以支持也可以不支持)。例如,对I/O口的非法访问并不产生象“段失效”之类的错误,因为硬件不支持端口注册。
文件/proc/ioports中保存了已注册的端口资源,每一条目指定了以十六进制形式给出的由某一驱动程序或硬件独占的端口空间。
Ports
当往系统添加新设备而该设备的I/O空间由跳线来设置时,可使用/proc/ioports文件来避免端口冲突:用户可检查已使用的端口并为新设备分配可用的I/O空间。
事实上,更重要的是ioports文件背后的数据结构,当驱动程序对设备进行初始化时,它就知道哪些端口已经被使用;驱动需要探测I/O端口来侦测新设备时,它不会去探测已被其他设备或驱动所使用的端口。
用于访问I/O寄存器的编程接口通过以下三个函数实现:
int check_region(unsigned long
start, unsigned long len);
struct resource
*request_region(unsigned long start, unsigned long len, char *name);
void release_region(unsigned long
start, unsigned long len);
驱动程序可调用check_region函数来查看一个端口区域是否可用,若否则返回一个负的错误代码(如-EBUSY或-EINVAL)。request_region函数则用于实际分配端口空间,若申请并分配成功则返回一个非NULL指针值。驱动程序不必使用或保存返回的实际指针,而只需要检查是否返回NULL(仅当内核的资源管理子系统对该函数进行内部调用时才会用到这个返回的实际指针)。当驱动程序不再使用端口时则调用release_region释放端口。这三个函数实际上是声明于
注册端口的典型顺序如下面的示例驱动程序skull所示(函数skull_probe_hw包含设备相关代码,因而这里没有给出具体代码):
# include
# include
static int
skull_detect(unsigned int port, unsigned int range)
{
int err;
if ((err=check_region(port,range))<0) return err; /* busy */
if (skull_probe_hw(port, range) !=0) return –ENODEV; /* not found */
request_region(port, range, “skull”); /* Can’t fail */
return 0;
}
程序首先查看所请求的端口区域是否可用,如果端口不可分配,则不会去搜索硬件。实际的端口分配要等到系统发现硬件设备之后才进行。request_region不会出现调用失败的情况,因为内核在同一时刻只能加载一个模块,因此在硬件检测阶段不会出现由于其他模块插入而抢走端口的情况。
任何由驱动指派的I/O口最后都要释放,可在cleanup_module中调用release_region来释放:
static void
skull_release(unsigned int port, unsigned int range)
{
release_region(port, range);
}
Memory
与I/O端口类似,I/O
memory信息在/proc/iomem文件中。
关于驱动程序,对I/O memory的寄存器的访问方式与I/O端口是一样的,因为它们基于相同的内部机制。也有三个函数:
int
check_mem_region(unsigned long start, unsigned long len);
int
request_mem_region(unsigned long start, unsigned long len, char *name);
int
release_mem_region(unsigned long start, unsigned long len);
典型的驱动程序通常已经知道其自己的I/O memory空间,因而相对于前面I/O端口的注册代码来说就缩减为如下形式:
if
(check_mem_region(mem_addr, mem_size)){
printk(“drivername:memory
already in use\n”);
return –EBUSY;
}
request_mem_region(mem_addr,
mem_size, “drivername”);
即省去了对申请memory空间是否成功的判断,因为申请的总是预先特定为某一设备分配或说预留的I/O memory空间。
2.5.2
Resource Allocation in Linux 2.4
该部分描述资源分配机制。基本的资源分配函数(request_region等)仍通过宏来实现并且仍普遍使用,这是为了与以前的内核版本相兼容。
Linux资源管理可以控制任意资源,并以分层方式进行管理。已知的整体资源可以划分为小的子集——例如,对于一个特定的总线插槽相关的资源,如有必要,各个驱动程序可以进一步细分各自占用的空间。
资源空间通过一个resource结构体来描述,它在
struct resource{
const char *name;
unsigned long start, end;
unsigned long flags;
struct resource *parent, *sibling, *child;
}
顶层(root)资源在系统启动时创建。例如,描述I/O端口空间的资源结构体创建如下:
struct resource
ioport_resource=
{“PCI IO”, 0x0000, IO_SPACE_LIMIT, IORESOURCE_IO};
因此,资源名称为PCI IO,空间范围从0到IO_SPACE_LIMIT,后者根据不同的硬件平台而定,可以是0xffff(16位地址空间,如x86,IA-64,Alpha,M68k和MIPS),0xffff ffff(32位:SPARC,PPC,SH)或0xffff ffff ffff
ffff(64位:SPARC64)。
一个给定的资源的子区域可以用allocate_resource来创建。例如,PCI的初始化就创建了一项新资源,而实际上一个物理设备只用到其中的一个区域。当PCI代码读这些端口或存储区域时,就会为它们创建一项新资源,并在ioport_resource或iomem_resource下分派这些端口。
驱动程序可以请求某一特定资源的子集(实际上就是一个global resource的subrange)并通过调用__request_region将其标记为busy,这时就返回一个指针指向一个新的struct resource类型的数据结构,该数据结构描述了被请求的资源(若请求出错则返回NULL)。该结构体已经是global resource
tree的一部分,驱动程序不可随意使用它。
感兴趣的话可以浏览一下kernel/resource.c中的资源以及相关细节。
分层机制有很多优点。一是使得系统的I/O结构在内核数据结构中非常明晰,资源分配结果保存在文件/proc/ioports,例如:
e800-e8ff :Adaptec
AHA-2940u2/W / 7890
e800-e8be:aic7xxx
空间e800-e8ff分配给一个Adaptec卡,它告诉PCI总线驱动自己的身份。aic7xxx驱动程序则请求了该空间的大部分。
2.6
Automatic and Manual Configuration
驱动程序所需要的一些参数可能会依系统而不同。例如,driver必须知道硬件的实际I/O地址,或者存储器空间(对于设计优良的总线接口不会存在这样的问题,只用于ISA设备)。有时你不得不传递必要的参数给driver以帮助它找到对应的设备或使能/禁止某些特性。
除I/O地址外可能还有其他参数影响driver的行为,例如设备品牌和序列号。Driver必须知道这些信息才能正确工作。在设备初始化时需要用相关参数的正确值来设置driver。
有两种方法来得到正确值:要么用户指定,要么driver自动检测。前者易于实现,后者则无疑是更好的driver
configuration的方法。应尽量用后者实现。当然开发的初始阶段可以手动配置,以专注于设备模块的驱动实现,其后再实现自动配置。
许多driver也保留一些配置选项给用户选择,例如IDE(Integrated
Device Electronics)driver允许用户控制DMA操作选项。因此,即使在加载硬件时driver会对参数自动配置,可能仍需要保留一些配置选项留给用户。
参数值可在加载模块时通过insmod或modprobe来指定;后者还可从配置文件/etc/modules.conf中读取parameter
assignment。命令可接受命令行中的整型和string类型值。因此,如果要给一个模块提供一个skull_ival整型参数和一个skull_sval字符串参数,则在模块加载时可在命令行下用insmod如下设置参数:
insmod skull skull_ival=666
skull_sval=”the beast”
注意,在使用insmod来改变模块参数值之前,模块必须先使参数有效,即事先在驱动程序中使用宏MODULE_PARM来声明这些参数,该宏定义在module.h中。MODULE_PARM使用两个参数:一是变量名,二是描述该变量类型的字符串。该宏应该放在函数外,并通常放在源程序的头部。上面提到的两个参数可以在源程序中如下声明:
int skull_ival=0;
char *skull_sval;
MODULE_PARM (skull_ival, “i”
);
MODULE_PARM (skull_sval, “s”);
目前支持5种类型的模块参数:b,字节;h,短整型(两个字节);I,整型;l,长整型;以及s,字符串型。如果是字符串型,则需要声明一个指针变量;insmod将为用户提供的参数以及用户设置的相应的参数值分配存储空间。
也可以声明数组,例如:
int skull_array[4];
MODULE_PARM (skull_array, “2-4i”);
声明了长度为4的整型数组,其中“2-4i”表示在命令行下用insmod传递参数值时,最少要给出2个,最多不超过4个整型(i)参数值。更多信息可见
还有一个宏是MODULE_PARM_DESC,它允许程序员对模块的参数进行描述。该描述信息保存在目标文件(object file)中;并可使用类似objdump工具来查看,还可以通过自动系统管理工具来显示。给出一个实例如下:
int base_port=0x300;
MODULE_PARM (base_port, “i”);
MODULE_PARM_DESC
(base_port, “The base I/O port (default 0x300)”);
所有模块参数必须给一个缺省值;只有当用户显示地告知要改变参数值时insmod才改变它们的值。自动参数配置就可以如此设计:如果待配置变量有缺省值,则进自动检测(autodetection);否则就保持当前值。缺省值需要设置为在实际加载时绝不会需要用户来指定的值。
下面的代码显示了skull是如何自动检测一个设备的端口地址的。该例中,自动检测用于搜索多个设备,而手动配置(manual
configuration)则限于一个设备。函数skull_detect已在前面“Ports”部分定义,skull_init_board负责设备相关的(device-specific)初始化,这里没有给出。
/ *
* port ranges: the device can reside between
* 0x280 and 0x300, in steps of 0x10. It uses 0x10 ports.
*/
# define SKULL_PORT_FLOOR
0x280
# define
SKULL_PORT_CEIL 0x300
# define SKULL_PORT_RANGE
0x010
/ *
* the following function performs autodetection, unless a specifie
* value was assigned by insmod to ‘skull_port_base’
*/
static int
skull_port_base=0; /* 0 forces autodetection */
MODULE_PARM
(skull_port_base, “i”);
MODULE_PARM_DESC
(skull_port_base, “Base I/O port for skull”);
static int
skull_find_hw(void) /* returns the #
of devices */
{
/* base is either the load-time value or the first trial */
int base=skull_port_base ? skull_port_base
: SKULL_PORT_FLOOR;
int result=0;
/* loop one time if value assigned, try them all if
autodetecting */
do{
if (skull_detect(base, SKULL_PORT_RANGE)==0){
skull_init_board(base);
result++;
}
base+=SKULL_PORT_RANGE; /* prepare for next trial */
}while (skull_port_base == 0 && base<
SKULL_PORT_CEIL);
return result;
}
如果配置变量(configuration variables)仅在驱动程序内部使用(而不发布内核符号表中),则为方便用户在insmod命令行中给出相应的参数,driver writer可以去掉配置变量的前缀(本例中就是skull_)。
另外,还有三个宏用于将documentation放入目标文件中:
MODULE_AUTHOR (name)
将作者名字放入目标文件。
MODULE_DESCRIPTION (desc)
将对模块的描述放入目标文件。
MODULE_SUPPORTED_DEVICE (dev)
将一个说明模块所支持的设备的条目放入目标文件。内核源文件建议将这个参数最终用于协助自动模块加载,在这里没有这种作用。
写一个用户程序直接对设备端口进行读写对于初次涉足内核问题的程序员来说更为容易。