LV010-内核模块的编译
一、内核模块怎么编写?
这里我们以一个最简单的字符设备为例,先来了解一下大概的流程,后续再继续详细学习。
1. 模块源文件
我们需要编写一个模块源文件用于测试:
cd ~/7Linux/imx6ull-kernel
cd drivers/char可以看到这里有很多的源文件:

我们就在这里新建 hello_world_demo.c 源文件,并添加以下内容:
#include <linux/kernel.h>
#include <linux/init.h> /* module_init module_exit */
#include <linux/module.h> /* MODULE_LICENSE */
// 模块入口函数
int __init hello_world_demo_init(void)
{
printk("hello_world_demo module is running!\n");
return 0;
}
// 模块出口函数
void __exit hello_world_demo_exit(void)
{
printk("hello_world_demo will exit\n");
}
// 将__init 定义的函数指定为驱动的入口函数
module_init(hello_world_demo_init);
// 将__exit 定义的函数指定为驱动的出口函数
module_exit(hello_world_demo_exit);
/* 模块信息(通过 modinfo hello_world_demo 查看) */
MODULE_LICENSE("GPL"); /* 源码的许可证协议 */
MODULE_AUTHOR("sumu"); /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description"); /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */2. Kconfig 修改
此步骤可以向 linux 的图形配置界面添加新功能配置选项。
cd driver/char/ # 进入 hello_world_demo.c 的同级目录
vim Kconfig # 打开相关配置文件找到如下语句:
source "drivers/tty/Kconfig"在该语句下边添加以下内容:
config MY_HELLO
tristate "This is my hello test!"
help
This is a test for kernel new function
然后保存退出即可,我们可以试一下有什么效果,我们回到顶层源码目录下,打开图形配置界面:
cd ~/7Linux/imx6ull-kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig我们按以下层级找到我们新添加的配置项:
Device Drivers --->
Character devices --->
< > This is my hello_world_demo module load test! (NEW)如下图所示:

我们在这里,光标移动到新增加的配置项,按空格键,前面尖括号中的符号就会发生改变,* 表示编译进内核,M 表示编译成内核模块。这个分别对应两种加载方式,后面我们再讨论。
3. Makefile 修改
我们添加了新源码文件,自然要参与编译的,我们打开 hello_world_demo.c 同级目录下的 Makefile:
vim drivers/char/Makefile我么在这里面添加一句:
obj-$(CONFIG_MY_HELLO) += hello_world_demo.o
【注意】这里需要是 CONFIG_xxx,这个 xxx 就是上边修改 Kconfig 的时候添加的 config MY_HELLO 的 MY_HELLO。这样的话后面我们需要在内核的图形配置界面打开这个模块,才能进行编译。
二、模块的编译
模块的编译有两种方式:一种是直接编译进内核,这样就只会生成一个中间文件 .o,另一种是编译成单独的内核模块,会生成一个 .ko 文件。
1. 编译进内核
1.1 配置编译方式
这个我们在 kernel 的图形配置界面进行配置,我们这里打开 linux 内核图形配置界面:
cd ~/7Linux/imx6ull-kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig然后就会打开图形配置界面,我们按照下面的配置项路径找到刚才新增的配置项,选中后按空格键,将新功能前面尖括号的标识改为 *,这就表示将新功能编译到内核中:
Device Drivers --->
Character devices --->
<*> This is my hello_world_demo module load test!效果是这样的:
.config - Linux/arm 4.19.71 Kernel Configuration
> Device Drivers > Character devices ─────────────────────────────────────────
┌─────────────────────────── Character devices ───────────────────────────┐
│ Arrow keys navigate the menu. <Enter> selects submenus ---> (or empty │
│ submenus ----). Highlighted letters are hotkeys. Pressing <Y> │
│ includes, <N> excludes, <M> modularizes features. Press <Esc><Esc> to │
│ exit, <?> for Help, </> for Search. Legend: [*] built-in [ ] │
│ ┌────^(-)─────────────────────────────────────────────────────────────┐ │
│ │ [*] Unix98 PTY support │ │
│ │ [ ] Legacy (BSD) PTY support │ │
│ │ [ ] Non-standard serial port support │ │
│ │ < > HSDPA Broadband Wireless Data Card - Globe Trotter │ │
│ │ < > GSM MUX line discipline support (EXPERIMENTAL) │ │
│ │ < > Trace data sink for MIPI P1149.7 cJTAG standard │ │
│ │ [*] Automatically load TTY Line Disciplines │ │
│ │ <*> This is my hello_world_demo module load test! │ │
│ │ [*] /dev/mem virtual device support │ │
│ │ [ ] /dev/kmem virtual device support │ │
│ └────v(+)─────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ <Select> < Exit > < Help > < Save > < Load > │
└─────────────────────────────────────────────────────────────────────────┘然后我们选择保存,最后退出即可。然后我们可以看一下.config 文件的变化:

1.2 编译内核源码
我们不需要清空之前的配置文件,直接重新编译即可,注意这里不要执行这个使用默认配置文件的命令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alpha_emmc_defconfig若是执行了,我们前面的配置就会被清空,需要重新在图形界面配置。
# make ARCH = arm CROSS_COMPILE = arm-linux-gnueabihf- distclean # 这个清理的命令可以不执行
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16 # 只编译内核编译完毕后如下:

我们看一下 drivers/char 目录,可以看到这里只生成了一个.o 文件:

1.3 启动测试
因为这个是直接编译进内核源码的,在启动的时候我们不需要做任何操作,不需要像后面独立模块一样需要用一些加载命令去加载,内核就会自动加载这个新功能,这里我们可以直接试一下:
cp arch/arm/boot/zImage ~/3tftp/ #将编译出来带有 hello_world_demo 功能的镜像拷贝到 tftp 服务器然后我们启动开发板,之前设置的从 tftp 下载 zImage,所以这里我们直接拷贝到 tftp 服务器目录下即可,启动后我们可以看到打印日志:

2. 编译成独立模块
新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件 .ko。然后可以通过一些命令进行插入或者卸载这个内核模块。
这种编译方式又有两种形式,主要是根据我们的模块源码位置来区分,我们可以在 linux 内核源码中编写模块的源码然后编译,这种方式显然不利于我们对多个模块源码的管理。内核模块的源码可以在 linux 内核源码外,通过调用内核源码相关的内容完成对独立模块的编译,第二种显然更利于我们对驱动模块源码的管理。接下来我们就来看一看这两种编译方式怎么实现。
2.1 模块源码在 linux 内核源码中
2.1.1 配置编译方式
我们还是使用上边的源文件 hello_world_demo.c,该文件还是包含在 linux 内核源码中,这一次我们配置编译方式为另外一种:
修改新功能编译方式
cd ~/7Linux/imx6ull-kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig然后将新功能前边配置改为 M,表示编译成独立的模块:
Device Drivers --->
Character devices --->
<M> This is my hello_world_demo module load test!最后效果是这样的:
.config - Linux/arm 4.19.71 Kernel Configuration
> Device Drivers > Character devices ─────────────────────────────────────────
┌─────────────────────────── Character devices ───────────────────────────┐
│ Arrow keys navigate the menu. <Enter> selects submenus ---> (or empty │
│ submenus ----). Highlighted letters are hotkeys. Pressing <Y> │
│ includes, <N> excludes, <M> modularizes features. Press <Esc><Esc> to │
│ exit, <?> for Help, </> for Search. Legend: [*] built-in [ ] │
│ ┌────^(-)─────────────────────────────────────────────────────────────┐ │
│ │ [ ] Non-standard serial port support │ │
│ │ < > HSDPA Broadband Wireless Data Card - Globe Trotter │ │
│ │ < > GSM MUX line discipline support (EXPERIMENTAL) │ │
│ │ < > Trace data sink for MIPI P1149.7 cJTAG standard │ │
│ │ [*] Automatically load TTY Line Disciplines │ │
│ │ <M> This is my hello_world_demo module load test! │ │
│ │ [*] /dev/mem virtual device support │ │
│ │ [ ] /dev/kmem virtual device support │ │
│ │ Serial drivers ---> │ │
│ │ <*> Serial device bus ---> │ │
│ └────v(+)─────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────────┤
│ <Select> < Exit > < Help > < Save > < Load > │
└─────────────────────────────────────────────────────────────────────────┘我们来看一下配置文件.config 的变化:

2.1.2 编译内核
这里我们需要重新编译内核,主要是去掉之前内核镜像中带的 hello_world_demo 模块,到后面编译成独立模块的时候,就不需要吧镜像完整的编译一遍了。
cd ~/7Linux/imx6ull-kernel # 回到顶层源码目录下
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16 # 只编译内核用重新编译过的这个镜像的话,再在开发板上启动就不会有我们 demo 中打印的相关信息了。
2.1.3 编译模块
接下来我们编译模块:
cd ~/7Linux/imx6ull-kernel # 回到顶层源码目录下
make V=0 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules -j16 # 编译模块不出意外的话我们应该可以看到以下打印信息:

我们来看一下 drivers/char 目录:

会发现这里生成了一个 ko 文件,这就是我们最后要的独立模块。
2.2 模块源码独立在其他目录
这样每次都要使用 linux 源码目录下操作,文件也太多了,而且,这样会把所有的内核模块都编译一遍,还是很麻烦的。我们现在尝试在其他的目录单独创建模块驱动源码文件,然后调用 linux 源码进行编译,生成我们所需要的 .ko 内核模块文件。
注意:这种方式就不需要 linux 的图形界面的各种配置了。
2.2.1 源码编写
- (1)新建一个目录用于存放我们的模块源码
mkdir ~/imx6ull-driver-demo/02_module_load/out_of_source # 新建一个目录
cd ~/imx6ull-driver-demo/02_module_load/out_of_source # 进入相应目录这里随便自定义一个目录就是了,都一样的。
- (2)编写模块源码文件
这里我直接拷贝之前的文件到这里来:
cp ~/7Linux/imx6ull-kernel/drivers/char/hello_world_demo.c .2.2.2 Makefile 文件编写
# 模块名和模块测试APP名称
MODULE_NAME := hello_world_demo
# NFS 共享目录
TFTP_SERVER ?= ~/3tftp
NFS_SERVER ?= ~/4nfs
TFTP_DIR ?= $(TFTP_SERVER)
ROOTFS ?= $(NFS_SERVER)/imx6ull_rootfs
ifeq ($(KERNELRELEASE),)
# 选择可执行文件运行的平台
KERNELDIR ?= ~/7Linux/imx6ull-kernel
PWD := $(shell pwd)
# 编译模块和测试程序
modules:
$(MAKE) ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERNELDIR) M=$(PWD) modules
.PHONY: clean
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions .cache.mk
help:
@echo "\033[1;32m================================ Help ================================\033[0m"
@echo "Ubuntu may need to add sudo:"
@echo "dmesg # View information printed by the kernel"
@echo "file <module_name>.ko # View \".Ko\" file information"
@echo "make ARCH=arm # arm platform"
@echo "\033[1;32m======================================================================\033[0m"
print:
@echo "KERNELDIR = $(KERNELDIR)"
else
CONFIG_MODULE_SIG = n
obj-m += $(MODULE_NAME).o
endif2.2.3 编译模块
make # 编译模块不出意外的话应该会有这些提示信息出现:

然后我们看一下当前目录下都有哪些文件:

发现这里生成了一堆的中间文件,同样生成了我们所需要的 ko 文件。