大家好,又见面了,我是你们的朋友全栈君。
内核模块
实验目的
内核模块是Linux操作系统中一个比较独特的机制。通过这一章学习,希望能够理解Linux提出内核模块这个机制的意义;理解并掌握Linux实现内核模块机制的基本技术路线;运用Linux提供的工具和命令,掌握操作内核模块的方法。
实验内容
针对三个层次的要求,本章安排了3个实验。
第一个实验,编写一个很简单的内核模块。虽然简单,但它已经具备了内核模块的基本要素。与此同时,初步阅读编制内核模块所需要的Makefile。
第二个实验,演示如何将多个源文件,合并到一个内核模块中。上述实验过程中,将会遇到Linux为此开发的内核模块操作工具lsmod、insmod、rmmod等。
第三个实验,考察如何利用内核模块机制,在/proc文件系统中,为特殊文件、设备、公共变量等,创建节点。它需要自主完成,本书只交待基本思路和部分源代码。程序的完善,以及调试工作,留给大家完成。
实验指导
- 什么是内核模块
Linux操作系统的内核是单一体系结构(monolithic kernel)的。也就是说,整个内核是一个单独的非常大的程序。与单一体系结构相对的是微内核体系结构(micro kernel),比如Windows NT采用的就是微内核体系结构。对于微内核体系结构特点,操作系统的核心部分是一个很小的内核,实现一些最基本的服务,如创建和删除进程、内存管理、中断管理等等。而文件系统、网络协议等其它部分都在微内核外的用户空间里运行。
这两种体系的内核各有优缺点。使用微内核的操作系统具有很好的可扩展性而且内核非常的小,但这样的操作系统由于不同层次之间的消息传递要花费一定的代价所以效率比较低。对单一体系结构的操作系统来说,所有的模块都集成在一起,系统的速度和性能都很好,但是可扩展性和维护性就相对比较差。
据作者理解,正是为了改善单一体系结构的可扩展性、可维护性等,Linux操作系统使用了一种全新的内核模块机制。用户可以根据需要,在不需要对内核重新编译的情况下,模块能动态地装入内核或从内核移出。
模块是在内核空间运行的程序,实际上是一种目标对象文件,没有链接,不能独立运行,但是其代码可以在运行时链接到系统中作为内核的一部分运行或从内核中取下,从而可以动态扩充内核的功能。这种目标代码通常由一组函数和数据结构组成,用来实现一种文件系统,一个驱动程序,或其它内核上层的功能。模块机制的完整叫法应该是动态可加载内核模块(Loadable Kernel Module)或 LKM,一般就简称为模块。与前面讲到的运行在微内核体系操作系统的外部用户空间的进程不同,模块不是作为一个进程执行的,而像其他静态连接的内核函数一样,它在内核态代表当前进程执行。由于引入了模块机制,Linux的内核可以达到最小,即内核中实现一些基本功能,如从模块到内核的接口,内核管理所有模块的方式等等,而系统的可扩展性就留给模块来完成。
1.1 内核模块的特点
使用模块的优点:
- 使得内核更加紧凑和灵活
- 修改内核时,不必全部重新编译整个内核,可节省不少时间,避免人工操作的错误。系统中如果需要使用新模块,只要编译相应的模块然后使用特定用户空间的程序将模块插入即可。
- 模块可以不依赖于某个固定的硬件平台。
- 模块的目标代码一旦被链接到内核,它的作用和静态链接的内核目标代码完全等价。 所以,当调用模块的函数时,无须显式的消息传递。
但是,内核模块的引入也带来一定的问题:
- 由于内核所占用的内存是不会被换出的,所以链接进内核的模块会给整个系统带来一定的性能和内存利用方面的损失。
- 装入内核的模块就成为内核的一部分,可以修改内核中的其他部分,因此,模块的使用不当会导致系统崩溃。
- 为了让内核模块能访问所有内核资源,内核必须维护符号表,并在装入和卸载模块时修改符号表。
- 模块会要求利用其它模块的功能,所以,内核要维护模块之间的依赖性。
模块是和内核在同样的地址空间运行的,模块编程在一定意义上说也就是内核编程。但是并不是内核中所有的地方都可以使用模块。 一般是在设备驱动程序、文件系统等地方使用模块,而对Linux内核中极为重要的地方,如进程管理和内存管理等,仍难以通过模块来实现,通常必须直接对内核进行修改。
在Linux内核源程序中,经常利用内核模块实现的功能,有文件系统,SCSI高级驱动程序,大多数的SCSI驱动程序,多数CD-ROM驱动程序,以太网驱动程序等等。
1.2 编写一个简单的内核模块
看了这些理论概念,你是不是有点不耐烦了:“我什么时候才能开始在机子上实现一个模块啊?” 好吧,在进一步介绍模块的实现机制以前,我们先试着写一个非常简单的模块程序,它可以在2.6.15的版本上实现,对于低于2.4的内核版本可能还需要做一些调整,这儿就不具体讲了。
helloworld.c
代码语言:javascript复制#include <linux/module.h> /* Needed by all modules */
#include <linux/kernel.h> /* Needed for KERN_INFO */
int init_module(void)
{
printk(KERN_INFO “Hello World!n”);
return 0;
}
void cleanup_module(void)
{
printk(KERN_INFO “Goodbye!n”);
}
MODULE_LICENSE(“GPL”);
说明:
- 任何模块程序的编写都需要包含linux/module.h这个头文件,这个文件包含了对模块的结构定义以及模块的版本控制。文件里的主要数据结构我们会在后面详细介绍。
- 函数init_module()和函数cleanup_module( )是模块编程中最基本的也是必须的两个函数。init_module()向内核注册模块所提供的新功能;cleanup_module()负责注销所有由模块注册的功能。
- 注意我们在这儿使用的是printk()函数(不要习惯性地写成printf),printk()函数是由Linux内核定义的,功能与printf相似。字符串KERN_INFO表示消息的优先级,printk()的一个特点就是它对于不同优先级的消息进行不同的处理。
接下来,我们就要编译和加载这个模块了。在前面的章节里我们已经学习了如何使用gcc,现在还要注意的一点就是:确定你现在是超级用户。因为只有超级用户才能加载和卸载模块。在编译内核模块前,先准备一个Makefile文件:
代码语言:javascript复制TARGET = helloworld
KDIR = /usr/src/linux
PWD = $(shell pwd)
obj-m = $(TARGET).o
default:
make -C $(KDIR) M=$(PWD) modules
然后简单输入命令make:
#make
结果,我们得到文件“helloworld.ko”。然后执行内核模块的装入命令:
#insmod helloworld.ko
Hello World!
这个时候,生成了字符串“Hello World!”,它是在init_module()中定义的。由此说明,helloworld模块已经加载到内核中了。我们可以使用lsmod命令查看。lsmod命令的作用是告诉我们所有在内核中运行的模块的信息,包括模块的名称,占用空间的大小,使用计数以及当前状态和依赖性。
root# lsmod
Module Size Used by
helloworld 464 0 (unused)
…
最后,我们要卸载这个模块。
# rmmod helloworld
Goodbye!
看到了打印在屏幕上的“Goodbye!”,它是在cleanup_module()中定义的。由此说明,helloworld模块已经被删除。如果这时候我们再使用lsmod查看,会发现helloworld模块已经不在了。
关于insmod和rmmod这两个命令,现在只能简单地告诉你,他们是两个用于把模块插入内核和从内核移走模块的实用程序。前面用到的insmod, rmmod和lsmod都属于modutils模块实用程序。
好了,你已经成功地在机子上实现了一个最简单的模块程序。我们再接再厉,进行下一个阶段的学习。
- 模块实现机制
2.1内核模块和应用程序的比较
在深入研究模块的实现机制以前,我们有必要了解一下内核模块与我们熟悉的应用程序之间的区别。
最主要的一点,我们必须明确,内核模块是在“内核空间”中运行的,而应用程序运行在“用户空间”。内核空间和用户空间是操作系统中最基本的两个概念,也许你还不是很清楚它们之间的区别,那么我们先一起复习一下。
操作系统的作用之一,就是为应用程序提供资源的管理,让所有的应用程序都可以使用它需要的硬件资源。然而,目前的常态是,主机往往只有一套硬件资源;现代操作系统都能利用这一套硬件,支持多用户系统。为了保证内核不受应用程序的干扰,多用户操作系统都实现了对硬件资源的授权访问,而这种授权访问机制的实现,得益于在CPU内部实现不同的操作保护级别。以INTEL的CPU为例,在任何时候,它总是在四个特权级当中的一个级别上运行,如果需要访问高特权级别的存储空间,必须通过有限数目的特权门。 Linux系统就是充分利用这个硬件特性设计的,它只使用了两级保护级别(尽管i386系列微处理器提供了四级模式)。 在Linux系统中,内核在最高级运行。在这一级,对任何设备的访问都可以进行。而应用程序则运行在最低级。在这一级,处理器禁止程序对硬件的直接访问和对内核空间的未授权访问。所以,对应于在最高级运行的内核程序,它所在的内存空间是内核空间。而对应于在最低级运行的应用程序,它所在的内存空间是用户空间。Linux通过系统调用或者中断,完成从用户空间到内核空间的转换。执行系统调用的内核代码在进程上下文中运行,它代表调用进程完成在内核空间上的操作,而且还可以访问进程的用户地址空间的数据。但对中断来说,它并不存在于任何进程上下文中,而是由内核来运行的。
好了,下面我们可以比较具体地分析内核模块与应用程序的异同。让我们看一下表6-1。
表6-1 应用程序和内核模块程序编程方式的比较
C语言普通应用程序 模块程序 |
---|
入口 main() init_module() 出口 无 cleanup_module() 编译 gcc –c 编制专用Makefile,并调用gcc 连接 gcc insmod 运行 直接运行 insmod 调试 gdb kdbug, kdb, kgdb等内核调试工具 |
这个表里,我们看到内核模块必须通过init_module()函数告诉系统,“我来了”;通过cleanup_module()函数告诉系统,“我走了”。这也就是模块最大的特点,可以被动态地装入和卸载。insmod是内核模块操作工具集modutils中,把模块装入内核的命令,我们会在后面详细介绍。因为地址空间的原因,内核模块不能像应用程序那样自由地使用在用户空间定义的函数库如libc,例如printf();模块只能使用在内核空间定义的那些资源受到限制的函数,例如printk()。应用程序的源代码,可以调用本身没有定义的函数,只需要在连接过程中用相应的函数库解析那些外部引用。应用程序可调用的函数printf(),是在stdio.h中声明,并在libc中存在目标可连接代码。然而对于内核模块来说,它无法使用这个打印函数,而只能使用在内核空间中定义的printk()函数。printk()函数不支持浮点数的输出,而且输出数据量受到内核可用内存空间的限制。
内核模块的另外一个困难,是内核失效对于整个系统或者对于当前进程常常是致命的,而在应用程序的开发过程中,缺段(segment fault)并不会造成什么危害,我们可以利用调试器轻松地跟踪到出错的地方。所以在内核模块编程的过程中,必须特别的小心。
好了,下面我们可以具体地看一看内核模块机制究竟是怎么实现的。
2.2 内核符号表
首先,我们来了解一下内核符号表这个概念。内核符号表是一个用来存放所有模块可以访问的那些符号以及相应地址的特殊的表。模块的连接就是将模块插入到内核的过程。模块所声明的任何全局符号都成为内核符号表的一部分。内核模块根据系统符号表从内核空间中获取符号的地址,从而确保在内核空间中正确地运行。
这是一个公开的符号表,我们可以从文件/proc/kallsyms中以文本的方式读取。在这个文件中存放数据地格式如下:
内存地址 属性 符号名称 【所属模块】
在模块编程中,可以利用符号名称从这个文件中检索出该符号在内存中的地址,然后直接对该地址内存访问从而获得内核数据。对于通过内核模块方式导出的符号,会包含第四列“所属模块”,用来标志这个符号所属的模块名称;而对于从内核中释放出的符号就不存在这一列的数据了。
内核符号表处于内核代码段的_ksymtab部分,其开始地址和结束地址是由C编译器所产生的两个符号来指定:__start___ksymtab和__stop___ksymtab。
2.3 模块依赖
内核符号表记录了所有模块可以访问的符号及相应地址。一个内核模块被装入后,它所声明的符号就会被记录到这个表里,而这些符号当然就可能会被其他模块所引用。这就引出了模块依赖这个问题。
一个模块A引用另一个模块B所导出的符号,我们就说模块B被模块A引用,或者说模块A装载到模块B的上面。如果要链接模块A,必须先要链接模块B。否则,模块B所导出的那些符号的引用就不可能被链接到模块A中。这种模块间的相互关系就叫做模块依赖。
2.4 内核代码分析
内核模块机制的源代码实现,来自于Richard Henderson的贡献。2002年后,由Rusty Russell重写。较新版本的Linux内核,采用后者。
2.4.1 数据结构
跟模块有关的数据结构存放在include/linux/module.h中,当然,首推struct module,
代码语言:javascript复制include/linux/module.h
232 struct module
233 {
234 enum module_state state;
235
236 /* Member of list of modules */
237 struct list_head list;
238
239 /* Unique handle for this module */
240 char name[MODULE_NAME_LEN];
241
242 /* Sysfs stuff. */
243 struct module_kobject mkobj;
244 struct module_param_attrs *param_attrs;
245 const char *version;
246 const char *srcversion;
247
248 /* Exported symbols */
249 const struct kernel_symbol *syms;
250 unsigned int num_syms;
251 const unsigned long *crcs;
252
253 /* GPL-only exported symbols. */
254 const struct kernel_symbol *gpl_syms;
255 unsigned int num_gpl_syms;
256 const unsigned long *gpl_crcs;
257
258 /* Exception table */
259 unsigned int num_exentries;
260 const struct exception_table_entry *extable;
261
262 /* Startup function. */
263 int (*init)(void);
264
265 /* If this is non-NULL, vfree after init() returns */
266 void *module_init;
267
268 /* Here is the actual code data, vfree'd on unload. */
269 void *module_core;
270
271 /* Here are the sizes of the init and core sections */
272 unsigned long init_size, core_size;
273
274 /* The size of the executable code in each section. */
275 unsigned long init_text_size, core_text_size;
276
277 /* Arch-specific module values */
278 struct mod_arch_specific arch;
279
280 /* Am I unsafe to unload? */
281 int unsafe;
282
283 /* Am I GPL-compatible */
284 int license_gplok;
285
286 /* Am I gpg signed */
287 int gpgsig_ok;
288
289 #ifdef CONFIG_MODULE_UNLOAD
290 /* Reference counts */
291 struct module_ref ref[NR_CPUS];
292
293 /* What modules depend on me? */
294 struct list_head modules_which_use_me;
295
296 /* Who is waiting for us to be unloaded */
297 struct task_struct *waiter;
298
299 /* Destruction function. */
300 void (*exit)(void);
301 #endif
302
303 #ifdef CONFIG_KALLSYMS
304 /* We keep the symbol and string tables for kallsyms. */
305 Elf_Sym *symtab;
306 unsigned long num_symtab;
307 char *strtab;
308
309 /* Section attributes */
310 struct module_sect_attrs *sect_attrs;
311 #endif
312
313 /* Per-cpu data. */
314 void *percpu;
315
316 /* The command line arguments (may be mangled). People like
317 keeping pointers to this stuff */
318 char *args;
319 };
在内核中,每一个内核模块信息都由这样的一个module对象来描述。所有的module对象通过list链接在一起。链表的第一个元素由static LIST_HEAD(modules)建立,见kernel/module.c第65行。如果阅读include/linux/list.h里面的LIST_HEAD宏定义,你很快会明白,modules变量是struct list_head类型结构,结构内部的next指针和prev指针,初始化时都指向modules本身。对modules链表的操作,受module_mutex和modlist_lock保护。
下面就模块结构中一些重要的域做一些说明。
234 state表示module当前的状态,可使用的宏定义有:
MODULE_STATE_LIVE
MODULE_STATE_COMING
MODULE_STATE_GOING
240 name数组保存module对象的名称。
244 param_attrs指向module可传递的参数名称,及其属性
248-251 module中可供内核或其它模块引用的符号表。num_syms表示该模块定义的内核模块符号的个数,syms就指向符号表。
- 300 init和exit 是两个函数指针,其中init函数在初始化模块的时候调用;exit是在删除模块的时候调用的。
294 struct list_head modules_which_use_me,指向一个链表,链表中的模块均依靠当前模块。
在介绍了module{}数据结构后,也许你还是觉得似懂非懂,那是因为其中有很多概念和相关的数据结构你还不了解。例如kernel_symbol{} (见include/linux/module.h)
struct kernel_symbol
{
unsigned long value;
const char *name;
};
这个结构用来保存目标代码中的内核符号。在编译的时候,编译器将该模块中定义的内核符号写入到文件中,在读取文件装入模块的时候通过这个数据结构将其中包含的符号信息读入。
value定义了内核符号的入口地址
name指向内核符号的名称
2.4.2 实现函数
接下来,我们要研究一下源代码中的几个重要的函数。正如前段所述,操作系统初始化时,static LIST_HEAD(modules)已经建立了一个空链表。之后,每装入一个内核模块,则创建一个module结构,并把它链接到modules链表中。
我们知道,从操作系统内核角度说,它提供用户的服务,都通过系统调用这个唯一的界面实现。那么,有关内核模块的服务又是怎么做的呢?请参看arch/i386/kernel/syscall_table.S,2.6.15版本的内核,通过系统调用init_module装入内核模块,通过系统调用delete_module卸载内核模块,没有其它途径。这下,代码阅读变得简单了。
代码语言:javascript复制kernel/module.c
1931 asmlinkage long
1932 sys_init_module(void __user *umod,
1933 unsigned long len,
1934 const char __user *uargs)
1935 {
1936 struct module *mod;
1937 int ret = 0;
1938
1939 /* Must have permission */
1940 if (!capable(CAP_SYS_MODULE))
1941 return -EPERM;
1942
1943 /* Only one module load at a time, please */
1944 if (down_interruptible(&module_mutex) != 0)
1945 return -EINTR;
1946
1947 /* Do all the hard work */
1948 mod = load_module(umod, len, uargs);
1949 if (IS_ERR(mod)) {
1950 up(&module_mutex);
1951 return PTR_ERR(mod);
1952 }
1953
1954 /* Now sew it into the lists. They won't access us, since
1955 strong_try_module_get() will fail. */
1956 stop_machine_run(__link_module, mod, NR_CPUS);
1957
1958 /* Drop lock so they can recurse */
1959 up(&module_mutex);
1960
1961 down(¬ify_mutex);
1962 notifier_call_chain(&module_notify_list, MODULE_STATE_COMING, mod);
1963 up(¬ify_mutex);
1964
1965 /* Start the module */
1966 if (mod->init != NULL)
1967 ret = mod->init();
1968 if (ret < 0) {
1969 /* Init routine failed: abort. Try to protect us from
1970 buggy refcounters. */
1971 mod->state = MODULE_STATE_GOING;
1972 synchronize_sched();
1973 if (mod->unsafe)
1974 printk(KERN_ERR "%s: module is now stuck!n",
1975 mod->name);
1976 else {
1977 module_put(mod);
1978 down(&module_mutex);
1979 free_module(mod);
1980 up(&module_mutex);
1981 }
1982 return ret;
1983 }
1984
1985 /* Now it's a first class citizen! */
1986 down(&module_mutex);
1987 mod->state = MODULE_STATE_LIVE;
1988 /* Drop initial reference. */
1989 module_put(mod);
1990 module_free(mod, mod->module_init);
1991 mod->module_init = NULL;
1992 mod->init_size = 0;
1993 mod->init_text_size = 0;
1994 up(&module_mutex);
1995
1996 return 0;
1997 }
函数sys_init_module()是系统调用init_module( )的实现。入口参数umod指向用户空间中该内核模块image所在的位置。image以ELF的可执行文件格式保存,image的最前部是elf_ehdr类型结构,长度由len指示。uargs指向来自用户空间的参数。系统调用init_module( )的语法原型为:
long sys_init_module(void *umod, unsigned long len, const char *uargs);
1940-1941 调用capable( )函数验证是否有权限装入内核模块。
1944-1945 在并发运行环境里,仍然需保证,每次最多只有一个module准备装入。这通过down_interruptible(&module_mutex)实现。
1948-1952 调用load_module()函数,将指定的内核模块读入内核空间。这包括申请内核空间,装配全程量符号表,赋值__ksymtab、__ksymtab_gpl、__param等变量,检验内核模块版本号,复制用户参数,确认modules链表中没有重复的模块,模块状态设置为MODULE_STATE_COMING,设置license信息,等等。
1956 将这个内核模块插入至modules链表的前部,也即将modules指向这个内核模块的module结构。
1966-1983 执行内核模块的初始化函数,也就是表6-1所述的入口函数。
1987 将内核模块的状态设为MODULE_STATE_LIVE。从此,内核模块装入成功。
代码语言:javascript复制/kernel/module.c
573 asmlinkage long
574 sys_delete_module(const char __user *name_user, unsigned int flags)
575 {
576 struct module *mod;
577 char name[MODULE_NAME_LEN];
578 int ret, forced = 0;
579
580 if (!capable(CAP_SYS_MODULE))
581 return -EPERM;
582
583 if (strncpy_from_user(name, name_user, MODULE_NAME_LEN-1) < 0)
584 return -EFAULT;
585 name[MODULE_NAME_LEN-1] = '