LV10-01-内核模块-01-编译与加载
本文主要是内核模块的编译与加载相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
Linux开发板 | 华清远见 底板: FS4412_DEV_V5 核心板: FS4412 V2 |
u-boot | 2013.01 |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
文件 | 下载链接 |
--- | --- |
一、内核模块简介
1. 内核模块的概念
什么是Linux
内核模块?我查了百度:Linux内核模块_百度百科 (baidu.com)
尽管Linux
是”单块内核“(monolithic
)的操作系统——这是说整个系统内核都运行于一个单独的保护域中,但是linux
内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。这些代码(包括相关的子线程、数据、函数入口和函数出口)被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块中,或简称为模块。
支持模块的好处是基本内核镜像尽可能的小,因为可选的功能和驱动程序可以利用模块形式再提供。模块允许我们方便地删除和重新载入内核代码,也方便了调试工作。而且当热插拔新设备时,可通过命令载入新的驱动程序。
类似于浏览器、eclipse
这些软件的插件开发,Linux
提供了一种可以向正在运行的内核中插入新的代码段、在代码段不需要继续运行时也可以从内核中移除的机制,这个可以被插入、移除的代码段被称为内核模块。可用于加载到linux
内核的文件后缀为.ko
,.ko
文件是kernel object
文件,也就是kernel
下的模块加载文件。
内核模块的本质:一段隶属于内核的“动态”代码,与其它内核代码是同一个运行实体,共用同一套运行资源,只是存在形式上是独立的。
2. 内核模块的特点
内核模块的出现,解决了以下问题:
- 单内核扩展性差的缺点。
- 减小内核镜像文件体积,一定程度上节省内存资源。
- 提高开发效率。
但是内核模块也有相应的缺点,就是它不能彻底解决稳定性低的缺点:内核模块代码出错可能会导致整个系统崩溃。
二、模块的加载
模块的加载有两种方式:一是静态加载法,在linux
源码中进行修改,配置,然后直接编译进linux
内核;另一种是动态加载法,编写好模块后,通过一些列命令讲模块插入到linux
内核。
1. 静态加载
静态加载就是新功能源码与内核其它代码一起编译进uImage
文件内,这种编译方式不会生成.ko
内核模块文件,只会生成一个.o
文件。
1.1 编写模块源文件
我们需要编写一个模块源文件用于测试,我们在linux-3.14/driver/char/
目录下新建myhello.c
文件:
1 | cd ~/5linux/linux-3.14/driver/char/ # 进入相应的目录 |
并添加以下内容:
点击查看 myhello.c 测试源码
1 | /** ===================================================== |
1.2 修改Kconfig
此步骤可以向linux
的图形配置界面添加新功能配置选项。
1 | cd ~/5linux/linux-3.14/driver/char/ # 进入myhello.c的同级目录 |
找到如下语句(应该在第7
行):
1 | source "drivers/tty/Kconfig" |
在该语句下边添加以下内容:
1 | config MY_HELLO |
然后保存退出即可,我们可以试一下有什么效果,我们回到顶层源码目录下,打开图形配置界面:
1 | cd ~/5linux/linux-3.14/ |
我们按以下层级找到我们新添加的配置项:
1 | Device Drivers ---> |
如下图所示:
1.3 修改Makefile
我们添加了新源码文件,自然要参与编译的,我们打开myhello.c
同级目录下的Makefile
:
1 | vim ~/5linux/linux-3.14/drivers/char/Makefile |
我们找到如下语句(应该在第18
行):
1 | obj-$(CONFIG_BFIN_OTP) += bfin-otp.o |
我们在这一句下边添加如下内容:
1 | obj-$(CONFIG_MY_HELLO) += myhello.o |
【注意】这里需要是CONFIG_xxx
,这个xxx
就是上边修改Kconfig
的时候添加的config MY_HELLO
的MY_HELLO
。
1.4 配置新功能编译方式
我们这里打开linux
内核图形配置界面:
1 | cd ~/5linux/linux-3.14 |
然后将新功能修改为*
,表示将新功能编译到内核中。
1 | Device Drivers ---> |
1.5 编译内核并测试
- 编译内核
1 | cd ~/5linux/linux-3.14 # 回到顶层源码目录下 |
出现以下提示信息表示编译成功:
1 | Image Name: Linux-3.14.0-gc6094bb-dirty |
- 拷贝内核文件并重启开发板测试
1 | cp arch/arm/boot/uImage ~/3tftp/fs4412/ |
开发板重启后,启动内核时有以下信息出现,则说明新功能添加成功:
2. 动态加载
新功能源码与内核其它源码不一起编译,而是独立编译成内核的插件(被称为内核模块)文件.ko
。然后通过一些命令进行插入或者卸载。
2.1 模块编译
2.1.1 模块源码在linux
内核源码中
我们还是使用上边静态加载使创建的文件myhello.c
,该文件包含在linux
内核源码中,编译模块的步骤如下:
- (1)编写模块源码文件(同
1.1
)
1 | cd ~/5linux/linux-3.14/driver/char/ # 进入相应的目录 |
- (2)配置
Kconfig
1 | cd ~/5linux/linux-3.14/driver/char/ # 进入myhello.c的同级目录 |
- (3)修改
Makefile
(同1.3
)
1 | vim ~/5linux/linux-3.14/drivers/char/Makefile |
- (4)修改新功能编译方式
1 | cd ~/5linux/linux-3.14 # 回到顶层源码目录下 |
然后将新功能前边配置改为M
,表示编译成独立的模块:
1 | Device Drivers ---> |
- (5)重新编译内核
1 | cd ~/5linux/linux-3.14 # 回到顶层源码目录下 |
- (6)模块编译
1 | cd ~/5linux/linux-3.14 # 回到顶层源码目录下 |
完成模块编译后,我们就会发现在myhello.c
目录下生成了一个myhello.ko
文件,这就是我们后边要使用的内核模块文件。
2.1.2 模块源码独立在其他目录
这样每次都要使用linux
源码目录下操作,文件也太多了,其实我们也可以在其他的目录单独创建模块驱动源码文件,然后调用linux
源码进行编译,生成我们所需要的.ko
内核模块文件。一般步骤如下:
- (1)新建一个目录用于存放我们的模块源码
1 | mkdir ~/myhello # 新建一个目录 |
- (2)编写模块源码文件
这里我直接拷贝之前的文件到这里来:
1 | cp ~/5linux/linux-3.14/drivers/char/myhello.c . |
- (3)编写
Makefile
文件
点击查看 Makefile 文件内容
1 | ##============================================================================# |
- (4)编译模块
1 | make # 编译可在Ubuntu中运行的模块 |
2.2 命令介绍
上边我们已经得到了.ko
内核模块文件,下边是一些相关的命令。
2.2.1 file
file
命令可用于查看指定的.ko
文件可以用于哪种平台,一般使用格式如下:
1 | file module_name.ko |
输出信息中,带x86
字样的适用于主机ubuntu linux
,带arm
字样的适用于开发板linux
。
2.2.2 insmod
insmod
命令可用于加载.ko
文件,一般使用格式如下:
1 | insmod module_name.ko |
【注意】在主机Ubuntu
中加载模块的时候需要加上sudo
。
2.2.3 lssmod
lsmod
命令可用显示已经加载的模块,一般使用格式如下:
1 | lsmod module_name.ko |
【注意】在主机Ubuntu
中加载模块的时候不加sudo
,也可以正常使用。
2.2.4 rmsmod
rmmod
命令可用于卸载已经加载的模块,一般使用格式如下:
1 | rmmod module_name |
【注意】在主机Ubuntu
中加载模块的时候需要加上sudo
。
2.2.5 dmesg
dmesg
命令可用于查看内核的打印的提示信息,主要是在Ubuntu
中加载模块的使用后使用,我们的开发板会实时在串口终端界面打提示信息,一般使用格式如下:
1 | sudo dmesg -C # 清除内核已打印的信息 |
【注意】在主机Ubuntu
中加载模块的时候需要加上sudo
,在开发板中不需要使用该命令。
2.3 加载测试
- 拷贝
.ko
文件到根文件系统中
1 | sudo cp myhello.ko ~/4nfs/rootfs/01myDrivers/ |
【注意】需要加上sudo
,否则可能会提示没有权限拷贝。
- 加载测试
1 | insmod myhello.ko |
若出现以下提示,则说明加载成功:
三、模块基础代码解析
【注意】一个模块必须要有的三个部分:入口函数、出口函数和MODULE__LICENSE
。
1. 几个头文件
1 |
linux/module.h
:包含模块编程相关的宏定义,如MODULE_LICENSE
。linux/kernel.h
:包含内核编程最常用的函数声明,如printk
。linux/init.h
:包含模块加载与卸载函数,如module_init
和module_exit
。
2. 模块加载与卸载
模块有加载和卸载两种操作,我们在编写驱动的时候需要注册这两种操作函数,模块的加载和卸载注册函数如下 :
1 | module_init(xxx_init); //注册模块加载函数 |
2.1 模块加载
1 | /* 模块入口函数 */ |
模块加载函数位于linux/init.h
中,module_init
函数用来向 Linux
内核注册一个模块加载函数,参数 xxx_init
就是需要注册的具体函数,当使用insmod
命令加载驱动的时候,xxx_init
这个函数就会被调用,也就是说在模块被插入到内核的时候,xxx_init
函数被调用,它主要作用是为新功能做好准备工作。
__init
说明
__init
的作用 :(1)一个宏,展开后为:
__attribute__ ((__section__ (".init.text")))
实际是gcc
的一个特殊链接标记。(2)指示链接器将该函数放置在
.init.text
区段。(3)在模块插入时方便内核从
.ko
文件指定位置读取入口函数的指令到特定内存位置。
module_init()
说明
(1)
module_init
是一个宏,使用格式就是:module_init(模块入口函数名)
(2)动态加载模块的时候,对应的函数被调用;静态加载模块的时候,在内核启动过程中对应函数被调用。
(3)对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(
.initcall
段),方便系统初始化统一调用。(4)对于动态加载的模块,由于内核模块的默认入口函数名是
init_module
,用该宏可以给对应模块入口函数起别名。
2.2 模块卸载
1 | /* 模块出口函数 */ |
模块卸载函数位于linux/init.h
中,module_exit
函数用来向 Linux
内核注册一个模块卸载函数,参数 xxx_exit
就是需要注册的具体函数,当使用rmmod
命令卸载驱动的时候,xxx_exit
这个函数就会被调用,也就是说该函数在模块从内核中被移除时调用,主要作用做些xxx_init
函数的反操作,被称为模块的出口函数。
__exit
说明
__exit
的作用 :(1)一个宏,展开后为:
__attribute__ ((__section__ (".exit.text")))
实际也是gcc
的一个特殊链接标记。(2)指示链接器将该函数放置在
.exit.text
区段。(3)在模块插入时方便内核从
.ko
文件指定位置读取出口函数的指令到另一个特定内存位置。
module_exit()
说明
(1)
module_exit
是一个宏,使用格式就是:module_exit(模块出口函数名)
(2)动态加载模块在卸载的时候,对应的函数被调用;静态加载的模块,可以认为在系统退出时,对应函数被调用,实际上对应函数被忽略。
(3)对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(
.exitcall
段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。(4)对于动态加载的模块,由于内核模块的默认出口函数名是
cleanup_module
,用该宏可以给对应模块出口函数起别名。
3. 模块信息宏
模块信息宏用来描述一些当前模块的信息,有一些是可选的,如MODULE_DESCRIPTION
,有一些是必须的,如MODULE_LICENSE
,这些宏的本质是定义static
字符数组用于存放指定字符串内容,这些字符串内容链接时存放在.modinfo
字段,可以用modinfo
命令来查看这些模块信息,一般格式如下:
1 | modinfo 模块文件名 |
3.1 MODULE_LICENSE
MODULE_LICENSE
是一个宏,一般使用格式如下:
1 | MODULE_LICENSE(字符串常量); |
字符串常量内容为源码的许可证协议 可以是GPL
、 GPL v2
、GPL and additional rights
、Dual BSD/GPL"
、 Dual MIT/GPL
、Dual MPL/GPL
等。
其中GPL
最常用,其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko
文件指定位置说明本模块源码遵循的许可证在模块插入到内核时,内核会检查新模块的许可证是不是也遵循GPL
协议,如果发现不遵循GPL
,则在插入模块时打印信息:
1 | myhello:module license 'unspecified' taints kernel |
同时也会导致新模块没法使用一些内核其它模块提供的高级功能。
3.2 MODULE_AUTHOR
MODULE_AUTHOR
是一个宏,一般使用格式如下:
1 | MODULE_AUTHOR(字符串常量); /* 字符串常量内容为模块作者说明 */ |
3.3 MODULE_DESCRIPTION
MODULE_DESCRIPTION
是一个宏,一般使用格式如下:
1 | MODULE_DESCRIPTION(字符串常量); /* 字符串常量内容为模块功能说明 */ |
3.4 MODULE_ALIAS
MODULE_ALIAS
是一个宏,一般使用格式如下:
1 | MODULE_ALIAS(字符串常量); /* 字符串常量内容为模块别名 */ |
4. 模块基础框架
所以一个模块的编写至少需要以下几个部分:
1 | /** ===================================================== |
四、Makefile
1. 代码解析
1.1 几个变量
为了方便使用,这里定义了几个变量:
1 | # 模块名 |
MODULE_NAME
:就是这个模块的名称。ROOTFS
:就是NFS
共享目录的路径,这里其实是代表了网络挂载的开发板的根文件系统的路径。MODULE_DIR
:根文件系统下的驱动存放的目录,我们要在开发板加载驱动模块,需要先将模块拷贝到开发板,这个就是存放的目录路径。
1.2 选择内核源码目录
1 | # 选择可执行文件运行的平台 |
ARCH
:我们使用make
参数的时候指定的,我们使用make ARCH=arm
的时候,这里的ARCH
就是arm
。KERNELDIR
:开发板所使用的Linux
内核源码目录,使用绝对路径 。
这一段,根据我们make
的时候参数的不同,可以编译出用于x86
平台和arm
平台的.ko
文件:
1 | make # 编译可在Ubuntu中运行的模块 |
1.3 编译命令
1 | PWD := $(shell pwd) |
-C
:表示将当前的工作目录切换到指定目录中 ,也就是KERNERLDIR
目录 。M
:表示模块源码目录,make modules
命令中加入M=dir
以后程序会自动到指定的dir
目录中读取模块的源码并将其编译为.ko
文件。modules
: 表示编译模块 。
1.4 obj-m
1 | obj-m += $(MODULE_NAME).o |
obj-m
:它后边是目标文件名,表示将$(MODULE_NAME).c
编译成$(MODULE_NAME).ko
模块。
2. 多源文件编译
有时候我们要是一个模块依赖于多个.c
源文件怎么编译呢?我们修改Makefile
的obj-m
部分即可:
1 | obj-m += $(MODULE_NAME).o |
例如,
点击查看实例
1 |
|
1 |
|
1 | ##============================================================================# |