Linux 内核模块

Linux 内核空间和用户空间 提到,在 32 位系统上,Linux 内核将 4GB 空间分为 0~3 GB 的用户空间和 3~4 GB 的内核空间。用户程序运行在用户空间,可通过中断或系统调用进入内核空间,而 Linux 内核及内核模块则只能运行在内核空间。

什么是内核模块

模块(module)就是可以在系统运行中动态加载到内核和从内核卸载的代码,这个过程无须重启系统,实现了对内核功能的动态扩展。Linux 内核具有很强的可裁剪性,很多功能或者外设驱动都可以编译成模块。

模块具有如下特点:

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

编写内核模块

头文件

内核模块需要包含内核相关头文件,不同的模块根据其功能的差异所需要的头文件也不一样,但是 <linux/init.h><linux/module.h> 所有模块都必需的。

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

模块加载函数

模块加载函数负责模块的初始化工作(例如注册模块)。如果一个内核模块没有被注册,则其内部的各种方法无法被应用程序使用,只有已注册模块的方法才能够被应用程序使用。模块并不是内核内部的代码,而是独立于内核之外的,通过初始化,能够让内核之外的代码来替内核完成本应该由内核完成的功能。模块注册相当于模块与内核之间衔接的桥梁,告诉内核“我是什么模块,我有什么功能,我已经准备好了”。

模块加载函数定义通常如下:

static int __init xxx_init(void)
{
    /* 初始化代码 */
    return 0;
}
module_init(xxx_init);

说明:

  • 模块初始化函数一般都声明为 static 静态函数,因为它对于其他源码文件没有任何意义;
  • __init 表示初始化函数仅仅在初始化期间使用,一旦初始化完毕,则将释放初始化函数所占用的内存,类似的还有 __initdata
  • 模块加载函数以 module_init(函数名) 的形式指定,如果没有这个定义,那么内核将无法执行初始化代码。module_init 宏定义会在模块的目标代码中增加一个特殊的代码段,用于说明该初始化函数所在的位置。

当使用 insmod 命令将模块加载进内核的时候,初始化函数的代码将会被执行。模块初始化代码只与内核模块管理子系统打交道,并不与应用程序交互。

模块卸载函数

当系统不再需要某个模块时,可以卸载这个模块,从而释放该模块所占用的资源。实现模块退出的函数通常称为模块的退出函数或者清除函数,其定义通常如下:

static void __exit xxx_exit(void)
{
    /* 释放代码 */
}
module_exit(xxx_exit);

说明:

  • 模块退出函数没有返回值;
  • __exit 标记这段代码仅用于模块卸载;
  • 模块卸载函数以 module_exit(函数名) 的形式指定,但这不是必须的,比如有些模块加载之后并不需要卸载,则可以不添加 module_exit 定义,但需要支持模块卸载的必须添加。

当使用 rmmod 卸载模块时,退出函数的代码将被执行。模块退出代码只与内核模块管理子系统打交道,并不直接与应用程序交互。

许可证声明

Linux 内核是开源的,遵守 GPL 协议,所以要求加载进内核的模块也最好遵循相关协议。使用 MODULE_LICENSE 宏来声明,例如:

MODULE_LICENSE("GPL");

许可证(LICENSE)声明描述内核模块的许可权限,如果不声明 LICENSE,模块被加载时将收到内核被污染(Kernel Tainted)的警告。目前,内核可接收的 LICENSE 包括 GPL、GPL v2、GPL and additional rights、Dual BSD/GPL、Dual MPL/GPL 和 Proprietary(关于模块是否可以采用非 GPL 许可权,如 Proprietary,这在学术界和法律界都有争议)。

符号导出

在 Linux 2.4 内核中,默认的非 static 函数和变量都会自动导入到内核空间,出于安全考虑,Linux 2.6 开始修改为默认不导出所有的符号,需要导出的符号必须使用 EXPORT_SYMBOL() 进行标记。

EXPORT_SYMBOL(module_symbol);

Linux 的 /proc/kallsyms 文件对应着内核符号表,它记录了符号以及符号所在的内存地址。

除了使用 EXPORT_SYMBOL 宏,Linux 内核还提供了 EXPORT_SYMBOL_GPL 宏,它们的作用都是将符号导出到内核符号表。区别在于 EXPORT_SYMBOL_GPL 仅将符号导出到采用 GPL 许可的模块,而 EXPORT_SYMBOL 导出到所有可加载的模块。

模块描述

在内核模块中,我们还可以用 MODULE_AUTHORMODULE_DESCRIPTIONMODULE_VERSIONMODULE_DEVICE_TABLEMODULE_ALIAS 等宏分别声明模块的作者、描述、版本、设备表和别名。

MODULE_AUTHOR("作者");
MODULE_DESCRIPTION("功能描述");
MODULE_VERSION("版本");
MODULE_DEVICE_TABLE("设备表");
MODULE_ALIAS("别名");

使用 modinfo 命令可以查看模块描述信息。

最简单的内核模块

hello.c

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

static int __init hello_init(void)
{
        printk(KERN_INFO "hello module init\n");
        return 0;
}

static void __exit hello_exit(void)
{
        printk(KERN_INFO "hello module exit\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Rudy <rudy@getiot.tech>");
MODULE_DESCRIPTION("A simple Hello-World module");
MODULE_ALIAS("a simplest module");
MODULE_VERSION("V1.0");

Makefile

PWD := $(shell pwd)
KVER := $(shell uname -r)
KDIR :=/lib/modules/$(KVER)/build/

# Kernel modules
obj-m := hello.o

# Specify flags for the module compilation
#EXTRA_CFLAGS=-g -O0

all:
    $(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
    $(MAKE) -C $(KDIR) M=$(PWD) clean

编译运行

将 Makefile 和 hello.c 放在同一目录下,执行 make 即可编译 hello 模块,编译完成后会生成 hello.ko 目标文件。如果开启 EXTRA_CFLAGS=-g -O0,可以得到包含调试信息的 hello.ko 模块。

make

通过 insmod 命令将模块加载到内核(非 root 用户需要加上 sudo)

insmod hello.ko

如果一个模块包含多个 .c 文件(例如 file1.c 和 file2.c),则应该以如下方式编写 Makefile:

obj-m := modulename.o
modulename-objs := file1.o file2.o

Leave a Reply