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函数主要就是通过模块中重定位节的信息将模块中需要重定位的符号地址重新定位。
六、模块的卸载
模块卸载主要完成对模块是否可以卸载,先是检查用户是否有这个权限,如果没有权限是不能卸载模块的。如果有其它模块在引用该模块,也不能卸载该模块,根据用户给的模块名到模块链表中查找模块,如果引用模块的计数不为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注销和释放模块占用的内存 … }
在Linux下和Windows下遍历目录的方法及如何达成一致性操作
最近因为测试目的需要遍历一个目录下面的所有文件进行操作,主要是读每个文件的内容,只要知道文件名就OK了。在Java中直接用File类就可以搞定,因为Java中使用了组合模式,使得客户端对单个文件和文件夹的使用具有一致性,非常方便。但在C中就不一样了,而且在不同的平台下使用方法也不同。在Linux下实现该功能就非常方便,因为自带有API库,几个函数用起来得心应手(虽然有些小问题,后面说),在Windows下实现就不是那么方便,虽然也有自己的API,但用法有些晦涩难懂,因为没有封装起来,需要自己一步一步进行操作,因为用的是Windows API库函数所以如果对Windows编程不熟悉的话,照搬网上的代码错了也不易调试。为此,我把这些操作都封装成类似Linux下的库函数,一方面简化透明了操作,另一方面(也许更重要)就是移植性,这样将包含该功能的程序从Windows上移植到Linux下就无需改动代码了(删掉实现封装的文件,因为Linux下自带了),当然从Linux下移植到Windows下同样方便(增加实现封装的文件即可),这就是所谓的OCP原则吧(开放封闭原则,具体见:程序员该有的艺术气质—SOLID原则)。好了,首先看下Linux下是如何实现这个功能的。
一、Linux下遍历目录的方法
Linux下实现目录操作的API函数都在头文件dirent.h中,截取部分该文件内容如下:
/** structure describing an open directory. */ typedef struct _dirdesc { int dd_fd; /** file descriptor associated with directory */ long dd_loc; /** offset in current buffer */ long dd_size; /** amount of data returned by getdirentries */ char *dd_buf; /** data buffer */ int dd_len; /** size of data buffer */ long dd_seek; /** magic cookie returned by getdirentries */ long dd_rewind; /** magic cookie for rewinding */ int dd_flags; /** flags for readdir */ struct pthread_mutex *dd_lock; /** lock */ struct _telldir *dd_td; /** telldir position recording */ } DIR; typedef void * DIR; DIR *opendir(const char *); DIR *fdopendir(int); struct dirent *readdir(DIR *);
void seekdir(DIR *, long);
long telldir(DIR *); void rewinddir(DIR *); int closedir(DIR *);
struct dirent { long d_ino; /* inode number*/ off_t d_off; /* offset to this dirent*/ unsigned short d_reclen; /* length of this d_name*/ unsigned char d_type; /* the type of d_name*/ char d_name[1]; /* file name (null-terminated)*/ };
关键部分就是DIR这个结构体的定义,包括文件描述符、缓冲区偏移、大小、缓冲区内容等,下面定义的就是具体的目录操作函数了,有打开目录、读目录、重置读取位置、关闭目录等,这里我所需要的就是打开、读和关闭这三个最基本的目录操作,下面是使用例子:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <dirent.h> #define MAX_LEN 65535 int main(void) { DIR *dir; struct dirent *ptr; char *flow[MAX_LEN]; int num = 0, i = 0; if ((dir=opendir("./data")) == NULL) { perror("Open dir error..."); exit(1); } // readdir() return next enter point of directory dir while ((ptr=readdir(dir)) != NULL) { flow[num++] = ptr->d_name; // printf("%s\n", flow[num - 1]); } for(i = 0; i < num; i++) { printf("%s\n", flow[i]); } closedir(dir); }
运行结果如下:
一看这结果就不对,输出的都是同一个文件名(最后一个文件的文件名), 哪里出了问题呢?将代码中// printf("%s\n", flow[num - 1]);这行注释去掉再运行,发现注释处输出的是正确的,两者都是输出的flow数组元素怎么结果不一样呢?经过调试发现是flow[num++] = ptr->d_name;这句代码的问题,因为这是引用拷贝(地址拷贝),所有的flow元素全部指向同一个对象ptr->d_name,虽然ptr->d_name对象每次的内容不同(也就是前面正确输出的原因),但所有内容都共享一个地址,用一个简单的图说明就是:
当然这个问题也比较好解决,也是比较常见的问题,用字符串拷贝或内存拷贝就行了,给flow每个元素重新申请一块内存。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <dirent.h> #define MAX_LEN 65535 int main(void) { DIR *dir; struct dirent *ptr; char *flow[MAX_LEN]; int num = 0, i = 0; if ((dir=opendir("./data")) == NULL) { perror("Open dir error..."); exit(1); } // readdir() return next enter point of directory dir while ((ptr=readdir(dir)) != NULL) { flow[num] = (char*)malloc(sizeof(char)); strcpy(flow[num], ptr->d_name); num++; } for(i = 0; i < num; i++) { printf("%s\n", flow[i]); } closedir(dir); }
最终结果就正确了。
二、Windows下遍历目录的方法
在Windows下就比较麻烦了,所要用到的函数都在windows.h中,Windows编程本来就比较繁琐,下面就不一一介绍所用到的函数了,直接给出封装的过程。
1. 首先模拟Linux下自带的头文件dirent.h
不同的是DIR中去掉了一些不需要的属性,及只定义了三个我所需要的操作(按需定义)。
// dirent.h
#ifndef _SYS_DIRENT_H #define _SYS_DIRENT_H typedef struct _dirdesc { int dd_fd; /** file descriptor associated with directory */ long dd_loc; /** offset in current buffer */ long dd_size; /** amount of data returned by getdirentries */ char *dd_buf; /** data buffer */ int dd_len; /** size of data buffer */ long dd_seek; /** magic cookie returned by getdirentries */ } DIR; # define __dirfd(dp) ((dp)->dd_fd) DIR *opendir (const char *); struct dirent *readdir (DIR *); void rewinddir (DIR *); int closedir (DIR *); #include <sys/types.h> struct dirent { long d_ino; /* inode number*/ off_t d_off; /* offset to this dirent*/ unsigned short d_reclen; /* length of this d_name*/ unsigned char d_type; /* the type of d_name*/ char d_name[1]; /* file name (null-terminated)*/ }; #endif
2. 三个目录操作函数的实现
当然这是最关键的部分,我不知道Linux下是怎么实现的(找了下没找到),Windows下实现如下,主要是FindFirstFile()和FindNextFile()这两个Windows函数,对Windows编程不精,也不好解释什么,需要搞明白为啥这样实现请上网搜或MSDN。
// dirent.c
#include <stdio.h> #include <windows.h> #include "dirent.h" static HANDLE hFind; DIR *opendir(const char *name) { DIR *dir; WIN32_FIND_DATA FindData; char namebuf[512]; sprintf(namebuf, "%s\\*.*",name); hFind = FindFirstFile(namebuf, &FindData ); if(hFind == INVALID_HANDLE_VALUE) { printf("FindFirstFile failed (%d)\n", GetLastError()); return 0; } dir = (DIR *)malloc(sizeof(DIR)); if(!dir) { printf("DIR memory allocate fail\n"); return 0; } memset(dir, 0, sizeof(DIR)); dir->dd_fd = 0; // simulate return return dir; } struct dirent *readdir(DIR *d) { int i; static struct dirent dirent; BOOL bf; WIN32_FIND_DATA FileData; if(!d) { return 0; } bf = FindNextFile(hFind,&FileData); //fail or end if(!bf) { return 0; } for(i = 0; i < 256; i++) { dirent.d_name[i] = FileData.cFileName[i]; if(FileData.cFileName[i] == '\0') break; } dirent.d_reclen = i; dirent.d_reclen = FileData.nFileSizeLow; //check there is file or directory if(FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { dirent.d_type = 2; } else { dirent.d_type = 1; } return (&dirent); } int closedir(DIR *d) { if(!d) return -1; hFind=0; free(d); return 0; }
3. 使用方法
与Linux下使用一模一样,不需要改动一句代码就可应用,但却发现了与Linux下自带实现同样的问题,即也是引用拷贝,如下。
因为这是我们自己实现的代码,所以字符串拷贝不是最佳解决方案,修改原实现代码才是最好的方法,当然如果是为了可移植性,就不需要改动了,就用字符串拷贝这样代码到Linux下就不需要改动了。下面看如何修改原实现解决:
a. 首先定位问题,可以很明显的知道是readdir这个函数的问题;
b. 然后找出问题根源,通过前面的分析可知问题的根源在于每次ptr->d_name使用的是同一内存地址,即ptr地址不变,而ptr是readdir返回的struct dirent指针,所以问题的根源在于readdir返回的dirent结构体地址问题,从上面代码中可以看到static struct dirent dirent; 这句代码,其中dirent的地址就是返回的地址,注意到dirent被定义为static,大家都知道C中static声明的变量调用一次后地址就不变了,存在静态存储区,也就是每次readdir返回的地址都是不变的,但指向的内容每次都被覆写,这就是问题所在;
c. 最后解决问题,知道问题根源后,问题就比较容易解决了,就是每次给dirent重新申请内存,看如下我的做法,注意我这里不能简单的struct dirent *dirent = (struct dirent *)malloc(sizeof(struct dirent))就结束了,看前面dirent结构体定义中char d_name[1];这里我只给d_name一个内存空间,显然不够,所以也要给它申请内存,我这里是按需申请内存,如果定义为char d_name[256];这样的就不需要了(一般文件名不是太长吧)。
struct dirent *readdir(DIR *d) { int i; BOOL bf; WIN32_FIND_DATA FileData; if(!d) { return 0; } bf=FindNextFile(hFind,&FileData); //fail or end if(!bf) { return 0; } struct dirent *dirent = (struct dirent *)malloc(sizeof(struct dirent)+sizeof(FileData.cFileName)); for(i = 0; i < 256; i++) { dirent->d_name[i] = FileData.cFileName[i]; if(FileData.cFileName[i] == '\0') break; } dirent->d_reclen = i; dirent->d_reclen = FileData.nFileSizeLow; //check there is file or directory if(FileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { dirent->d_type = 2; } else { dirent->d_type = 1; } return dirent; }
最终Windows运行结果如下:
PS:不知道这里大家有没有注意一个很小的细节,就是输出的不同(用的是一个相同的目录结构),Linux下输出了当前目录.和上层目录..而Windows下只输出了上层目录..,当然这没关系,因为我要的只是下面的文件名即可。OK,终于完成了,中间找bug花了不少时间,嘿嘿~~~
参考资料:
http://blog.csdn.net/lindabell/article/details/8181866