Linux的模块机制

说明:

  1. 所有代码均在Ubuntu 18.04(内核版本linux 5.4.0-52-generic)上编译运行通过
  2. 本文同步发表于www.linuxrevead.com

一、什么是内核模块

  内核模块实际上是实现能够为内核扩展某个具体功能的、高聚合的代码。它可以引用内核的API,但一般不对外提供API。之所以称其为模块的原因是因为它的代码可以独立于内核之外,编译结果也可以独立于内核镜像vmlinux之外。在使用时,通过某些方法将预先编译好的模块文件放入到内核的文件系统中,通过内核提供的模块相关工具在内核运行时,将该模块从内核中加载或者卸载,从而实现给内核扩展某种功能。

  使用内核模块最显著的特点是Linux kernel运行时即可动态的完成模块的安装和卸载,而并不需要关闭或者重启kernel,这样节省了功能升级成本和服务器切换的时间成本。

二、如何编写内核模块

  在学习编写Linux模块之前,有必要说明一下,初学者只需要记住模块编写的格式,重要函数的作用以及它们被调用的时机(实际上就是Linux内核模块的框架)即可,而并不需要完全理解代码中每一行的意思。等真正能够灵活使用模块,或者对于内核有一定了解,再去理解“Linux内核模块框架为什么是这样的”、“背后的实现逻辑是什么”就会简单很多。这就类似于初学者学习C语言的时候,最开始只是机械的记住“在代码第一行写include ”、“代码中要有且只能有一个main函数”、“程序运行时会从main函数执行”是一个道理。

2.1 编写"Hello world"内核模块

  按照国际惯例,我们首先学习一下"Hello world"内核模块代码。新建一个hello.c文件,在其中写入下列代码:

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

static int __init hello_init(void)
{
    printk(KERN_ALERT "Hello world!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_ALERT "Goodbye world!\n");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

我们简单介绍一下这个模块的组成。
  首先,包含了内核其他模块提供的两个头文件,用于声明后面我们要使用到的“module_init”、 “module_exit” 和 “MODULE_LICENSE”。
  其次,声明了“hello_init”函数,在其中输出了“Hello world!”;声明了“hello_exit”函数,在其中输出了“Goodbye world!”。
  最后,“hello_init”函数被“module_init”修饰,表示模块加载的时候被调用。“hello_exit”函数被“module_exit”修饰,表示模块卸载的时候被调用。

  上述模块实现了以下两个功能:

  1. 在模块被加载到内核中的时候,hello_init函数会被调用,从而在内核的log中输出“Hello world!
  2. 在模块从内核中卸载的时候,hello_exit函数会被调用,从而在内核的log中输出“Goodbye world!

2.2 将模块添加到kernel代码中

  由于后面还要基于这个模块编写字符设备驱动,所以,我们现在在drivers/char下新建hello这个文件夹,并把hello.c这个模块放置于drivers/char/hello目录下。Kernel使用Makefile来管理所有的文件,因此如果想在编译kernel的时候,把我们新加入的drivers/char/hello/hello.c一起进去,那么需要修改两个Makefile。

修改Makefile——编译drivers/char/hello目录

drivers/char/Makefile的最后一行添加以下代码:

obj-y               += hello/

表示drivers/char/hello需要进行编译。

修改Makefile——编译drivers/char/hello/hello.c

drivers/char/hello目录下新增Makefile,并加入以下内容:

obj-m               += hello.o

表示drivers/char/hello/hello.c需要进行编译,并将其编译模块。其中,

  • m表示将hello.c编译为模块;
  • m可以修改为y,表示将hello.c默认编入Linux内核镜像(即vmlinux)中;
  • m可以修改为n,表示不编译hello.c

  对于一个模块,我们想配置它时,可以直接修改Makefile,但是kernel中有数百个模块,而且有些模块之间依赖关系,那么用修改Makefile的方法工作量就太大了。Kernel中提供了另外一种机制去配置这些模块是否编译,或者是否编译成模块,即Kconfig。

增加hello模块的Kconfig

如果使用Kconfig来配置hello.c,需要以下步骤:
1)将drivers/char/hello/Makefile中的m替换成一个变量,该变量的命名规则,我们沿用Linux的规则,定义为CONFIG_HELLO
2)在drivers/char/hello/Kconfog中增加以下内容来定义及描述该变量

config HELLO
    tristate "Device driver sample code, named hello"
    default m
    help
        Configure the device driver sample
  • 第一行表示,定义变量CONFIG_HELLO
  • 第二行的tristate表示CONFIG_HELLO变量可以取三个值ymn
  • 第二行的“Device driver sample code, named hello” 表示对该变量简要的解释
  • 第三行的"default m"表示CONFIG_HELLO变量的默认值
  • 第四行“help”及其后的内容表示对CONFIG_HELLO变量的详细解释。

3)在drivers/char/Kconfog增加下面的代码,以包含drivers/char/hello/Kconfog

source "drivers/char/hello/Kconfig"

增加上述代码之后,我们就可以使用Linux提供的menuconfig这个图形化工具来配置kernel了。在代码根目录下执行"make menuconfig",即可看到下面的界面:

  在该界面下,使用上下键可以导航不同的条目,使用回车进入条目的子条目,使用空格可以切换当前选中的条目的配置(即在ymn之间循环切换)。依次进入“Device Drivers --->” -> "Character devices --->" 就可以看到我们刚刚新增加的条目"<M> Device driver sample code, named hello",默认被配置为m,如下图所示。

2.3 编译kernel代码

  按照本博客《实验二》树莓派基本开发描述的方法编译完成后,就可以在代码目录的out/drivers/char/hello目录下生成hello.ko

三、模块的加载和卸载

3.1 加载内核模块

  内核模块运行于内核空间,因此需要sudo权限来执行。可以使用insmod来加载内核模块。根据上述对hello模块的解释,其作用是在加载的时候,将“Hello world!”输出到内核log中。

  树莓派进入Home界面后,使用U盘,将Ubuntu上编译好的hello.ko复制到树莓派上来。在终端中输入sudo insmod hello.ko,即可将上述编译好的hello.ko动态加载到内核中。

$ sudo insmod hello.ko

然后在终端中输入dmesg查看kernel log,在输出的log最后一行,我们就可以看到预期的现象 —— 模块加载的时候,打印出“Hello world!”。从而证明我们自己写的hello.ko模块已成功加载到内核中。

  另外,我们已可以使用lsmod命令来查看已加载到内核的所有模块:

$ lsmod
Module                  Size  Used by
hello                  16384  0             <-- 我们刚刚加载的内核模块
thunderbolt           167936  0
cmac                   16384  1
...

3.2 卸载内核模块

  卸载内核模块的操作环境同加载模块一样,在终端中执行sudo rmmod hello,即可完成内核模块的卸载。如下所示:

$ sudo rmmod hello

执行完毕之后,在终端中执行dmesg,查看kernel log:

[ 4011.853307] Goodbye world!

表示刚刚加载的hello.ko已成功卸载,并输出了我们预期的log —— 模块卸载的时候,打印出“Goodbye world!

  好的,目前为止,我们就学会了如何编写一个最简单的内核模块,并将其添加到kernel的代码中了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注