cangel1988的专栏

神马都是浮云

Linux学习笔记 - 内核模块

模块是Linux高效利用微内核,同时不会降低系统性能与优点的一种方法。几乎Linux内核的每个高层组件 --- 文件系统、设备驱动、网络,都可以作为模块进行编译。Linux的发布版,充分使用模块方式全面地支持多种品牌型号的硬件。但在某个计算机上只会有效加载其中一个驱动程序。这样内核就不会因为装载那些数以百计的很少使用的程序而变得非常庞大。

 

何时使用模块

       当系统程序员希望给Linux内核增加新功能时,都倾向于把新代码作为一个模块来实现。然而,有些Linux代码必须被静态链接,在内核编译时就被包含。这种情况一般发生在组件需要对内核中的某个数据结构或函数进行修改。例如,在描述符或者内核数据结构中引入新字段,或者替换静态链接的代码。

       内核管理模块的任务主要有两个:第一是确保内核的其他部分可以访问该模块的全局符号,例如指向模块主要实现函数的入口地址。模块还必须知道这些符号在内核及其他模块中的地址。因此,在链接模块时,一定要解决模块间的引用关系。第二个任务是记录模块的使用情况,以便在卸载这个模块时,判断其是否被内核的其他部分使用。系统使用了一个简单的引用计数器来记录每个模块的引用次数。

       Linux内核许可证GPL严格禁止在非GPL许可证下发行相关的源代码, 因此,Linux内核开发者要求模块开发者在模块源代码中使用MODULE_LICENSE宏标出许可证类型。如果许可证是非GPL兼容,模块就不能使用内核的许多核心函数和数据结构。而且,使用非GPL许可证的模块会“污染”内核,内核开发者不会为其提供技术支持。

 

模块的实现

       模块作为ELF格式的文件存放在文件系统中,并通过执行insmod程序链接到内核中。对于每个模块,系统都会分配一个包含以下数据的内存区:

  • 一个module对象
  • 表示模块唯一名称的字符串,以null结束
  • 实现模块功能的代码

       module对象描述一个模块,所有module对象被一个双向循环列表串联起来。链表头部存放在 modules变量中,而指向相邻单元的指针存放在每个module对象的list字段中。linux/module.h(kernel version 2.6.32)中的module对象描述节选如下:

struct module
{
 enum module_state state;      /* 模块的内部状态 */
 struct list_head list;       /* 模块链表指针 */
 char name[MODULE_NAME_LEN];      /* 模块名,60字节 */

 

 struct module_kobject mkobj;     /* 包含一个kobject结构 */
 struct module_attribute *modinfo_attrs;   
 const char *version;       
 const char *srcversion;
 struct kobject *holders_dir;

 

 const struct kernel_symbol *syms;    /* 导出符号数组指针 */
 const unsigned long *crcs;      /* 导出符号CRC数组指针 */
 unsigned int num_syms;       /* 导出符号个数 */

 

 struct kernel_param *kp;      /* 内核参数描述符数组指针 */
 unsigned int num_kp;       /* 内核参数个数 */

 

 unsigned int num_gpl_syms;      /* GPL格式导出符号数 */
 const struct kernel_symbol *gpl_syms;   /* GPL格式导出符号数组指针 */
 const unsigned long *gpl_crcs;     /* GPL格式导出符号CRC数组指针 */

 

 const struct kernel_symbol *unused_syms;  /* 未使用的导出符号数组指针 */
 const unsigned long *unused_crcs;    /* 未使用的导出符号CRC数组指针 */
 unsigned int num_unused_syms;     /* 未使用的导出符号数 */

 

 unsigned int num_unused_gpl_syms;    /* 未使用的GPL格式导出符号数 */
 const struct kernel_symbol *unused_gpl_syms; /* 未使用的GPL格式导出符号数组指针 */
 const unsigned long *unused_gpl_crcs;   /* 未使用的GPL格式导出符号CRC数组指针 */

 

 const struct kernel_symbol *gpl_future_syms; /* 保留的GPL格式导出符号数组指针 */
 const unsigned long *gpl_future_crcs;   /* 保留的GPL格式导出符号CRC数组指针 */
 unsigned int num_gpl_future_syms;    /* 保留的GPL格式导出符号数 */

 

 unsigned int num_exentries;      /* 模块异常表项个数 */
 struct exception_table_entry *extable;   /* 模块异常表指针 */

 

 int (*init)(void);        /* 模块初始化函数 */
 void *module_init;        /* 用于模块初始化的动态内存指针,由vfree在init()返回时释放 */
 void *module_core;        /* 用于模块核心函数与数据结构,由vfree在卸载时释放 */
 unsigned int init_size, core_size;    /* 模块初始化与模块核心函数及数据结构的动态内存大小 */
 unsigned int init_text_size, core_text_size; /* 模块初始化与模块核心函数及数据结构的可执行代码大小,只在链接时使用 */

 

 struct mod_arch_specific arch;     /* 依赖于体系结构的字段 */
 unsigned int taints;       /* 内核污染标志位 */

 Elf_Sym *symtab, *core_symtab;     /* /proc/kallsyms文件模块ELF核心符号数组指针 */
 unsigned int num_symtab, core_num_syms;   /* /proc/kallsyms文件模块ELF符号表数与核心符号数 */
 char *strtab, *core_strtab;      /* /proc/kallsyms文件模块ELF核心符号字符串指针 */

 

 struct module_sect_attrs *sect_attrs;   /* 模块的段属性描述符数组指针 */

 void *percpu;         /* 特定于CPU的内存区指针 */
 char *args;          /* 链接模块时的命令行参数 */

 struct list_head modules_which_use_me;   /* 依赖于该模块的模块链表 */


 struct task_struct *waiter;      /* 等待该模块卸载的进程 */

 void (*exit)(void);        /* 模块卸载函数 */
 local_t ref;         /* CPU使用计数器 */
};

state字段记录模块内部状态,它可以是:

  • MODULE_STATE_LIVE (模块为活动的)
  • MODULE_STATE_COMING (模块正在初始化)
  • MODULE_STATE_GOING (模块正在卸载)

       每个模块都有自己的异常表。该表包括(如果有)模块的修正代码的地址。在链接模块时,该表被拷贝到RAM中,其开始地址保存在module对象的extable字段中。

       每个模块都有一组使用计数器,每个CPU一个,存放在相应module对象的ref字段中。在模块功能所涉及的操作开始执行时递增这个计数器,在操作结束时递减这个计数器。只有所有使用计数器的和为0时,模块才可以被取消链接。模块的总的引用计数器就是所有CPU计数器的总和。

 

导出符号

       当链接一个模块时,必须用合适的地址替换在模块对象代码中引用的所有全局内核符号(变量和函数)。这个操作与在用户态编译程序时链接程序所执行的操作非常类似,这是委托给insmod外部程序完成的。内核使用一些专门的内核符号(kernel symbol table),用于保存模块访问的符号和相应的地址。它们在内核代码段中分三个节:

  • __kstrtab节,保存符号名
  • __ksymtab节,所有模块可使用的符号地址
  • __ksymtab_gpl节,GPL兼容许可证下发布的模块可以使用的符号地址

       只有某一现有的模块实际使用的内核符号才会保存在这个表中。如果需要在某些模块中需要访问一个尚未导出的内核符号,只要在源码中增加相应的EXPORT_SYMBOL_GPL宏导出就可以了。如果许可证不是GPL兼容的,就不能为模块合法导出一个新符号。已链接的模块也可以导出自己的符号,这样其他模块就可以访问这些符号。

 

模块依赖

       当模块B引用另一个模块A所导出的符号,在这种情况下,我们就说B装载在A的上面。为了链接模块B,必须首先链接模块A。否则,对于模块A所导出的那些符号的引用就不能适当地链接到B中。即两个模块之间存在着依赖。

       模块对象的modules_which_use_me字段是一个依赖链表的头部,该链表保存其使用的所有模块。链表中的每个元素是一个小型module_use描述符,该描述符保存指向链表中相邻元素的指针及一个指向相应模块对象的指针。对于上述情况,指向B模块对象的module_use描述符将出现在A的modules_which_use_me链表中。只要有模块装载在A上,modules_which_use_me链表就必须动态更新。如果A的依赖链表非空,模块A就不能卸载。

 

模块的链接和取消

用户可以通过执行insmod外部程序把一个模块链接到正在运行的内核中。该程序执行以下操作:

1. 从命令行中读取要链接的模块名。

2. 确定模块对象代码文件所在的位置,从磁盘读入存有模块目标代码的文件。

3. 调用init_module(),执行sys_init_module()系统调用,传入存有模块目标代码的用户态缓冲区地址、目标代码长度和存有insmod程序所需参数的用户态内存区作为参数。

4. 结束。

sys_init_module()展开:

1. 检查是否允许用户链接模块,即当前进程必须具有CAP_SYS_MODULE权能。只要给内核增加功能,安全就是至关重要的。

2. 为模块目标代码分配一个临时内存区,然后拷入作为系统调用第一个参数的用户态缓冲区数据。

3. 验证内存区中的数据是否是有效的ELF模块对象,若不是则返回错误码。

4. 为传给insmod程序的参数分配一个内存区,并存入用户态缓冲区的数据,该缓冲区地址是系统调用传入的第三个参数。

5. 查找modules链表,以验证模块未被链接。通过比较模块名(module对象的name字段)进行这一检查。

6. 为模块核心可执行代码分配一个内存区,并存入模块相应节的内容。

7. 为模块初始化代码分配一个内存区,并存入模块相应节的内容。

8. 为新模块确定模块对象地址,对象映像保存在模块ELF文件的正文段gnu.linkonce.this_module一节,而模块对象保存在第6步中的内存区。

9. 将第6和7步中分配的内存区地址存入模块对象的module_code和module_init字段。

10. 初始化模块对象的modules_which_use_me链表。当前执行CPU的计数器设为1,而其余所有的模块引用计数器设为0。

11. 根据模块对象许可证类型设定模块对象的license_gplok标志。

12. 使用内核符号表与模块符号表,重置模块目标码。这意味着用相应的逻辑地址偏移量替换所有外部与全局符号的实例值。

13. 初始化模块对象的syms和gpl_syms字段,使其指向模块导出的内存中符号表。

14. 模块异常表保存在模块ELF文件的_extable一节,因此它在第6步中已拷入内存区,将其地址存入模块对象的extable字段。

15. 解析insmod程序的参数,并相应地设定模块变量的值。

16. 注册模块对象mkobj字段中的kobject对象,这样在sysfs文件系统的module目录中就有一个新的子目录。

17. 释放第2步中分配的临时内存区。

18. 将模块对象追加到modules链表。

19. 将模块状态设为MODULE_STATE_COMING。

20. 如果模块对象的init方法已定义,则执行它。

21. 将模块状态设为MODULE_STATE_LIVE。

22. 结束并返回0(成功)。

 

用户需要调用rmmod外部程序来取消模块的链接,该程序执行以下操作:

1. 从命令行中读取要取消的模块的名字。

2. 打开/proc/modules文件,其中列出了所有链接到内核的模块,检查待取消模块是否有效链接。

3. 调用delete_module()系统调用,向其传递要卸载的模块名。

4. 结束。

sys_delete_module()展开:

1. 检查是否允许用户取消模块链接,即当前进程必须具有CAP_SYS_MODULE权能。

2. 将模块名存入内核缓冲区。

3. 从modules链表查找模块的module对象。

4. 检查模块的modules_which_use_me依赖链表,如果非空就返回一个错误码。

5. 检查模块状态,如果不是MODULE_ STATE_ LIVE,就返回错误码。

6. 如果模块有自定义init方法,函数就要检查是否有自定义exit方法。如果没有自定义exit方法,模块就不能卸载,返回一个退出码。

7. 为了避免竞争条件,除了运行sys_delete_module()服务例程的CPU外,暂停系统中所有CPU的运行。

8. 把模块状态设为MODULE_STATE_GOING。

9. 如果所有模块引用计数器的累加值大于0,就返回错误码。

10. 如果已定义模块的exit方法,则执行它。

11. 从modules链表删除模块对象,并且从sysfs文件系统注销该模块。

12. 从刚才使用的模块依赖链表中删除模块对象。

13. 释放相应内存区,其中存有模块可执行代码、module对象及有关符号和异常表。

14. 返回0(成功)。

 

根据需要链接模块

       模块可以在系统需要其所提供的功能时自动进行链接,之后也可以自动删除。为了自动链接模块,内核要创建一个内核线程来执行modprobe外部程序,该程序链接命令行中指定的模块,同时考虑由于模块依赖所引起的所有可能因素,并递归地链接命令行中模块所使用的所有其他模块。对模块依赖进行解析,以及对模块进行查找的操作最好都在用户态中实现,因为这需要查找和访问文件系统中的模块对象文件。

       modprobe之所以能够知道模块间的依赖关系,又是依靠系统启动时执行的一条depmod命令。该命令查找放在/lib/module目录下,为正在运行的内核而编译的所有模块。然后它就把所有的模块间依赖关系写入一个名为modules.dep的文件。这样,modprobe就可以对该文件中存放的信息和/proc/module文件产生的链接模块链表进行比较。实际上,modprobe只检查模块依赖关系,每个模块的实际的链接工作是通过创建一个进程并执行insmod命令来实现的。

 

阅读更多
个人分类: Linux内核
想对作者说点什么? 我来说一句

LINUX内核模块编程

2008年12月29日 23KB 下载

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭