就整个kernel运行在单个保护域而言,Linux kernel 成为了“单内核”,但是Linux kernel是组件模式的,在运行时允许“代码”动态地插入到kernel和从kernel中移除。模块就是相关的一些子程序,数据,入口点和出口点共同组合成的一个单一的二进制映像,即一个可装载的kernel目标文件。模块的支持使得系统可以拥有一个最小的基本的内核映像,并且通过模块的方式支持一些可选的特征和驱动程序。模块对内核代码动态地插入到kernel中和从kernel中移除提供了一种简便方法;它有助于调试内核程序(例如,当我们在板子上调试一个驱动程序时,可以采用模块的方式,也可以采用静态的编译到内核当中,如果采用后者,每次修改驱动程序的时候,都必须重新烧写内核;如果采用前者,只需把驱动程序下载到板子,然后动态插入到内核);当有新的设备“热插”到的系统时,可以通过模块方式按需装载新的驱动程序。在这章中,我们将学习模块的基本知识和实现模块的基本原理以及如何写自己的模块。 1. Hello World 模块的开发很像写一个应用程序,它有自己的入口点,出口点和自己的“生活空间”。下面我们一起来写“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. * Returns zero if successfully loaded, nonzero otherwise. */ static int hello_init(void) { printk(KERN_ALERT "I bear a charmed life.\n" ) ; return 0; }
/* * hello_exit the exit function, called when the module is removed. */ static void hello_exit(void) { printk(KERN_ALERT "Out, out, brief candle!\n" ) ; }
2.2 Installing Modules 编译后的模块要放在/lib/modules/version/kernel/。下面的命令把所有编译后的模块放到相应的目录之下 (需要root权限)。 make modules_install 2.3 Generating Module Dependencies Linux模块实用工具能够理解模块间的依赖性。这意味着,如果模块chum依赖于模块bait,那么当装载模块chum时,模块bait会自动装载。这种依赖性信息不是凭空存在的,而是事先要建立起来。大多数发行的Linux能够自动产生这种映射关系,并且在每次启动的时候,建立最新的依赖关系。为了建立模块间的依赖信息,在root权限下运行如下命令: depmod 为了快速更新模块间的依赖信息,即如果有新模块,重新建立依赖信息;如果没有新模块,会保留原来的依赖信息。在root权限下运行如下命令: depmod –A 模块间的依赖信息存放在文件/lib/modules/version/modules.dep之中。 2.4 Loading Modules 用insmod装载模块是一种最简单的方法。它请求kernel装载指定的模块。insmod不会检查模块间的依赖关系,也不会执行是否有错误的检查。用法很简单。在root权限下,运行下列命令: insmod module 这里module是模块名。 同样,移除模块,使用rmmod。在root权限下,运行下列命令: rmmod module 这两个实用工具,虽然用法简单,但是缺乏智能性。实用工具modprobe提供了依赖关系的解决方案,智能的错误检查和汇报,等等。装载模块的时候,是我们的首选。 用modprobe装载模块,在root权限下,运行下列命令: modprobe module { module parameters } 这里module是模块名。模块的参数(请看第七节)是要传递到模块的参数。有点像DOS下执行程序的时候附带几个参数。 modprobe命令不仅试图装载写在其后的模块,还试图装载它依赖的所有模块。因此,我们要首选它。 modprobe也可以用于从kernel中移除模块,在root权限下,运行下列命令: modprobe r modules 这里modules指的是要移除的模块名,可以是多个模块名。不像rmmod仅移除指定的模块,modprobe还移除它依赖的并且不在使用中的其它模块。 2.5 Managing Configuration Options 在2.6 kernel中,由于有了新的 “kbuild”系统,因此增添一项新的配置是相当容易的。就是在Kconfig文件中增添一项新的配置内容。Kconfig文件用于衔接整个kernel源码树。对于驱动程序而言,Kconfig文件在驱动程序源码的同级目录。如果你的驱动程序在drivers/usb/gadget/下,那么你用的是driversusb/gadget/Kconfig。 如果你创建了一个新目录,那么要在其下创建新的Kconfig文件,并且从一个已存在的Kconfig中“source”它。即在已存在的Kconfig(例如,drivers/usb/gadget/Kconfig)中添加类似下面的一行: source “drivers/usb/gadget/online/Kconfig” 然后在新建的Kconfig中添加类似以下的内容: config USB_GADGET_ONLINE tristate “ Gadget Netmeeting support” default n help If you say Y here, netmeeting driver will be compiled 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指令表示其后面是帮助文本。有助于用户和开发者理解相应的程序和建立自己的内核。 还有其他另外一些指令。比如depends指令用于指定要使这个配置选项有效,则必须先要设置其它配置选项有效。如果这种依赖关系不能满足,那么配置选项就会失效。例如,如果你添加了如下的指令: depends on PXA27X 要使CONFIG_GADGET_NETMEETING这个配置选项有效,必须首先要使CONFIG_PXA27X这个配置选项有效。 指令select与depends有些类似,不同点在于:如果选了当前配置选项,那么select后的配置选项也会选中。由于指令select会自动“打开”其他配置选项,因此它不如depends常用。用法如下: select PXA27X 如果打开CONFIG_GADGET_NETMEETING配置选项,那么自动打开CONFIG_PXA27X配置选项。 对于select和depends指令,使用&&,||和!组合多个配置选项。例如: depends on DUMB_DRIVERS && !ONE_DRIVER 意思是仅当CONFIG_DUMB_DRIVERS配置选项打开而CONFIG_ONE_DRIVER配置选项关闭时,CONFIG_GADGET_NETMEETING配置选项才打开。 指令if可以跟在指令tristate和bool之后,这样为配置选项设置了一个条件选项。如果条件不符合,不仅关闭了配置选项甚至在配置实用工具内也不会出现这个配置选项。例如: bool “Deep Sea Mode” if OCEAN 其表示只有配置选项CONFIG_OCEAN打开之后,配置实用工具才会显示配置选项名Deep Sea Mode,而且也会打开CONFIG_GADGET_NETMEETING配置选项。 指令if也可以跟在指令default之后,其意思是仅当条件成立时,才会赋默认值。 为了更容易地建立配置,配置系统提供了几个meta-options。当且仅当用户希望打开设计用来禁止关键特性(比如在嵌入式系统上保留精确的内存)的选项时,那么打开配置选项CONFIG_ EMBEDDED( This option allows certain base kernel options and settings to be disabled or tweaked. This is for specialized environments which can tolerate a “non-standard” kernel. Only use this if you really know what you are doing)。配置系统CONFIG_BROKEN_ON_SMP是用于指定一个驱动程序不是SMP-safe。通常这个选项是没有打开的,这样强制用户清晰地认识到一个驱动程序在SMP环境下具有“破坏性”。当然,新开发的驱动程序,不要使用这个选项。 最后,CONFIG_EXPERIMENTAL配置选项用于标志一个驱动程序是处于试验阶段或者beta版本阶段,这个选项默认是关闭的,这使得用户在使用驱动程序之前清晰地认识到其中的风险性。 2.6 Module Parameters 对于如何向模块传递参数,Linux kernel 提供了一个简单的框架。其允许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,在驱动程序里,参数的用法如同全局变量。这些模块参数也能够在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中相应文件的访问权限。访问权限用通常的八进制格式来表示,例如,用0644(表示ower具有读写权限,group和everyone只读权限), 或者用通常的S_Ifoo定义,例如,S_IRUGO | S_IWUSR (表示everyone具有读权限,用户具有写权限)。用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_name(maximum_line_test, 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大小(可以小于buffer的大小,但是没有意义),perm表示sysfs的访问权限(或者perm是零,表示完全关闭相对应的sysfs项)。例如: static char species[BUF_LEN]; module_param_string(specifies, species, BUF_LEN, 0); 上面说得只是给模块传入一个参数的情况,如果给模块传入多个参数,那该怎么办呢?可以通过宏module_param_array()给模块传入多个参数。 用法如下: module_param_array(name, type, nump, perm); 这里,name既是外部模块的参数名又是程序内部的变量名,type是数据类型,perm是sysfs的访问权限。指针nump指向一个整数,其值表示有多少个参数存放在数组name中。值得注意是name数组必须静态分配。例如: static int finsh[MAX_FISH]; static int nr_fish; module_param_array(fish, int, &nr_fish, 0444); 通过宏module_param_array_named()使得内部的数组名与外部的参数名有不同的名字。例如: module_param_array_named(name, array, type, nump, perm); 这里的参数意义与其它宏一样。 最后,通过宏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>。 2.7 Exported Symbols 当装载模块的时候,模块动态地链接入内核之中。动态链接的二进制代码只能调用外部函数,然而,外部函数必须明确地输出,在内核中,通过EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL来达到这个目的。 输出的函数可以被其它模块调用。没有输出过的函数不能被其它模块调用。模块比核心内核映像代码具有更严格的链接和调用规则。因为所有核心源文件链接成一个单一的作为基础的映像,因此在内核中核心代码可以调用任何非静态的接口。当然,输出符号也必须是非静态属性。 一套输出的内核符号称之为输出的内核接口,甚至称之为kernel API。 输出一个内核符号是举手之劳之事。当函数声明之时,在其后用EXPORT_SYMBOL()把函数输出。 例如: /* it will receive control requests including set_configuration(), which enables non-control requests. */ int usb_gadget_register_driver(struct usb_gadget_driver *driver) { … } EXPORT_SYMBOL(usb_gadget_register_driver) ; 从此以后,任何模块都可以调用函数usb_gadget_register_driver(),只要在源文件中包含声明这个函数的头文件,或者extern这个函数的声明。 有些开发者希望他们的接口只让遵从GPL的模块调用。通过MODULE_LICENSE()的使用,内核链接器能够强制保证做到这点。如果你希望前面的函数仅被标有GPL许可证的模块访问,那么你可以用如下方式输出符号: EXPORT_SYMBOL_GPL(usb_gadget_register_driver); 如果你的代码配置为模块方式,那么必须确保:源文件中使用的所有接口必须是已经输出的符号,否则导致在装载时链接错误。 2.8 Wrapping Up Modules 这章我们学习了如何写模块,建立模块,装载和卸载模块。我们讨论了什么是模块,Linux如何动态装载模块代码,而且我们还讨论了模块的参数和输出符号。