Linux内核模块详解

2022-09-15 10:47:41 浏览数 (1)

大家好,又见面了,我是你们的朋友全栈君。

内核模块

实验目的

内核模块是Linux操作系统中一个比较独特的机制。通过这一章学习,希望能够理解Linux提出内核模块这个机制的意义;理解并掌握Linux实现内核模块机制的基本技术路线;运用Linux提供的工具和命令,掌握操作内核模块的方法。

实验内容

针对三个层次的要求,本章安排了3个实验。

第一个实验,编写一个很简单的内核模块。虽然简单,但它已经具备了内核模块的基本要素。与此同时,初步阅读编制内核模块所需要的Makefile。

第二个实验,演示如何将多个源文件,合并到一个内核模块中。上述实验过程中,将会遇到Linux为此开发的内核模块操作工具lsmod、insmod、rmmod等。

第三个实验,考察如何利用内核模块机制,在/proc文件系统中,为特殊文件、设备、公共变量等,创建节点。它需要自主完成,本书只交待基本思路和部分源代码。程序的完善,以及调试工作,留给大家完成。

实验指导

  1. 什么是内核模块

Linux操作系统的内核是单一体系结构(monolithic kernel)的。也就是说,整个内核是一个单独的非常大的程序。与单一体系结构相对的是微内核体系结构(micro kernel),比如Windows NT采用的就是微内核体系结构。对于微内核体系结构特点,操作系统的核心部分是一个很小的内核,实现一些最基本的服务,如创建和删除进程、内存管理、中断管理等等。而文件系统、网络协议等其它部分都在微内核外的用户空间里运行。

这两种体系的内核各有优缺点。使用微内核的操作系统具有很好的可扩展性而且内核非常的小,但这样的操作系统由于不同层次之间的消息传递要花费一定的代价所以效率比较低。对单一体系结构的操作系统来说,所有的模块都集成在一起,系统的速度和性能都很好,但是可扩展性和维护性就相对比较差。

据作者理解,正是为了改善单一体系结构的可扩展性、可维护性等,Linux操作系统使用了一种全新的内核模块机制。用户可以根据需要,在不需要对内核重新编译的情况下,模块能动态地装入内核或从内核移出。

模块是在内核空间运行的程序,实际上是一种目标对象文件,没有链接,不能独立运行,但是其代码可以在运行时链接到系统中作为内核的一部分运行或从内核中取下,从而可以动态扩充内核的功能。这种目标代码通常由一组函数和数据结构组成,用来实现一种文件系统,一个驱动程序,或其它内核上层的功能。模块机制的完整叫法应该是动态可加载内核模块(Loadable Kernel Module)或 LKM,一般就简称为模块。与前面讲到的运行在微内核体系操作系统的外部用户空间的进程不同,模块不是作为一个进程执行的,而像其他静态连接的内核函数一样,它在内核态代表当前进程执行。由于引入了模块机制,Linux的内核可以达到最小,即内核中实现一些基本功能,如从模块到内核的接口,内核管理所有模块的方式等等,而系统的可扩展性就留给模块来完成。

1.1 内核模块的特点

使用模块的优点:

  1. 使得内核更加紧凑和灵活
  2. 修改内核时,不必全部重新编译整个内核,可节省不少时间,避免人工操作的错误。系统中如果需要使用新模块,只要编译相应的模块然后使用特定用户空间的程序将模块插入即可。
  3. 模块可以不依赖于某个固定的硬件平台。
  4. 模块的目标代码一旦被链接到内核,它的作用和静态链接的内核目标代码完全等价。 所以,当调用模块的函数时,无须显式的消息传递。

但是,内核模块的引入也带来一定的问题:

  1. 由于内核所占用的内存是不会被换出的,所以链接进内核的模块会给整个系统带来一定的性能和内存利用方面的损失。
  2. 装入内核的模块就成为内核的一部分,可以修改内核中的其他部分,因此,模块的使用不当会导致系统崩溃。
  3. 为了让内核模块能访问所有内核资源,内核必须维护符号表,并在装入和卸载模块时修改符号表。
  4. 模块会要求利用其它模块的功能,所以,内核要维护模块之间的依赖性。

模块是和内核在同样的地址空间运行的,模块编程在一定意义上说也就是内核编程。但是并不是内核中所有的地方都可以使用模块。 一般是在设备驱动程序、文件系统等地方使用模块,而对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”);

说明:

  1. 任何模块程序的编写都需要包含linux/module.h这个头文件,这个文件包含了对模块的结构定义以及模块的版本控制。文件里的主要数据结构我们会在后面详细介绍。
  2. 函数init_module()和函数cleanup_module( )是模块编程中最基本的也是必须的两个函数。init_module()向内核注册模块所提供的新功能;cleanup_module()负责注销所有由模块注册的功能。
  3. 注意我们在这儿使用的是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模块实用程序。

好了,你已经成功地在机子上实现了一个最简单的模块程序。我们再接再厉,进行下一个阶段的学习。

  1. 模块实现机制

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就指向符号表。

  1. 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(&notify_mutex);

1962       notifier_call_chain(&module_notify_list, MODULE_STATE_COMING, mod);

1963       up(&notify_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] = '';

586

587        if (down_interruptible(&module_mutex) != 0)

588               return -EINTR;

589

590        mod = find_module(name);

591        if (!mod) {

592              ret = -ENOENT;

593              goto out;

594        }

595

596        if (!list_empty(&mod->modules_which_use_me)) {

597              /* Other modules depend on us: get rid of them first. */

598              ret = -EWOULDBLOCK;

599              goto out;

600        }

601

602        /* Doing init or already dying? */

603        if (mod->state != MODULE_STATE_LIVE) {

604               /* FIXME: if (force), slam module count and wake up

605                 waiter --RR */

606              DEBUGP("%s already dyingn", mod->name);

607              ret = -EBUSY;

608              goto out;

609        }

610

611        /* If it has an init func, it must have an exit func to unload */

612        if ((mod->init != NULL && mod->exit == NULL)

613            || mod->unsafe) {

614                forced = try_force_unload(flags);

615                if (!forced) {

616                    /* This module can't be removed */

617                    ret = -EBUSY;

618                    goto out;

619              }

620        }

621

622        /* Set this up before setting mod->state */

623        mod->waiter = current;

624

625        /* Stop the machine so refcounts can't move and disable module. */

626        ret = try_stop_module(mod, flags, &forced);

627        if (ret != 0)

628             goto out;

629

630        /* Never wait if forced. */

631        if (!forced && module_refcount(mod) != 0)

632             wait_for_zero_refcount(mod);

633

634        /* Final destruction now noone is using it. */

635        if (mod->exit != NULL) {

636              up(&module_mutex);

637              mod->exit();

638              down(&module_mutex);

639        }

640        free_module(mod);

641

642  out:

643        up(&module_mutex);

644        return ret;

645  }

函数sys_delete_module()是系统调用delete_module()的实现。调用这个函数的作用是删除一个系统已经加载的内核模块。入口参数name_user是要删除的模块的名称。

580-581 调用capable( )函数,验证是否有权限操作内核模块。

583-585 取得该模块的名称

590-594 从modules链表中,找到该模块

597-599 如果存在其它内核模块,它们依赖该模块,那么,不能删除。

635-638 执行内核模块的exit函数,也就是表6-1所述的出口函数。

640 释放module结构占用的内核空间。

源代码的内容就看到这里。kernel/module.c文件里还有一些其他的函数,如果你有兴趣可以自己尝试着分析一下,对于模块机制的实现会有更深的理解。

  1. 使用内核模块

3.1 模块的加载

系统调用当然是将内核模块插入到内核的可行方法。但是毕竟太底层了。此外,Linux环境里还有两种方法可达到此目的。一种方法稍微自动一些,可以做到需要时自动装入,不需要时自动卸载。这种方法需要执行modprobe程序。我们待一会介绍modprobe。

另一种是用insmod命令,手工装入内核模块。在前面分析helloworld例子的时候,我们提到过insmod的作用就是将需要插入的模块以目标代码的形式插入到内核中。注意,只有超级用户才能使用这个命令。insmod的格式是:

# insmod [path]modulename.ko

insmod其实是一个modutils模块实用程序,当我们以超级用户的身份使用这个命令的时候,这个程序完成下面一系列工作:

  1. 从命令行中读入要链接的模块名,通常是扩展名为“.ko”,elf格式的目标文件。
  2. 确定模块对象代码所在文件的位置。通常这个文件都是在lib/modules的某个子目录中。
  3. 计算存放模块代码、模块名和module对象所需要的内存大小。
  4. 在用户空间中分配一个内存区,把module对象、模块名以及为正在运行的内核所重定位的模块代码拷贝到这个内存里。其中,module对象中的init域指向这个模块的入口函数重新分配到的地址;exit域指向出口函数所重新分配的地址。
  5. 调用init_module(),向它传递上面所创建的用户态的内存区的地址,其实现过程我们已经详细分析过了。
  6. 释放用户态内存, 整个过程结束。

3.2 模块的卸载

要卸载一个内核模块使用rmmod命令。rmmod程序将已经插入内核的模块从内核中移出,rmmod会自动运行在内核模块自己定义的出口函数。它的格式是:

# rmmod [path]modulename

当然,它最终还是通过delete_module()系统调用实现的。

3.3 模块实用程序modutils

Linux内核模块机制提供的系统调用大多数都是为modutils程序使用的。可以说,是Linux的内核模块机制和modutils两者的结合提供了模块的编程接口。modutils(modutils-x.y.z.tar.gz)可以在任何获得内核源代码的地方获得, 选择最高级别的patchlevel x.y.z等于或者小于当前的内核版本,安装后在/sbin目录下就会有insmod、rmmod、ksyms、lsmod、modprobe等等实用程序。当然,通常我们在加载Linux内核的时候,modutils已经被装入了。

  1. lsmod的使用

调用lsmod 程序将显示当前系统中正在使用的模块信息。 实际上这个程序的功能就是读取/proc文件系统中的文件/proc/modules中的信息。所以这个命令和cat /proc/modules等价。它的格式就是:

# lsmod

  1. ksyms

显示内核符号和模块符号表的信息,可以读取/proc/kallsyms文件。

  1. modprobe的使用

modprobe是由modutils提供的根据模块之间的依赖性自动插入模块的程序。前面讲到的按需装入的模块加载方法会调用这个程序来实现按需装入的功能。举例来讲,如果模块A依赖模块B,而模块B并没有加载到内核里,当系统请求加载模块A时,modprobe程序会自动将模块B加载到内核。

与insmod类似,modprobe程序也是链接在命令行中指定的一个模块,但它还可以递归地链接指定模块所引用到的其他模块。从实现上讲,modprobe只是检查模块依赖关系,真正的加载的工作还是由insmod来实现的。那么,它又是怎么知道模块间的依赖关系的呢? 简单的讲,modprobe通过另一个modutils程序depmod来了解这种依赖关系。而depmod是通过查找内核中所有的模块并把它们之间的依赖关系写入/lib/modules/2.6.15-1.2054_FC5目录下,一个名为modules.dep的文件。

  1. kmod的实现

在以前版本的内核中,模块机制的自动装入通过一个用户进程kerneld来实现,内核通过IPC和内核通信,向kerneld发送需要装载的模块的信息,然后kerneld调用modprobe程序将这个模块装载。 但是在最近版本的内核中,使用另外一种方法kmod来实现这个功能。kmod与kerneld比较,最大的不同在于它是一个运行在内核空间的进程,它可以在内核空间直接调用modprobe,大大简化了整个流程。

  1. 实例

为了便于更直观地认识内核模块的功能,下面用实例来说明模块单元是怎样与系统内核交互的。

4.1 内核模块的make文件

首先我们来看一看模块程序的make文件应该怎么写。自2.6版本之后,Linux对内核模块的相关规范,有很大变动。例如,所有模块的扩张名,都从“.o”改为“.ko”。详细信息,可参看Documentation/kbuild/makefiles.txt。针对内核模块而编辑Makefile,可参看Documentation/kbuild/modules.txt

我们练习“helloworld.ko”时,曾经用过简单的Makefile:

代码语言:javascript复制
TARGET = helloworld

KDIR = /usr/src/linux

PWD = $(shell pwd)

obj-m  = $(TARGET).o

default:

       make -C $(KDIR) M=$(PWD) modules

$(KDIR)表示源代码最高层目录的位置。

“obj-m = (TARGET).o”告诉kbuild,希望将(TARGET),也就是helloworld,编译成内核模块。

“M=$(PWD)”表示生成的模块文件都将在当前目录下。

4.2 多文件内核模块的 make文件

现在,我们把问题引申一下,对于多文件的内核模块该如何编译呢?同样以“Hello,world”为例,我们需要做以下事情:

在所有的源文件中,只有一个文件增加一行#define __NO_VERSION__。这是因为module.h一般包括全局变量kernel_version的定义,该全局变量包含模块编译的内核版本信息。如果你需要version.h,你需要自己把它包含进去,因为定义了 __NO_VERSION__后module.h就不会包含version.h。

下面给出多文件的内核模块的范例。

Start.c

代码语言:javascript复制
/* start.c

*

 * "Hello, world" –内核模块版本

* 这个文件仅包括启动模块例程

 */

   

/* 必要的头文件 */



/* 内核模块中的标准 */

#include <linux/kernel.h>   /*我们在做内核的工作 */

#include <linux/module.h> 



/*初始化模块 */

int init_module()

{

  printk("Hello, world!n");



/* 如果我们返回一个非零值, 那就意味着

* init_module 初始化失败并且内核模块

* 不能加载 */

  return 0;

}

stop.c

代码语言:javascript复制
/* stop.c

*"Hello, world" -内核模块版本

*这个文件仅包括关闭模块例程

 */
   

/*必要的头文件 */
/*内核模块中的标准 */
#include <linux/kernel.h>   /*我们在做内核的工作 */
#define __NO_VERSION__     
#include <linux/module.h>  
#include <linux/version.h>   /* 不被module.h包括,因为__NO_VERSION__ */

/* Cleanup - 撤消 init_module 所做的任何事情*/

void cleanup_module()

{

  printk("Bye!n");

}

/*结束*/

这一次,helloworld内核模块包含了两个源文件,“start.c”和“stop.c”。再来看看对于多文件内核模块,该怎么写Makefile文件

Makefile

代码语言:javascript复制
TARGET = helloworld

KDIR = /usr/src/linux

PWD = $(shell pwd)

obj-m  = $(TARGET).o

$(TARGET)-y := start.o stop.o

default:

    make -C $(KDIR) M=$(PWD) modules

相比前面,只增加一行:

$(TARGET)-y := start.o stop.o

实验思考

内核模块机制,和/proc文件系统,都是Linux系统中具有代表性的特征。可否利用这些便利,为特殊文件、设备、公共变量等,创建/proc目录下对应的节点?答案当然是肯定的。

这块实验需要自主完成,本书只交待基本思路和部分源代码。程序的完善,以及调试工作,留给大家完成。

内核模块与内核空间之外的交互方式有很多种,/proc文件系统是其中一种主要方式。

本书有专门章节介绍/proc文件系统,在这里我们再把一些基本知识复习一下。文件系统是操作系统在磁盘或其它外设上,组织文件的方法。Linux支持很多文件系统的类型:minix,ext,ext2,msdos,umsdos,vfat,proc,nfs,iso9660,hpfs,sysv,smb,ncpfs等等。与其他文件系统不同的是,/proc文件系统是一个伪文件系统。之所以称之为伪文件系统,是因为它没有任何一部分与磁盘有关,只存在内存当中,而不占用外存空间。而它确实与文件系统有很多相似之处。例如,以文件系统的方式为访问系统内核数据的操作提供接口,而且可以用所有一般的文件工具操作。例如我们可以通过命令cat,more或其他文本编辑工具察看proc文件中的信息。更重要的是,用户和应用程序可以通过proc得到系统的信息,并可以改变内核的某些参数。由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取proc文件时,proc是动态从系统内核读出所需信息并提交的。/proc文件系统一般放在/proc目录下。

怎么让/proc文件系统反映内核模块的状态呢?我们来看看下面这个稍微复杂一些的实例。

代码语言:javascript复制
proc_example.c

…………

int init_module()

{

            int rv = 0;

   

            /* 创建目录 */

            example_dir = proc_mkdir(MODULE_NAME, NULL);

            if(example_dir == NULL) {

                    rv = -ENOMEM;

                    goto out;

            }

            example_dir->owner = THIS_MODULE;

           

            /* 快速创建只读文件 jiffies */

            jiffies_file = create_proc_read_entry("jiffies", 0444, example_dir,

                                           proc_read_jiffies, NULL);

            if(jiffies_file == NULL) {

                    rv  = -ENOMEM;

                    goto no_jiffies;

            }

            jiffies_file->owner = THIS_MODULE;

   

            /* 创建规则文件foo 和 bar */

            foo_file = create_proc_entry("foo", 0644, example_dir);

            if(foo_file == NULL) {

                    rv = -ENOMEM;

                    goto no_foo;

            }

            strcpy(foo_data.name, "foo");

            strcpy(foo_data.value, "foo");

            foo_file->data = &foo_data;

            foo_file->read_proc = proc_read_foobar;

            foo_file->write_proc = proc_write_foobar;

            foo_file->owner = THIS_MODULE;

                   

            bar_file = create_proc_entry("bar", 0644, example_dir);

            if(bar_file == NULL) {

                    rv = -ENOMEM;

                    goto no_bar;

            }

            strcpy(bar_data.name, "bar");

            strcpy(bar_data.value, "bar");

            bar_file->data = &bar_data;

            bar_file->read_proc = proc_read_foobar;

            bar_file->write_proc = proc_write_foobar;

            bar_file->owner = THIS_MODULE;

               

       /* 创建设备文件 tty */

            tty_device = proc_mknod("tty", S_IFCHR | 0666, example_dir, MKDEV(5, 0));

            if(tty_device == NULL) {

                    rv = -ENOMEM;

                    goto no_tty;

            }

            tty_device->owner = THIS_MODULE;

   

            /* 创建链接文件jiffies_too */

            symlink = proc_symlink("jiffies_too", example_dir, "jiffies");

            if(symlink == NULL) {

                    rv = -ENOMEM;

                    goto no_symlink;

            }

            symlink->owner = THIS_MODULE;

   

            /* 所有创建都成功 */

            printk(KERN_INFO "%s %s initialisedn",

                   MODULE_NAME, MODULE_VERSION);

            return 0;

    /*出错处理*/

    no_symlink:  remove_proc_entry("tty", example_dir);

    no_tty:      remove_proc_entry("bar", example_dir);

    no_bar:      remove_proc_entry("foo", example_dir);

    no_foo:      remove_proc_entry("jiffies", example_dir);

    no_jiffies:    remove_proc_entry(MODULE_NAME, NULL);

    out:        return rv;

    }

    …………

内核模块proc_example首先在/proc目录下创建自己的子目录proc_example。然后在这个目录下创建了三个proc普通文件(foo,bar,jiffies),一个设备文件(tty)以及一个文件链接(jiffies_too)。具体来说,foo和bar是两个可读写文件,它们共享函数proc_read_foobar和proc_write_foobar。jiffies是一个只读文件,取得当前系统时间jiffies。jiffies_too为文件jiffies的一个符号链接。

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/163619.html原文链接:https://javaforall.cn

0 人点赞