先说一下kernel module大概是怎么一回事。说到 linux kernel大家都感觉非常牛逼的样子,可是我们却看不见摸不着,唯一能看见的是目录中有一个二进制文件vmlinuz,一般是在boot目录下,名字叫vmlinuz。这个就是最小版的linux kernel,非常的小,一是kernel本来就小,二是这还是个压缩文件。就是这个文件会被解压缩然后装载进内存,然后它实现内核的功能。可是我们想要扩展和维护内核时怎么办,作为传统的单内核,那就只能把整个内核源码拿来再加代码,再重新编译出一个vmlinuz出来,替换掉,这非常麻烦。所以linux就从传统的单内核给改进成了kernel module机制。所谓的kernel module机制用白话说就是,除了这个主内核,我们在硬盘上其他地方又放着其他的文件(一般是在/lib/modules目录下,
扩展名一般为.o或者.ko),这些文件用某种机制给加载进内核,而且是加载进内核空间,具有和主内核一样的权限作为内核的一部分实现其内核功能.用白话文说出来的kernel module
似乎和微内核的实现几乎一样,只是这里有个主内核vmlinuzl罢了。
但是再来讲kernel module的具体实现,就知道这并非微内核。是用什么手段把module
加载进内核空间作为内核的一部分的呢?内核又是怎么样来维护各个模块让其成为一个
整体工作的呢?
内核提供了几个系统调用,linux都会有个指令insmod来实现装载模块,insmod便是
集成了几个系统调用。insmod大致用sys_create_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结构的大小,在此处放上模块的名字;最
后,剩余的空间给模块映像。
· 新建module结构只填充了三个值,其余值有待于从用户空间传递过来。
· put_mod_name()释放局部变量name所占的空间。
· 将新创建的模块结构链入module_list链表的首部。
struct module数据结构原型如下(包含有大量信息,所以用红色):
struct module
{
unsigned long size_of_struct; /* 模块结构的大小,即sizeof(module) */
struct module *next; /* 指向下一个模块 */
const char *name; /*模块名,最长为64个字符*/
unsigned long size; /*以页为单位的模块大小*/
union
{
atomic_t usecount; /*使用计数,对其增减是原子操作*/
long pad;
} uc; /* Needs to keep its size - so says rth */
unsigned long flags; /* 模块的标志 */
unsigned nsyms; /* 模块中符号的个数 */
unsigned ndeps; /* 模块依赖的个数 */
struct module_symbol *syms; /* 指向模块的符号表,表的大小为nsyms */
struct module_ref deps; /*指向模块引用的数组,大小为ndeps */
struct module_ref *refs;
int (*init)(void); /* 指向模块的init_module()函数 */
void (*cleanup)(void); /* 指向模块的cleanup_module()函数 */
const struct exception_table_entry *ex_table_start;
const struct exception_table_entry *ex_table_end;
/* 以下域是在以上基本域的基础上的一种扩展,因此是可选的。可以调用
mod_member_present()函数来检查以下域的存在与否。 */
const struct module_persist *persist_start; /*尚未定义*/
const struct module_persist *persist_end;
int (*can_unload)(void);
int runsize /*尚未使用*/
const char *kallsyms_start; /*用于内核调试的所有符号 */
const char *kallsyms_end;
const char *archdata_start; /* 与体系结构相关的特定数据*/
const char *archdata_end;
const char *kernel_data; /*保留 */
};
上面给出了struct module原型,也写出来sys_create_module()函数,但
是sys_create_module函数仅仅在内核为模块开辟了一块空间,但是模块的
代码根本没有拷贝过来。实际上,模块的真正安装工作及其他的一些初始化工作由
sys_init_module()函数完成,
该函数的原型为:
asmlinkage long sys_init_module(const char *name_user, struct
module *mod_user)
此函数有点长,但大概用途就是步步为营曲折的把内核中开辟出来的空间用正确
数据填满。以上模块安装已经差不多结束了,但insmod命令还集成了调用
init_module的功能,其实就是模块初始化函数。所以应该是初始化后才能算是安装
完成。
以上讲了kernel module是如何实现的,但本文最后还要提一下,模块是如何被用
的,其实module 的使用和kernel的使用没有区别。说白了,就是你在模块里写了一些函数,然后把这些函数注册给Linux内核,怎么使用,就看具体应用程序了。至于如何在内核
注册的,大概是通过内核符号表。这个内核符号应该是内核中一些函数名,变量变的符号和地址的映射表。通过这种地址的映射,让各个模块和主内核的函数及变量能被其他的模块甚至是应用程序(做成系统调用)找到,从而调用。