10.2.2 实现机制的分析

当你新建立了最小内核(如何建立新内核,请看相关的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_modulesinclude/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()。这几个系统调用是在实现insmodrmmod实用程序的过程中被调用的,用户开发的程序一般不应该、也不必要直接调用这些系统调用。

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(&current->sigmask_lock);

          tmpsig = current->blocked;

          siginitsetinv(&current->blocked, sigmask(SIGKILL) | sigmask(SIGSTOP));

          recalc_sigpending(current);

          spin_unlock_irq(&current->sigmask_lock);

  

          waitpid_result = waitpid(pid, NULL, __WCLONE);

          atomic_dec(&kmod_concurrent);

  

          /* Allow signals again.. */

          spin_lock_irq(&current->sigmask_lock);

          current->blocked = tmpsig;

         recalc_sigpending(current);

          spin_unlock_irq(&current->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程序真正地安装要安装的模块以及所依赖的任何模块。

·      创建内核线程成功以后,先把当前进程信号中除SIGKILLSIGSTOP以外的所有信号都屏蔽掉,免得当前进程在等待模块安装的过程中受到干扰,然后就通过waitpid()使当前进程睡眠等待,直到exec_modprobe()内核线程完成模块安装后退出。当前进程被唤醒而从waitpid()返回时,又要恢复当前进程原有信号的设置。根据waitpid()的返回值可以判断exec_modprobe()操作的成功与否。如果失败,就通过prink()在系统的运行日志“/var/log/message中记录一条出错信息。