- 论坛徽章:
- 0
|
第 11 章 kmod 和高级模块化
在本书的第二部分,我们要讨论更为高级的内容。我们将再次从模块化讲起。
第2章对模块化的介绍只是其中的一部分,内核和 modutils 包支持一些更高级的特性,它们比前面所讨论的安装和运行一个基本的驱动程序所需的特性要更为复杂。本章将讨论 kmod 进程以及模块中的版本支持(一种设施,如果利用该设施,则在升级内核时不必重新编译各个模块)。我们还将讨论如何从内核代码中运行用户空间的辅助程序。
随着时间的推移,按需加载模块的实现部分发生了显著的变化。和前面章节一样,本章也将讨论 2.4 内核中的实现方法。示例程序也尽可能地能够在 2.0 和 2.2 内核上运行,在本章的末尾,我们会介绍 2.4 与 2.0、2.2之间的不同之处。
11.1 按需加载模块
为了方便用户加载和卸载模块,并且避免把不再使用的模块继续保留在内核中浪费内核的存储空间,同时又使得内核可以广泛地支持各种各样的硬件,Linux 提供了对模块自动加载和卸载的支持。要利用这一特性,在编译内核前进行的配置中,必须打开对 kmod 的支持选项。大多数 Linux 发行版安装的内核都开启了对 kmod 特性的支持。这种可以在需要时请求加载额外模块的能力,对于使用堆叠式模块的驱动程序尤其有用。
隐藏在 kmod 背后的思想很简单,但却很有效。一旦内核试图访问某种资源并发现该资源不可用时,它会对 kmod 子系统进行一次特殊的调用而不仅仅是返回一个错误。kmod 会加载相关的模块以获取该资源,如果它成功则内核继续工作,否则将返回错误。实际上请求任何一种资源都可以使用这种办法:诸如字符设备和块设备、文件系统、线路规程(line discipline)和网络协议等等。
一个得益于按需加载的例子是 ALSA(Advanced Linux Sound Architecture)声卡驱动程序组,也许未来某一天它会取代目前内核中使用的 OSS(Open Sound System)实现*。ALSA 被分割成许多片段。其中共用的部分会首先被加载,其余片段是否加载则取决于所配硬件型号(这里指声卡)以及是否需要相应的功能(例如,MIDI 音序器、合成器、混音器以及 OSS 兼容功能等等)。于是,一个大而复杂的系统可以被分解成许多小部件,只有那些必不可少的部分才被真正载入到运行系统中。
注:ALSA 驱动程序可以从
www.alsa-project.org
获得。
自动加载模块的另外一个常见应用是发行版中所安装的"万能内核"。Linux 发行商们总是希望他们的内核能够支持尽可能多的硬件设备,然而,那种简单地将任何可能用到的驱动程序全部配置进内核的做法是不现实的。那样做的结果将导致内核因尺寸过大而无法加载,而且有如此之多的驱动程序去探测硬件将非常容易导致冲突和混乱。通过自动装载机制,在所安装的每一个独立的系统中,内核都将会根据它所找到的硬件配置加载相应的模块以适应之。
11.1.1 在内核中请求模块
任何内核空间的代码在需要时都可以通过调用 kmod 程序来请求加载模块。kmod 最初被实现为一个处理模块装载请求的独立内核进程,但是,很久以前该进程就被简化成不需要单独的进程上下文。要利用 kmod,必须在驱动程序中包含 头文件。
要请求加载模块,调用 request_module:
int request_module(const char *module_name);
module_name 既可以是特定的模块文件名,也可以是更为通用的模块功能信息。该函数的返回值为0,如果发生错误将返回常规的负的错误码。
注意 request_module 的调用是同步的--它将进入睡眠直到模块的加载动作完成。当然,这也意味着 request_module 不能够在中断上下文中调用。同样要注意 request_module 的成功返回,并不能保证模块提供的功能会立即可用。函数的返回值只是表明该函数成功调用了 modprobe 但是并不表示 modprobe 本身的状态是成功的。许多问题和配置上的错误都会导致 request_module 返回一个成功状态,但实际却没有真正加载你所需的模块。
因此,函数 request_module 的正确用法,通常是进行两次测试以确保所需的相应功能特性的确已经存在:
if ( (ptr = look_for_feature()) == NULL) {
/* if feature is missing, create request string */
sprintf(modname, "fmt-for-feature-%i\n", featureid);
request_module(modname); /* and try lo load it */
}
/* Check for existence of the feature again; error if missing */
if ( (ptr = look_for_feature()) == NULL)
return -ENODEV;
第一次检查避免了对 request_module 的重复调用。如果内核中没有我们所需的功能特性,就生成一个请求字符串并通过 request_module 去加载它;最后一次检查用来确定所请求的功能特性是否已经可用。
11.1.2 用户空间方面
模块加载任务的真正完成需要用户空间程序的帮助,原因很简单:在用户空间上下文中,达到所需要的可配置性和灵活性要容易的多。当内核代码调用 request_module 时,一个新的"内核线程"进程会被创建,它会在用户上下文中运行一个辅助程序,这个程序就是我们在本书前面部分已经简要介绍过的 modprobe。
modprobe 可以作非常多的事情。最简单的情况下,它会直接使用 request_module 传过来的模块名字作为参数去调用 insmod。然而,内核代码经常会使用一个更为抽象的、用来代表所需功能特性的名字,例如:scsi_hostadapter,这时, modprobe 会找到并且加载正确的模块。modprobe 也可以处理模块之间的依赖关系;如果所需加载的模块需要其他模块,modprobe 会将它们一并加载--前提是模块被安装之后已经运行了 depmod -a 命令。*modprobe 通过文件 /etc/modules.conf 进行配置。*读者可查阅 modules.conf 手册页获得该文件所支持入口项的完整清单。下面简要描述一些最常用的入口项:
注:大多数发行版会在启动时自动运行 depmod -a ,所以不必对此担心,除非你在重新启动之后安装了新的模块。请参照 modprobe 的文档了解详细信息。
注:在以前的版本中,相应的文件是/etc/conf.modules,出于兼容性考虑,目前仍然支持这种文件名,但并不提倡使用它。
path[misc]=directory
path[misc] 指令告诉 modprobe 各种杂项模块可在给定目录的 misc 子目录中找到。其它值得设置的路径包括 boot,它指示了在系统启动时应该加载的模块所在的目录;toplevel,指出模块子目录树的顶层目录。通常,我们还需要包含一个单独的 keep 指令。
keep
通常,路径指令将导致 modprobe 放弃其它所有的路径(包括默认路径),通过将 keep 放在所有其他的路径指令之前,可以让 modprobe 将路径添加到路径列表,而不是替换掉已有路径。
alias alias_name real_name
alias 会使 modprobe 在要求加载 alias_name 模块时加载 real_name 模块。通常,别名用来标识特定的功能特性:它可能是 scsi_hostadapter, eth0 或 sound 等等。通过这种方式,可让一般性请求(比如"用于第一个以太网卡的驱动程序")映射到特定的模块。系统安装程序经常会创建 alias 行;一旦安装程序在特定系统中找到了某一硬件,它就会为其创建适当的别名入口项以保证能够加载正确的驱动程序。
options [-k] module opts
options 提供了加载给定模块时的选项(opts)。当设置 -k 标志时,该模块不会在执行 modprobe -r 时自动卸载。
pre-install module command
post-install module command
pre-remove module command
post-remove module command"
前两个指令指定给定的模块被加载之前/之后要执行的命令,而后两个指令则指定模块被卸载之前/之后要执行的动作。使用这些指令可以使我们在加载或卸载模块时,方便地调用额外的用户进程或启动所需的守护进程。其中,command 应该给出完整的路径名,以避免因此而产生的问题。
注意,对于模块卸载时要运行的命令,只有在使用 modprobe 卸载的模块,才会执行相应的命令;它们不会由于模块被 rmmod 命令卸载,或者系统被关闭(不管是正常还是异常关机)而得到执行。
modprobe 所支持的指令远不只上边列举的几个,但其它指令通常只用在非常复杂的场合。
一个典型的 /etc/modules.conf 文件通常是这样的:
alias scsi_hostadapter aic7xxx
alias eth0 eepro100
pre-install pcmcia_core /etc/rc.d/init.d/pcmcia start
options short irq=1
alias sound es1370
该文件告诉 modprobe,要想要使 SCSI 系统、以太网卡和声卡正常工作需要加载哪些驱动程序。它同时确保在加载 PCMCIA 驱动程序之前,首先先调用一个启动脚本以启动 PC 卡服务守护进程。最后,为驱动程序 short 提供了一个命令选项。
11.1.3 模块加载和安全性
由于被加载的模块代码会在最高的权限级别运行,很显然模块的加载会涉及到一些安全性的问题。正因为这样,在面对一个可加载模块的系统时应该格外小心。
当编辑文件 modules.conf 时,我们应该时刻记住,任何可以加载模块的人对整个系统有着完全的控制权。因此,任何被添加到模块加载路径列表的目录,以及 modules.conf 文件本身都应该仔细加以保护。
值得注意的是,insmod 通常会拒绝加载非 root 帐号所拥有的模块;这样做是在尽量防范取得模块加载路径写权限的攻击者。可以通过给 insmod 传一个选项(或者在 modules.conf 文件中添加一行)来强制取消这种检查,不过这样做会降低系统的安全性。
另一点需要注意的是,作为参数传递给 request_module 的模块名最终会成为为 modprobe 的命令行参数。如果模块名是某个用户空间程序提供的,则必须在传递给 request_module 之前进行仔细的验证。例如,考虑对网络接口进行配置的系统调用。在响应 ifconfig 的调用时,这个系统调用会告诉 request_module 为(用户指定的)接口加载驱动程序。一个怀有敌意的用户可以精心挑选一个虚构的接口名使得 modprobe 做出一些不适当的操作,这实在是一个安全性隐患,而且直到在 2.4.0-test 开发周期的后期才被发现。最严重的问题已经被清除,但是系统还是容易受到通过某些恶意模块名进行的攻击。
11.1.4 模块加载实例
现在,让我们实际地使用按需加载模块的功能。在这里,我们将会使用两个模块:master 和 slave。读者可在 O'Reilly FTP 站点的 misc-modules 目录下找到它们的源代码。
为了无需将模块安装到默认的搜索路径之下也可以运行这段测试代码,可以在 /etc/modules.conf 文件中添加如下几行:
keep
path[misc]=~rubini/driverBook/src/misc-modules
slave 模块并不实现任何功能,而 master 模块的代码如下所示:
#include
#include "sysdep.h"
int master_init_module(void)
{
int r[2]; /* results */
r[0]=request_module("slave");
r[1]=request_module("nonexistent");
printk(KERN_INFO "master: loading results are %i, %i\n", r[0],r[1]);
return 0; /* success */
}
void master_cleanup_module(void)
{ }
在加载时,master 试着加载两个模块:slave 模块和一个并不存在的模块。printk 会将调试信息加到系统日志中,而且,如果使用默认的日志等级,调试信息会出现在控制台终端上。下面是当系统被配置成支持 kmod 并且该守护进程已经激活的时候在控制台下执行一下命令时的结果:
morgana.root# depmod -a
morgana.root# insmod ./master.o
master: loading results are 0, 0
morgana.root# cat /proc/modules
slave 248 0 (autoclean)
master 740 0 (unused)
es1370 34832 1
request_module 的返回值和 /proc/modules 文件(在第2章的"初始化和终止"一节中描述过)均显示 slave 模块已经被正确加载。然而,请注意加载不存在模块时的返回值也是成功的,这是因为 request_module 只要成功调用了 modprobe,它就会返回成功标志,而不去理会 modprobe 执行的情况如何。
我们看看在卸载 master时会发生什么:
morgana.root# rmmod master
morgana.root# cat /proc/modules
slave 248 0 (autoclean)
es1370 34832 1
结果显示,slave 留在内核中。它将一直留在内核中,直到下一次模块清除过程结束(通常在现代操作系统中不会发生)。
11.1.5 运行用户态辅助程序
正如我们所看到的,request_module 程序运行了一个用户态程序(作为单独的进程,以非特权模式在用户空间内运行)来帮助它完成任务。在 2.3 系列的开发系列中,内核开发人员加入了"运行用户辅助程序"的机制。如果你的驱动程序需要一个用户态程序的支持其操作,则可以利用这个机制。由于它是 kmod 实现的一部分,我们将在这里讨论它。如果读者对这一机制感兴趣,推荐你看一看 kernel/kmod.c;它的代码不多而且对如何使用用户辅助程序做了很好的阐述。
运行辅助程序的接口函数非常简单。在内核 2.4.0-test9 中,有这样一个函数:call_usermodehelper,它主要用于热插拔子系统(比如 USB 设备等)中,以便在新设备连接到系统时,能够执行模块加载和配置任务。它的函数原型如下:
int call_usermodehelper(char *path, char **argv, char **envp);
它的参数形式并不陌生,分别是:所要执行的程序名,要传递给它的参数(依照惯例,argv[0] 是程序本身的名字),以及指向环境字符串指针数组的指针。这两个指针数组都要以 NULL 结尾,就象 execve 系统调用的那样。call_usermodehelper 将会睡眠直到辅助程序启动,然后返回操作的状态。
以这种方式运行的辅助程序实际上是作为一个叫做 keventd 的内核线程的子进程来运行的。这种设计意味着一个很重要的实事:你将无法知道什么时候辅助程序将会结束,以及他的返回状态如何。运行辅助程序的行为包含着对该程序的一种信任。
值得指出的是,真正使用用户辅助程序的场合是很少见的。在大多数情况下,较之于在内核代码中调用用户辅助程序,建立一个脚本以便在模块加载时进行所有必要工作的做法要好的多。
11.2 模块间通讯
在内核 pre-2.4.0 开发系列的很晚阶段,内核开发者提供了一个新的可以提供模块间简单通讯的接口。这一机制允许模块注册若干指向所关注数据的字符串,其它模块可检索这些字符串取得相关数据。我们接下来使用稍为变形的 master 和 slave 模块来简单讨论这个接口。
我们使用相同的 master 模块,但引入了一个新的称为 inter 的 slave 模块。inter 提供了与 ime_string 字符串和 ime_function 函数(其中的 ime 意指"intermodule example")。它的代码如下面所示:
static char *string = "inter says 'Hello World'";
void ime_function(const char *who)
{
printk(KERN_INFO "inter: ime_function called by %s\n", who);
}
int ime_init(void)
{
inter_module_register("ime_string", THIS_MODULE, string);
inter_module_register("ime_function", THIS_MODULE, ime_function);
return 0;
}
void ime_cleanup(void)
{
inter_module_unregister("ime_string");
inter_module_unregister("ime_function");
}
这段代码使用了函数 inter_module_register,它的原型如下:
void inter_module_register(const char *string, struct module *module,
const void *data);
string 是其他模块用来找到数据的字符串;module 是指向 data 所有者的指针,它的值通常取 THIS_MODULE;data 可以指向任何要共享的数据;。注意,data 被声明成 const 指针,这意味着它以只读方式导出。如果给定的 string 已经被注册过了,inter_module_register 会(通过 printk )表明错误。
在数据不再需要共享时,模块应该调用 inter_module_unregister 清除共享数据:
void inter_module_unregister(const char *string);
下面两个函数用来访问通过 inter_module_register共享的数据:
const void *inter_module_get(const char *string);
该函数查找给定的 string 并返回与之关联的 data 指针,如果 string 没有注册,将会返回 NULL。
const void *inter_module_get_request(const char *string, const char *module);
该函数与 inter_module_get 相似,但增加了如下特性:如果没有找到给定的 string,它将使用给定的模块名去调用 request_module,之后会再尝试用 string 查找一次。
这两个函数都会增加注册数据的模块的使用计数。因此通过 inter_module_get 或inter_module_get_request 得到的指针将会一直保持有效,直至被显式释放。在此期间,建立该指针的模块至少不会被卸载;但存在这样的可能性,即这个模块本身可以进行一些操作从而使该指针无效。
在完成与该指针相关的操作后,必须释放它以使得产生该指针的模块的使用计数被适当减少。调用函数
void inter_module_put(const char *string);
将释放该指针,在此之后不应再次使用该指针。
在我们例子中,模块 master 调用 inter_module_get_request,使得 inter 模块被加载从而取得字符串指针和函数指针。字符串仅仅用来打印,而函数指针用来实现从 master 模块对 inter 模块内函数的调用。master 模块其余的代码如下所示:
static const char *ime_string = NULL;
static void master_test_inter();
void master_test_inter()
{
void (*ime_func)();
ime_string = inter_module_get_request("ime_string", "inter");
if (ime_string)
printk(KERN_INFO "master: got ime_string '%s'\n", ime_string);
else
printk(KERN_INFO "master: inter_module_get failed");
ime_func = inter_module_get("ime_function");
if (ime_func) {
(*ime_func)("master");
inter_module_put("ime_function");
}
}
void master_cleanup_module(void)
{
if (ime_string)
inter_module_put("ime_string");
}
注意,其中一次对 inter_module_put 的调用在模块 master 清除时才进行,这会导致模块 inter 的使用计数在模块 master 被卸载之前始终保持(至少)为1。
在使用模块间通讯函数时,还有一些值得紧记的细节。首先,即使在配置成不支持可加载模块的内核中,它们仍然是可用的,因此没有必要增加针对它们的 #ifdef 分支。其次,模块间通讯函数的名字空间是全局的,在选择名字时应该格外小心,否则将会导致冲突。最后,模块间的共享数据被简单地存储在链表中,大量的查找或过多的字符串存储将会导致性能上的损失。这一设施被设计为面向少量使用的,而绝非一个象字典一样的子系统。
11.3 模块中的版本控制
模块机制的主要问题之一是版本依赖性,在第 2 章我们曾经介绍过这方面的内容。在我们运行若干定制模块时,如果针对每一个要使用的内核版本,都要重新编译每个模块,将是件非常痛苦的事情。如果运行的是以二进制形式发布的商业模块时,甚至连编译也是不可能的。
幸运的是,内核开发者们找到的一个灵活的办法来处理版本问题。其思想是,只有内核提供的软件接口发生改变时,才会出现与新内核版本不兼容的问题。软件接口可以由函数原型以及函数调用所涉及的所有数据结构的确切定义来表示。最后,可以使用一个 CRC 算法*把所有关于软件接口的信息映射到一个单一的 32 位数值上去。
注:CRC,即循环冗余校验(cyclic redundancy check),一种根据任意数量的数据生成唯一数值的方法。
这样,版本依赖性问题可通过在每个由内核导出的符号名中,包含与该符号相关的所有信息的校验和来得到处理,这些相关信息通过解析头文件来获得。这一设施是可选的,并可在编译阶段打。各种 Linux 发行版自带的内核一般都起用了版本化支持。
例如,在提供版本化支持时,符号 printk 是以类似 printk_R12345678 的形式向模块导出的,其中 12345678 是该函数使用的软件接口的校验和(16 进制表示)。要加载模块到内核时,仅当每个加到模块内符号上的校验和都与加到内核中相同符号上的校验和相匹配时,insmod(或 modprobe)才可以完成它的任务。
上述做法有一些局限性。常见的问题在将一个针对 SMP 的模块加载到单处理器的系统(或者相反)时出现。因为许多内联函数(例如,自旋锁操作)和符号在 SMP 内核具有不同的定义,因此,保持模块和内核在 SMP 支持上一致性是很重要的。2.4 版本和近期推出的 2.2 版本的内核在编译支持 SMP 的系统时会给每一个符号前都额外地加一个 smp_ 字符串以处理这一特殊情况。然而,还存在着一些潜在的问题。模块和内核会由于编译时所采用的编译器、它们所采用的内存布局,以及所支持的处理器版本等等的差异而不同。版本支持方案可以解决大多数常见问题,但是仍然要小心。
让我们来看看内核和模块均开启了版本支持的时候,会发生些什么:
- 内核本身并不修改符号。连接进程以通常的方式工作,并且 vmlinux 文件的符号表看起来也和以前一样。
- 公共符号表使用版本化的名字创建,如 /proc/ksyms 文件所显示的那样。
- 模块必须使用合并后的名字编译,这些名字在目标文件中是以未定义符号的形式出现的。
- 装载程序(insmod)用模块中未定义的符号匹配内核中的公共符号,因此要使用版本信息。
注意,内核和模块必须就是否支持版本化达成一致,否则 insmod 将拒绝加载模块。
11.3.1 在模块中使用版本支持
如果希望模块支持版本化,驱动程序编写者就必须在代码中显式地加入支持。可以在两处之一加入版本控制:在 makefile 中或在源代码本身。由于 modutils 包的文档描述了如何在 makefile 添加版本支持,因此,我们在这里说明如何在 C 源代码中加入版本支持。用于演示 kmod 工作机制的 master 模块可支持版本化的符号。如果用于编译模块的内核使用了版本化支持的话,这种功能就会自动启动。
用于合并符号名字的主要设施定义在文件 中,它包含了所有公共内核符号的预处理定义。该文件作为编译内核过程一部分而(确切的说是"make depend")创建,如果你的内核从来没有编译过,或者没有编译成版本化支持的,那么该文件中就不会有我们所感兴趣的东西了。 一定要在包含其它任何头文件之前包含。然而,通常的做法是通过一个编译命令告诉 gcc 来做这件事。
gcc -DMODVERSIONS -include /usr/src/linux/include/linux/modversions.h...
包含头文件之后,无论何时模块使用内核符号,编译器都将看到合并之后的符号。
如果内核已经启用了版本支持,为了在模块中启用,则必须确保在 中已定义过 CONFIG_MODVERSIONS。该头文件(在编译时)控制着在当前内核中启用了哪些特性。每个 CONFIG_ 宏定义说明相应选项已被激活*。
注:CONFIG_ 宏定义在文件 中定义。然而,读者应该包含 而不是 ,因为前者可避免自己被多次包含,而后者仅用于内部使用。而且内容源自 。
于是,master.c 的初始化部分包含如下代码:
#include /* retrieve the CONFIG_* macros */
#if defined(CONFIG_MODVERSIONS) && !defined(MODVERSIONS)
# define MODVERSIONS /* force it on */
#endif
#ifdef MODVERSIONS
# include
#endif
在针对版本化的内核编译这个文件时,目标文件的符号表会引用版本化的符号,这些符号与内核本身导出的符号相匹配。下面的屏幕快照显示了 master.o 中储存的符号名称。在 nm 的输出中,"T"代表"文本(text)" ,"D"代表"数据(data)","U"代表"未定义(undefined)"。"未定义"表示目标文件引用了但没有被声明的符号。
00000034 T cleanup_module
00000000 t gcc2_compiled.
00000000 T init_module
00000034 T master_cleanup_module
00000000 T master_init_module
U printk_Rsmp_1b7d4074
U request_module_Rsmp_27e4dc04
morgana% fgrep 'printk' /proc/ksyms
c011b8b0 printk_Rsmp_1b7d4074
因为添加到 master.o 中的符号名上的校验和来自 printk 和 request_module 的完整原型,因此,该模块可与大部分的内核版本兼容。然而,如果与其中任一函数有关的数据结构发生了变化,insmod 将会因为模块与内核的不兼容而拒绝加载它。
11.3.2 导出版本化符号
前面的讨论中未涉及的情况是,当其它模块要使用另一个模块导出出的符号时,将会出现什么情况。如果依赖版本信息获得模块的可移植性,我们也希望把 CRC 校验码加到我们自己的符号上去。这个问题比仅仅连接到内核的技巧性要高一些,因为我们需要将合并后的符号名导出给其它模块,为此,我们需要一种办法来生成校验和。
分析头文件和生成校验和的任务是由随 modutils 包一起发行的一个工具 genksyms 完成的。该程序在自身的标准输入接收 C 预编译器的输出,并在标准输出上打印一个新的头文件。这个输出文件中定义了原始源文件中导出的每个符号的校验和版本。genksyms 的输出通常以 .ver 为后缀保存,以下我们将遵循同样的惯例。
为了说明如何导出符号,我们编写了两个名为 export.c 和 import.c 的模块文件。export 将导出一个叫做 export_function 的简单函数,它将由第二个模块 import.c使用。该函数接收两个整形变量并返回它们的和--我们感兴趣的不是它的功能,而是连接过程。
在 misc-modules 目录中的 Makefile 有一条从 export.c 生成 export.ver 文件的规则,这样,export_function 的校验和符号可以被 import 模块使用:
ifdef CONFIG_MODVERSIONS
export.o import.o: export.ver
endif
export.ver: export.c
$(CC) -I$(INCLUDEDIR) $(CFLAGS) -E -D_ _GENKSYMS_ _ $^ | \
$(GENKSYMS) -k 2.4.0 > $@
这几行代码演示的如何生成 export.ver,并且只有定义了 MODVERSIONS 之后,才会把它加入到两个目标文件的依赖关系中去。如果内核启用了版本支持,还要添加几行到 Makefile 中处理 MODVERSIONS,但并不值得在这里展示它们。必须使用 -k 选项以通知 genksyms 为哪一内核版本进行工作,这样做的目的是要决定输出文件的格式。genksyms 并不需要匹配当前系统中运行着的内核。
另外一些值得说明的是 GKSMP 符号的定义。如前面提到的,如果内核被创建为支持 SMP 系统,则会在每个校验和前加一个前缀(-p smp_)。除非 genksyms 工具被明确告知,否则并不会自动添加前缀,Makefile 中如下的代码可保证适当设置这个前缀。
ifdef CONFIG_SMP
GENKSYMS += -p smp_
endif
然后,源文件必须为每个可能的预处理器步骤声明正确的预处理符号:不论是给 genksyms 的输入还是真正的编译过程,在启用或关闭版本支持的情况下都要声明适当的符号。进而,export.c 应该能够像 master.c 那样自动检测内核中的版本支持。下面几行说明了如何成功地做到这一点:
#include /* retrieve the CONFIG_* macros */
#if defined(CONFIG_MODVERSIONS) && !defined(MODVERSIONS)
# define MODVERSIONS
#endif
/*
* Include the versioned definitions for both kernel symbols and our
* symbol, *unless* we are generating checksums (_ _GENKSYMS_ _
* defined) */
#if defined(MODVERSIONS) && !defined(_ _GENKSYMS_ _)
# include
# include "export.ver" /* redefine "export_function" to include CRC */
#endif
这些代码虽然有些杂乱,但好处是可以让 Makefile 处于一个干净的状态。另一方面,由 make 来传递正确的标志,涉及到为各种情况编写冗长的命令行,因此,在这里我们没有这样做。
import 模块很简单,它传递两个数字(均为 2)作为参数调用 export_function ,其结果当然是 4。下面的例子说明 import 确实连接到了 export 中的版本化符号,并调用了函数。版本化符号出现在 /proc/ksyms 文件中。
morgana.root# insmod ./export.o
morgana.root# grep export /proc/ksyms
c883605c export_function_Rsmp_888cb211 [export]
morgana.root# insmod ./import.o
import: my mate tells that 2+2 = 4
morgana.root# cat /proc/modules
import 312 0 (unused)
export 620 0 [import]
11.4 向后兼容性
在 2.1 系列的开发中,按需加载功能被完整重新实现。幸运的是,很少有模块需要注意这些改变。然而,出于完整性的考虑,我们在这里对旧的实现方式进行一下描述。
在 2.0 的时候,按需加载是被一个称为 kerneld 的独立的、用户空间的守护进程处理的。这个守护进程通过一个特殊的接口连接到内核,并在内核代码生成模块加载(卸载)请求时接收这些请求。这样的处理方式存在很多缺点,其中之一就是这样一个事实:在系统初始化进行到相当程度而启动 kerneld 之前,任何模块都不可能被加载。
然而,在模块看来,request_module 函数保持不变,但是需要包含 取代对 的包含。
2.0 版本内核中面向 SMP 系统的符号没有使用 smp_ 前缀,这将会导致下面的结果:insmod将把一个面向 SMP 的模块加载到单处理器的内核中,反之亦然。通常这种不匹配将导致严重的混乱。
运行用户态辅助程序的功能以及模块间的通讯机制,直到 Linux 2.4 才出现。
11.5 快速索引
本章介绍了以下一些内核符号:
/etc/modules.conf
modprobe 和 depmod 的配置文件,它用于配置按需加载模块。在这两个程序的手册页中有描述。
#include
int request_module(const char *name);
该函数执行模块的按需加载。
void inter_module_register(const char *string, struct module *module, const void *data);
void inter_module_unregister(const char *);
inter_module_register 通过模块间通讯系统使数据可以为其他模块所用,取消对该数据的共享由 inter_module_unregister 函数完成。
const void *inter_module_get(const char *string);
const void *inter_module_get_request(const char *string, const char *module);
void inter_module_put(const char *string);
前两个函数在模块间通讯系统中查找字符串 string;当没有找到 string 时,inter_module_get_request 还会尝试着用给定的名字加载模块。两个函数都会增加导出 string 的模块的使用计数,inter_module_put 在不需要数据指针时,减少该使用计数。
#include
CONFIG_MODVERSIONS
只有当前内核被编译成支持版本化符号时,这个宏才会被定义。
#ifdef MODVERSIONS
#include
这个头文件只有在 CONFIG_MODVERSIONS 有效时才存在,它包含了内核开放的所有符号的版本化名字。
_ _GENKSYMS_ _
当 genksyms 读入预处理文件并生成新的版本代码时,make 定义了这个宏。在生成新的校验和时,该宏用于防止包含 头文件。
int call_usermodehelper(char *path, char *argv[], char *envp[]);
该函数在 keventd 进程上下文中运行一个用户态辅助程序。
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/23470/showart_171404.html |
|