Linux 动态载入 Module 介绍
作者 :
hlchou
本文为各位介绍 Linux Module 载入系统的过程,及 Linux Kernel 在 2.0.x 及 2.1.x 版之间,Kernel 载入 Device Driver 的不同处。文章的最后,则是着墨于 module stack 的机制...
前言
提到 Linux 中的 Device Driver 各位对于 Module 一定不陌生。Linux 中关于 Device Driver 的写作,在 2.0.x 及 2.1.x 版的Kernel之间有些微的不同,各位可以在 "Linux Device Drivers"这本书中,得到不少写作上的资讯。
本文希望可以为各位介绍 Linux Module 载入系统的过程,及 Linux Kernel 在 2.0.x 及 2.1.x 版之间,Kernel 载入 Device Driver 的不同处。文章的最后,则是着墨于 module stack 的机制。
虽然笔者尽力使这篇文章内容无误,如果仍有笔者疏忽之处,还希望 Linux 界的前辈们可以给予笔者指教。撰写本文的原意即是在让更多人了解 Linux 的系统运作,希望可以为各位尽一份心力,谢谢。
Module 的档案格式
在 Linux 中,我们常见到的档案格式为 ELF(Executable and Linkable Format)。由下图(一),为执行档的 ELF Header,可以由e_type来得知此档为 "Executable File"。若由图(二),来看 module 的 ELF Header,可以看到 e_type 为 "Relocatable
File"。
图(一),执行档的 ELF header
图(二),目的档的 ELF header
其实,"Relocatable File"即为 ELF 目的档(Object File)的格式。也因此在 module 中包含了许多尚未经过连结的 Symbol。读者如果有在 Linux 中透过 "insmod"载入 module 的话,想必偶尔会遇到 unresolved symbol 的问题。因为,每当 module 在载入到系统时,它会进行动态连结的动作,把此时在
module 中还未解决的 symbol,透过动态连结的方式,与目前存在系统中的 symbol 做连结,如果所要求的 symbol 并不存在此时系统中,便会发生 unresolved symbol 的错误讯息。
当然罗,一旦新的 module 载入到系统后,新的 module 中所包含的 symbol,亦会加入目前 kernel 所拥有的 symbol 中。此后,我们若要载入其它的 module 时,便多了上回 module 所载入的 symbol 可供我们连结。
我们可以透过 nm 这个程序来查看 module 中尚未连结的 symbol,如下图(三)为执行 "nm msdos.o" 的结果
图(三),透过 nm 察看 msdos.o 的结果
我们可以看到 "printk_R2gig_1b7d4074" 前面有一个符号 "U",这即表示 printk 这个 symbol 尚未经过连结。而这些将透过动态载入的过程中,一一解决。我们可以透过 /proc/ksyms 来得知目前 已存在 kernel 的 symbol,如果我们所要载入的 module 含有目前 kernel 不存在的
symble,则"unresolved symbol"的错误,就会发生罗....^_^。
此外,"printk_R2gig_1b7d4074"中,函式"printk"之后的" R2gig_1b7d4074"字符串,为 Linux 解决 kernel 版本问题,而在各 kernel symbol 之后附加的 32 位的 CRC(Cyclic Redundancy Code)。当 module 载入到系统时,insmod 会去比对所载入 module
使用的 symbol CRC 值是否与目前 kernel 所提供的CRC值一致。两者如果一样的话,表示此函式与载入 module 所要呼叫的函式相同,并未有版本兼容的问题。除了 CRC 值的确认外,亦可透过取得 module 中所纪录的 "kernel_version",与目前的 kernel 版本做比较。有关这部分的说明,读者可以参考本文有关 "insmod" 的运作说明。
2.0.x 与 2.1.x 以后的 Module 的差异
在 2.0.x 版本的 Linux 中,主要是透过一个 User Mode 的 "Kerneld" Daemon,处理来自 Kernel 要求载入 module 的工作,并经由执行 "modprobe" 指令载入所需的 Module。到了 2.1.x 以后的版本,这份工作则交由 "kmod" 这个 Kernel Mode 程序,透过产生一个
kernel thread 来执行"modprobe"指令载入 module。 其实,笔者也曾质疑为何要把 kernel 2.0.x 在User Mode 的 Kerneld,换成 kernel 2.1.x 在 Kernel Mode 的 kmod。后来,我在 Linux Source Code 的"/Document/kmod.txt" 档案中找到了我想要的资料,也算是给了我一个还算满意的答案。以下就是这份文件的部份内容:
kerneld used SysV IPC, which can now be made into a module. Besides, SysV IPC is ugly and should therefore be avoided (well, certainly for kernel level stuff)
(虽然 kerneld 所使用的 SysV IPC 已经可以在 module 中直接呼叫,不过,SysV IPC 的设计仍不够完美,应尽量避免在 kernel level 中使用。)
both kmod and kerneld end up doing the same thing (calling modprobe), so why not skip the middle man?
(kmod及kerneld最后都会去呼叫modprobe,为何不跳过kerneld这个中间人呢?) removing kerneld related stuff from ipc/msg.c made it 40% smaller
(移除kerneld相关的程序,使得 ipc/msg.c这个档案小了40%。) kmod reports errors through the normal kernel mechanisms, which avoids the chicken and egg problem of kerneld and modular Unix domain sockets
(kmod会透过kernel的函式来回报错误,可以避免kerneld引发的鸡生蛋,蛋生鸡的问题。)
lsmod 在新旧版本的不同
指令 "lsmod" ,在 Linux 中可以用来查看目前有哪些 module 已载入到系统中。如下图(四):
图(四),透过 lsmod 指令,列出已载入的 module
在 kernel 2.0.x 时,指令"lsmod"是去开启档案 "/proc/modules" 来得知系统中,已载入哪些 Module。不过到了kernel 2.1.x以后,系统提供了函式" query_module"。因此,此时"lsmod"的实作便是透过呼叫 query_module 来取得系统已载入 module 的相关资料。有关 lsmod 的实作,可以参考 modutils-2.1.85 中的 lsmod.c。
既然提到了kernel 2.1.x之后所提供的函式"query_module",我们以 /arch/i386/kernel/entry.s 这档案来比较 kernel 2.0.35 与 2.2.12 在 sys_call_table 的不同。其中,2.0.35 共有 0□166 个 System Call (亦即 80 号中断的服务),而2.2.12则有0---190个System Call,其中笔者所提到的函式"query_module"则为第167个函式。在 Linux 中,System Call
随版本的更新,不断的扩充。笔者随着各版本的更新使用至今,实在是很敬佩那群为 Linux 付出贡献的前辈们。
.long SYMBOL_NAME(sys_sched_get_priority_min) /* 160 */
.long SYMBOL_NAME(sys_sched_rr_get_interval)
.long SYMBOL_NAME(sys_nanosleep)
.long SYMBOL_NAME(sys_mremap)
.long SYMBOL_NAME(sys_setresuid)
.long SYMBOL_NAME(sys_getresuid) /* 165 */
.long SYMBOL_NAME(sys_vm86)
.long SYMBOL_NAME(sys_query_module)
.long SYMBOL_NAME(sys_poll)
.long SYMBOL_NAME(sys_nfsservctl)
.long SYMBOL_NAME(sys_setresgid) /* 170 */
.long SYMBOL_NAME(sys_getresgid)
.long SYMBOL_NAME(sys_prctl)
.long SYMBOL_NAME(sys_rt_sigreturn)
.long SYMBOL_NAME(sys_rt_sigaction)
Kerneld 的运作
在此,笔者将先介绍在 kernel 2.0.x 中所使用的 "kerneld"。在 Linux 的文件计划中(LDP),有一篇很不错的文章 "kerneld HowTo",读者们若是希望更多方面的了解 kerneld,倒是可以从那篇文章中学到不少有用的资讯。
首先,由下图(五)我们可以看到 kerneld 程序运作的简图,kerneld 在启动时会把自己初始化为一个 user mode 的 daemon,在程序一切准备就绪后,便开始接收系统所发出的 IPC(Internal Process Comunication) Message。若 kernel 要把某个 module 载入系统时,便会透过发出 IPC Message 的方式,来通知 kerneld 载入该 module。它所能辨识的 Message
Type 有数种,我在图(五)中仅列出其中的两种。
如果收到的 Message Type 为 KERNELD_REQUEST_MODULE,则透过外部呼叫,执行 "modprobe"这只程序,来把所要求的 module 载入系统中。若收到的 Message Type 为 KERNELD_RELEASE_MODULE,亦透过"modprobe"来移除 module。
图(五),kerneld 的流程
笔者在 kernel 2.0.35 版本 Source Code 的档案"/Drivers/Block/md.c"中,找到函式 "static int do_md_run (int minor, int repart)",它实作了以下的程序码:
if (!pers[pnum])
{
#ifdef CONFIG_KERNELD
char module_name[80];
sprintf (module_name, "md-personality-%d", pnum);
request_module (module_name);
if (!pers[pnum])
#endif
return -EINVAL;
}
而 request_module 便是在 kernel mode 中要求载入 module 时,所呼叫的函式,之后在 /include/linux/kerneld.h 我们可以得知此函式的内容如下:
/*
* Request that a module should be loaded.
* Wait for the exit status from insmod/modprobe.
* If it fails, it fails... at least we tried...
*/
static inline int request_module(const char *name)
{
return kerneld_send(KERNELD_REQUEST_MODULE,0 | KERNELD_WAIT, strlen(name), name, NULL);
}
此函式会呼叫 kerneld_send 来传送 Message 给 kerneld daemon,以完成载入 module 的动作。在 "/IPC/msg.c" 里, 函式"kerneld_send"的实作,有以下这样一段程序码:
int kerneld_send(int msgtype, int ret_size, int msgsz,const char *text, const char *ret_val)
{
。。。。。。。。。。。。。。。。。。
status = real_msgsnd(kerneld_msqid, (struct msgbuf *)&kmsp, msgsz, msgflg);
if ((status >= 0) && (ret_size & KERNELD_WAIT))
{
ret_size &= ~KERNELD_WAIT;
kmsp.text = (char *)ret_val;
status = real_msgrcv(kerneld_msqid, (struct msgbuf *)&kmsp,
KDHDR + ((ret_val)?ret_size:0),
kmsp.id, msgflg);
if (status > 0) /* a valid answer contains at least a long */
status = kmsp.id;
。。。。。。。。。。。。。。。
}
在档案 "/ipc/msg.c" 中的 real_msgsnd (int msqid, struct msgbuf *msgp, size_t msgsz, int msgflg)函式,可透过 IPC 把 kernel mode 中,所要求的 Message 传给在 User Mode 的 kerneld daemon,而在 User Mode 的 kerneld 则透过函式 msgrcv() 来取得 kernel 所送过来的 Message,再来载入或移除
kernel 所要求的 Module。
紧接着,笔者简单介绍 kernel 2.0.x "kerneld" 的运作之后。我将以 kernel 2.1.x 之后的 "kmod" ,作为我们所要讨论的主题。在这版本中,kernel mode 要载入 module 时,无须再透过 user mode 的 daemon 来间接处理,将以在 kernel 中直接呼叫执行 "modprobe" 的方式,来简化整个流程。
Kmod 的运作
我们可以在 /kernel/kmod.c 中看到 kmod.c 中看包括 3 个主要的函式,
use_init_file_context(void) exec_modprobe(void * module_name) request_module(const char * module_name)
kernel 2.1.x 之后,当 kernel 需要载入某个 module 时,在 kmod 的机制下,无须由 kernel 发出 IPC Message 给 user mode 的 kerneld,可以经由 kernel mode 中函式request_module(),来达成直接执行 "modprobe"载入 module 的目的。
在request_module()中,会呼叫函式 "kernel_thread()" 产生 kernel thread 来执行函式 "exec_modprobe()",在函式 "exec_modprobe()" 中则透过函式 "execve()" 以便在 kernel mode 里外部执行 modprobe。而 modprobe_path 储存了 modprobe 在系统中的路径。
看到这我们不难发现,两种不同 kernel 载入 module 的机制,不过最终都是透过外部执行 "modprobe" 来得以顺利的载入 module。kmod 的建立,便是为了让这整个流程可以更有效率的运作。
如图(六),为 kmod 运作的一个简要流程图。其中有一段在函式 "use_init_file_context()" 的文字是我不甚了解的部分,由于没有很深的体会,对此我也不敢妄加评论,因此我把它列在图中,让各位作一个参考。
图(六),kmod 的流程
如同我们在介绍 kerneld 时,所举的例子 "/drivers/block/md.c",其中的函式 "do_md_run()" 在新版的 kernel 中,动态载入 module 的部份程序码如下:
if (!pers[pnum])
{
#ifdef CONFIG_KMOD
char module_name[80];
sprintf (module_name, "md-personality-%d", pnum);
request_module (module_name);
if (!pers[pnum])
#endif
return -EINVAL;
}
其实,同样都是呼叫函式 "request_module",只是函式 request_module 内部的运作已跟过去版本的 kernel 有所不同罗!
Modprobe
之前我们看到 kernel 中载入 module,到了后来都会去执行 modprobe 这个程序来载入 module,因此在这我们便进一步的来探讨 modprobe 的运作情形。
图(七),modprobe 流程
modprobe 会根据所要载入的 module 是否有用到其它的 module 而作必要的载入。如上图(七)所示,modprobe 当察知所要载入的 module 有用到目前尚未载入 module 的函式时,便会透过函式 "system()" 呼叫 insmod 来载入该 module。
如下图(八)所示,当我们透过指令 "insmod"载入 msdos.o 时,由于 msdos.o 使用到了 fat.o 的函式,因此在 insmod 载入过程中会发生"unresolved symbol"的错误,
图(八),
当然罗!如果我们如下图(九),透过 insmod 先把 fat.o 载入后,再把 msdos.o 载入系统中,就不会发生之前的错误了。不过这样做对使用者而言,毕竟是有些麻烦,尤其是面对一个不熟悉的 module,若又同时用到了许多目前尚未载入的 module 时,那使用者可是会手忙脚乱了。
图(九),
因此,透过 modprobe 我们可以简便的解决这类 "module stack" 的问题(稍后会针对这点讨论)。如下图(十),我们可以在系统中并没有fat.o载入的情况下,透过 modprobe 载入 msdos.o,透过之前说过的 modprobe 运作流程,我们可以知道此时它会去察觉目前载入的 module,是否有呼叫到其它 module 的函式,及他们是否已被载入到系统中,进而循序的透过呼叫 "insmod" 来载入各个所需的 module,而帮我们自动把这些问题给处理掉。
图(十),
有关 Module 的资讯
首先,我们可以在 /proc/ksyms 中看到如下图(十一)的资讯,ksyms 中所提供的资讯包括 kernel 中为 "EXPORT_SYMBOL()" 的 symbol,以及系统目前已载入 module 所提供的 symbol。在下图中,我们可以看到各函式后方被框起来的部分便是提供该 symbol 的 module 名称。每个函式后方的编号,是该函式版本的 32 位 CRC 资料,用来确保函式呼叫的版本正确性。例如,函式
ABCD() 在 2.1 版的 kernel 时有 3 的引数,如:
ABCD(int A,int B,int C)
不过到了 2.2 版时,函式 ABCD() 经过修改后引数成为 4 个,如:
ABCD(int A,int B,int C,int D)
透过这类型的检查,便可以避免这些不必要的错误。
图(十一),
由下图(十二),为此时系统中所载入的 module 列表。我们与图(十一)比较时,可以看到在 /proc/ksyms 中,函式后方白线所框起来的 module name,为目前系统中所存在的 module。
图(十二),
以下为在 /kernel/ksyms.c 中的部分程序行表,我们可以看到被以 EXPORT_SYMBOL() 处理的函式名称。
EXPORT_SYMBOL(panic);
EXPORT_SYMBOL(printk);
EXPORT_SYMBOL(sprintf);
EXPORT_SYMBOL(vsprintf);
EXPORT_SYMBOL(kdevname);
EXPORT_SYMBOL(bdevname);
EXPORT_SYMBOL(cdevname);
EXPORT_SYMBOL(simple_strtoul);
EXPORT_SYMBOL(system_utsname); /* UTS data */
EXPORT_SYMBOL(uts_sem); /* UTS semaphore */
EXPORT_SYMBOL(sys_call_table);
EXPORT_SYMBOL(machine_restart);
EXPORT_SYMBOL(machine_halt);
Insmod Module
我以 2.1.85 版本的 insmod.c 来做一个说明,当我们执行此一个命令时"insmod ModuleA",它会去执行以下的流程
取得 ModuleA 的档案位置, 可透过函式 search_module_path(),或由使用者指定位置。 取得档案位置后,由 fopen() 开档。开档成功后,则由obj_load()把 ModuleA 载入,对 Module 不熟悉的读者可能会想说为何是 obj_load() 呢? 其实,Module在 Linux 中是以 Obj 档的形式存在,在载入到系统后,再与 Kernel 各部份连结,而成为 Kernel 的一部份。
透过函式 get_kernel_version() 取得 Kernel 版本资讯。在取得 Module 版本资讯时,会先透过 new_get_module_version(),如果传回错误,则再透过 old_get_module_version() 取得,如此可使载入 Module 的动作,兼容于两个版本的 Linux 环境。
比对 Kernel 版本及由 Module 取出的版本是否一致,若有设 "Force loading" (ex: insmod -f),则不论 Kernel 版本是否一致,都将继续执行。否则,便结束载入的动作。
接着执行 "query_module(NULL, 0, NULL, 0, NULL)" 来得知是否为支持 query_module 系统函式的 Kernel。若是,则上述呼叫将传回 0,否则传回非 0 值。(ex:有关 sys_query_module 函式的 Source 请参考 kernelmodule.c,在 System Call Table 的位置可参考 archi386kernelentry.s,得知可透过 Int 80h ax=A7h 呼叫)
在得知 Kernel 新旧版本后,则分别执行" new_get_kernel_symbols()、new_is_kernel_checksummed()" 及 "old_get_kernel_symbols()、old_is_kernel_checksummed()"。我们以新版本的 new_get_kernel_symbols() 来说明,如下图
图(十三),
呼叫 new_get_kernel_symbols() 后。接着呼叫 new_is_kernel_checksummed() ,这个函式会寻找 Symbol Name 为"Using_Versions"的 kernel symble,并把该值传回给 k_crcs。
接着取得我们所要载入的 module 中 using_checksums 栏位的值,存入 m_crcs。比对 Kernel 及 Module 的 CRC 值是否吻合。
if (m_crcs != k_crcs)
obj_set_symbol_compare(f, ncv_strcmp, ncv_symbol_hash);
接着呼叫 add_kernel_symbols,把载入的 module 中未定义的 symbol,与目前 kernel 及已载入的 module 之 symbol 做连结。
图(十四),
呼叫 new_create_this_module 或 old_create_mod_use_count 依不同的版本建立此 Module 的 Symbol Table 及 String Table.......等。
呼叫 obj_check_undefineds,确认并未定义过同一个Module。 obj_allocate_commons 建立 Common Symbol。 把载入 Module 时使用者所加入的引数传给 Module,同样亦包含了 new_process_module_arguments 及 old_process_module_arguments 两个新旧不同的版本。
呼叫 new_create_module_ksymtab 建立新的 Module 的 Symbol Table。 透过 obj_load_size 取得 Module 最终的大小,并透过 create_module 函式把 Module 真正建立起来,此时 create_module 会传回一个记忆体地址,此记忆体地址会在下一步用到。
呼叫 obj_relocate 把 Module 重新更动到呼叫 create_module 时,所传回来的记忆体地址。 呼叫 new_init_module 或 old_init_module 去初始化该Module。 最后检查如果发生错误,则透过函式 delete_module 把 Module 移除。
Module Stack
当然罗,Module 所 Reference 到的 Symbol 也是有可能存在于其它的 Module 中,若 A Module 使用了 B Module 的函式,当 A Module 载入到系统时,若 B Module 此时不存在系统中,便会发生 unresolved symbol 的错误。因此要解决 Module Stack 所引发的问题,我们可以在 insmod A Module 前,先把 B Module 载入到系统中。
或是可以透过 modprobe 这个程序,来得知 A Module 有参考到的 Symbol,并自动的帮我们把 B Module 载入到系统中。
我觉得有一篇不错的文章 "Loadable Kernel Modules"(请参阅参考资料3),其中对 Module Stack 有很不错的说明,如果读者对这部份的有兴趣的话,可以进一步去取的这方面的资料,以下我针对这个部份做一个简短的说明,希望可以对各位有一些帮助。
如下图(十五),module Y 呼叫了 module X 所提供的函式,则 module Y->deps 会指向一个 module_ref 结构(可参考图(十九))。 module_ref 结构有三个成员,分别为:
- dep:存放被参考 module 的地址。
- ref:存放参考者 module 的地址。
- next_ref:存放下一个在 dep 栏位中,有同一个被参考 module 地址的 module_ref 地址。
由于 module X 函式被 module Y 呼叫,所以 module X->refs 会指向图(十五)中的 module_ref 结构。而 module Y 因为呼叫了其它 module 的函式,本身并未被其它的 module 所呼叫,所以 module Y->refs 为 NULL。相同的,module X 函式被 module Y 所呼叫,本身并没有呼叫其它的 module,所以 module X->deps 为 NULL。
接着,我们看图中的 module_ref 结构。由于 module Y 呼叫 module X 的函式,而需参考图(十五)中的 module_ref,所以 module_ref->ref 指向 module Y。相同的,module_ref 依赖 module X 来提供资讯给 module Y,所以 module_ref->dep 指向 module X。最后,由于这是单纯 module Y 呼叫 module X 函式的 module stack,并没有同一个 module 函式被一个以上的 module
呼叫。所以,在图(十五)中的 module_ref->next_ref 为 NULL。
图(十五),module Y 呼叫了 module X 的函式
再来,我们以三个module的例子来做说明。由图(十五)进一步扩充,如下图(十六),在载入 module X 与 module Y 后,接着载入 module Z。module Z 同时呼叫了 module X 与 module Y 的函式。
首先,module Z呼叫module Y 的函式,所以 module Z->deps 指向 module_ref A,module Z 并没有被其它 module 所呼叫,故 module Z->refs为NULL。而 module Y 函式被 module Z 呼叫,module Y->refs 指向 module_ref A。如同图(十五)例子的说明,module_ref A->ref 指向 module Z,而 module_ref A->dep 指向 module Y。
接着,我们再看呼叫 module X 函式的情形。module Z 与 module Y 都呼叫了 module X 的函式,在图(十六)中我们可以看到 module_ref B 所在位置为 module_ref A 之后,而 module_ref B->ref 指向 module Z,module Z 透过参考 module_ref B 来取得 module X 资讯。而 module_ref B->dep 指向 module X,值得注意的是 module_ref B 的 next_ref 指向 module_ref
C。因为 module Z 与 module Y 共同呼叫了 module X 的函式,所以这两个 module_ref 亦建立了关系。在 module_ref C 中,module_ref C->ref 指向 module Y,而 module_ref C->dep 指向 module X,module_ref C->next_ref 为 NULL。由于 module Y 呼叫了 module X 的函式,所以在图(十六)中,module Y->deps 指向 module_ref C。
图(十六),moduleZ 呼叫了 module Y、module X 的函式
图(十七),module 的 struct
图(十八),module_symbol 的 struct
图(十九),module_ref 的 struct
结语
最后,读者可以由以下图(二十)与图(二十一)了解两种载入 module 机制的不同。笔者在写这篇文章的过程中收获实在不少。有许多自己不曾注意过的细节,为了能够清楚的表达,我都尽可能的自己去走一遍。最后能够有机会把我的心得与各位分享,真的很开心,希望各位有任何意见的话,可以写信与我联络,我的 E-Mail 是:
n9688408@sparc1.cc.ncku.edu.tw。
图(二十),kerneld 载入 driver 的流程图
图(二十一),kmod 载入 driver 的流程图
参考资料
Alessandro Rubini,"Dynamic Kernels:Modularized Device Driver," Linux Journal,Issue 23,Mar. 1996
Alessandro Rubini,"Dynamic Kernels:Discovery",Linux Journal,Issue 24,Apr. 1996
Juan-Mariano de Goyeneche and Elena Apolinario Fernandez de Sousa,"Loadable Kernel Modules",IEEE Software,January/February 1999
ALESSANDRO RUBINI,"LINUX DEVICE DRIVER",O'REILLY',1998 Linux 2.0.35 and 2.2.12 kernel source code