linux内核设计与实现学习笔记-模块
模块
1、概念:
如果让LINUX Kernel单独运行在一个保护区域,那么LINUX Kernel就成为了“单内核”。
LINUX Kernel是组件模式的,所谓组件模式是指:LINUX Kernel在运行时,允许“代码”动态的插入或者移出Kernel。
所谓模块是指:相关的一些子程序,数据、入口点和出口点共同组合成的一个单一的二进制映像,也就是一个可装载的Kernel目标文件。
模块的支持,使得系统可以拥有一个最小的内核映像,并且通过模块的方式支持一些可选的特征和驱动程序。
模块可动态的插入Kernel和从Kernel中移除,提供了一种调试内核程序的简便方法。模块的加载方式分为两种:静态加载和动态加载。
静态加载是将模块直接编译入内核,若模块需要修改和升级,我们就得重新编译整个内核,并且必须重新烧写内核,工作量加大。
动态加载是需要时,加载入内核,不需要时,从内核卸载。不需对内核进行编译和烧写,就可方便的对模块进行修改和升级,大大减小了工作量,也省去了我们很多的麻烦。
本章主要学习模块的基本知识和实现模块的基本原理以及如何编写自己的模块。
2、Hello World:
不管开发哪个语言下面的应用程序,我们的学习都是从一个叫Hello World的程序开始的。
模块的开发就像写一个应用程序,它有自己的入口点,出口点和自己的“生活空间“,也有生命期。
/* *hello.c Hello,World! As a Kernel Module */ #include #include #include /* *hello_init the init function , called when the module is loaded. *Return zero if successfully loaded, nozero otherwisw. */ static int hello_init(void) { printk(KERN_ALERT" Hello,World!\n"); return 0; } /* *hello_exit the exit function ,called when the module is removed. */ static void hello_exit(void) { printk(KERN_ALERT"Good,Bye!\n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("HongTung"); 注释: module_init()和module_exit()都是宏。 module_init()把hello_init()函数注册为这个模块的入口点。当模块被装载时,内核调用hello_init()函数。 module_init()的任务是把它的唯一参数作为相应模块的初始化函数。 初始化函数必须有如下的形式:int my_init(void) 由于初始化函数不会被外部的代码直接调用,所以,不必export这个函数。所以将初始化函数标志为static会更加的合理。 初始化函数的返回值:如果初始化成功,返回0;否则返回非零。 此处的初始化函数仅仅是打印出一句话。实际开发的模块中,初始化函数一般完成的工作是:注册资源,为数据结构分配内存等等。 同理,module_exit()是把hello_exit()函数注册为这个模块的出口点。当模块从内核中移除时,内核调用这个函数。 退出函数在返回之前,必须清除模块所占的资源,确保硬件处于一致状态等等。 退出函数必须有如下形式:void my_exit(void);同上,将函数标志为static会更合理。 注意:若采取静态加载的方式,将模块编译进内核,则内核启动时,调用static int my_init(void);但是退出函数static void my_exit(void);不会包含在内核的映像内,它也不会被调用。因为静态的加载方式,是将模块当作内核的一部分编译进内核中,所以代码永远不会从内核中删除。 宏MODULE_LICENSE()用于指定这个文件的版权许可。 MODULE_AUTHOR()用于指定本文件的作者。宏的值完全是为了提供说明信息。
3、代码存放路径及编译方式
代码存放路径两种:
一、把模块的源码增加到内核源码的一个合适的地方,即可以把文件作为一个"patch”,最终也可以将代码合到官方的源码树内。
二、在源码树之外维护和编译模块。
3.1内核源码内
我们通常的选择是将模块放入源码树之内,作为LINUX的一部分。这样模块可以生存在内核的源码树内。
我们需确定模块生活在源码树的哪个小区。如果,我们的模块是一个通过USB线把手机当作PC机的一台联机小电脑的驱动程序。
既然是和USB相关的一个驱动,我们应将其放入drivers/usb/目录下,此之所谓人以群分,首先得确定其国籍,然后遣送回国:drivers/下的USB/目录。我们进入drivers/usb/gadget/目录下,我们发现gadget/目录下有很多驱动程序,都是些和USB相关的驱动。因此,我们将模块放在此目录下
建立文件夹,gadget/online/,将模块的所有文件放于此目录下。
在gadget/子目录下创建online/子目录之后,我们必须在gadget/下的Makefile中添加一行:
obj-m += online/
目的在于编译模块时,告诉build系统找到online/子目录。
一般情况下,为了控制是不是需要编译驱动程序,我们使用一个特定的配置选项来达到这个目的,例如:CONFIG_USB_GADGET_ONLINE(在第6节会详细讲述,如何添加一个新的配置选项)。因此我们把上面的一行修改为:(gadget/Makefile)
obj-$(CONFIG_USB_GADGET_ONLINE) += online/
然后,我们在online/目录下创建文件Makefile,并且添加如下一行到其中。
obj-m += netmeeting.o
到此为止,build系统能够沿着源码树往下找到online/子目录,并且根据netmeeting.c文件建立netmeeting.ko模块。虽然在Makefile文件中netmeeting.o的扩展名是.o,但是编译后模块的扩展名是.ko。
一般情况下,为了控制是不是需要编译该驱动程序,我们使用一个特定的配置选项来达到这个目的,如下:(online/Makefile)
obj-$(CONFIG_USB_GADGET_ONLINE) += netmeeting.o
因为,实现手机当作PC机的一台联机小电脑的驱动程序需要许多文件,那么Makefile该如下所示:
obj-$(CONFIG_USB_GADGET_ONLINE) += netmeeting.o
netmeeting-objs :=one.o two.o three.o four.o
这样,netmeeting.ko由one.c,two.c,three.c,four.c四个文件编译连接而成。
如果,想要为这些文件指定额外的gcc编译选项,在Makefile文件中添加类似如下的一行:
EXTRA_CFLAGS += -ONLINE_NETMEETING
如果我们将模块的所有文件放置在目录gadget/下,则将online/目录下的Makefile中的内容,添加到目录gadget/目录下的Makefile中。
3.2置于源码树之外
如果将模块源码置于源码树之外,那么在自己的源码目录下创建Makefile文件,并且添加:
obj-m := netmeeting.o
这样会把netmeeting.c编译成netmeeting.ko。如果有多个源文件,那么在Makefile文件中添加:
obj-m := netmeeting.o
netmeeting-objs :=one.o two.o three.o four.o
放置于源码树之外和放置于源码树之内,主要区别在于build过程。
放置于源码树之外,编译模块时,需要使用命令make去找到内核源码文件和基本的Makefile文件。如下所示:
make -C /kernel/source/location SUBDIRS=$PWD modules
/keinel/source/location是已经配置过的内核源码树的位置。
注释:
$(
CONFIG_USB_GADGET_ONLINE) 是一个整体,$(xxx)表示引用变量 xxx
比如定义 CONFIG_TEST=y
$(CONFIG_TEST)就是y
obj-$(CONFIG_TEST) 就是 obj-y
编译后的模块要放在目录/lib/modules/version/kernel/下。一般是在Makefile中添加:
modules_install: cp netmeeting.ko /lib/modules/version/kernel/ .PHONY: modules_install
此步操作,需要root权限。
5、生成模块依赖(Generating Modules Dependencies)
LINUX模块实用工具能够理解模块间的依赖性。
也就是说如果模块chun依赖于模块bait,那么在装载模块chum时,模块bait会自动的装载。
也就是LINUX下常说的依赖性编译。文件A的编译依赖于B,B的编译依赖于文件C。
在root权限下,运行如下命令来建立模块间的依赖信息。
depmod 注:这个命令,red hat 9下有。但Fedora 8下没有这个shell命令。这个问题有待商榷。
该命令详细的使用方法和规则,可通过下面命令来查看帮助:depmod --help
模块间的依赖信息存放在文件/lib/modules/version/modules.dep之中。
6、装载模块(loading Modules)
用命令insmod装载模块是最简单的一种方法。它请求内核装载指定的模块。
命令insmod不会检查模块间的依赖关系,也不会执行是否有错误的检查。
加载模块: insmod (模块名)
卸载模块: rmmod (模块名)
这个工具很简单,实用,但是缺乏智能性。
因此,实用工具modprobe提供了依赖性关系的解决方案,智能的错误检查和汇报等等。装载模块时,使我们的首选。root权限下,执行命令:
加载模块: modprobe (模块名) (模块参数) 注:这个命令,在Fedora 8下同样没有。
modprobe命令,不仅试图装载写在其后的模块,还试图装载它依赖的所有模块。因此,我们要首先它。
卸载模块: modprobe r (模块名)
这里的modprobe可以移除多个模块,还可以移除它依赖的并且不在使用中的其它模块。
7、管理配置选项(Managing Configuration Options)
不同于2.4内核,在2.6内核中,由于有了新的“kbuild”系统,因此增添一项新的配置是相当容易的。
就是在Kconfig文件中增添一项新的配置内容。
Kconfig文件用于衔接整个Kernel源码树。
对于驱动程序而言,Kconfig文件在驱动程序源码的同级目录。若你的驱动在目录drivers/usb/gadget/online/下,那么你用的Kconfig是drivers/usb/gadget/online/Kconfig。
如果,你创建了一个新目录,那么要在其下创建新的Kconfig文件。
首先,我们要在一个已经存在的Kconfig文件中添加如下一行:
source "drivers/usb/gadget/online/Kconfig"
source命令的作用是,让后面带的文件或者目录下的文件生效。相当于,让文件执行一下,让修改生效。省去了重启电脑的麻烦。
由于我们的Kconfig是新建的,所以,它并没有生效。
所以,我们在一个已经存在的Kconfig文件中,调用source命令 , 让我们新建的Kconfig文件生效。
然后,修改我们新建的文件Kconfig。在新建的Kconfig中添加如下内容:
config USB_GADGET_ONLINE # 第一行定义了配选项。事先已经假设存在有CONFIG_前缀,因此用不着我们写 tristate "Gadget Netmeeting support" # 第二行说明了这个配置选项是三态的,即有三种选择方式。第一种是选择Y,表示相对应的程序编译到Kernel之内;第二种选择是M,表示相对应的程序编译成模块;第三 # 种选择是N,表示不编译相对应的程序。
# 如果没有编译成模块这个选项,可以用bool代替tristate。指令tristate后带引号的文本是配置选项名,用于各种配置实用程序的选项显示。
default n # 第三行为这个配置选项指定一个默认值,这里的默认值是选择n。即:不编译相对应的程序。 help # 第四行help指令表示其后面是帮助文本。有助于用户和开发人员理解相应的程序和建立自己的内核。 If you say Y here,netmeeting driver will be compiles 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.
还有一些其它的指令。
depends指令用于指定要使这个配置选项有效,则必须先要设置其它配置选项有效。如果,这种依赖关系不能被满足,那么配置选项就会失败。
例如:若添加了如下的指令: depends on PXA27X
要使CONFIG_GADGET_NETMEETING这个配置选项有效,必须首先要使CONFIG_PXA27X这个配置选项有效。
指令Select和depends类似,不同之处是:如果选择了当前配置选项,那么select后的配置选项也会选中。由于select命令会自动“打开”其它的配置选项,因此不如depends常用。
select用法如下: 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之后,表示只有if的条件成立,才会赋默认值。
为了更容易的建立配置,配置系统提供了几个meta-options。当且仅当用户希望打开那些被设计用来禁止关键特性(关键特性:如在嵌入式系统上保留精确的内存)的选项时,那么打开配置选项CONFIG_EMBEDDED(This option allows certain base kernel options and settings to be disabled or tewaked.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版本阶段(beta版本:测试版本)。这个选项默认是关闭的,这使得用户在使用驱动程序之前清晰地认识到其中的风险性。
8、模块参数(Module Parameters)
对于如何向模块传递参数,LINUX Kernel提供了一个简单的框架。其允许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,在驱动程序中,参数的用法如同全局变量。这些模块参数也能够在sysfs中显示出来(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中相应文件的访问权限。
访问权限用通常的八进制格式来表示或者通常的S_Ifoo定义。
八进制格式的使用和操作系统下是一致的。
0755:表示所有制是读写、执行的权限。所在组是读和执行的权限。其他用户是读和执行的权限。
S_Ifoo下:例如:
S_IRUGO|S_IWUSR(表示其它用户具有读权限,用户具有写权限)。用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_named(maximum,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大小(len可以小于buffer的大小,但是没有意义)
perm:sysfs的访问权限(或者perm为零,表示完全关闭相对应的sysfs项)。
以上的都是只能传递一个参数给模块。如果要给模块传递多个参数,可以通过宏
module_param_array(name,type,nump,perm);
name:既是外部模块的参数名又是程序内部的变量名。name数组必须静态分配。
type:是数据类型。
perm:sysfs的访问权限。
nump:是一个指针。其值表示有多少个参数存放在数组name中。
例如:
static int finish[MAX_FISH];
static int nr_fish;
module_param_array(fish,int,&nr_fish,0444);
我们可以通过宏module_param_array_named(name,array,type,nump,perm);
参数的意义和宏module_param_named()是一样的。
最后,用宏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");
9、输出符号(Exported Symbols)
当装载模块的时候,模块是动态的链接入内核之中。
然而,动态链接的二进制代码只能调用外部函数,所以外部函数必须明确的输出,才能被模块调用。
在内核中,通过EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()来达到目的。
输出的函数,可以被其它的模块调用。
没有输出的函数,不能被其它模块调用。
模块比核心内核映像代码具有更严格的链接和调用规则。因为所有核心源文件链接成一个单一的作为基础的映像,因此在内核中核心代码可以调用任何非静态的接口。
当然,输出符号也必须是非静态属性。
一套输出的内核符号称之为输出的内核接口,也称之为Kernel API。
当函数声明时,用EXPORT_SYMBOL()把函数输出。
例如:
int usb_gadget_register_driver(struct usb_gadget_driver *driver) { ....... } EXPORT_SYMBOL(usb_gadget_register_driver);
这样,任何模块都可以调用函数usb_gadget_register_driver(),只要在源文件中包含了声明这个函数的头文件,或者extern这个函数的声明(这点同C语言)。
若你希望你的接口让只遵守GPL的模块调用。那么通过MODULE_LICENSE()的使用,内核链接器能够保证做到这一点。
EXPORT_SYMBOL_GPL(usb_gadget_register_driver);只允许标有GPL许可证的模块访问函数usb_gadget_register_driver()。
如果你的代码配置为模块方式,那么必须确保:源文件中使用的所有接口必须是已经输出的符号,否则导致在装载时链接错误。