【Linux】Linux内核模块开发

Linux内核模块开发

零、关于

1、概述

最近在学习Linux相关的东西,学习了U-Boot的编译,Linux的编译,能够在开发板上运行自己编译的U-Boot和Linux了,那么接下来就是在自己编译的Linux上做应用级或者系统级的开发了。本文以字符设备驱动为例介绍如何开发Linux内核的模块,包括静态编译、动态加载和模块之间的依赖等内容。

若要继续实践下面的内容,需要以你能够自己编译Linux内核为前提。

2、内核模块

Linux内核模块是用于扩展内核功能的一些代码。本质上是.ko格式的独立目标文件,内核模块通过与内核链接,实现对硬件驱动、文件系统、网络协议等功能的灵活扩展。

内核模块分为设备驱动程序、文件系统模块、网络协议模块、网络服务模块、硬件架构与系统支持模块、内核子系统扩展模块和特殊功能模块等类型,我们可以针对某个部分对内核进行扩展。

3、处理方式

在Linux中有两种内核模块的处理方式,一种是静态编译,另一种是动态加载。

其中,静态编译是指我们在编译Linux时把我们的模块代码一起编译进系统可执行文件中去,使其成为内核不可分割的一部分。这样做的好处是在Linux启动时我们的代码就被加载到Linux内核系统中运行了,而且不用生成另外的模块文件,方便分发。但是缺点是我们需要对内核模块代码进行改动时需要重新编译Linux,内核运行期间也无法单独卸载或更新我们的内核模块,这样不方便随时修改。

而动态加载则是指我们的内核模块代码不会与Linux内核代码一起编译,而是另外再单独编译。这样会生成一个后缀为.ko(Kernel Object)的内核模块文件。我们需要使用此内核模块时,可以使用加载内核模块命令(insmod/modprobe)把内核模块加载到Linux内核中,不需要使用此模块时,可以使用卸载内核模块命令(rmmod)把内核模块从Linux内核中卸载,加载操作和卸载操作都无需重启内核。

壹、代码模板

在介绍如何编译和使用内核模块之前,我们需要对内核模块的代码有一个大致的了解。

1、内核模块代码

下面的代码模板构建了一个简单的Linux内核模块,在模块加载时会输出hello_yu init消息,在模块卸载时会输出hello_yu exit消息。通过printk函数,这些消息会被记录到内核日志中,可使用dmesg命令查看。

一个简单的内核模块代码文件hello.c内容如下:

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>

int __init hello_yu_init(void)
{
    printk("hello_yu init\n");
    return 0;
}

void __exit hello_yu_exit(void)
{
    printk("hello_yu exit\n");
}

MODULE_LICENSE("GPL");
module_init(hello_yu_init);
module_exit(hello_yu_exit);

下面对其做一个更详细的介绍:

1)、头文件包含

c 复制代码
#include <linux/module.h>
#include <linux/kernel.h>
  • #include <linux/module.h>:这个头文件定义了构建内核模块所需的函数和宏,像module_initmodule_exit以及MODULE_LICENSE等。
  • #include <linux/kernel.h>:此头文件包含了内核编程常用的函数和数据结构,例如printk函数等。

2)、模块初始化函数

c 复制代码
int __init hello_yu_init(void)
{
    printk("hello_yu init\n");
    return 0;
}
  • int __init hello_yu_init(void):这是模块的初始化函数,当模块被加载到内核时会调用此函数。__init是一个宏,其作用是告知编译器该函数仅在模块初始化时使用,之后就可以释放相关内存。
  • printk("hello_yu init\n");:printk 是内核中的打印函数,功能类似于用户空间的printf。它会把消息输出到内核日志缓冲区,可通过dmesg命令查看。
  • return 0;:返回值为0表明模块初始化成功。若返回非零值,则意味着初始化失败,模块将无法加载。

3)、模块退出函数

c 复制代码
void __exit hello_yu_exit(void)
{
    printk("hello_yu exit\n");
}
  • void __exit hello_yu_exit(void):这是模块的退出函数,当模块从内核卸载时会调用此函数。__exit是一个宏,其作用是告知编译器该函数仅在模块卸载时使用。
  • printk("hello_yu exit\n");:当模块卸载时,会将此消息输出到内核日志缓冲区。

4)、模块许可证声明

c 复制代码
MODULE_LICENSE("GPL");
  • MODULE_LICENSE("GPL");:此宏用于声明模块所采用的许可证。在Linux内核中,使用GPL许可证是较为常见的。若不声明许可证,内核会发出警告。

5)、模块初始化和退出函数注册

c 复制代码
module_init(hello_yu_init);
module_exit(hello_yu_exit);
  • module_init(hello_yu_init);:该宏把hello_yu_init函数注册为模块的初始化函数,当模块被加载时会调用此函数。
  • module_exit(hello_yu_exit);:该宏把hello_yu_exit函数注册为模块的退出函数,当模块被卸载时会调用此函数。

2、Kconfig配置代码

Kconfig文件是Linux内核配置系统的一部分,它定义了内核编译时可配置的选项。用户可以通过make menuconfig来配置这些选项,进而决定哪些功能会被编译进内核。下面的这段Kconfig代码定义了一个名为HELLO_YU的可配置选项,它有三种状态可供选择。选项设置的描述信息是"This is Kernel Object Test by yu.",帮助信息为 "This is just a kernel object test."。用户可以通过内核配置工具来选择是否将该选项对应的功能编译进内核或者编译成内核模块。

kconfig 复制代码
config HELLO_YU
        tristate "This is Kernel Object Test by yu."
        help
          This is just a kernel object test.

这段内核Kconfig代码用于在内核配置系统中定义一个可配置选项,下面对配置代码做个简单的介绍:

1)、config HELLO_YU

  • configKconfig语法中的关键字,用于定义一个新的配置选项。
  • HELLO_YU是这个配置选项的名称,在内核代码中可以通过这个名称来引用该配置选项。例如,在C代码里可以使用#ifdef HELLO_YU来判断这个选项是否被启用。

2)、tristate "This is Kernel Object Test by yu."

  • tristate表示这个配置选项有三种状态:y:代表 "是",意味着该选项对应的功能会被直接编译进内核。n:代表 "否",即该选项对应的功能不会被编译进内核。m:代表 "模块",表示该选项对应的功能会被编译成一个内核模块,在需要的时候可以动态加载到内核中。
  • "This is Kernel Object Test by yu."是该配置选项的描述信息,在配置界面中会显示这个描述,让用户了解该选项的用途。

3)、help

  • help关键字用于提供该配置选项的详细帮助信息。
  • This is just a kernel object test.是具体的帮助文本,当用户在配置界面中选择该选项并查看帮助信息时,就会显示这段文本,进一步说明该选项的用途。

3、Makefile配置代码

Makefile是一种用于自动化编译和构建项目的文件,它定义了一系列的规则来描述如何从源文件生成目标文件和可执行文件。Make工具会根据Makefile中的规则,检查哪些文件需要重新编译,从而提高编译效率。我们的内核模块代码要是手动编译的话就比较麻烦,故使用Make工具帮助我们编译我们的内核模块项目。一个简单的用于编译内核模块的Makefile内容如下:

makefile 复制代码
ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /home/yu/kernel/linux-3.14
ROOTFS ?= /home/yu/share/rootfs
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install

clean:
	rm -rf  *.o  *.ko  .*.cmd  *.mod.*  modules.order  Module.symvers   .tmp_versions

else
obj-m += hello_yu.o
hello_yu-objs := hello.o

endif

整个脚本逻辑大致如下:

脚本采用了嵌套编译的方式,主要分为两个部分,通过判断$(KERNELRELEASE)是否为空来区分。$(KERNELRELEASE)是内核Makefile在编译内核模块时会定义的一个变量,当它为空时,表示在顶层Makefile环境中;当它不为空时,表示是在内核Makefile环境中进行子Makefile的编译。

1)、顶层Makefile环境($(KERNELRELEASE)为空时)

makefile 复制代码
ifeq ($(ARCH),arm)
KERNELDIR ?= /home/yu/kernel/linux-3.14
ROOTFS ?= /home/yu/share/rootfs
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)
  • ifeq ($(ARCH),arm):判断目标架构是否为ARM。
    若为ARM架构,KERNELDIR变量被设置为/home/yu/kernel/linux-3.14,这是ARM内核源码的路径;ROOTFS变量被设置为/home/yu/share/rootfs,这是开发板根文件系统的路径。
    若不是ARM架构,KERNELDIR变量被设置为当前系统正在使用的内核源码的构建目录,通过/lib/modules/$(shell uname -r)/build获取。
  • PWD := $(shell pwd):将当前工作目录的路径赋值给PWD变量。
makefile 复制代码
modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
  • modules目标:用于编译内核模块,默认执行此目标。$(MAKE)实际上是make命令,-C $(KERNELDIR)表示切换到内核源码目录$(KERNELDIR)下进行编译,M=$(PWD)表示将当前工作目录$(PWD)下的代码作为外部模块进行编译,modules是内核Makefile中的一个目标,用于编译模块。
makefile 复制代码
modules_install:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules INSTALL_MOD_PATH=$(ROOTFS) modules_install
  • modules_install目标:用于安装编译好的内核模块。在编译模块后,通过INSTALL_MOD_PATH=$(ROOTFS)指定将模块安装到$(ROOTFS)所指向的根文件系统目录中。
makefile 复制代码
clean:
	rm -rf  *.o  *.ko  .*.cmd  *.mod.*  modules.order  Module.symvers   .tmp_versions
  • clean目标:用于清理编译生成的文件。它会删除所有的目标文件(.o)、内核模块文件(.ko)、命令文件(.cmd)、模块相关文件(.mod.*)、模块顺序文件(modules.order)、符号表文件(Module.symvers)以及临时版本目录(.tmp_versions)。

2)、内核Makefile环境($(KERNELRELEASE)不为空)

makefile 复制代码
obj-m += hello_yu.o
hello_yu-objs := hello.o
  • obj-m += hello_yu.o:表示要将hello_yu.o编译成一个可加载的内核模块。
  • hello_yu-objs := hello.o:表示hello_yu.o这个模块是由hello.o这个目标文件组成的。

贰、静态编译

这种处理方式需要我们把内核模块源码放置到内核源码目录之下,我们要开发的字符设备驱动是存放于目录drivers/char之下的,故我们需要进入此目录。

1、文件编写

1)、将内核模块代码写入到drivers/char/hello.c,使用如下命令:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ vi drivers/char/hello.c

内核模块代码的代码解释见壹节,写入完之后保存退出~

注意:文件名尽量不要和模块名重名!除非你的内核模块代码只由一个.c文件组成。

2)、编辑同一目录下的Kconfig文件,在Kconfig中配置新的模块代码,使用如下命令编辑Kconfig文件:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ vi drivers/char/Kconfig


Kconfig中配置代码的位置是跟以后的设置界面有关的,为了保持分组逻辑,我们在此处加入我们的配置代码是比较合适的。Kconfig的代码解释见壹节。

完成之后保存退出~

3)、为了让我们的代码能够被正确编译,我们还需要编辑同目录下的Makefile文件,使用如下代码编辑同目录下的Makefile文件:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ vi drivers/char/Makefile

同样,为了保持分组逻辑,我们选择在此处加入相关代码。Makefile的代码解释见壹节。

编辑完成后保存退出~

2、配置编译

完成上面步骤的文件编写后,我们就可以在make menuconfig中看到我们的内核模块选项了。为了能让我们的模块能被编译进内核中,我们需要在make menuconfig中配置一下。

1)、使用如下命令打开配置界面:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ make menuconfig

根据我们模块代码的存放位置,我们在菜单中选择"Device Drivers",然后再选择"Character devices",就能在菜单中看到我们的模块配置标题了,我们通过空格把我们的模块设置为"将该功能模块静态编译进内核",即设置为"*"。然后通过方向键选择"Save"以保存配置。

2)、配置完成后我们重新编译Linux内核,如何编译的本文不做介绍,我们使用如下命令重新编译Linux内核:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ make uImage

可以看到我们的代码被编译了。

3、运行内核

我们把编译好的uImage文件上传到TFTP服务器,让开发板运行我们新编译的Linux内核:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ cp arch/arm/boot/uImage ~/share/tftp/linux

可以看到我们的模块代码在内核启动的时候被启动了。

成功~

叁、动态加载:模块代码与内核代码放在一起

有时候为了方便随时修改我们的模块代码,我们可以选择动态加载方式来使用我们的内核模块代码,本小节介绍的是我们的模块代码仍然放在Linux源码目录中,我们通过make menuconfig配置我们的模块代码为"模块编译",即M,然后再通过make modules命令编译成.ko文件动态加载进内核中去使用。

1、配置编译

1)、前面的步骤是与静态编译一样的,我们在配置阶段和编译阶段略有不同,我们使用如下命令重新打开make menuconfig界面:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ make menuconfig

配置完成后记得使用方向键选择"Save"保存后再退出~

2)、接着我们需要重新编译内核,使用如下命令重新编译内核:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ make uImage

编译后需要复制到TFTP服务器上,方便开发板使用:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ cp arch/arm/boot/uImage ~/share/tftp/linux

3)、上一步是没有编译我们的内核模块的,对于设置为M的内核模块,我们还需要手动编译,使用如下命令编译内核模块:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ make modules

编译后需要复制到NFS服务器上,方便开发板使用:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ cp drivers/char/hello.ko ~/share/rootfs/

可以看到我们的模块被编译了~

注意:在编译模块前需要编译内核,内核模块所适用的平台与内核一致。

2、动态加载使用

重启开发板,使其运行新的内核。

可以看到我们刚刚编译生成的内核模块文件hello.ko

1)、使用insmod命令动态加载内核模块:

bash 复制代码
[root@yieq4412]#insmod hello.ko

可以看到,我们的内核模块成功被加载,打印出了我们初始化函数中的内容。

2)、使用lsmod命令查看已经动态加载的模块列表:

bash 复制代码
[root@yieq4412]#lsmod

可以看到,目前开发板上的Linux内核只加载了我们这一个模块。

3)、使用rmmod命令卸载我们的内核模块:

bash 复制代码
[root@yieq4412]#rmmod hello

内核模块成功被卸载,执行了退出函数中的内容。

肆、动态加载:模块代码与内核代码分开存放

有时候我们的模块代码太多,并不想有与Linux源码挤在一起从而导致代码结构混乱等问题的发生,因此我们需要更好的代码管理与编译方式。

我们希望Linux源码与自己开发的内核模块代码分开,分开存放、分开管理和分开编译。本小节介绍这种开发方式。

1、建立内核模块项目

我们在另外的目录中创建一个文件夹作为内核模块项目的文件夹,使用如下命令来创建项目文件夹:

bash 复制代码
yu@Yubuntu:~$ mkdir -p project/hello_yu_module

将来关于此内核模块的代码都放到这里。

我们将内核模块代码写入到此文件夹下的hello.c文件中:

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ vi hello.c

大家可以根据自己的需要调整相关代码,内核模块源代码的解释大家可以去看壹节。

我们将动态加载的Makefile代码模板写入到此文件夹下的Makefile文件中:

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ vi Makefile

大家需要根据自己的实际情况调整一下Makefile文件中的变量值,Makefile文件代码的解释大家可以去看壹节。

2、编译加载内核模块:指定CPU架构为ARM

因为我们的开发板是ARM架构的CPU,而当前我们的开发环境所用的CPU架构是AMD64,故我们编译适用于ARM架构CPU的内核模块时需要特殊指定一下。

先确保你的Linux内核已经被编译完成,跟我们上次编译内核模块时的先决条件是一样的。

1)、在内核模块项目目录下执行如下代码以编译适用于ARM架构CPU的内核模块代码:

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ make ARCH=arm

可以看到已经生成了内核模块文件hello.ko

2)、我们把生成的hello.ko文件复制到NFS服务器上方便开发板上的Linux使用,使用如下命令把hello.ko复制到NFS服务器上:

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ cp hello.ko ~/share/rootfs/

3)、在开发板上测试我们编译好的内核模块文件:

测试成功~

3、编译加载内核模块:当前CPU架构

我们自己当前的开发环境(电脑,Ubuntu22,CPU架构:AMD64)也是用的Linux内核,那么同样能动态加载和卸载我们现在编写的内核模块,本小节介绍如何编译和使用适用于当前开发环境中CPU架构的内核模块。

1)、使用如下命令编译内核模块:

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ make

可以看到也是成功生成了hello.ko文件。

这两个警告是说在使用函数前没有声明该函数的原型,这样做可能会出现一些问题,你可以选择忽略,也可以选择在函数前声明一下函数原型。

2)、使用如下命令在电脑中动态加载内核模块:

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ sudo insmod hello.ko

但是好像并没有输出我们初始化函数中的内容呢?

这是因为在我们Ubuntu电脑中,内核输出的信息并不直接输出到Bash中,需要通过dmesg命令查看。

3)、使用如下命令查看内核输出信息:

bash 复制代码
sudo dmesg

一下流出了好多好多信息呢,下次试验前我们可以使用sudo dmesg -C命令清空一下内核输出信息再做试验。

4)、使用如下命令查看电脑中已动态加载的内核模块:

bash 复制代码
lsmod

也是一大堆信息,不过我们也是能在其中找到了我们刚刚动态加载的内核模块。

5)、使用如下命令卸载内核模块:

bash 复制代码
sudo rmmod hello_yu

可以看到我们的模块被正常卸载了~

伍、内核模块的多文件编译

有时候我们的内核模块不止一个源代码文件,比如我们再加两个文件yu.hyu.c到我们的内核模块中,那么这样的话我们该如何编写Makefile文件呢?

因为是编译相关的问题,所以只是Makefile的编写不一样,那么这样的话问题分成两种情况,一种是内核模块源代码与Linux源码放在一起的情况,另外一种是内核模块源代码单独存放的情况。

1、放在一起

以之前的例子为基础做修改。

1)、添加两个文件到drivers/char目录下,yu.hyu.c

就简单的写了个头文件和C文件。

2)、另外对我们之前的drivers/char/hello.c进行一下简单修改:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ vi drivers/char/hello.c

我们在这里调用一下刚刚新加的两个文件。

3)、另外,我们的代码修改后,Makefile文件也需要修改一下:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ vi drivers/char/Makefile

涉及多个文件的模块,先是要把原来的条目更改一下,可以把文件名.o改为模块名.o,然后再在编译条目下一行加如下代码把需要用到的文件全部添加进来即可:

makefile 复制代码
<模块名>-objs := 文件名1.o 文件名2.o ...

修改完成后保存退出~

注意:.o不要重名!

4)、在make menuconfig中设置为编译进内核中,然后重新编译:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ make menuconfig
bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ make uImage

5)、复制到TFTP服务器上在开发板上运行:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ cp arch/arm/boot/uImage ~/share/tftp/linux

成功~

6)、或者设置成模块,然后重新编译,放到开发板上运行:

成功~

2、单独存放

1)、以之前的项目~/project/hello_yu_module为基础,在其目录下添加两个文件,也是yu.hyu.c

2)、同样,需要在之前的hello.c中稍微修改一下,调用我们新加的文件:

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ vi hello.c

保存退出~

3)、同样,我们的代码文件有改动后需要修改一下Makefile文件:

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ vi Makefile

同样的,我们需要把原来的文件名.o修改为模块名.o,然后在下面添加此模块用到的所有文件,代码格式为:

makefile 复制代码
<模块名>-objs := 文件名1.o 文件名2.o ...

基本与上一种情况一致。

4)、当前CPU架构的编译和加载运行:

成功~

4)、指定CPU架构为ARM的编译和加载运行:

成功~

陆、内核模块参数传递

有时候我们希望能像正常的APP那样,传递一些启动参数到主程序中,主程序通过int argcchar* argv[]来读取我们传入的参数,我们在内核模块上如何实现这样的操作呢?

1、宏介绍

在开始之前我们介绍两个宏的用法,一个是module_param(name, type, perm);,另一个是module_param_array(name, type, &num, perm);,这两个宏在Linux内核编程里的核心作用就是把指定的全局变量设置成模块参数,以此实现向内核模块传递参数。

1)、其中module_param(name, type, perm);是设置非数组参数的,它的三个参数介绍如下:

①、 参数name:全局变量名

②、 参数type:设置参数的类型,因为有的类型中间包含空格,不方便使用,故使用如下表格的符号代替:

使用符号 实际类型 传参方式
bool bool insmod <文件名>.ko 变量名=0 或 1
invbool bool insmod <文件名>.ko 变量名=0 或 1
charp char * insmod <文件名>.ko 变量名="字符串内容"
short short insmod <文件名>.ko 变量名=数值
int int insmod <文件名>.ko 变量名=数值
long long insmod <文件名>.ko 变量名=数值
ushort unsigned short insmod <文件名>.ko 变量名=数值
uint unsigned int insmod <文件名>.ko 变量名=数值
ulong unsigned long insmod <文件名>.ko 变量名=数值

③、 参数perm:给对应的文件/sys/module/name/parameters/变量名指定操作权限,可以直接传递数字,也可以传递下表中的宏:

建议
#define S_IRWXU 00700
#define S_IRUSR 00400
#define S_IWUSR 00200
#define S_IXUSR 00100
#define S_IRWXG 00070
#define S_IRGRP 00040
#define S_IWGRP 00020
#define S_IXGRP 00010
#define S_IRWXO 00007
#define S_IROTH 00004
#define S_IWOTH 00002 不要用,编译出错
#define S_IXOTH 00001

一般设置为0664,即执行权限不需要,用户和组都为可读写,其他用户只读。

2)、另外一个module_param_array(name, type, &num, perm);是设置数组参数的,其中的参数nametypepermmodule_param(name, type, perm);是一样的,另外一个参数介绍如下:
①、 参数&num:传递的参数为数组时,此参数为存放数组大小变量的地址,若不需要,可以填NULL,但是要确保传参个数不越界。传递数组参数的方式:

bash 复制代码
insmod <文件名>.ko 数组名=元素值0,元素值1,...元素值num-1

2、参数传递编程

1)、我们对内核模块~/project/hello_yu_module进行修改,使之能够接收一些参数,我们编辑hello.c

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ vi hello.c

这里接收了3个参数,一个整型,一个字符串,一个整型数组,修改完成后保存退出~

2)、使用如下命令进行编译:

bash 复制代码
yu@Yubuntu:~/project/hello_yu_module$ make

3)、加载测试:

成功~

柒、模块依赖

内核模块的代码与内核其他代码运行于同一环境中。尽管内核模块在形式上独立存在,但在运行时,它与内核其他部分构成一个统一整体,同属内核程序的有机组成部分。这种运行时的整体性,使得内核模块与内核其他源码能够相互访问彼此的全局变量和函数,共享内核空间的全局资源。

本小节介绍我们自己的内核模块如何导出全局资源和使用其他内核代码的全局资源。

1、小知识

1)、导出符号:一个内核模块中可以被其它内核源代码使用的全局特性的名称(变量名、函数名等)被称为导出符号。

2)、符号表:若把所有导出符号放在一个表中,这个表被称为符号表。

3)、nm命令可以查看elf格式的可执行文件或目标文件中包含的符号表,用法:nm 文件名。符号表中会标明符号的类型,类型说明如下:

符号 段类型 数据/代码特征 示例(全局符号) 示例(局部符号)
B/b BSS 段 未初始化或零初始化的全局变量 int global_var; static int static_var;
D/d 已初始化数据段 初始值非零的全局变量 int var = 10; static int s_var = 20;
T/t 文本段 可执行代码(函数) void func(); static void s_func();
U 未定义符号 在目标文件中被引用,但未在当前文件中定义,需链接其他文件解析 extern int external_var;
R 只读数据段 字符串常量、const 全局变量等只读数据 const int const_var = 5;
N 调试符号 用于调试信息的符号
w 弱符号 多个定义时以强符号为准

4)、用于导出内核模块中全局特性的名称的两个宏:EXPORT_SYMBOL(函数名或全局变量名)EXPORT_SYMBOL_GPL(函数名或全局变量名),其中后者需要GPL许可证协议验证。

在使用导出符号的地方,需要对这些位于其他内核模块的导出符号使用extern声明后才能使用这些符号。

5)、内核模块B使用了内核模块A的导出符号,我们称内核模块B依赖于内核模块A,对于它们有如下要求:

  • 编译次序:先编译内核模块A,再编译内核模块B,当两个模块源码在不同目录时,需要:①. 编译被使用了导出符号的内核模块A;②. 复制内核模块A目录中的Module.symvers文件到内核模块B的目录中;③. 编译使用内核模块A的导出符号的内核模块B,否则编译内核模块B时会有符号未定义的错误。
  • 加载次序:先插入内核模块A,再插入内核模块B,否则内核模块B会插入失败。
  • 卸载次序:先卸载内核模块B,再卸载内核模块A,否则内核模块A会卸载失败。

2、内核模块依赖:同目录

接下来会构建两个具有依赖关系的内核模块来举例说明如何搞定它们的依赖关系。

1)、首先是两个存在依赖关系的内核模块在同一目录的情况,我们在~/project目录下创建一个ii_module文件夹:

bash 复制代码
yu@Yubuntu:~/project$ mkdir ii_module

2)、再在里面创建两个内核模块.c源文件:

bash 复制代码
yu@Yubuntu:~/project/ii_module$ vi yu_a.c
bash 复制代码
yu@Yubuntu:~/project/ii_module$ vi yu_b.c

3)、再创建Makefile文件:

bash 复制代码
yu@Yubuntu:~/project/ii_module$ vi Makefile

注意这里的顺序,由于是yu_b使用了yu_a中的导出符号,所以yu_a要在yu_b之前编译。

4)、编译运行:当前CPU架构

成功~

5)、编译运行:CPU架构为ARM

成功~

3、内核模块依赖:不同目录

1)、其次是两个存在依赖关系的内核模块不在同一目录的情况,我们在~/project目录下创建lukya_modulelukyb_module两个文件夹,并把之前的yu_a.cyu_b.c分别复制进去,再把Makefile也都复制过去一份:

2)、分别修改两个目录中的Makefile

bash 复制代码
yu@Yubuntu:~/project$ vi lukya_module/Makefile
bash 复制代码
yu@Yubuntu:~/project$ vi lukyb_module/Makefile

其中的KBUILD_EXTRA_SYMBOLS := /home/yu/project/lukya_module/Module.symvers为指定内核模块lukya_module的符号表的位置。

3)、尝试先编译lukyb_module

bash 复制代码
yu@Yubuntu:~/project/lukyb_module$ make

是直接报错的~

4)、先编译lukya_module

bash 复制代码
yu@Yubuntu:~/project/lukya_module$ make

通过!

5)、再编译lukyb_module

成功~

6)、测试:

通过~

4、Linux内核的符号表

上面的几个部分介绍的是我们自己写的内核模块之间的依赖关系,那么Linux内核提供了哪些导出符号给我们呢?

1)、在Linux运行时,文件/proc/kallsyms中的内容即是当前Linux的符号表,可以使用文本查看相关的命令直接查看里面的内容:

bash 复制代码
cat /proc/kallsyms

由于是运行时的符号表,故地址都为0000000000000000

2)、另外Linux系统还在/boot/System.map-<版本号>中存有符号表,版本号每个人都不太一样的,也是直接用文本查看相关的命令直接查看里面的内容(以下命令使用于我当前的Ubuntu系统):

bash 复制代码
yu@Yubuntu:~$ sudo cat /boot/System.map-6.8.0-51-generic

3)、另外,我们可以通过nm命令查看我们自己编译出来的elf格式的Linux内核文件vmlinux的符号表:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ nm vmlinux

4)、我们自己编译出来的Linux中也有文本类型的符号表,在源码目录下的System.map文件中:

bash 复制代码
yu@Yubuntu:~/kernel/linux-3.14$ cat System.map

捌、开发小知识

1、GPL许可证协议验证

1)、GPL(GNU General Public License,GNU 通用公共许可证)是自由软件基金会(FSF)发布的一种开源许可证,也是开源领域最具影响力的 "强 Copyleft" 许可证之一。它的核心目标是确保软件的自由使用、修改和分发权利,同时要求基于 GPL 许可的软件及其衍生作品必须继续遵循 GPL 条款,从而保障用户的自由不被剥夺。

2)、GPL许可证协议验证是一种确保代码合规性的机制,其核心是通过检查模块的许可证声明,确保导出的符号仅被符合GPL协议的模块使用。

2、模块传参中的参数说明

1)、可用MODULE_PARAM_DESC宏对每个参数进行作用描述,用法如下:

c 复制代码
MODULE_PARM_DESC(变量名, 字符串常量);

2)、重新编译后可以使用modinfo命令查看.ko文件的参数描述。

3)、示例如下:

3、内核模块信息宏

我们可以在内核模块源代码中加入如下一些宏来描述当前内核模块的信息:

c 复制代码
MODULE_AUTHOR(字符串常量);  // 作者信息
MODULE_DESCRIPTION(字符串常量);  // 模块描述
MODULE_ALIAS(字符串常量);  // 模块别名

这些宏的本质是定义static字符数组用于存放指定字符串内容,这些字符串内容链接时存放在.modinfo字段,可以用modinfo 内核模块文件名命令来查看这些模块信息。

4、常用命令

1)、file命令,用于查看elf格式文件的相关信息,例如我们编译内核模块时指定为ARM或者默认为当前CPU架构时,使用file命令会看到区别:

2)、dmesg命令,用于查看内核输出信息,dmesg -C是清除内核输出信息,我们之前介绍过。

3)、modinfo命令,用于查看内核模块文件的相关信息,之前介绍过。

4)、nm命令,用于查看elf格式文件的导出符号表,之前介绍过。

玖、参考资料

  1. https://www.doubao.com/thread/w376d871a527cb868
  2. https://www.cnblogs.com/willwuss/p/13696573.html
  3. https://blog.csdn.net/XiaoYuHaoAiMin/article/details/147092721?spm=1011.2415.3001.5331
  4. https://blog.csdn.net/Lihuihui006/article/details/112199469
  5. https://www.bilibili.com/video/BV1tyWWeeEpp/?spm_id_from=333.337.search-card.all.click
  6. https://www.doubao.com/thread/w5e99f0e281945c2a
  7. https://www.doubao.com/thread/wbdbee496c2ad372c