Linux模块
一、为什么要使用模块
由于linux使用的是整体结构,不是模块化的结构,整体结构实现的操作系统可扩展性差。linux为了扩展系统,使用了模块的技术,模块能够从系统中动态装入和卸载,这样使得linux也具有很好的可扩展性。
二、linux中哪些代码作为模块实现,哪些直接编译进内核?
当然我们是尽量把代码编译成模块,这样就可以根据需要进行链接,内核的代码量也会少很多。几乎所有的高层组件—文件系统、设备驱动程序、可执行格式、网络层等等—都可以作为模块进行编译。
然而有些代码确必须直接编译进内核。这些代码通常是对数据结构或者函数进行修改。如内核中已经定义好了的数据结构,如果要改变这个数据结构,那么只有从新编译内核了。
三、管理模块
内核主要完成管理模块的两个任务。第一个任务是确保内核的其它部分可以访问该模块的全局符号,模块还必须知道全局符号在内核及其它模块中的地址。因此,在链接模块时,一定要解决模块间的引用关系。第二个任务是记录模块的使用情况,以便再其它模块或者内核的其它部分正在使用这个模块时,不能卸载这个模块。
四、模块使用的数据结构
每个模块都用一个module描述符描述,并且链接到一个以modules变量为链表头的双向循环链表中。
module描述符:
struct module { enum module_state state; //模块内部状态 struct list_head list; //用于链接到链表中 char name[MODULE_NAME_LEN]; //模块名字 struct module_kobject mkobj; //用于Sysfs的kobject struct module_param_attrs *param_attrs; //指向模块参数描述符 const struct kernel_symbol *syms; //指向导出符号数组的指针 unsigned int num_syms; //导出符号数 const unsigned long *crcs; //指向导出符号CRC值数组指针 const struct kernel_symbol *gpl_syms; //GPL格式导出符号 unsigned int num_gpl_syms; const unsigned long *gpl_crcs; unsigned int num_exentries; //模块异常表项数 const struct exception_table_entry *extable; //指向模块异常表的指针 int (*init)(void); //模块初始化方法 void *module_init; //用于模块初始化的动态内存区指针 void *module_core; //用于模块核心函数与数据结构的动态内存区指针 unsigned long init_size, core_size; //模块初始化动态内存区大小,模块核心函数与数据结构的动态内存区大小 unsigned long init_text_size, core_text_size; //模块初始化的可执行代码大小,模块核心可执行代码的大小,只在连接模块时使用 struct mod_arch_specific arch; int unsafe; int license_gplok; #ifdef CONFIG_MODULE_UNLOAD struct module_ref ref[NR_CPUS]; //每cpu使用计数器变量 /* What modules depend on me? */ struct list_head modules_which_use_me; //依赖于该模块的模块链表 struct task_struct *waiter; //正在等待模块被卸载的进程,即卸载模块的进程 void (*exit)(void); //模块退出的方法 #endif #ifdef CONFIG_KALLSYMS Elf_Sym *symtab; //proc/kallsysms文件中所列模块ELF符号数组指针 unsigned long num_symtab; char *strtab; //proc/kallsysms文件中所列模块ELF符号的字符串表 struct module_sect_attrs *sect_attrs; //模块分节属性描述符数组指针 #endif void *percpu; char *args; //模块连接时使用的命令行参数 };
module数据结构主要描述了模块导出符号,模块使用的动态内存,模块的加载和释放函数,模块的引用等。
当装载一个模块到内核中时,必须用合适的地址替换在模块对象代码中引用的所有全局内核符号。这主要由insmod程序来完成。内核使用一些专门的内核符号表,用于保存模块访问的符号和相应的地址。它们在内核代码分三节:__kstrtab节(保存符号名)、__ksymtab节(所有模块可使用的符号地址)和__ksymtab_gpl节(GPL兼容许可证下发布的模块可以使用的符号地址)。
已经装载到内核中的模块也可以导出自己的符号,这样其它模块就可以访问这些符号。模块符号部分表保存在模块代码段__ksymtab、__ksymtab_gpl和__kstrtab部分中。可以使用宏EXPOPT_SYMBOL和EXPORT_SYMPOL_GPL来导出符号。当模块装载进内核时,模块的导出符号被拷贝到两个内存数组中,而数组的地址保存在module描述符的syms和gpl_syms字段中。
一个模块可以引用另一个模块所导出的符号。module描述符中有个字段modules_which_use_me,它是一个依赖链表的头部,该链表保存了使用该模块的所有其他模块。链表中每个元素都是一个module_use描述符,该描述符保存指向链表中相邻元素的指针以及一个指向相应模块对象的指针。只有依赖链表不为空,就不能卸载该模块。
五、模块的装载
模块的装载主要通过sys_init_module服务例程来实现的,是由insmod外部程序通过系统调用来调用该函数。下面我们来分析sys_init_module函数:
asmlinkage long sys_init_module(void __user *umod, unsigned long len, const char __user *uargs) { struct module *mod; int ret = 0; … mod = load_module(umod, len, uargs); … if (mod->init != NULL) ret = mod->init(); //调用模块初始化函数初始化模块 … mod->state = MODULE_STATE_LIVE; module_free(mod, mod->module_init); //释放初始化使用的内存 mod->module_init = NULL; mod->init_size = 0; mod->init_text_size = 0; … }
这个函数主要是调用load_module函数加载模块代码到内存中,并初始化该模块对象mod;调用初始化模块函数初始化模块,释放模块中的初始化代码动态内存空间。其中传递的参数umod是insmod程序在用户态时将模块文件拷贝到内存中的起始地址,len是模块文件的大小,uargs是调用命令insmod时的命令行参数。
加载模块的工作其实主要还是由函数load_module来完成,这个函数完成了将模块文件从用户空间加载到临时内核空间,对模块文件进行合法性检查,并抽取出模块文件中的核心函数和数据结构到内核的另一个动态内存区,并重定位模块中的符号,初始化module对象,将mod对象加入到sysfs文件系统中。
static struct module *load_module(void __user *umod, unsigned long len, const char __user *uargs) { Elf_Ehdr *hdr; Elf_Shdr *sechdrs; char *secstrings, *args, *modmagic, *strtab = NULL; unsigned int i, symindex = 0, strindex = 0, setupindex, exindex, exportindex, modindex, obsparmindex, infoindex, gplindex, crcindex, gplcrcindex, versindex, pcpuindex; long arglen; struct module *mod; long err = 0; void *percpu = NULL, *ptr = NULL; /* Stops spurious gcc warning */ struct exception_table_entry *extable; … if (len > 64 * 1024 * 1024 || (hdr = vmalloc(len)) == NULL) //超过64MB,或者分配内存失败,否则分配一个临时的内核空间来存放内核模块 return ERR_PTR(-ENOMEM); if (copy_from_user(hdr, umod, len) != 0) { //用空间将模块目标代码拷贝到内核 err = -EFAULT; goto free_hdr; } … //省略的代码为检查模块的合法性 sechdrs = (void *)hdr + hdr->e_shoff; //节的头表 secstrings = (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset; //节点头字符串表 sechdrs[0].sh_addr = 0; for (i = 1; i < hdr->e_shnum; i++) { if (sechdrs[i].sh_type != SHT_NOBITS //SHT_NOBITS表示该节点在文件中无内容 && len < sechdrs[i].sh_offset + sechdrs[i].sh_size) goto truncated; /* Mark all sections sh_addr with their address in the temporary image. */ sechdrs[i].sh_addr = (size_t)hdr + sechdrs[i].sh_offset; //把每个节点的地址设置为在内存中对应的地址 if (sechdrs[i].sh_type == SHT_SYMTAB) { //节点为符号表 symindex = i; strindex = sechdrs[i].sh_link; //字符串表在节点头表中的索引 strtab = (char *)hdr + sechdrs[strindex].sh_offset; //字符串表 } #ifndef CONFIG_MODULE_UNLOAD //没有定义模块卸载 /* Don't load .exit sections */ //不将.exit节加载到内存 if (strncmp(secstrings+sechdrs[i].sh_name, ".exit", 5) == 0) sechdrs[i].sh_flags &= ~(unsigned long)SHF_ALLOC; #endif } modindex = find_sec(hdr, sechdrs, secstrings, ".gnu.linkonce.this_module"); //.gnu.linkonce.this_module在节点头表中的索引 … mod = (void *)sechdrs[modindex].sh_addr; … //省略代码处理参数和处理每cpu变量 mod->state = MODULE_STATE_COMING; layout_sections(mod, hdr, sechdrs, secstrings); //节的从新布局,合并所有带有SHF_ALLOC标记的节,并计算每个节的大小和偏移量,包括计算初始化代码和核心代码的空间大小 ptr = module_alloc(mod->core_size); //为模块代码分配动态内存 … memset(ptr, 0, mod->core_size); mod->module_core = ptr; ptr = module_alloc(mod->init_size); //为模块初始化代码分配动态内存 … memset(ptr, 0, mod->init_size); mod->module_init = ptr; … for (i = 0; i < hdr->e_shnum; i++) { //将临时内核模块的数据拷贝到新的动态内存中 void *dest; if (!(sechdrs[i].sh_flags & SHF_ALLOC)) continue; if (sechdrs[i].sh_entsize & INIT_OFFSET_MASK) dest = mod->module_init + (sechdrs[i].sh_entsize & ~INIT_OFFSET_MASK); else dest = mod->module_core + sechdrs[i].sh_entsize; if (sechdrs[i].sh_type != SHT_NOBITS) memcpy(dest, (void *)sechdrs[i].sh_addr, sechdrs[i].sh_size); sechdrs[i].sh_addr = (unsigned long)dest; //更新节在内存中的地址 DEBUGP("\t0x%lx %s\n", sechdrs[i].sh_addr, secstrings + sechdrs[i].sh_name); } mod = (void *)sechdrs[modindex].sh_addr; //mod指向新内存 module_unload_init(mod); //初始化mod的卸载字段 //修正符号表中的地址值 err = simplify_symbols(sechdrs, symindex, strtab, versindex, pcpuindex, mod); … for (i = 1; i < hdr->e_shnum; i++) { //重定位各个节中的符号 const char *strtab = (char *)sechdrs[strindex].sh_addr; unsigned int info = sechdrs[i].sh_info; if (info >= hdr->e_shnum) continue; if (!(sechdrs[info].sh_flags & SHF_ALLOC)) continue; if (sechdrs[i].sh_type == SHT_REL) //当前节是重定位节 err = apply_relocate(sechdrs, strtab, symindex, i,mod); if (err < 0) goto cleanup; } … vfree(hdr); //释放临时分配的内核空间 … }
代码中的simplify_symbols主要就是查找内核符号表,将模块符号表中未决的符号修改为内核符号表中对应的符号的值,即符号对应的线性地址。对于在模块内部定义的符号,则根据符号所在节的起始地址来修改符号的地址。apply_relocate函数主要就是通过模块中重定位节的信息将模块中需要重定位的符号地址重新定位。
static int simplify_symbols(Elf_Shdr *sechdrs, unsigned int symindex, const char *strtab, unsigned int versindex, unsigned int pcpuindex, struct module *mod) { Elf_Sym *sym = (void *)sechdrs[symindex].sh_addr; unsigned long secbase; unsigned int i, n = sechdrs[symindex].sh_size / sizeof(Elf_Sym); //符号表中表项的个数 int ret = 0; for (i = 1; i < n; i++) { switch (sym[i].st_shndx) { case SHN_COMMON: printk("%s: please compile with -fno-common\n", mod->name); ret = -ENOEXEC; break; case SHN_ABS: break; case SHN_UNDEF: //解决引用模块外部符号的地址 sym[i].st_value = resolve_symbol(sechdrs, versindex, strtab + sym[i].st_name, mod); … break; default: //符号所在的段可以找到,说明该符号是模块内部定义的符号 if (sym[i].st_shndx == pcpuindex) //符号是每cpu变量 secbase = (unsigned long)mod->percpu; else secbase = sechdrs[sym[i].st_shndx].sh_addr; sym[i].st_value += secbase; //st_value是所在段的偏移+段的起始地址就是真正的地址了 break; } } return ret; }
六、模块的卸载
模块卸载主要完成对模块是否可以卸载,先是检查用户是否有这个权限,如果没有权限是不能卸载模块的。如果有其它模块在引用该模块,也不能卸载该模块,根据用户给的模块名到模块链表中查找模块,如果引用模块的计数不为0,则阻塞当前进程,否则将模块从modules链中删除;如果模块自定义了exit函数,则执行该函数,将模块从文件系统sysfs注销,释放模块占用的内存区。
asmlinkage long sys_delete_module(const char __user *name_user, unsigned int flags) { struct module *mod; char name[MODULE_NAME_LEN]; int ret, forced = 0; if (!capable(CAP_SYS_MODULE)) return -EPERM; if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0) return -EFAULT; name[MODULE_NAME_LEN-1] = '\0'; mod = find_module(name); //查找模块 if (!list_empty(&mod->modules_which_use_me)) { //查看是否有其它模块使用当前模块 ret = -EWOULDBLOCK; goto out; } if (mod->state != MODULE_STATE_LIVE) { //判断模块是否是正常运行的 /* FIXME: if (force), slam module count and wake up waiter --RR */ DEBUGP("%s already dying\n", mod->name); ret = -EBUSY; goto out; } if ((mod->init != NULL && mod->exit == NULL) //如果模块有init却没有exit,则不能卸载模块 || mod->unsafe) { forced = try_force(flags); if (!forced) { ret = -EBUSY; goto out; } } mod->waiter = current; //卸载该模块的进程 ret = try_stop_module(mod, flags, &forced); if (!forced && module_refcount(mod) != 0) //等待模块引用计数为0 wait_for_zero_refcount(mod); if (mod->exit != NULL) { //调用该模块定义的exit函数 up(&module_mutex); mod->exit(); down(&module_mutex); } free_module(mod); //将模块从sysfs注销和释放模块占用的内存 … }