Linux内核设计与实现---模块

Linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。

与开发的内核核心子系统不同,模块开发更接近编写新的应用程序,因为至少要在模块文件中具有入口点和出口点。

下面是hello_world内核模块

#include  <linux/init.h>
#include  <linux/module.h>
#include <linux/kernel.h>


static int hello_init(void)
{
    printk(KERN_ALERT "I bear a charmed life.\n");
    return 0;
}

static void hello_exit(void)
{
    printk(KERN_ALERT "Out,out,brief candle !\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Shakespeare");

hello_init()函数是模块的入口点,它通过module_init()注册到系统中,在模块装载时被调用。调用module_init()实际上不是真正的函数调用,而是一个宏调用,它唯一的参数便是模块的初始化函数。模块的所有初始化函数必须符合下面的形式:

int my_init(void);

因为init函数通常不会被外部函数直接调用,所以不必导出该函数,故它可被标记为static类型。init函数会返回一个int型数据,如果初始化顺利完成,那么它的返回值为0,失败的话,返回一个非0值。

hello_exit()函数是模块的出口函数,它由module_exit()注册到系统,在模块从内存卸载时,内核便会调用hello_exit()。在退出函数返回后,模块就被卸载了。

退出函数必须符号以下形式:

void my_exit(void);

MODULE_LICENSE()宏用于指定模块的版权,如果载入非GPL模块到系统内存,则会在内核中设置被污染标志。MODULE_AUTHOR()宏指定代码作者,完全是用作信息记录目的。

1 构建模块

在2.6内核中,由于采用了新的kbuild构建系统,现在构建模块相比从前更加容易,构建过程中的第一步是决定在哪里管理模块代码,你可以把模块源码加入到内核源代码树中,也可以在内核源码树外维护构建模块源码。

放在内核源代码树中

最理想的情况莫过于模块正式成为Linux内核的一部分,这样就会被存放在内核源代码树中。

首先你要清楚你的模块应该在内核源代码树中何处。设备驱动程序存放在内核源码树根目录drivers/的子目录下,在drivers内部,设备驱动文件被进一步细分。如字符设备存在于drivers/char/目录下,而块设备存放在drivers/block/目录下,USB设备则存放在drivers/usb/目录下。

假设你有一个字符设备,而且希望放在drivers/char/目录下,那么你要注意,在该目录下同时会存在大量的C源代码文件和其他目录。所以对于仅仅只有一两个源文件的设备驱动程序,可以直接存放在该目录下。如果驱动程序包含许多源文件和其他辅助文件,那么可以创建一个新子目录。

假设你想创建自己代码的子目录,你的驱动程序是一个钓鱼杆和计算机的接口,名为Fish Master XL 2000 Titanium,那么你应在drivers/char/目录下建立一个名为fishing的子目录。现在你需要项drivers/char/下的Makefile文件中添加一行。编辑drivers/char/Makefile加入:

obj-m += fishing/

这行编译指令告诉模块构建系统在编译模块时需要进入fishing/子目录中。更可能发生的情况是,你的驱动程序的编译取决于一个特殊配置选项,比如,可能的CONFIG_FISHING_POLE。如果这样,你需要用下面的指针替代刚才那条指令:

obj-$(CONFIG_FISHING_POLE) += fishing/

最后,在drivers/char/fishing/下,需要添加一个新的Makefile文件,其中需要有下面这样:

obj-m += fishing.o

此刻构建系统运行就将会进入fishing/目录下,并且将fishing.c编译为fishing.ko模块,虽然拓展名是.o,但是模块被编译后的拓展名是.ko。
要是你的钓鱼杆驱动程序编译时有编译选项,那么你可能需要这么来做:

obj-$(CONFIG_FISHING_POLE) += fishing.o

如果喜欢把源文件置于drivers/char/目录下,并且不建立新目录。那么你要做的便是将前面提到的行(也就是原来处于drivers/char/fishing/下你自己的Makefile中的)都加入到drivers/char/Makefile中。

开始编译吧,运行内核构建过程,如果模块编译取决于配置选项,比如有CONFIG_FISHING_POLE约束,那么在编译前首先要确保选项被允许。

放在内核代码外

如果你喜欢脱离内核源代码树来维护和构建你的模块,那么你要做的就是在你自己的源代码树目录建立一个Makefile文件,它只需要一行指令:

obj-m := fishing.o

就可以把fishing.c编译成fishing.ko。

模块在内核内或内核外构建的最大区别 在于构建过程。当你的模块在内核源代码树外时,你必须告诉make如何找到内核源代码文件和基础的Makefile文件。

make -C /kernel/sourece/location SUBDIRS=$PWD modules

/kernel/source/location是你以配置的内核源码树。

2 安装模块

编译后的模块将被装入到目录/lib/modules/version/kernel/下。比如,如果使用的是2.6.10内核,而且你将你的模块源代码直接放在drivers/char/下,那么编译后的钓鱼杆驱动程序的存放路径是:/lib/modules/2.6.10/kernel/drivers/char/fishing.ko

下面的构建命令用来安装编译的模块到合适的目录下:

make modules_install

3 产生模块依赖性

Linux模块之间存在依赖性,也就是说钓鱼模块依赖鱼饵模块,那么当载入钓鱼模块时,鱼饵模块会被自动载入,这里需要的依赖信息必须事先生成。若想产生内核依赖关系的信息,root用户可运行命令:

depmod

为了执行更快的更新操作,可以只为新模块生成依赖信息,而不是生成所有的依赖关系,这时root用户可运行命令:

depmod -A

模块依赖关系信息存放在/lib/modules/version/modules.dep文件中

4 载入模块

载入模块最简单的方法是通过insmod命令,这是个功能很有限的命令,它的作用就是请求内核载入你指定的模块。insmod程序不执行任何依赖性分析或进一步的错误检查,它用法简单,以root运行命令:

insmod module

需要载入的模块名称由参数module指定,比如装载钓鱼杆模块,那你就执行命令:

insmod fishing

卸载一个模块,你可使用rmmod命令,同样用root身份执行:

rmmod module

比如,rmmod fishing将卸载钓鱼杆模块。

系统为我们提供了一个更先进的工具modprobe,它提供了模块依赖性分析,错误智能检查,错误报告以及许多其他功能和选项,推荐用这个命令:

modprobe module [module parameters]

module指定需要载入的模块名称,后面的参数将在模块加载时传入内核。modprobe命令不但会加载指定的模块,而且会自动加载任何它所依赖的有关模块。所以说它是加载模块的最佳技术。

modprobe命令也可用来从内核中卸载模块,当然这也需要root身份运行。

modprobe -r modules

参数modules指定一个或多个需要卸载的模块,与rmmod命令不同,modprobe也会卸载给定模块所依赖的相关模块,前提是这些相关模块没有被使用。

5 管理配置选项

2.6内核中新引入了kbuid系统,加入一个新配置选项是很容易的。你所需做的全部就是向Kconfig文件中添加一项,用于对应内核源码树。对驱动程序而言,Kconfig通常和源码处于同一目录。如果钓鱼杆驱动程序子drivers/char/下,那么你便会发现drivers/char/Konfig同时存在。
如果你建立了一个新子目录,而且也希望Kconfig文件存在于该目录中的话,那么必须在一个已存在的Kconfig文件中将它引入

source "drivers/char/fishing/Kconfig"

可以很方便地在Kconfig文件中加入一个配置选项,请看钓鱼杆模块的选项如下所示:
在这里插入图片描述
配置选项第一行定义了该选项所代表的配置文件,注意CONFIG_前缀不需要写上。这个就是构建模块时,在Makefile里加入的特殊配置选项CONFIG_FISHING_POLE。
第二行声明选项类型为tristate,也就是说被编译进内核(Y),也可作为模块编译(M),或干脆不编译它(N),选Y,CONFIG_FISHING_POLE的值就是Y,依次类推。如果编译选项代表的是一个系统功能,而不是一个模块,那么编译选项将用bool代替tristate,这说明它不允许被编译成模块。处于指令之后的引号内文件为该选项指定了名称。
第三行指定了该选项的默认选择,这里默认操作是不编译它。

help指令是为该选项提供帮助文档。

除了上述选项外,还存在其他选项。

6 模块参数

Linux允许驱动程序声明参数,从而用户可以在系统启动或者模块装载时再指定参数值,这些参数对于你的驱动程序属于全局变量,模块参数同时也将出现在sysfs文件系统中。
定义一个模块参数可通过宏module_param()完成:

module_param(name,type,perm);

参数name既是用户可见的参数名,也是模块中存放模块参数的变量名。参数type则存放参数的类型,最后一个参数perm指定了模块在sysfs文件系统下对应文件的权限,该值可以是八进制格式,比如0666,或是S_Ifoo的定义形式,比如S_IRUGO|S_IWUSR,如果该值为0,则表示禁止所有的sysfs项。

上面的宏并没有定义变量,你必须在使用该宏前进行变量定义。通常使用类似下面的语句完成定义。

static int allow_live_bait = 1;
module_param(allow_live_bait,bool,0666);

有可能模块的外部参数名称不同于它对应的内部变量名称,这是就该使用宏module_param_named()定义了:

module_param_named(name,variable,type,perm);

参数name是外部可见的参数名称,参数variable是参数对应的内部全局变量名称。比如:

static unsigned int max_text = DEFAULT_MAX_LINE_TEST;
module_param_named(maximum_line_test,max_test,int ,0)

其他宏:

module_param_string(name,string,len,name);	/* 拷贝字符串到指定的字符数组 */
module_param_array(name,type,nump,perm);	/* 接受逗号分割的参数序列 */
module_param_array_named(name,array,type,nump,perm);	/* 将内部参数数组命名区别于外部参数 */

上述所有宏被定义在linux/moduleparam.h文件中。

7 导出符号表

模块被载入后,就会动态连接到内核。注意,它与用户空间中的动态连接库类型,只有当被显式导出后的外部函数,才可以被动态库调用。在内核中,导出内核内核函数需要使用特殊的指令:
EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()。

导出的内核函数可以被模块调用,而未导出的函数模块则无法使用。导出的内核符号表被看做是导出的内核接口,甚至称为内核API。

导出符号相当简单,在声明函数后紧跟上EXPORT_SYMBOL()指令就搞定了,比如:

int get_priate_beard_color(void)
{
	return pirate->beard->color;
}
EXPORT_SYMBOL(get_priate_beard_color);

假定get_priate_beard_color同时也定义在一个可访问的文件中,那么任何模块现在都可以访问它。

有一些开发者希望自己的接口仅仅对GPL兼容的模块可见,内核连接器使用MODULE_LICENSE()宏可满足这个要求,如果你希望先前的函数仅仅对标记为GPL协议的模块可见,那么你就需要用:

EXPORT_SYMBOL_GPL(get_priate_beard_color);

如果你的代码被设置为模块,那么就必须确保它被编译为模块时所用的全部接口已被导出,否则就会产生连接错误(而且模块不能成功编译)。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值