📄 (ldd) ch02-编写和运行模块(转载).txt
字号:
(LDD) Ch02-编写和运行模块(转载)
第2章 编写和运行模块
非常高兴现在终于可以开始编程了。本章将介绍模块编程和内核编程所需的所有必要的
概念。我们将要不多的篇幅来编写和运行一个完整的模块。这种专业技术(expertise)
是编写如何模块化设备驱动程序的基础。为了避免一下子给你很多概念,本章仅介绍模
块,不介绍任何类别的设备。
这里介绍的所有内核内容(函数,变量,头文件和宏)也将在本章最后的参考部分再次
介绍。
如果你已经座不住了,下面的代码是一个完整的“Hello, World”模块(这个模块事实
上并没什么功能)。它可以在Linux 2.0或以上版本上编译通过,但不能低于或等于1.2
,关于这一点本章将在稍后的部分解释*。
(代码)
函数printk是由Linux内核定义的,功能与printf相似;模块可以调用printk,这是因为
在insmod加载了模块后,模块就被连编到内核中了,也就可以调用内核的符号了。字符
串<1>是消息的优先级。我之所以在模块中使用了高优先级是因为,如果你使用的是内核
2.0.x和旧的klogd守护进程,默认优先级的消息可能不能显示在控制台上(关于这个问
题,你可以暂且忽略,我们将在第4章,“调试技术”,的“Printk”小节中详细解释)
。
通过执行insmod和rmmod命令,你可以试试这个模块,其过程如下面的屏幕输出所示。注
意,只有超级用户才能加载 托对啬 块。
(代码)
正如你所见,编写一个模块很容易。通过本章我们将深入探讨这个内容。
模块与应用程序
在深入探讨模块之前,很有必要先看一看内核模块与应用程序之间的区别。
一个应用从头到尾完成一个任务,而模块则是为以后处理某些请求而注册自己,完成这
个任务后它的“主”函数就立即中止了。换句话说就是,init_module()(模块的入口点
)的任务就是为以后调用模块的函数做准备;这就好比模块在说,“我在这,这是我能
)的任务就是为以后调用模块的函数做准备;这就好比模块在说,“我在这,这是我能
做的。”模块的第二个入口点,cleanup_module,仅当模块被下载前才被调用。它应该
跟内核说,“我不在这了,别再让我做任何事了。”能够卸载也许是你最喜爱的模块化
的特性之一,它可以让你减少开发时间;你无需每次都花很长的时间开机关机就可以测
试你的设备驱动程序。
作为一个程序员,你一定知道一个应用程序可以调用应用程序本身没有定义的函数:前
后的连编过程可以用相应的函数库解析那些外部引用。printf就是这样一个函数,它定
义在libc中。然而,内核要仅能连编到内核中,它能调用的仅是由内核开放出来的那些
函数。例如,上面的helllo.c中的printk函数就是内核版的printf,并由内核开放给模
块给使用;除了没有浮点支持外,它和原函数几乎一模一样。
如图2-1所示,它勾画了为了在运行的内核中加入新函数,是如何调用函数以及如何使用
函数指针的。
由于没有库连接到模块中,源码文件不应该模块任何常规头文件。与内核有关的所有内
容都定义在目录/usr/include/linux和/usr/include/asm下的头文件中。在编译应用程
序也会间接使用这些头文件;其中的内核代码通过#ifdef __KERNEL__保护起来。这两个
内核头文件目录通常都是到内核源码所在位置的符号连接。如果你根本就想要整个内核
源码,你至少还要这两个目录的头文件。在比较新的内核中,你还可以在内核源码中发
现net和scsi头文件目录,但很少有模块会需要这两个目录。
内核头文件的作用将稍后需要它们的地方再做介绍,
内核头文件的作用将稍后需要它们的地方再做介绍,
内核模块与应用程序的另一个区别是,你得小心“名字空间污染”问题。程序员在写小
程序时,往往不注意程序的名字空间,但当这些小程序成为大程序的一部分时就会造成
许多问题了。名字空间污染是指当存在很多函数和全局变量时,它们的名字已不再富有
足够的意义来很容易的区分彼此的问题。不得不处理这种应用程序的程序员必须花很大
的精力来单单记住这些“保留”名,并为新符号寻找新的唯一的名字。如果在写内核代
码时出现这样的错误,这对我们来说是无法忍受的,因为即便最小的模块也要连编到整
个内核中。防止名字空间污染的最佳方法是把所有你自己的符号都声明为static的,而
且给所有的全局量加一个well-defined前缀。此外,你还可以通过声明一个符号表来避
免使用static声明,这些内容将在本章的“注册符号表”小节中介绍。即便是模块内的
私有符号也最好使用选定的前缀,这样有时会减轻调试的工作。通常,内核中使用的前
缀都是小写的,今后我们将贯彻这一约定。
内核编程和应用程序编程的最后一个区别是如何处理失效:在应用程序开发期间,段违
例是无害的,利用调试器可以轻松地跟踪到引起问题的错误之处,然而内核失效却是致
命的,如果不是整个系统,至少对于当前进程是这样的。我们将在第4章“调试系统失效
”小节中介绍如何跟踪内核错误。
用户空间和内核空间
本节的讨论概而言之就是,模块是在所谓的“内核空间”中运行的,而应用程序则是在
“用户空间”中运行的。这些都是操作系统理论的最基本概念。
事实上,操作系统的作用就是给程序提供一个计算机硬件的一致的视图。此外,操作系
统处理程序的独立操作,并防止对资源的未经授权的访问。当且仅当CPU可以实现防止系
统软件免受应用软件干扰的保护机制,这些不同寻常的工作才有可能实现。
每种现代处理器都能实现这种功能。人们选择的方案是在CPU内部实现不同的操作模式(
或级)。不同的级有不同的作用,而且某些操作不允许在最低级使用;程序代码仅能通
过有限数目的“门”从一个级切换到另一个级。Unix系统就是充分利用这一硬件特性设
计而成的,但它只使用了两级(与此不同,例如,Intel处理器就有四级)。在Unix系统
中,内核在最高级执行(也称为“管理员态”),在这一级任何操作就可以,而应用程
序则执行在最低级(所谓的“用户态”),在这一级处理器禁止对硬件的直接访问和对
内存的未授权访问。
正如前面所述,在谈到软件时,我们通常称执行态为“内核空间”和“用户空间”,它
们分别引用不同的内存映射,也就是程序代码使用不同的“地址空间”。
Unix通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。执行系统调用的
内核代码在进程的上下文上执行――它代表调用进程操作而且可以访问进程地址空间的
数据。但与此不同,处理中断的代码相对进程而言是异步的,而且与任何一个进程都无
关。
模块的功能就是扩展内核的功能;运行在内核中的模块化的代码。通常,一个设备驱动
程序完成上面概括的两个任务:模块的某些函数做为系统调用执行,而某些函数则负责
事实上,操作系统的作用就是给程序提供一个计算机硬件的一致的视图。此外,操作系
统处理程序的独立操作,并防止对资源的未经授权的访问。当且仅当CPU可以实现防止系
统软件免受应用软件干扰的保护机制,这些不同寻常的工作才有可能实现。
每种现代处理器都能实现这种功能。人们选择的方案是在CPU内部实现不同的操作模式(
或级)。不同的级有不同的作用,而且某些操作不允许在最低级使用;程序代码仅能通
过有限数目的“门”从一个级切换到另一个级。Unix系统就是充分利用这一硬件特性设
计而成的,但它只使用了两级(与此不同,例如,Intel处理器就有四级)。在Unix系统
中,内核在最高级执行(也称为“管理员态”),在这一级任何操作就可以,而应用程
序则执行在最低级(所谓的“用户态”),在这一级处理器禁止对硬件的直接访问和对
内存的未授权访问。
正如前面所述,在谈到软件时,我们通常称执行态为“内核空间”和“用户空间”,它
们分别引用不同的内存映射,也就是程序代码使用不同的“地址空间”。
Unix通过系统调用和硬件中断完成从用户空间到内核空间的控制转移。执行系统调用的
内核代码在进程的上下文上执行――它代表调用进程操作而且可以访问进程地址空间的
数据。但与此不同,处理中断的代码相对进程而言是异步的,而且与任何一个进程都无
关。
模块的功能就是扩展内核的功能;运行在内核中的模块化的代码。通常,一个设备驱动
程序完成上面概括的两个任务:模块的某些函数做为系统调用执行,而某些函数则负责
程序完成上面概括的两个任务:模块的某些函数做为系统调用执行,而某些函数则负责
处理中断。
内核中的并发
内核编程新手首先要问的问题之一就是多任务是如何管理的。事实上,除了调度器之外
,关于多任务并没有什么可以多说的,而且调度器也超出了程序员的一般活动范围。你
可能会遇到这些任务,除了掌握如下这些原则外,模块编写者无需了解多任务。
与串行的应用程序不同,内核是异步工作的,代表进程执行系统调用。内核负责输入/输
出以及系统内对每一个进程的资源管理。
内核(和模块)函数完全在一个线程中执行,除非它们要“睡眠”,否则通常都是在单
个进程的上下文中执行――设备驱动程序应该能够通过交织不同任务的执行来支持并发
。例如,设备可能由两个不同的进程同时读取。设备驱动程序串行地响应若干read调用
,每一个都属于不同的进程。由于代码需要区别不同的数据流,内核(以及设备驱动程
序)必须维护内部数据结构以区分不同的操作。这与一个学生学习交织在一起的若干门
课程并非不无相似之处:每门课都有一个不同的笔记本。解决多个访问问题的另一个方
法就是避免它,禁止对设备的并发访问,但这种怠惰的技术根本不值的讨论。
当内核代码运行时,上下文切换不可能无意间发生,所以设备驱动程序无需是可重入的
,除非它自己会调用schedule。必须等待数据的函数可以调用sleep_on,这个函数接着
又调用schedule。不过你必须要小心,存在某些函数会无意导致睡眠,特别是任何对用
户空间的访问。利用“天然非抢占”特性不是什么好的方法。我将在第5章,“字符设备
户空间的访问。利用“天然非抢占”特性不是什么好的方法。我将在第5章,“字符设备
驱动程序的扩展操作”的“编写可重入代码”小节中讲解可重入函数。
就对设备驱动程序的多个访问而言,有许多不同的途径来分离这些不同的访问,但都是
依赖于任务相关的数据。这种数据可以是全局内核变量或是传给设备驱动程序函数的进
程相关参数。最重要的用来跟踪进程的全局变量是current:一个指向struct
task_struct结构的指针,在<linux/sched.h>中定义。current指针指向当前正在运行的
用户进程。在系统调用执行期间,如open或read,当前进程就是调用这个调用的进程*。
如果需要的话,内核代码就可以利用current使用进程相关信息。第5章“设备文件的访
问控制”小节中就有使用这种技术的例子。
编译器就象外部引用printk一样处理current。模块可以在任何需要的地方引用current
,insmod会在加载时解析出所有对它的引用。例如,如下语句通过访问struct
task_struct中的某些域打印当前进程的进程ID和命令名:
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -