00. 目录
文章目录
01. 概述
在Linux系统中,设备驱动会以内核模块的形式出现,学习Linux内核模块编程是驱动开发的先决条件。
Linux是一个跨平台的操作系统,支持众多的设备,在Linux内核源码中有超过50%的代码都与设备驱动相关。 Linux为宏内核架构,如果开启所有的功能,内核就会变得十分臃肿。 内核模块就是实现了某个功能的一段内核代码,在内核运行过程,可以加载这部分代码到内核中, 从而动态地增加了内核的功能。基于这种特性,我们进行设备驱动开发时,以内核模块的形式编写设备驱动, 只需要编译相关的驱动代码即可,无需对整个内核进行编译。
内核模块的引入不仅提高了系统的灵活性,对于开发人员来说更是提供了极大的方便。 在设备驱动的开发过程中,我们可以随意将正在测试的驱动程序添加到内核中或者从内核中移除, 每次修改内核模块的代码不需要重新启动内核。 在开发板上,我们也不需要将内核模块程序,或者说设备驱动程序的ELF文件存放在开发板中, 免去占用不必要的存储空间。当需要加载内核模块的时候,可以通过挂载NFS服务器, 将存放在其他设备中的内核模块,加载到开发板上。 在某些特定的场合,我们可以按照需要加载/卸载系统的内核模块,从而更好的为当前环境提供服务。
02. 第一个内核模块
test.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init hello_init(void)
{
printk(KERN_EMERG "Hello Module Init\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_EMERG "Hello Module Exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("uplooking");
MODULE_DESCRIPTION("hello module");
MODULE_ALIAS("test_module");
03. 代码框架分析
Linux内核模块的代码框架通常由下面几个部分组成:
- 模块加载函数(必须): 当通过insmod或modprobe命令加载内核模块时,模块的加载函数就会自动被内核执行,完成本模块相关的初始化工作。
- 模块卸载函数(必须): 当执行rmmod命令卸载模块时,模块卸载函数就会自动被内核自动执行,完成相关清理工作。
- 模块许可证声明(必须): 许可证声明描述内核模块的许可权限,如果模块不声明,模块被加载时,将会有内核被污染的警告。
- 模块参数: 模块参数是模块被加载时,可以传值给模块中的参数。
- 模块导出符号: 模块可以导出准备好的变量或函数作为符号,以便其他内核模块调用。
- 模块的其他相关信息: 可以声明模块作者等信息。
上面示例的hello module程序只包含上面三个必要部分以及模块的其他信息声明。
头文件包含了<linux/init.h>和<linux/module.h>,这两个头文件是写内核模块必须要包含的。 模块初始化函数hello_init调用了printk函数,在内核模块运行的过程中,他不能依赖于C库函数, 因此用不了printf函数,需要使用单独的打印输出函数printk。该函数的用法与printf函数类似。 完成模块初始化函数之后,还需要调用宏module_init来告诉内核,使用hello_init函数来进行初始化。 模块卸载函数也用printk函数打印字符串,并用宏module_exit在内核注册该模块的卸载函数。 最后,必须声明该模块使用遵循的许可证,这里我们设置为GPL2协议。
04. 头文件
前面我们已经接触过了Linux的应用编程,了解到Linux的头文件都存放在/usr/include中。 编写内核模块所需要的头文件,并不在上述说到的目录,而是在Linux内核源码中的include文件夹。
- #include <linux/module.h>: 包含了内核加载module_init()/卸载module_exit()函数和内核模块信息相关函数的声明
- #include <linux/init.h>: 包含一些内核模块相关节区的宏定义
- #include <linux/kernel.h>: 包含内核提供的各种函数,如printk
头文件路径
deng@local:~/a72/x3399/kernel/include/linux$ pwd
/home/deng/a72/x3399/kernel/include/linux
deng@local:~/a72/x3399/kernel/include/linux$
编写内核模块中经常要使用到的头文件有以下两个:<linux/init.h>和<linux/module.h>。 我们可以看到在头文件前面也带有一个文件夹的名字linux,对应了include下的linux文件夹,我们到该文件夹下,查看这两个头文件都有什么内容。
init.h头文件(位于内核源码 /include/linux/init.h)
/* These macros are used to mark some functions or
* initialized data (doesn't apply to uninitialized data)
* as `initialization' functions. The kernel can take this
* as hint that the function is used only during the initialization
* phase and free up used memory resources after
*
* Usage:
* For functions:
*
* You should add __init immediately before the function name, like:
*
* static void __init initme(int x, int y)
* {
* extern int z; z = x * y;
* }
*
* If the function has a prototype somewhere, you can also add
* __init between closing brace of the prototype and semicolon:
*
* extern int initialize_foobar_device(int, int, int) __init;
*
* For initialized data:
* You should insert __initdata or __initconst between the variable name
* and equal sign followed by value, e.g.:
*
* static int init_variable __initdata = 0;
* static const char linux_logo[] __initconst = { 0x32, 0x36, ... };
*
* Don't forget to initialize data not at file scope, i.e. within a function,
* as gcc otherwise puts the data into the bss section and not into the init
* section.
*/
/* These are for everybody (although not all archs will actually
discard it in modules) */
#define __init __section(.init.text) __cold notrace __noretpoline
#define __initdata __section(.init.data)
#define __initconst __constsection(.init.rodata)
#define __exitdata __section(.exit.data)
#define __exit_call __used __section(.exitcall.exit)
#define __exit __section(.exit.text) __exitused __cold notrace
init.h头文件主要包含了内核模块用到的一些宏定义,因此,只要我们涉及内核模块的编程,就需要加上该头文件。
module.h头文件(位于内核源码 /include/linux/module.h)
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
/* Generic info of form tag = "info" */
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
以上代码中,包含了内核模块的加载、卸载函数的声明,还列举了module.h文件中的部分宏定义,这部分宏定义, 有的是可有可无的,但是MODULE_LICENSE这个是指定该内核模块的许可证,是必须要有的。
注意: 在本教程使用的4.x版本内核中, module_init
和 module_exit
函数声明在 /include/linux/module.h
文件中,旧版本的内核这两个函数声明在 /include/linux/init.h
文件中。
05. 模块加载/卸载函数
5.1 module_init函数
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
返回值:
- 0: 表示模块初始化成功,并会在/sys/module下新建一个以模块名为名的目录
- 非0: 表示模块初始化失败
宏定义module_init用于通知内核初始化模块的时候, 要使用哪个函数进行初始化。它会将函数地址加入到相应的节区section中, 这样的话,开机的时候就可以自动加载模块了。
[root@rk3399:/]# cd /sys/module/
[root@rk3399:/sys/module]# ls
8250 firmware_class pstore sr_mod
8250_core ftdi_sio ramoops stmmac
asix fuse rcupdate sunrpc
auth_rpcgss hci_vhci rcutree suspend
bfusb hid rfcomm sysrq
block hidp rfkill tc35874x
bluetooth i2c_algo_bit rk817_battery tcp_cubic
brd i2c_hid rk817_charger tpm
btbcm input_polldev rk_vcodec tpm_i2c_infineon
btintel ipv6 rndis_wlan uinput
btmrvl kernel rng_core usb_storage
btmrvl_sdio keyboard rockchip_pwm_remotectl usbcore
btrtl libertas_tf rtc_pcf8563 usbhid
btusb lkdtm sbs_battery usbtouchscreen
cdc_ncm lockd scsi_mod uvcvideo
cec loop sdhci v4l2_mem2mem
cfg80211 mac80211 sierra video_rkisp1
configfs mali snd videobuf2_core
cpuidle midgard_kbase snd_pcm videobuf2_dma_sg
devres mmcblk snd_seq videobuf_core
dns_resolver module snd_seq_dummy vt
drm mpp_dev_common snd_seq_midi workqueue
drm_kms_helper nfs snd_soc_rk817 xhci_hcd
dynamic_debug ntfs snd_timer xz_dec
ehci_hcd nvme snd_usb_audio
elan_i2c pcie_aspm spidev
fiq_debugger printk spurious
[root@rk3399:/sys/module]#
参考示例
static int __init func_init(void)
{
}
module_init(func_init);
func_init函数在内核模块中也是做与初始化相关的工作。
在C语言中,static关键字的作用如下:
- static修饰的静态局部变量直到程序运行结束以后才释放,延长了局部变量的生命周期;
- static的修饰全局变量只能在本文件中访问,不能在其它文件中访问;
- static修饰的函数只能在本文件中调用,不能被其他文件调用。
内核模块的代码,实际上是内核代码的一部分, 假如内核模块定义的函数和内核源代码中的某个函数重复了, 编译器就会报错,导致编译失败,因此我们给内核模块的代码加上static修饰符的话, 那么就可以避免这种错误。
__init、__initdata宏定义(位于内核源码/include/linux/init.h)
#define __init __section(.init.text) __cold __latent_entropy __noinitretpoline
#define __initdata __section(.init.data)
以上代码 __init、__initdata宏定义(位于内核源码/linux/init.h)中的__init用于修饰函数,__initdata用于修饰变量。 带有__init的修饰符,表示将该函数放到可执行文件的__init节区中,该节区的内容只能用于模块的初始化阶段, 初始化阶段执行完毕之后,这部分的内容就会被释放掉。
5.2 module_exit
与内核加载函数相反,内核模块卸载函数func_exit主要是用于释放初始化阶段分配的内存, 分配的设备号等,是初始化过程的逆过程。
/**
* module_exit() - driver exit entry point
* @x: function to be run when driver is removed
*
* module_exit() will wrap the driver clean-up code
* with cleanup_module() when used with rmmod when
* the driver is a module. If the driver is statically
* compiled into the kernel, module_exit() has no effect.
* There can only be one per module.
*/
#define module_exit(x) __exitcall(x);
__exit、__exitdata宏定义(位于内核源码/include/linux/init.h)
#define __exit __section(.exit.text) __exitused __cold notrace
#define __exitdata __section(.exit.data)
类比于模块加载函数,__exit用于修饰函数,__exitdata用于修饰变量。 宏定义module_exit用于告诉内核,当卸载模块时,需要调用哪个函数。
参考示例
static void __exit func_exit(void)
{
}
module_exit(func_exit);
与函数func_init区别在于,该函数的返回值是void类型,且修饰符也不一样, 这里使用的使用__exit,表示将该函数放在可执行文件的__exit节区, 当执行完模块卸载阶段之后,就会自动释放该区域的空间。
06. 模块信息
表 内核模块信息声明函数
函数 | 作用 |
---|---|
MODULE_LICENSE() | 表示模块代码接受的软件许可协议,Linux内核遵循GPL V2开源协议,内核模块与linux内核保持一致即可。 |
MODULE_AUTHOR() | 描述模块的作者信息 |
MODULE_DESCRIPTION() | 对模块的简单介绍 |
MODULE_ALIAS() | 给模块设置一个别名 |
6.1 许可证
Linux是一款免费的操作系统,采用了GPL协议,允许用户可以任意修改其源代码。 GPL协议的主要内容是软件产品中即使使用了某个GPL协议产品提供的库, 衍生出一个新产品,该软件产品都必须采用GPL协议,即必须是开源和免费使用的, 可见GPL协议具有传染性。因此,我们可以在Linux使用各种各样的免费软件。 在以后学习Linux的过程中,可能会发现我们安装任何一款软件,从来没有30天试用期或者是要求输入激活码的。
/*
* The following license idents are currently accepted as indicating free
* software modules
*
* "GPL" [GNU Public License v2 or later]
* "GPL v2" [GNU Public License v2]
* "GPL and additional rights" [GNU Public License v2 rights and more]
* "Dual BSD/GPL" [GNU Public License v2
* or BSD license choice]
* "Dual MIT/GPL" [GNU Public License v2
* or MIT license choice]
* "Dual MPL/GPL" [GNU Public License v2
* or Mozilla license choice]
*
* The following other idents are available
*
* "Proprietary" [Non free products]
*
* There are dual licensed components, but when running with Linux it is the
* GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL
* is a GPL combined work.
*
* This exists for several reasons
* 1. So modinfo can show license info for users wanting to vet their setup
* is free
* 2. So the community can ignore bug reports including proprietary modules
* 3. So vendors can do likewise based on their own policies
*/
#define MODULE_LICENSE(_license) MODULE_INFO(license, _license)
内核模块许可证有 “GPL”,“GPL v2”,“GPL and additional rights”,“Dual SD/GPL”,“Dual MPL/GPL”,“Proprietary”等等。
6.2 作者信息
内核模块作者宏定义(位于内核源码/include/linux/module.h)
/*
* Author(s), use "Name <email>" or just "Name", for multiple
* authors use multiple MODULE_AUTHOR() statements/lines.
*/
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)
我们前面使用modinfo中打印出的模块信息中“author”信息便是来自于宏定义MODULE_AUTHOR。 该宏定义用于声明该模块的作者。
6.3 模块描述信息
模块描述信息(位于内核源码/include/linux/module.h)
/* What your module does. */
#define MODULE_DESCRIPTION(_description) MODULE_INFO(description, _description)
模块信息中“description”信息则来自宏MODULE_DESCRIPTION,该宏用于描述该模块的功能作用。
6.4 模块别名
内核模块别名宏定义(位于内核源码/inlcude/linux/module.h)
/* For userspace: you can also call me... */
#define MODULE_ALIAS(_alias) MODULE_INFO(alias, _alias)
模块信息中“alias”信息来自于宏定义MODULE_ALIAS。该宏定义用于给内核模块起别名。 注意,在使用该模块的别名时,需要将该模块复制到/lib/modules/内核源码/下, 使用命令depmod更新模块的依赖关系,否则的话,Linux内核怎么知道这个模块还有另一个名字。
07. 输出函数
printk函数
/**
* printk - print a kernel message
* @fmt: format string
*
* This is printk(). It can be called from any context. We want it to work.
*
* We try to grab the console_lock. If we succeed, it's easy - we log the
* output and call the console drivers. If we fail to get the semaphore, we
* place the output into the log buffer and return. The current holder of
* the console_sem will notice the new output in console_unlock(); and will
* send it to the consoles before releasing the lock.
*
* One effect of this deferred printing is that code which calls printk() and
* then changes console_loglevel may break. This is because console_loglevel
* is inspected when the actual printing occurs.
*
* See also:
* printf(3)
*
* See the vsnprintf() documentation for format string extensions over C99.
*/
asmlinkage __visible int printk(const char *fmt, ...)
- printf:glibc实现的打印函数,工作于用户空间
- printk:内核模块无法使用glibc库函数,内核自身实现的一个类printf函数,但是需要指定打印等级。
- #define KERN_EMERG “<0>” 通常是系统崩溃前的信息
- #define KERN_ALERT “<1>” 需要立即处理的消息
- #define KERN_CRIT “<2>” 严重情况
- #define KERN_ERR “<3>” 错误情况
- #define KERN_WARNING “<4>” 有问题的情况
- #define KERN_NOTICE “<5>” 注意信息
- #define KERN_INFO “<6>” 普通消息
- #define KERN_DEBUG “<7>” 调试信息
查看当前系统printk打印等级:cat /proc/sys/kernel/printk
, 从左到右依次对应当前控制台日志级别、默认消息日志级别、 最小的控制台级别、默认控制台日志级别。
[root@rk3399:/sys/module]# cat /proc/sys/kernel/printk
7 4 1 7
打印内核所有打印信息:dmesg,注意内核log缓冲区大小有限制,缓冲区数据可能被覆盖掉。
08. Makefile
对于内核模块而言,它是属于内核的一段代码,只不过它并不在内核源码中。 为此,我们在编译时需要到内核源码目录下进行编译。 编译内核模块使用的Makefile文件,和我们前面编译C代码使用的Makefile大致相同, 这得益于编译Linux内核所采用的Kbuild系统,因此在编译内核模块时,我们也需要指定环境变量ARCH和CROSS_COMPILE的值。
KERNEL_DIR=/home/deng/a72/x3399/kernel
CROSS_COMPILE=aarch64-linux-gnu-gcc
obj-m := test.o
all:
$(MAKE) -C $(KERNEL_DIR) M=`pwd` modules
.PHONE:clean
clean:
$(MAKE) -C $(KERNEL_DIR) M=$(PWD) clean
以上代码中提供了一个关于编译内核模块的Makefile。
- 第1行:该Makefile定义了变量KERNEL_DIR,来保存内核源码的目录。
- 第2行: 指定了工具链
- 第3行:变量obj-m保存着需要编译成模块的目标文件名。
- 第4行:’$(MAKE)modules’实际上是执行Linux顶层Makefile的伪目标modules。通过选项’-C’,可以让make工具跳转到源码目录下读取顶层Makefile。’M=$(CURDIR)’表明返回到当前目录,读取并执行当前目录的Makefile,开始编译内核模块。pwd设置为当前目录。
编译命令说明
输入如下命令来编译驱动模块:
deng@local:~/code/test/1module$ make
make -C /home/deng/a72/x3399/kernel M=`pwd` modules
make[1]: 进入目录“/home/deng/a72/x3399/kernel”
CC [M] /home/deng/a72/code/1module/test.o
Building modules, stage 2.
MODPOST 1 modules
CC /home/deng/a72/code/1module/test.mod.o
LD [M] /home/deng/a72/code/1module/test.ko
make[1]: 离开目录“/home/deng/a72/x3399/kernel”
deng@local:~/code/test/1module$
编译成功后,目录下会生成名为“test.ko”的驱动模块文件
09. 内核模块加载和卸载
加载内核模块
[root@rk3399:/mnt/a72/code/1module]# insmod test.ko
[16099.893741] Hello Module Init
卸载内核模块
[root@rk3399:/mnt/a72/code/1module]# rmmod test
[16103.132570] Hello Module Exit
[root@rk3399:/mnt/a72/code/1module]#