【编者的话】Linux内核模块作为Linux内核的扩展手段,可以在运行时动态加载和卸载。它是设备和用户应用程序之间的桥梁,可以通过标准系统调用,为应用程序屏蔽设备细节。本文来自Derek Molloy的博客,介绍了内核模块的概念、用途,以及如何构建一个简单的“Hello World”内核模块。
在这系列文章中,将介绍如何为嵌入式Linux设备编写Linux内核模块。文章将从简单的可加载内核模块(loadable kernel module,LKM)“Hello World!”开始,进而开发通过使用中断请求控制嵌入式Linux设备(如BeagleBone)通用输入输出接口(GPIO)的模块。当我确定合适的应用程序时,我会添加更多的后续文章。
内核模块是一个复杂的话题,需要一定的时间来完成。因此,我将内容拆分成几篇文章,每篇提供一个可以实践的示例和结果。这个话题可以写一整本书,因此很难覆盖每一个方面。关于编写内核模块的其他文章也有很多,而本文的示例都在Linux内核3.8.X以上版本构建和测试,以确保这些材料是最新且贴切的。同时,本文主要关注嵌入式系统的硬件接口。在我的书《Exploring BeagleBone》中也有相同的示例,由于本文自身包含了这些代码,读者无须拥有该书的副本。
图1:内核空间GPIO性能
本文集中讨论构建和部署“Hello World!”内核模块所需的系统设置、工具和代码。本系列中的第二篇文章探讨了如何编写字符设备驱动和如何编写用户空间C/C++程序与内核空间模块进行交互。第三篇文章探讨内核空间GPIO库代码的使用,它结合了前两篇文章的内容,开发中断驱动代码,使之能够从Linux用户空间控制。例如,图1展示了示波器捕获的通过中断驱动内核模块处理按钮按下到LED亮起的图形。在常规嵌入式Linux中(即非实时Linux的变体),该代码展示忽略CPU开销后,响应时间大约为20毫秒(±5微秒)。
可加载内核模块(LKM)是Linux内核运行时加载和移除代码的机制。该机制对于设备驱动是理想的,这使得内核可以在不知道硬件如何工作的情况下和硬件进行交互。可加载内核模块的替代是将每个驱动代码构建到Linux内核中。
没有模块化能力,Linux内核将会变得非常大,因为它不得不支持BeagleBone开发板上所需的每个驱动。同时,在需要添加新硬件或者升级设备驱动时,必须重新构建内核。可加载内核模块功能的缺点是对于每个设备都必须维护一个驱动文件。可加载内核模块在运行时加载,他们不运行在用户空间,本质上是内核的一部分。
图2:Linux用户空间和内核空间
如图2所示,内核模块运行在内核空间,而应用程序运行在用户空间。内核空间和用户空间都有自己独立的内存地址,不会相互重叠。此方法确保了运行在用户空间中的应用程序对于硬件有一致的视图,不用关注硬件平台本身。内核服务通过系统调用以可控的方式提供给用户空间。同时,内核阻止独立的用户空间应用程序之间相互竞争或通过使用保护级别访问受限资源(比如超级用户与普通用户的权限)。
在嵌入式Linux中和电子电路交互,你接触到的是系统文件系统,并且使用低级别的文件操作来和电子电路交互。这种方式效率很低(尤其是如果你有传统嵌入式系统开发经验)。然而,对这些文件项进行内存映射后,对于许多应用程序来说性能是足够的。我在书中已经证明,通过在Linux用户空间使用pthread、回调函数和sys/poll.h,在忽略CPU开销下,是可以做到约三分之一毫秒的响应时间。
另一个实现是使用内核代码,它支持中断。然而内核代码难以编写和调试。我的建议是优先尝试在Linux用户空间完成任务,除非已确定没有其他可行方法。
本次讨论的所有代码都在为《Exploring BeagleBone》准备的GitHub仓库上。代码可以在ExploringBB GitHub仓库内核工程目录中公开查看,或者也可以将代码复制到BeagleBone(或者其他Linux设备):
molloyd@beaglebone:~$ sudo apt-get install git molloyd@beaglebone:~$ git clone https://github.com/derekmolloy/exploringBB.git
代码中/extras/kernel/hello目录是本文最重要的资源。为这些示例代码自动生成的Doxygen文档有HTML格式和PDF格式。
为了构建内核代码,需要在设备上安装Linux内核头文件。在典型的Linux桌面机器上,可以使用包管理器来查找和安装正确的包。例如,在64位Debian发行版中,可以这样做:
molloyd@DebianJessieVM:~$ sudo apt-get update molloyd@DebianJessieVM:~$ apt-cache search linux-headers-$(uname -r) linux-headers-3.16.0-4-amd64 - Header files for Linux 3.16.0-4-amd64 molloyd@DebianJessieVM:~$ sudo apt-get install linux-headers-3.16.0-4-amd64 molloyd@DebianJessieVM:~$ cd /usr/src/linux-headers-3.16.0-4-amd64/ molloyd@DebianJessieVM:/usr/src/linux-headers-3.16.0-4-amd64$ ls arch include Makefile Module.symvers scripts
本系列的前两篇文章的示例,可以在任何桌面Linux发行版中完成构建。然而,本系列文章中,我将在BeagleBone上直接构建内核模块,这相比于交叉编译可以简化步骤。安装的内核头文件必须和内核构建版本一致。和桌面版安装类似,使用uname命令来识别正确的安装版本。例如:
molloyd@beaglebone:~$ uname -a Linux beaglebone 3.8.13-bone70 #1 SMP Fri Jan 23 02:15:42 UTC 2015 armv7l GNU/Linux
BeagleBone平台的Linux内核头文件可以从Robert Nelson的网站下载。比如在http://rcn-ee.net/deb/precise-armhf/,选择准确的内核构建版本,并且在BeagleBone上下载和安装这些Linux内核头文件。例如:
molloyd@beaglebone:~/tmp$ wget http://rcn-ee.net/deb/precise-armhf/v3.8.13-bone70 /linux-headers-3.8.13-bone70_1precise_armhf.deb 100%[===========================>] 8,451,080 2.52M/s in 3.2s 2015-03-17 22:35:45 (2.52 MB/s) - 'linux-headers-3.8.13-bone70_1precise_armhf.deb' saved [8451080/8451080] molloyd@beaglebone:~/tmp$ sudo dpkg -i ./linux-headers-3.8.13-bone70_1precise_armhf.deb Selecting previously unselected package linux-headers-3.8.13-bone70
然后可以检查头文件是否正确安装:
molloyd@beaglebone:~/tmp$ cd /usr/src/linux-headers-3.8.13-bone70/ molloyd@beaglebone:/usr/src/linux-headers-3.8.13-bone70$ ls Documentation Module.symvers crypto fs ipc mm scripts tools Kconfig arch drivers include kernel net security usr Makefile block firmware init lib samples sound virt
给BeagleBone使用的3.8.13-bone47版本内核的Debian发行版中,需要执行一个特殊步骤在/usr/src/linux-headers-3.8.13-bone47/arch/arm/include/mach目录中创建一个空的timex.h文件(即touch timex.h)。bone70构建不需要此步骤。
编写和测试内核模块时很容易使系统崩溃。系统崩溃可能会损坏文件系统。虽然系统崩溃不太常见,但这是可能发生的。请备份数据或者使用一个嵌入式系统,如BeagleBone,他们能够很方便的被重新刷写。通过执行sudo reboot或者按BeagleBone上的重置按钮,通常能够恢复到正常状态。在写本系列文章过程中,尽管有很多很多次系统崩溃,但BeagleBones并没有损坏过。
传统计算机程序的运行生命周期相当简单。加载器为程序分配内存,然后加载程序和所需要的动态链接库。指令从一些入口开始执行(传统C/C++程序以main()函数作为入口),语句被执行,异常被抛出,动态内存被分配和释放,程序最终运行完成。当程序退出时,操作系统识别任何内存泄露,并释放到内存池。
内核模块不是应用程序,从一开始就没有main()函数。内核模块和普通应用程序的区别有:
以上概念有很多需要消化,重要的是,它们都被解决,但是没有都包含在第一篇文章中。列表1提供了第一个示例内核模块的的代码。当没有提供内核参数时,代码使用printk()函数显示“Hello world!...”,如果提供了参数“Derek”,日志会显示“Hello Derek!...”。列表1中的注释使用Doxygen样式,描述每个语句角色。更多的描述在代码列表下放。
/** * @file hello.c * @author Derek Molloy * @date 4 April 2015 * @version 0.1 * @brief 入门的可加载内核模块“Hello World!”,当模块加载和移除的时候,会在/var/log/kern.log文件输出消息。 * 该模块在加载的时候接受一个参数:名字,它将显示在内核日志文件中。 * @see http://www.derekmolloy.ie/ 查看完整描述和补充描述。 */ #include <linux/init.h> // 用于标记函数的宏,如__init、__exit #include <linux/module.h> // 加载内核模块到内核使用的核心头文件 #include <linux/kernel.h> // 包含内核使用的类型、宏和函数 MODULE_LICENSE("GPL"); ///< 许可类型,它会影响到运行时行为 MODULE_AUTHOR("Derek Molloy"); ///< 作者,当使用modinfo命令时可见 MODULE_DESCRIPTION("A simple Linux driver for the BBB."); ///< 模块描述,参见modinfo命令 MODULE_VERSION("0.1"); ///< 模块版本 static char *name = "world"; ///< 可加载内核模块参数示例,这里默认值设置为“world” module_param(name, charp, S_IRUGO); ///< 参数描述。charp表示字符指针(char ptr),S_IRUGO表示该参数只读,无法修改 MODULE_PARM_DESC(name, "The name to display in /var/log/kern.log"); ///< 参数描述 /** @brief 可加载内核模块初始化函数 * static关键字限制了该函数的可见范围为当前C文件。 * __init宏表示对于内置驱动(不是可加载内核模块),该函数只在初始化的时候执行, * 在此之后,该函数可以废弃,且内存可以被回收。 * @return 当执行成功返回0 */ static int __init helloBBB_init(void){ printk(KERN_INFO "EBB: Hello %s from the BBB LKM!\n", name); return 0; } /** @brief 可加载内核模块清理函数 * 和初始化函数类似,它是静态(static)的。__exit函数表示如果这个代码是给内置驱动(非可加载内核模块)使用,该方法是不需要的。 */ static void __exit helloBBB_exit(void){ printk(KERN_INFO "EBB: Goodbye %s from the BBB LKM!\n", name); } /** @brief 内核模块必须使用linux/init.h头文件提供的module_init()和module_exit()宏, * 它们标识了在模块插入时的初始化函数和移除时的清理函数(如上描述) */ module_init(helloBBB_init); module_exit(helloBBB_exit);
列表1:Hello World Linux可加载内核模块代码
除了列表1注释中描述的点之外,还有一些补充的点:
从本质上讲,当模块加载时,helloBBB_init()函数将会执行。当模块卸载时,helloBBB_exit()函数会被执行。
下一步是将代码构建成内核模块。
构建内核模块需要Makefile文件,事实上是一个特殊的kbuild Makefile。构建本文示例的内核模块所需要的kbuild Makefile文件参见列表2.
obj-m+=hello.o all: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build/ M=$(PWD) clean
列表2:构建Hello World可加载内核模块需要的Makefile文件
Makefile文件第一行被成为目标定义,它定义了需要构建的模块(hello.o)。它的语法惊人的复杂,例如obj-m定义了可加载模块目标,obj-y表示内置的对象目标。当模块需要从多个目标文件构建时,语法会变得更加复杂,但这个Makefile文件对构建示例模块已经足够了。
Makefile文件中需要提醒的内容和普通Makefile文件类似。$(shell uname -r)命令返回当前内核构建版本,这确保了一定程度的可移植性。-C选项在执行任何make任务前将目录切换到内核目录。M=$(PWD)变量赋值告诉make命令实际工程文件存放位置。对于外部内核模块来说,modules目标是默认目标。另一种目标是modules_install,它将安装模块(make命令必须使用超级用户权限执行且需要提供模块安装路径)。
一切都很顺利的情况下(如已经按照前文描述安装了Linux内核头文件),构建内核模块的过程应该是非常简单的。构建步骤如下:
molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ ls -l total 8 -rw-r--r-- 1 molloyd molloyd 154 Mar 17 17:47 Makefile -rw-r--r-- 1 molloyd molloyd 2288 Apr 4 23:26 hello.c molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ make make -C /lib/modules/3.8.13-bone70/build/ M=/home/molloyd/exploringBB/extras/kernel/hello modules make[1]: Entering directory '/usr/src/linux-headers-3.8.13-bone70' CC [M] /home/molloyd/exploringBB/extras/kernel/hello/hello.o Building modules, stage 2. MODPOST 1 modules CC /home/molloyd/exploringBB/extras/kernel/hello/hello.mod.o LD [M] /home/molloyd/exploringBB/extras/kernel/hello/hello.ko make[1]: Leaving directory '/usr/src/linux-headers-3.8.13-bone70' molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ ls Makefile Module.symvers hello.c hello.ko hello.mod.c hello.mod.o hello.o modules.order
现在,在构建目录中能够看见一个hello可加载内核模块,它的文件扩展名为.ko。
该模块目前能够使用内核模块工具加载:
molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ ls -l *.ko -rw-r--r-- 1 molloyd molloyd 4219 Apr 4 23:27 hello.ko molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ sudo insmod hello.ko molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ lsmod Module Size Used by hello 972 0 g_multi 50407 2 libcomposite 15028 1 g_multi omap_rng 4062 0 mt7601Usta 639170 0
通过modinfo命令,可以获得模块的信息,这个命令能够识别出模块的描述、作者和定义的任何模块参数:
molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ modinfo hello.ko filename: /home/molloyd/exploringBB/extras/kernel/hello/hello.ko description: A simple Linux driver for the BBB. author: Derek Molloy license: GPL srcversion: 9E3F5ECAB0272E3314BEF96 depends: vermagic: 3.8.13-bone70 SMP mod_unload modversions ARMv7 thumb2 p2v8 parm: name:The name to display in /var/log/kernel.log. (charp)
模块可以通过rmmod命令卸载:
molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ sudo rmmod hello.ko
重复上述步骤,可以在内核日志中看见使用printk()函数输出的结果。建议使用第二个中断窗口查看这个可加载内核模块加载和卸载时的输出,比如:
molloyd@beaglebone:~$ sudo su - [sudo] password for molloyd: root@beaglebone:~# cd /var/log root@beaglebone:/var/log# tail -f kern.log ... Apr 4 23:34:32 beaglebone kernel: [21613.495523] EBB: Hello world from the BBB LKM! Apr 4 23:35:17 beaglebone kernel: [21658.306647] EBB: Goodbye world from the BBB LKM! ^C root@beaglebone:/var/log#
列表1中的代码同时包含了自定义参数,它允许在初始化时向内核模块传递参数。这个功能能够这样测试:
molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ sudo insmod hello.ko name=Derek
这时如果查看/var/log/kern.log文件,会看见“Hello Derek”替换了“Hello world”。不过,首先要来看下/proc和/sys文件系统。
除了使用lsmod命令,还能够通过如下方式查找当前系统已经加载的内核模块:
molloyd@beaglebone:~/exploringBB/extras/kernel/hello$ cd /proc molloyd@beaglebone:/proc$ cat modules|grep hello hello 972 0 - Live 0xbf903000 (O)
这里查看到的信息和lsmod命令提供的相同,但是它同时提供了已加载模块在当前内核内存中的偏移量,这个数据在调试时非常有用。
可加载内核模块在/sys/module目录下也有目录项,它提供了用户直接访问自定义参数状态的方式。例如:
molloyd@beaglebone:/proc$ cd /sys/module molloyd@beaglebone:/sys/module$ ls -l|grep hello drwxr-xr-x 6 root root 0 Apr 5 00:02 hello molloyd@beaglebone:/sys/module$ cd hello molloyd@beaglebone:/sys/module/hello$ ls -l total 0 -r--r--r-- 1 root root 4096 Apr 5 00:03 coresize drwxr-xr-x 2 root root 0 Apr 5 00:03 holders -r--r--r-- 1 root root 4096 Apr 5 00:03 initsize -r--r--r-- 1 root root 4096 Apr 5 00:03 initstate drwxr-xr-x 2 root root 0 Apr 5 00:03 notes drwxr-xr-x 2 root root 0 Apr 5 00:03 parameters -r--r--r-- 1 root root 4096 Apr 5 00:03 refcnt drwxr-xr-x 2 root root 0 Apr 5 00:03 sections -r--r--r-- 1 root root 4096 Apr 5 00:03 srcversion -r--r--r-- 1 root root 4096 Apr 5 00:03 taint --w------- 1 root root 4096 Apr 5 00:02 uevent -r--r--r-- 1 root root 4096 Apr 5 00:02 version molloyd@beaglebone:/sys/module/hello$ cat version 0.1 molloyd@beaglebone:/sys/module/hello$ cat taint O
这里的版本值为0.1,对应源码中的MODULE_VERSION("0.1");taint值为0,对应所选择的许可,代码中是MODULE_LICENSE("GPL")。
自定义参数查看步骤为:
molloyd@beaglebone:/sys/module/hello$ cd parameters/ molloyd@beaglebone:/sys/module/hello/parameters$ ls -l total 0 -r--r--r-- 1 root root 4096 Apr 5 00:03 name molloyd@beaglebone:/sys/module/hello/parameters$ cat name Derek
这里name变量的状态可以查看到,并且读取这个值不需要超级用户权限。这是因为在定义内核参数的时候使用了S_IRUGO参数。这个值还能够设置为可写,但是模块代码中将会需要检测状态变化并依据变化做出响应。最后,可以移除模块并观察输出:
molloyd@beaglebone:/sys/module/hello/parameters$ sudo rmmod hello.ko
正如预期那样,这回在内核日志中有如下输出信息:
root@beaglebone:/var/log# tail -f kern.log … Apr 5 00:02:20 beaglebone kernel: [23281.070193] EBB: Hello Derek from the BBB LKM! Apr 5 00:08:18 beaglebone kernel: [23639.160009] EBB: Goodbye Derek from the BBB LKM!
希望根据此文读者能够构建第一个可加载内核模块。尽管这个模块功能非常简单,它覆盖了大量材料。到了本文的最后,读者应该对可加载内核模块如何工作有了概要认识,应该已经配置了构建系统,构建、加载、卸载了内核模块,有能力为自己的可加载内核模块自定义参数。
下一步的构建工作是通过开发一个基础字符设备驱动,让内核空间可加载内核模块能够和用户空间C/C++应用通信。请阅读《编写Linux内核模块——第二部分:字符设备驱动》,然后是更加感兴趣的任务:和通用输入输出接口交互。
查看英文原文:Writing a Linux Kernel Module — Part 1: Introduction
《他山之石》是InfoQ中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到[email protected]。