编写Linux内核模块——第一部分:前言

【编者的话】Linux内核模块作为Linux内核的扩展手段,可以在运行时动态加载和卸载。它是设备和用户应用程序之间的桥梁,可以通过标准系统调用,为应用程序屏蔽设备细节。本文来自Derek Molloy的博客,介绍了内核模块的概念、用途,以及如何构建一个简单的“Hello World”内核模块。 \

前言

\

在这系列文章中,将介绍如何为嵌入式Linux设备编写Linux内核模块。文章将从简单的可加载内核模块(loadable kernel module,LKM)“Hello World!”开始,进而开发通过使用中断请求控制嵌入式Linux设备(如BeagleBone)通用输入输出接口(GPIO)的模块。当我确定合适的应用程序时,我会添加更多的后续文章。 \

内核模块是一个复杂的话题,需要一定的时间来完成。因此,我将内容拆分成几篇文章,每篇提供一个可以实践的示例和结果。这个话题可以写一整本书,因此很难覆盖每一个方面。关于编写内核模块的其他文章也有很多,而本文的示例都在Linux内核3.8.X以上版本构建和测试,以确保这些材料是最新且贴切的。同时,本文主要关注嵌入式系统的硬件接口。在我的书《Exploring BeagleBone》中也有相同的示例,由于本文自身包含了这些代码,读者无须拥有该书的副本。 \

550792b5d64d7a0a149ae51bb8d08d35.png

\

图1:内核空间GPIO性能 \

本文集中讨论构建和部署“Hello World!”内核模块所需的系统设置、工具和代码。本系列中的第二篇文章探讨了如何编写字符设备驱动和如何编写用户空间C/C++程序与内核空间模块进行交互。第三篇文章探讨内核空间GPIO库代码的使用,它结合了前两篇文章的内容,开发中断驱动代码,使之能够从Linux用户空间控制。例如,图1展示了示波器捕获的通过中断驱动内核模块处理按钮按下到LED亮起的图形。在常规嵌入式Linux中(即非实时Linux的变体),该代码展示忽略CPU开销后,响应时间大约为20毫秒(±5微秒)。 \

什么是内核模块

\

可加载内核模块(LKM)是Linux内核运行时加载和移除代码的机制。该机制对于设备驱动是理想的,这使得内核可以在不知道硬件如何工作的情况下和硬件进行交互。可加载内核模块的替代是将每个驱动代码构建到Linux内核中。 \

没有模块化能力,Linux内核将会变得非常大,因为它不得不支持BeagleBone开发板上所需的每个驱动。同时,在需要添加新硬件或者升级设备驱动时,必须重新构建内核。可加载内核模块功能的缺点是对于每个设备都必须维护一个驱动文件。可加载内核模块在运行时加载,他们不运行在用户空间,本质上是内核的一部分。\

8d18393b22345ff8828fb6549e88211d.png

\

图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%[===========================\u0026gt;] 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()函数。内核模块和普通应用程序的区别有: \

  • 非顺序执行:内核模块使用初始化函数将自身注册并处理请求,初始化函数运行后就结束了。内核模块处理的请求在模块代码中定义。这和常用于图形用户界面(graphical-user interface,GUI)应用的事件驱动编程模型比较类似。 \
  • 没有自动清理:任何由内核模块申请的内存,必须要模块卸载时手动释放,否则这些内存将无法使用,直到系统重启。 \
  • 不要使用printf()函数:内核代码无法访问为Linux用户空间编写的库。内核模块运行在内核空间,它有自己独立的地址空间。内核空间和用户空间的接口被清晰的定义和控制。内核模块可以通过printk()函数输出信息,这些输出可以在用户空间查看到。 \
  • 会被中断:内核模块一个概念上困难的地方在于他们可能会同时被多个程序/进程使用。构建内核模块时需要小心,以确保在发生中断的时候行为一致和正确。BeagleBone有一个单核处理器(目前为止),但是我们仍然需要考虑多进程同时访问对模块的影响。 \
  • 更高级的执行特权:通常内核模块会比用户空间程序分配更多的CPU周期。这看上去是一个优势,然而需要特别注意内核模块不会影响到系统的综合性能。 \
  • 无浮点支持:对用户空间应用,内核代码使用陷阱(trap)来实现整数到浮点模式的转换。然而在内核空间中这些陷阱难以使用。替代方案是手工保存和恢复浮点运算,这是最好的避免方式,并将处理留给用户空间代码。

以上概念有很多需要消化,重要的是,它们都被解决,但是没有都包含在第一篇文章中。列表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 \u0026lt;linux/init.h\u0026gt;             // 用于标记函数的宏,如__init、__exit\#include \u0026lt;linux/module.h\u0026gt;           // 加载内核模块到内核使用的核心头文件\#include \u0026lt;linux/kernel.h\u0026gt;           // 包含内核使用的类型、宏和函数\\MODULE_LICENSE(\"GPL\");              ///\u0026lt; 许可类型,它会影响到运行时行为\MODULE_AUTHOR(\"Derek Molloy\");      ///\u0026lt; 作者,当使用modinfo命令时可见\MODULE_DESCRIPTION(\"A simple Linux driver for the BBB.\");  ///\u0026lt; 模块描述,参见modinfo命令\MODULE_VERSION(\"0.1\");              ///\u0026lt; 模块版本\\static char *name = \"world\";        ///\u0026lt; 可加载内核模块参数示例,这里默认值设置为“world”\module_param(name, charp, S_IRUGO); ///\u0026lt; 参数描述。charp表示字符指针(char ptr),S_IRUGO表示该参数只读,无法修改\MODULE_PARM_DESC(name, \"The name to display in /var/log/kern.log\");  ///\u0026lt; 参数描述\\/** @brief 可加载内核模块初始化函数\ *  static关键字限制了该函数的可见范围为当前C文件。\ *  __init宏表示对于内置驱动(不是可加载内核模块),该函数只在初始化的时候执行,\ *  在此之后,该函数可以废弃,且内存可以被回收。\ *  @return 当执行成功返回0\ */\static int __init helloBBB_init(void){\   printk(KERN_INFO \"EBB: Hello %s from the BBB LKM!\\
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值