Linux设备驱动之第 2 章 建立和运行模块

第 2 章 建立和运行模块

    本章介绍所有的关于模块和内核编程的关键概念.建立并运行一个完整的模块, 并且查看一些被所有模块共用的基本代码.开发这样的专门技术对任何类型的模块化的驱动都是重要的基础. 本章只论及模块,不涉及任何特别的设备类型.

2.1. 设置测试系统

        建议获得一个主流内核, 直接从www.kernel.org 来获取镜像网络,并把它安装到你的系统中. 供应商的内核可能是主流内核被重重地打了补丁并且和主流内核有分歧;偶尔, 供应商的补丁可能改变了设备驱动可见的内核 API.如果在编写一个必须在特别的发布上运行的驱动,要在相应的内核上建立和测试. 但是, 处于学习驱动编写的目的, 一个标准内核是最好的.

        不管内核来源, 建立 2.6.x 的模块需要有一个配置好并建立好的内核树在你的系统中. 这个要求是从之前内核版本的改变, 之前只要有一套当前版本的头文件就足够了.2.6模块针对内核源码树里找到的目标文件连接;  结果是一个更加健壮的模块加载器,还要求那些目标文件也是可用的.因此你的第一个商业订单是具备一个内核源码树,建立一个新内核, 并且安装到你的系统.

2.2. Hello World 模块

#include <linux/init.h>
#include <linux/module.h>
//内核模块加载函数
static __init int hello_init(void)
{
printk(KERN_INFO "Hello, world\n");
return 0;
}

// 内核模块卸载函数
static __exit void hello_exit(void)
{
printk(KERN_INFO "Goodbye, hello world\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("Dual BSD/GPL");

Makefile

ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:                               
        $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:                                             
        $(MAKE) -C $(KERNELDIR) M=$(PWD) clean
else
    obj-m := hello_world.o
endif

分析:

       这个模块定义两个函数,一个在模块加载到内核时被调用( hello_init ),一个在模块被去除时被调用( hello_exit ).moudle_init 和 module_exit这两行使用内核宏来指出这两个函数的角色.另一个特别的宏 (MODULE_LICENSE) 是用来告知内核,该模块带有一个自由的许可证; 没有这样的说明, 在模块加载时内核会抱怨.

      printk 函数在 Linux 内核中定义并且对模块可用; printk与标准 C 库函数 printf 的行为相似. 内核需要它自己的打印函数, 因为它靠自己运行, 没有 C 库的帮助. 模块能够调用printk 是因为, 在 insmod 加载了它之后, 模块被连接到内核并且可存取内核的公用符号.KERN_ALERT 是消息的优先级.

        在此模块中指定一个高优先级,因为使用缺省优先级的消息可能不会在任何有用的地方显示, 这依赖于运行的内核版本, klogd 守护进程的版本, 以及你的配置. 

        用 insmod rmmod 工具来测试这个模块.注意只有超级用户可以加载和卸载模块.

总结:为使上面的模块顺序工作,必须有正确配置和建立的内核树.困难的部分是理解你的设备, 以及如何获得最高性能

2.3. 内核模块相比于应用程序

        强调一下内核模块和应用程序之间的各种不同.不同于大部分的小的和中型的应用程序从头至尾处理一个单个任务, 每个内核模块只注册自己以便来服务将来的请求, 模块初始化函数的任务是为以后调用模块的函数做准备; 好像是模块说, " 我在这里, 这是我能做的."模块的退出函数在模块被卸载时调用. 它好像告诉内核, "我不再在那里了, 不要要求我做任何事了."这种编程的方法类似于事件驱动的编程, 但是虽然不是所有的应用程序都是事件驱动的, 每个内核模块都是. 另外一个主要的不同, 在事件驱动的应用程序和内核代码之间, 是退出函数: 一个终止的应用程序可以在释放资源方面懒惰, 或者完全不做清理工作, 但是模块的退出函数必须小心恢复每个由初始化函数建立的东西, 否则会保留一些东西直到系统重启.

        偶然地, 卸载模块的能力是你将最欣赏的模块化的其中一个特色,因为它有助于减少开发时间; 可测试你的新驱动的连续的版本, 而不用每次经历漫长的关机/重启周期.

        一个应用程序可以调用它没有定义的函数:链接阶段使用合适的函数库解决了外部引用. printf 是一个这种可调用的函数并且在 libc 里面定义.一个模块, 在另一方面, 只链接到内核, 它能够调用的唯一的函数是内核输出的; 没有库来连接.

        图 2.1. 连接一个模块到内核展示函数调用和函数指针在模块中如何使用来增加新功能到一个运行中的内核.

图 2.1. 连接一个模块到内核


        因为没有库链接到模块中, 源文件不应当包含通常的头文件,<stdarg.h>和非常特殊的情况是仅有的例外.只有是内核的一部分的函数才可以在内核模块里使用.内核相关的都在头文件里声明,这些头文件在已建立和配置的内核源码树里; 大部分相关的头文件位于 include/linux 和 include/asm,但是别的 include 的子目录已经添加到关联特定内核子系统的材料里了.

        在内核编程和应用程序编程之间的重要不同是每一个环境是如何处理错误:在应用程序开发中段错误是无害的, 一个调试器常常用来追踪错误到源码中的问题, 而一个内核错误至少会杀掉当前进程, 如果不终止整个系统.

2.3.1. 用户空间和内核空间

        一个模块在内核空间运行, 而应用程序在用户空间运行. 这个概念是操作系统理论的基础.操作系统的角色, 是给程序提供一个一致的计算机硬件的视角. 另外, 操作系统对于非授权的资源存取必须承担程序的独立操作和保护.这一不平凡的任务只有 CPU 增强系统软件对应用程序的保护才有可能.

        所有的现代处理器都具备这个功能。人们选择的方法是在CPU中实现不同的操作模式(或者级别)。不同的级别具有不同的功能,在较低的级别中将禁止某些操作。程序代码只能通过有限数目的"门"来从一个级别切换到另一个级别。Unix系统设计时利用了这种硬件特性,使用两个这样的级别。当前所有的处理器都至少具有两个保护级别,而其它一些的处理器,比如x86系列,有更多的级别。当处理器存在多个级别时,Unix使用最高级别和最低级别。在Unix中,内核运行在最高级别(超级用户态),在最高级别中可进行所有操作。应用程序运行在最低级别(用户态),在最低级别中,处理器控制着对硬件的直接访问以及对内存的非授权访问。

        通常将运行模式称作内核空间和用户空间。这两个术语不仅说明两种模式具有不同的优先权等级,而且还说明每个模式都有自己的内存映射,即自己的地址空间。

        每当应用程序执行系统调用或者被硬件中断挂起时,Unix将执行模式从用户空间切换到内核空间。执行系统调用的内核代码运行在进程上下文中,它代表调用进程执行操作,因此能够访问进程地址空间的所有数据。而处理硬件中断的内核代码和进程是异步的,与任何一个特定进程无关。

        模块化代码在内核空间中运行,用于扩展内核功能。通常来讲,一个驱动程序要执行两类任务:模块中的某些函数作为系统调用的一部分执行,而其他函数负责中断处理。

2.3.2.内核中的并发

        内核编程区别于应用程序编程的地方在于对并发的处理。大部分应用程序,除多线程应用程序之外,通常顺序执行,从头到尾,不需要关心因为其它一些事情的发生会改变它们的运行环境。内核代码并不在这样一个简单世界中运行,即使最简单的内核模块,都需要在编写时铭记:同一时刻,可能会有许多事情正在发生。

        有几个方面的原因促使内核编程必须考虑并发问题。首先,Linux系统中通常正在运行多个并发进程,并且可能有多个进程同时使用某个驱动程序。其次,大多数设备能够中断处理器,而中断处理程序异步运行,而且可能在驱动程序正视图处理其它任务时被调用。另外,有一些软件抽象(内核定时器)也在异步运行着。还有,Linux还可以运行在SMP系统上,因此可能同时有几个CPU运行某个驱动程序。最后,在2.6内核代码已经是可抢占的,这意味着即使在单处理器系统上也存在许多类似多处理器系统的并发问题。

        结果,Linux内核代码必须是可重入的,它必须能够同时运行在多个上下文中。因此,内核数据结构需要仔细设计才能保证多个线程分开执行,访问共享数据的代码也必须避免破坏共享数据。要编写能够处理并发问题而同时避免竞态的代码,需要一些技巧和细致的思考。对编写正确的内核代码来说,优良的并发管理是必需的。

        驱动程序编写者所犯的一个常见错误是,认为只要某段代码没有进入睡眠状态(或者阻塞),就不会产生并发问题。即使在之前的非抢占式内核中,这种假定也是错误的。在2.6中,内核代码(几乎)始终不能假定在给定代码段中能够独占处理器。如果在编写代码时不注意并发问题,将可能导致出现很难调试的灾难性错误。

2.3.3. 当前进程

        虽然内核模块不像应用程序那样顺序执行,然而内核执行的大多数操作还是和某个特定的进程相关。内核代码可通过访问全局项current来获得当前进程。current在<asm/current.h>中定义,是一个指向struct task_struct的指针,而task_struct结构体在<linux/sched.h>文件中定义。current指针指向当前正在运行的进程。在open、read等系统调用的执行过程当中,当前进程指的是调用系统调用的进程。如果需要,内核代码可以通过current获得与当前进程相关的信息。

        实际上,与早期Linux内核版本不同,2.6中current不再是一个全局变量。为了支持SMP系统,内核开发者设计了一种能够找到运行在相关CPU上的当前进程的机制。这种机制必须是快速的,因为对current的引用会频繁发生。这样,一种不依赖于特定架构的机制通常是,将指向task_struct结构体的指针隐藏在内核栈中。这种实现的细节同样也对其它内核子系统隐藏,设备驱动程序只要包含<linux/sched.h >头文件即可引用当前进程。

2.3.4. 其它一些细节

        内核编程在许多方面区别于用户空间的编程。在深入到内核的同时,还应该时刻牢记下面讲到的这些问题。

        应用程序在虚拟内存中布局,并具有一块很大的栈空间。栈是用来保存函数调用历史以及当前活动函数中自动变量的。而相反的是,内核具有非常小的栈,它可能只和一个4096字节大小的页那样小。自己的函数必须和整个内核空间调用链一同共享这个栈。因此,声明大的自动变量并不是一个好主意,如果需要大的结构,应该在调用时动态分配该结构。

        经常会在内核API中看到具有两个下划线前缀(__)的函数名称。具有这种名称的函数通常是接口的底层组件,应该谨慎使用。实质上,双下划线告诉程序员:谨慎调用,否则后果自负。

        内核代码不能实现浮点数运算。如果打开了浮点支持,在某些架构上,需要在进入和退出内核空间保存和恢复浮点处理器的状态。这种额外的开销没有任何价值,内核代码中不需要浮点运算。

猜你喜欢

转载自blog.csdn.net/xiezhi123456/article/details/80834433