Linux模块(modules) - STEMHA's Blog

Linux模块(modules)

前言

Linux 是一个整体式的内核(Monolithic Kernel)结构,也就是说,整个内核是一个单独的、非常大的程序。

  • 从实现机制来说,我们又把它划分为5个子系统(前一篇文章有介绍),内核的各个子系统都提供了内部接口(函数和变量),这些函数和变量可供内核所有子系统调用和使用。

Linux 的整体式结构决定了要给内核增加新的成分也是非常困难,因此Linux 提供了一种全新的机制—可装入模块(Loadable Modules,以下简称模块)

  • 用户可以根据自己的需要,在不需要对内核进行重新编译的条件下,模块能被动态地插入到内核或从内核中移走。

模块的特点

  • 模块本身可以不被编译入内核映像,从而控制了内核的大小。
  • 模块一旦被加载,它就和内核中的其他部分完全一样。

我们需要知道什么?

什么是模块?为什么要使用模块?

整体式内核or微内核

linux 内核是一体化内核,就是说,在内核模式下,linux内核执行在一个单独的地址空间。
然而,linux也借鉴了微内核设计中一些好的思想,比如:拥有模块化设计,支持内核抢占,支持内核线程,支持动态加载内核模块到内核映像。
相反,linux没有采用微内核中那些破坏性能的特性,比如:所有进程运行于内核模式且所有函数可以直接调用而不是通过消息传递的方式。
仅管如此,linux内核是模块化的,多线程的且内核自身是可调度的。

可以说linux是做出了各方面的权衡,典型的实用主义。实用主义又赢得了一些胜利。

什么是模块?

模块:

  • 是内核的一部分(通常是设备驱动程序),但是并没有被编译到内核里面去。
  • 它们被分别编译并连接成一组目标文件,这些文件能被插入到正在运行的内核,或者从正在运行的内核中移走,进行这些操作可以使用insmod(插入模块)rmmod(移走模块)命令,或者,在必要的时候,内核本身能请求内核守护进程(kerned)装入或卸下模块。

这里列出在Linux 内核源程序中所包括的一些模块:

  • 文件系统:minix,sysv,isofs,hpfs,smbfs,ext3,nfs,proc 等。
  • 所有的SCSI 高级驱动程序: disk, tape, cdrom, generic。
  • 大多数SCSI 驱动程序: aha1542, in2000等
  • 大多数以太网驱动程序:非常多,请看./Documentation/networking/net-modules.txt
  • 很多其他模块,例如:
    • binfmt_elf: elf 装入程序
    • binfmt_java: java 装入程序
    • serial: 串口(tty)

为什么要使用模块?

Linux内核的整体架构本就非常庞大,其包含的组件也非常多。如果把所有需要的功能都编译到Linux内核中。这会导致两个问题:

  • 一是生成的内核会很大,
  • 二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。

而使用模块机制可使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核。按需动态装入模块是非常吸引人的,因为这样可以保证内核达到最小并且使得内核非常灵活

挂载文件系统的例子

例如,当你可能偶尔使用 VFAT 文件系统,你只要安装(mount) VFAT,VFAT 文件系统就成为一个可装入模块,kerneld 通过自动装入VFAT 文件系统建立你的Linux 内核,当你卸下(unmount)VFAT 部分时,系统检测到你不再需要的FAT 系统模块,该模块自动地从内核中被移走。按需动态装入模块还意味着,你会有更多的内存给用户程序。

Linux 内核模块的优缺点

利用内核模块的动态装载性具有如下优点

  • 将内核映像的尺寸保持在最小,并具有最大的灵活性;
  • 便于检验新的内核代码,而不需重新编译内核并重新引导。

内核模块的引入带来的缺点

  • 对系统性能和内存利用有负面影响;
  • 装入的内核模块和其他内核部分一样,具有相同的访问权限,因此,差的内核模块会导致系统崩溃
  • 为了使内核模块访问所有内核资源,内核必须维护符号表,并在装入和卸载模块时修改这些符号表
  • 有些模块要求利用其他模块的功能,因此,内核要维护模块之间的依赖性
  • 内核必须能够在卸载模块时通知模块,并且要释放分配给模块的内存和中断等资源;
  • 内核版本和模块版本的不兼容,也可能导致系统崩溃,因此,严格的版本检查是必需的。

内核模块的Hello World

这个最简单的内核模块只包含内核模块加载函数、卸载函数和对GPL v2许可权限的声明以及一些描述信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
* a simple kernel module: hello
*
* Copyright (C) 2014 Barry Song (baohua@kernel.org)
*
* Licensed under GPLv2 or later.
*/

#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void) //内核模块加载函数
{
printk(KERN_INFO "Hello World enter\n");
return 0;
}
module_init(hello_init); //以“module_init(函数名)”的形式来指定模块加载函数。

static void __exit hello_exit(void) //模块卸载函数
{
printk(KERN_INFO "Hello World exit\n ");
}
module_exit(hello_exit); //以“module_exit(函数名)”的形式来指定模块卸载函数。

//模块声明与描述
MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>"); //声明模块的作者
MODULE_LICENSE("GPL v2"); //声明模块的许可证声明
MODULE_DESCRIPTION("A simple Hello World Module"); //声明模块的描述
MODULE_ALIAS("a simplest module"); //声明模块的别名

编译它会产生hello.ko目标文件,通过insmod ./hello.ko命令可以加载它,通过rmmod hello命令可以卸载它,加载时输出“Hello World enter”,卸载时输出“Hello World exit”。

说明:

  • 内核模块中用于输出的函数是内核空间的printk()而不是用户空间的printf()。printk()的用法和printf()基本相似,但前者可定义输出级别。printk()可作为一种最基本的内核调试手段,
  • moudle_init 和 module_exit 这几行使用了特别的内核宏来指出模块加载函数和卸载函数的角色.

内核是一个独特的环境, 它将它的要求强加于要和它接口的代码上.所以写内核代码需要遵循各种规范。

有几个文件对模块是特殊的, 必须出现在每一个可加载模块中. 因此, 几乎所有模块代码都有下面内容:

1
2
#include <linux/module.h>
#include <linux/init.h>
  • moudle.h 包含了大量加载模块需要的函数和符号的定义.
  • init.h 来指定初始化和清理函数
  • 大部分模块还包含 moudleparam.h, 使得可以在模块加载时传递参数给模块.

Linux内核模块程序结构

一个Linux内核模块主要由如下几个部分组成:

  1. 模块加载函数
    • 当通过insmodmodprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
  2. 模块卸载函数
    • 当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。
  3. 模块许可证声明
    • 许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染(Kernel Tainted)的警告。
    • 在Linux内核模块领域,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“DualBSD/GPL”、“Dual MPL/GPL”和“Proprietary”(关于模块是否可以采用非GPL许可权,如“Proprietary”,这个在学术界和法律界都有争议)。
    • 大多数情况下,内核模块应遵循GPL兼容许可权。Linux内核模块最常见的是以MODULE_LICENSE(“GPL v2”)语句声明模块采用GPL v2。
  4. 模块参数(可选)
    • 模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。
  5. 模块导出符号(可选)
    • 内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其他模块则可以使用本模块中的变量或函数。
  6. 模块作者等信息声明(可选)
    • 各种 MODULE_ 声明可以出现在你的源码文件的任何函数之外的地方. 但是惯例是把这些声明放在文件末尾。

模块加载函数

Linux内核模块加载函数一般以__init标识声明,示例模板如下:

1
2
3
static int _ _init initialization_function(void)
{ /* 初始化代码 */ }
module_init(initialization_function);

模块加载函数以“module_init(函数名)”的形式被指定。它返回整型值,若初始化成功,应返回0。而在初始化失败时,应该返回错误编码。
在Linux内核里,错误编码是一个接近于0的负值,在<linux/errno.h>中定义,包含-ENODEV、-ENOMEM之类的符号值。

总是返回相应的错误编码是种非常好的习惯,因为只有这样,用户程序才可以利用perror等方法把它们转换成有意义的错误信息字符串。

在Linux内核中,可以使用request_module(const char*fmt,…)函数加载内核模块,驱动开发人员可以通过调用下列代码灵活地加载其他内核模块:

1
request_module(module_name);

模块卸载函数

Linux内核模块卸载函数一般以__exit标识声明,示例模板如下:

1
2
3
static void _ _exit cleanup_function(void)
{ /* 释放代码 */ }
module_exit(cleanup_function);

模块卸载函数在模块卸载的时候执行,而不返回任何值,且必须以“module_exit(函数名)”的形式来指定。

用__exit来修饰模块卸载函数,可以告诉内核如果相关的模块被直接编译进内核(即built-in),则cleanup_function()函数会被省略,直接不链进最后的镜像。因为如果模块被内置进内核,就不可能卸载它了,卸载函数也就没有存在的必要了。

模块参数

可以用“module_param(参数名,参数类型,参数读/写权限)”为模块定义一个参数。例如下列代码定义了1个整型参数和1个字符指针参数:

1
2
3
4
static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);
static int book_num = 4000;
module_param(book_num, int, S_IRUGO);
  • 在装载内核模块时,用户可以向模块传递参数,形式为insmode(或modprobe)模块名参数名=参数值,如果不传递,参数将使用模块内定义的缺省值。
  • 如果模块被内置,就无法insmod了,但是bootloader可以通过在bootargs里设置“模块名.参数名=值”的形式给该内置的模块传递参数

导出符号

Linux的“/proc/kallsyms”文件对应着内核符号表,它记录了符号以及符号所在的内存地址。
模块可以使用如下宏导出符号到内核符号表中:

1
2
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
  • 导出的符号可以被其他模块使用,只需使用前声明一下即可。
  • EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块。

下面给出了一个导出整数加、减运算函数符号的内核模块的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <linux/init.h>
#include <linux/module.h>

int add_integar(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL_GPL(add_integar);

int sub_integar(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL_GPL(sub_integar);

MODULE_LICENSE("GPL v2");

从“/proc/kallsyms”文件中找出add_integar、sub_integar的相关信息:

1
2
3
4
5
6
7
8
9
# grep integar /proc/kallsyms
e679402c r __ksymtab_sub_integar [export_symb]
e679403c r __kstrtab_sub_integar [export_symb]
e6794038 r __kcrctab_sub_integar [export_symb]
e6794024 r __ksymtab_add_integar [export_symb]
e6794048 r __kstrtab_add_integar [export_symb]
e6794034 r __kcrctab_add_integar [export_symb]
e6793000 t add_integar [export_symb]
e6793010 t sub_integar [export_symb]

模块声明与描述

1
2
3
4
5
MODULE_AUTHOR(author);            //声明模块的作者
MODULE_DESCRIPTION(description); //声明模块的描述
MODULE_VERSION(version_string); //声明模块的版本
MODULE_DEVICE_TABLE(table_info); //声明模块的设备表
MODULE_ALIAS(alternate_name); //声明模块的别名

对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,以表明该驱动模块所支持的设备:

1
2
3
4
5
6
7
/* table of devices that work with this driver */
static struct usb_device_id skel_table [] = {
{ USB_DEVICE(USB_SKEL_VENDOR_ID,USB_SKEL_PRODUCT_ID) },
{ } /* terminating enttry */
};

MODULE_DEVICE_TABLE (usb, skel_table);

查询内核中已加载模块的信息

lsmod命令

lsmod命令格式

lsmod命令可以获得系统中已加载的所有模块以及模块间的依赖关系,例如:

1
2
3
4
5
6
7
8
$ lsmod
Module Size Used by
hello 9 472 0
nls_iso8859_1 12 032 1
nls_cp437 13 696 1
vfat 18 816 1
fat 57 376 1 vfat
...
  • Module :表示模块的名称。 Size:表示模块的大小。 Used:表示依赖模块的个数。 by:表示依赖模块的内容。

lsmod命令原理

lsmod命令实际上是读取并分析/proc/modules文件,与上述lsmod命令结果对应的“/proc/modules”文件。如下:

1
2
3
4
5
$ cat /proc/modules
hello 12393 0 - Live 0xe67a2000 (OF)
nls_utf8 12493 1 - Live 0xe678e000
isofs 39596 1 - Live 0xe677f000
vboxsf 42561 2 - Live 0xe6767000 (OF)…

/sys/module与tree -a

内核中已加载模块的信息也存在于/sys/module目录下:

  • 加载hello.ko后,内核中将包含/sys/module/hello目录,该目录下又有一个refcnt文件和一个sections目录,在/sys/module/hello目录下运行tree –a可得到如下目录树:
  • tree命令会以树状图的方式列出指定目录下的所有文件,包括目录里的文件,显示出指定目录的文件目录结构。
1
2
3
computer:/sys/module/hello# tree -a
.├── coresize├── holders├── initsize├── initstate├── notes│ └── .note.gnu.build-id├── refcnt├── sections│ ├── .exit.text
3 directories, 15 files

modinfo命令

使用modinfo<模块名>命令可以获得模块的信息,包括模块作者、模块的说明、模块所支持的参数以及vermagic:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# modinfo floppy
filename: /lib/modules/4.15.0-96-generic/kernel/drivers/block/floppy.ko
alias: block-major-2-*
license: GPL
author: Alain L. Knaff
srcversion: EECA1709167BA33BEFC75FD
alias: acpi*:PNP0700:*
alias: pnp:dPNP0700*
depends:
retpoline: Y
intree: Y
name: floppy
vermagic: 4.15.0-96-generic SMP mod_unload
signat: PKCS#7
signer:
sig_key:
sig_hashalgo: md4
parm: floppy:charp
parm: FLOPPY_IRQ:int
parm: FLOPPY_DMA:int

加载和卸载模块

modprobe命令

modprobe命令:

  • 可以加载模块和卸载模块
  • 比insmod命令要强大,它在加载某模块时,会同时加载该模块所依赖的其他模块。
  • 使用modprobe命令加载的模块若以“modprobe -r filename”的方式卸载,将同时卸载其依赖的模块。
  • modprobe可载入指定的个别模块,或是载入一组相依的模块。modprobe会根据depmod所产生的相依关系,决定要载入哪些模块。若在载入过程中发生错误,在modprobe会卸载整组的模块。

Linux modprobe命令-菜鸟教程
模块之间的依赖关系存放在根文件系统的/lib/modules/<kernel-version>/modules.dep文件中,实际上是在整体编译内核的时候由depmod工具生成的,它的格式非常简单:

1
2
3
4
5
6
7
kernel/lib/cpu-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/pm-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/lru_cache.ko:
kernel/lib/cordic.ko:
kernel/lib/rbtree_test.ko:
kernel/lib/interval_tree_test.ko:
updates/dkms/vboxvideo.ko: kernel/drivers/gpu/drm/drm.ko

insmod命令

  • insmod 实用程序必须找到请求装入的内核模块,请求装入的内核模块通常保存在/lib/modules/kernel-version/目录下。
  • 内核模块被连接成目标文件,与系统中其他程序不同的是,这种目标文件是可重定位的(它们是a.out 或ELF 格式的目标文件)。
  • insmods 实用程序位于/sbin 目录下

只有超级用户才能插入一个模块,其简单的命令如下:

1
insmod serial.o //serial.o 为串口的驱动程序。

但是,这条命令执行以后可能会出现错误信息,诸如模块与内核版本不匹配、不认识的符号等。

查看/proc/ksyms,从中就可以发现内核移出的所有符号,插入模块时候出现不认识的符号,说明模块中的符号并未包含到内核中。这种情况通常说明模块有一些依赖的模块没有装载到内核。

怎么才能知道所依赖的模块呢?

除了从符号名判断外,更有效的方法是使用depmod 和 modprobe 命令来代替insmod 命令。

传递参数给模块

通常情况下,当你插入模块时,还需要把参数传递给模块。
例如,一个设备驱动程序想知道它所驱动的设备的I/O 地址和IRQ,或者一个网络驱动程序想知道你要它进行多少次的诊断跟踪。这里给出一个例子:

1
2
insmod ne.o io=0x400 irq=10
//这里装入的是NE2000 类的以太网适配器驱动程序,并告诉它以太网适配器的I/O 地址为0x400,其所产生的中断为IRQ 10。
  • 对于可装入模块,并没有标准的参数形式,也几乎没有什么约定。每个模块的编写者可以决定insmod 可以用什么样的参数。
  • 对于Linux 内核现已支持的模块,Linux HOWTO 文档给出了每种驱动程序的参数信息。

rmmod命令

通过rmmod hello命令可以卸载hello模块

参考资料

《Linux设备驱动开发详解:基于最新的Linux4.0内核》
《深入分析Linux内核源码》

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×