当你新建立了最小内核(如何建立新内核,请看相关的HOWTO),并且重新启动后,你可以利用实用程序"insmod"和"rmmod",随意地给内核插入或从内核中移走模块。如果kerneld守护进程启动,则由kerneld自动完成模块的插拔。有关模块实现的源代码在 /kernel/module.c中,以下是对源代码中主要函数的分析。
1. 启动时内核模块的初始化函数-- init_modules()
当内核启动时,要进行很多初始化工作,其中,对模块的初始化是在main.c中调用init_modules()函数完成的。实际上,当内核启动时唯一的模块就为内核本身,因此,初始化要做的唯一工作就是求出内核符号表中符号的个数:
*/
* Called at
boot time
*/
void __init init_modules(void)
{
kernel_module.nsyms = __stop___ksymtab - __start___ksymtab;
arch_init_modules(&kernel_module);
}
因为内核代码被编译以后,连接程序进行连接时内核符号的符号结构就“移出”到了ksymtab区段,__start___ksymtab为第一个内核符号结构的地址,__stop___ksymtab为最后一个内核符号结构的地址,因此二者之差为内核符号的个数。其中,arch_init_modules是与体系结构相关的函数,对i386来说,arch_init_modules在include/i386/module.h中定义为:
#define arch_init_modules(x) do { } while (0)
可见,对i386来说,这个函数为空。
2. 创建一个新模块
当用insmod给内核中插入一个模块时,意味着系统要创建一个新的模块,即为一个新的模块分配空间,函数sys_create_module()完成此功能,该函数也是系统调用screate_module()在内核的实现函数,其代码如下:
/*
* Allocate
space for a module.
*/
asmlinkage unsigned long
sys_create_module(const char
*name_user, size_t size)
{
char *name;
long namelen, error;
struct module *mod;
unsigned long flags;
if (!capable(CAP_SYS_MODULE))
return -EPERM;
lock_kernel();
if ((namelen = get_mod_name(name_user,
&name)) < 0) {
error = namelen;
goto err0;
}
if (size < sizeof(struct module)+namelen) {
error = -EINVAL;
goto err1;
}
if (find_module(name) != NULL) {
error = -EEXIST;
goto err1;
}
if ((mod = (struct module *)module_map(size)) == NULL) {
error = -ENOMEM;
goto err1;
}
memset(mod, 0, sizeof(*mod));
mod->size_of_struct = sizeof(*mod);
mod->name = (char *)(mod + 1);
mod->size = size;
memcpy((char*)(mod+1), name, namelen+1);
put_mod_name(name);
spin_lock_irqsave(&modlist_lock, flags);
mod->next = module_list;
module_list = mod; /* link it in */
spin_unlock_irqrestore(&modlist_lock,
flags);
error = (long) mod;
goto err0;
err1:
put_mod_name(name);
err0:
unlock_kernel();
return error;
}
下面对该函数中的主要语句给予解释。
· capable(CAP_SYS_MODULE)检查当前进程是否有创建模块的特权。
· 参数size表示模块的大小,它等于module结构的大小加上模块名的大小,再加上模块映像的大小,显然,size不能小于后两项之和。
· get_mod_name()函数获得模块名的长度。
· find_module()函数检查是否存在同名的模块,因为模块名是模块的唯一标识。
· 调用module_map()分配空间,对i386来说,就是调用vmalloc()函数从内核空间的非连续区分配空间。
· memset()将分配给module结构的空间全部填充为0,也就是说,把通过module_map()所分配空间的开头部分给了module结构;然后(module+1)表示从mod所指的地址加上一个module结构的大小,在此处放上模块的名字;最后,剩余的空间给模块映像。
· 新建moudle结构只填充了三个值,其余值有待于从用户空间传递过来。
· put_mod_name()释放局部变量name所占的空间。
· 将新创建的模块结构链入module_list链表的首部。
3. 初始化一个模块
从上面可以看出,sys_create_module()函数仅仅在内核为模块开辟了一块空间,但是模块的代码根本没有拷贝过来。实际上,模块的真正安装工作及其他的一些初始化工作由sys_init_module()函数完成,该函数就是系统调用init_module()在内核的实现代码,因为其代码比较长,为此对该函数的主要实现过程给予描述。
该函数的原型为:
asmlinkage long sys_init_module(const char *name_user, struct
module *mod_user)
其中参数name_user为用户空间的模块名,mod_user为指向用户空间欲安装模块的module结构。
该函数的主要操作描述如下:
· sys_create_module()在内核空间创建了目标模块的module结构,但是这个结构还基本为空,其内容只能来自用户空间。因此,初始化函数就要把用户空间的module结构拷贝到内核中对应的module结构中。但是,由于内核版本在不断变化,因此用户空间module结构可能与内核中的module结构不完全一样,例如用户空间的module结构为2.2版,而内核空间的为2.4版,则二者的module结构就有所不同。为了防止二者的module结构在大小上的不一致而造成麻烦,因此,首先要把用户空间的module结构中的size_of_struct域复制过来加以检查。从前面介绍的module结构可以看出,2.4版中从persist_start开始的域是内核对module结构的扩充,用户空间的module结构中没有这些域,因此二者module结构大小的检查不包括扩充域。
· 通过了对结构大小的检查以后,先把内核中的module结构保存在堆栈中作为后备,然后就从用户空间拷贝其module结构。复制时是以内核中module结构的大小为准的,以免破坏内核中的内存空间。
· 复制过来以后,还要检查module结构中各个域的合理性。
· 最后,还要对模块名进行进一步的检查。虽然已经根据参数name_user从用户空间拷贝过来了模块名,但是这个模块名是否与用户空间module结构中所指示的模块名一致呢?显然,也可能存在不一致的可能,因此还要根据module结构的内容把模块映像中的模块名也复制过来,再与原来使用的模块名进行比较。
· 经过以上检查以后,可以从用户空间把模块的映像复制过来了。
· 但是把模块映像复制过来并不是万事大吉,模块之间的依赖关系还得进行修正,因为正在安装的模块可能要引用其他模块中的符号。虽然在用户空间已经完成了对这些符号的连接,但现在必须验证所依赖的模块在内核中还未被卸载。如果所依赖的模块已经不在内核中了,则对目标模块的安装就失败了。在这种情况下,应用程序(例如insmod)有责任通过系统调用delete_module()将已经创建的module结构从moudle_list中删除。
· 至此,模块的安装已经基本完成,但还有一件事要做,那就是启动待执行模块的init_moudle()函数,每个模块块必须有一个这样的函数,module结构中的函数指针init就指向这个函数,内核可以通过这个函数访问模块中的变量和函数,或者说,init_moudle()是模块的入口,就好像每个可执行程序的入口都是main()一样。
4.卸载模块的函数--sys_delete_module()
卸载模块的系统调用为delete_module(),其内核的实现函数为sys_delete_module(),该函数的原型为:
asmlinkage
long sys_delete_module(const char
*name_user)
与前面几个系统调用一样,只有特权用户才允许卸载模块。卸载模块的方式有两种,这取决于参数name_user, name_user是用户空间中的模块名。如果name_user非空,表示卸载一个指定的模块;如果为空,则卸载所有可以卸载的模块。
(1)
卸载指定的模块
一个模块能否卸载,首先要看内核中是否还有其他模块依赖该模块,也就是该模块中的符号是否被引用,更具体地说,就是检查该模块的refs指针是否为空。此外,还要判断该模块是否在使用中,即__MOD_IN_USE()宏的值是否为0。只有未被依赖且未被使用的模块才可以卸载。
卸载模块时主要要调用目标模块的cleanup_module()函数,该函数撤销模块在内核中的注册,使系统不再能引用该模块。
一个模块的拆除有可能使它所依赖的模块获得自由,也就是说,它所依赖的模块其refs队列变为空,一个refs队列为空的模块就是一个自由模块,它不再被任何模块所依赖。
(2)
卸载所有可以卸载的模块
如果参数name_user为空,则卸载同时满足以下条件的所有模块:
· 不再被任何模块所依赖
· 允许自动卸载,即安装时带有MOD_AUTOCLEAN标志位。
· 已经安装但尚未被卸载,即处于运行状态。
· 尚未被开始卸载
· 安装以后被引用过
· 已不再使用
以上介绍了init_module()、create_module()、delete_module()三个系统调用在内核的实现机制,还有一个查询模块名的系统调用query_module()。这几个系统调用是在实现insmod及rmmod实用程序的过程中被调用的,用户开发的程序一般不应该、也不必要直接调用这些系统调用。
5. 装入内核模块—request_module()函数
在用户通过insmod安装模块的过程中,内核是被动地接受用户发出的安装请求。但是,在很多情况下,内核需要主动地启动某个模块的安装。例如,当内核从网络中接收到一个特殊的packet或报文时,而支持相应规程的模块尚未安装;又如,当内核检测到某种硬件时,而支持这种硬件的模块尚未安装等等,类似情况还有很。在这种情况下,内核就调用request_module()主动地启动模块的安装。
request_module()函数在kernel/kmod.c中:
/**
*
request_module - try to load a kernel module
*
@module_name: Name of module
*
*
Load a module using the user mode module loader. The function returns
*
zero on success or a negative errno code on failure.
Note that a
*
successful module load does not mean the module did
not then unload
*
and exit on an error of its own. Callers must check
that the service
*
they requested is now available not blindly invoke it.
*
*
If module auto-loading support is disabled then this function
*
becomes a no-operation.
*/
int request_module(const char * module_name)
{
pid_t pid;
int waitpid_result;
sigset_t tmpsig;
int i;
static atomic_t kmod_concurrent = ATOMIC_INIT(0);
#define
MAX_KMOD_CONCURRENT 50
/* Completely arbitrary value - KAO */
static int kmod_loop_msg;
/* Don't allow request_module() before the root
fs is mounted! */
if ( ! current->fs->root ) {
printk(KERN_ERR "request_module[%s]: Root
fs not mounted\n", module_name);
return -EPERM;
}
/* If modprobe
needs a service that is in a module, we get a recursive
* loop. Limit the number of running kmod threads
to max_threads/2 or
*
MAX_KMOD_CONCURRENT, whichever is the smaller. A cleaner method
*
would be to run the parents of this process, counting how many times
* kmod was invoked.
That would mean accessing the internals of the
*
process tables to get the command line,
proc_pid_cmdline is static
*
and it is not worth changing the proc code just to
handle this case.
*
KAO.
*/
i
= max_threads/2;
if (i > MAX_KMOD_CONCURRENT)
i = MAX_KMOD_CONCURRENT;
atomic_inc(&kmod_concurrent);
if (atomic_read(&kmod_concurrent) > i) {
if (kmod_loop_msg++ < 5)
printk(KERN_ERR
"kmod: runaway modprobe loop assumed and
stopped\n");
atomic_dec(&kmod_concurrent);
return -ENOMEM;
}
pid = kernel_thread(exec_modprobe, (void*)
module_name, 0);
if (pid < 0) {
printk(KERN_ERR "request_module[%s]: fork
failed, errno %d\n", module_name, -pid);
atomic_dec(&kmod_concurrent);
return pid;
}
/* Block everything but SIGKILL/SIGSTOP */
spin_lock_irq(¤t->sigmask_lock);
tmpsig = current->blocked;
siginitsetinv(¤t->blocked,
sigmask(SIGKILL) | sigmask(SIGSTOP));
recalc_sigpending(current);
spin_unlock_irq(¤t->sigmask_lock);
waitpid_result = waitpid(pid, NULL, __WCLONE);
atomic_dec(&kmod_concurrent);
/* Allow signals again.. */
spin_lock_irq(¤t->sigmask_lock);
current->blocked = tmpsig;
recalc_sigpending(current);
spin_unlock_irq(¤t->sigmask_lock);
if (waitpid_result != pid) {
printk(KERN_ERR "request_module[%s]: waitpid(%d,...)
failed, errno %d\n",
module_name, pid, -waitpid_result);
}
return 0;
}
对该函数的解释如下:
· 因为request_module()是在当前进程的上下文中执行的,因此首先检查当前进程所在的根文件系统是否已经安装。
· 对request_module()的调用有可能嵌套,因为在安装过程中可能会发现必须先安装另一个模块。例如,MS-DOS模块需要另一个名为fat的模块,fat模块包含基于文件分配表(FAT)的所有文件系统所通用的一些代码。因此,如果fat模块还不在系统中,那么在系统安装MS-DOS模块时,fat模块也必须被动态链接到正在运行的内核中。因此,就要对嵌套深度加以限制,程序中设置了一个静态变量kmod_concurrent,作为嵌套深度的计数器,并且还规定了嵌套深度的上限为MAX_KMOD_CONCURRENT。不过,对嵌套深度的控制还要考虑到系统中对进程数量的限制,即max_therads,因为在安装的过程中要创建临时的进程。
· 通过了这些检查以后,就调用kernel_thread()创建一个内核线程exec_modprobe()。exec_modprobe()接受要安装的模块名作为参数,调用execve()系统调用执行外部程序/sbin/modprobe,然后, modprobe程序真正地安装要安装的模块以及所依赖的任何模块。
· 创建内核线程成功以后,先把当前进程信号中除SIGKILL和SIGSTOP以外的所有信号都屏蔽掉,免得当前进程在等待模块安装的过程中受到干扰,然后就通过waitpid()使当前进程睡眠等待,直到exec_modprobe()内核线程完成模块安装后退出。当前进程被唤醒而从waitpid()返回时,又要恢复当前进程原有信号的设置。根据waitpid()的返回值可以判断exec_modprobe()操作的成功与否。如果失败,就通过prink()在系统的运行日志“/var/log/message”中记录一条出错信息。