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 = ;
…
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 = ;
mod->init_text_size = ;
…
}
这个函数主要是调用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 = , strindex = , setupindex, exindex,
exportindex, modindex, obsparmindex, infoindex, gplindex,
crcindex, gplcrcindex, versindex, pcpuindex;
long arglen;
struct module *mod;
long err = ;
void *percpu = NULL, *ptr = NULL; /* Stops spurious gcc warning */
struct exception_table_entry *extable;
…
if (len > * * || (hdr = vmalloc(len)) == NULL) //超过64MB,或者分配内存失败,否则分配一个临时的内核空间来存放内核模块
return ERR_PTR(-ENOMEM);
if (copy_from_user(hdr, umod, len) != ) { //用空间将模块目标代码拷贝到内核
err = -EFAULT;
goto free_hdr;
}
… //省略的代码为检查模块的合法性
sechdrs = (void *)hdr + hdr->e_shoff; //节的头表
secstrings = (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset; //节点头字符串表
sechdrs[].sh_addr = ;
for (i = ; 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", ) == )
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, , mod->core_size);
mod->module_core = ptr;
ptr = module_alloc(mod->init_size); //为模块初始化代码分配动态内存
…
memset(ptr, , mod->init_size);
mod->module_init = ptr;
…
for (i = ; 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 = ; 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 < )
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 = ; for (i = ; 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 = ;
if (!capable(CAP_SYS_MODULE))
return -EPERM;
if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-) < )
return -EFAULT;
name[MODULE_NAME_LEN-] = '\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
wait_for_zero_refcount(mod);
if (mod->exit != NULL) { //调用该模块定义的exit函数
up(&module_mutex);
mod->exit();
down(&module_mutex);
}
free_module(mod); //将模块从sysfs注销和释放模块占用的内存
…
}
Linux模块的更多相关文章
- Linux模块编程框架
Linux是单内核系统,可通用计算平台的外围设备是频繁变化的,不可能将所有的(包括将来即将出现的)设备的驱动程序都一次性编译进内核,为了解决这个问题,Linux提出了可加载内核模块(Loadable ...
- Linux模块机制浅析
Linux模块机制浅析 Linux允许用户通过插入模块,实现干预内核的目的.一直以来,对linux的模块机制都不够清晰,因此本文对内核模块的加载机制进行简单地分析. 模块的Hello World! ...
- Linux模块机制浅析_转
Linux模块机制浅析 转自:http://www.cnblogs.com/fanzhidongyzby/p/3730131.htmlLinux允许用户通过插入模块,实现干预内核的目的.一直以来,对l ...
- linux 模块编译步骤(原)
linux 模块编译步骤(原) 博主推荐:<Linux命令模板Licote(原)> 本文将直接了当的带你进入linux的模块编译.当然在介绍的过程当中,我也会添加一些必要的注释,以便初学者 ...
- 【ARM-Linux开发】Linux模块机制浅析
Linux模块机制浅析 Linux允许用户通过插入模块,实现干预内核的目的.一直以来,对linux的模块机制都不够清晰,因此本文对内核模块的加载机制进行简单地分析. 模块的Hello World! ...
- 【linux驱动笔记】linux模块机制浅析
1. 模块module 操作系统分微内核和宏内核,微内核优点,可以使操作系统仅作很少的事,其它事情如网络处理等都作为应用程序来实现,微内核精简的同时,必然带来性能的下降.而linux的宏内核设 ...
- linux模块驱动之led(ioremap)
一:led内核驱动 (1)在编写led内核驱动时,我们首先要进行内核裁剪,因为友善之臂将LED灯的驱动默认加载到内核中,所以编写模块驱动程序前就要先把原先的LED灯驱动裁剪掉: led驱动在源码里面的 ...
- Linux模块的加载和卸载
Linux操作系统中模块操作相关命令解释lsmod 查看已经安装好的模块, 也可以查看/proc/modules文件的内容. 实际上,lsmod读命令就是通过查看/proc/modules的内容来显 ...
- linux模块导出符号 EXPORT_SYMBOL_GPL&EXPORT_SYMBOL(转)
转自:http://blog.csdn.net/angle_birds/article/details/7396748 一个模块mod1中定义一个函数func1:在另外一个模块mod2中定义一个函数f ...
随机推荐
- es6转es5
一.在没有IDE情况下的转换 在"我的电脑->D盘”新建个文件夹es6,文件夹里新建一个文件es6.js. 打开命令行窗口 1.先全局安装babel-cli,输入命令 npm inst ...
- [转]关于SVN的操作批处理示例
为了一句话:不要动手做机器能够做的事情. 天天工作用svn,更新啥的打开目录啥的动作天天在重复.每次写些命令也蛮无聊的,不说了,看下面: @echo off rem 显示部分 @echo 注 意 事 ...
- 学习Nodejs之mysql
学习Nodejs连接mysql数据库: 1.先安装mysql数据库 npm install mysql 2.测试连接数据库: var sql = require("mysql"); ...
- ThinkPHP3.* 模型操作相关函数
ThinkPHP3.* 版本,大家所不熟知的,且与数据库操作相关的函数做以简单罗列: 1.getLastSql 别名 _sql (鉴于getLastSql比较常用,故出现了别名函数_sql) 2.se ...
- myeclipse连接数据库sql server
1.打开数据库Microsoft sql server2008,输入以下命令: 此时可是看到端口号为1619,记住此端口号,等会儿会用到. 2.打开myeclipse2014,找到最上方的myecli ...
- 把token带到 http头部 或者验证一下referer
提交地址:http://baozoumanhua.com/users/8311358提交数据:-----------------------------195704664324Content-Disp ...
- The Solution of UESTC 2016 Summer Training #1 Div.2 Problem B
Link http://acm.hust.edu.cn/vjudge/contest/121539#problem/B Description standard input/output Althou ...
- iOS开发系列--并行开发其实很容易
--多线程开发 概览 大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算.可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行.但是机器码是按顺序执行的, ...
- c#实现redis客户端(一)
最近项目使用中要改造redis客户端,看了下文档,总结分享一下. 阅读目录: 协议规范 基础通信 状态命令 set.get命令 管道.事务 总结 协议规范 redis允许客户端以TCP方式连接,默认6 ...
- ASP.NET MVC 5 Web编程1 -- 入门
开篇引言 说起ASP.NET MVC,我想作为WebForms开发者第一点要问的是:为什么要使用它?我的理解是:MVC是更细节化的框架,“细节可控”意味着你的系统更精致.具体体现在应用上.MVC的出现 ...