Linux内核模块编程指南


下载 第1章 Hello, World 如果第一个程序员是一个山顶洞人,它在山洞壁 (第一台计算机)上凿出的第一个程序应该 是用羚羊图案构成的一个字符串“ Hello, Wo r l d”。罗马的编程教科书也应该是以程序“ S a l u t , M u n d i”开始的。我不知道如果打破这个传统会带来什么后果,至少我还没有勇气去做第一个 吃螃蟹的人。 内核模块至少必须有两个函数: i n i t _ m o d u l e和c l e a n u p _ m o d u l e。第一个函数是在把模块插 入内核时调用的;第二个函数则在删除该模块时调用。一般来说, i n i t _ m o d u l e可以为内核的 某些东西注册一个处理程序,或者也可以用自身的代码来取代某个内核函数 (通常是先干点别 的什么事,然后再调用原来的函数 )。函数c l e a n u p _ m o d u l e的任务是清除掉 i n i t _ m o d u l e所做的 一切,这样,这个模块就可以安全地卸载了。 1.1 内核模块的Makefiles文件 内核模块并不是一个独立的可执行文件,而是一个对象文件,在运行时内核模块被链接 到内核中。因此,应该使用 - c命令参数来编译它们。还有一点需要注意,在编译所有内核模 块时,都将需要定义好某些特定的符号。 • _ _KERNEL_ _— 这个符号告诉头文件:这个程序代码将在内核模式下运行,而不要 作为用户进程的一部分来执行。 • MODULE— 这个符号告诉头文件向内核模块提供正确的定义。 • L I N U X— 从技术的角度讲,这个符号不是必需的。然而,如果程序员想要编写一个重 要的内核模块,而且这个内核模块需要在多个操作系统上编译,在这种情况下,程序员 将会很高兴自己定义了L I N U X这个符号。这样一来,在那些依赖于操作系统的部分,这 个符号就可以提供条件编译了。 还有其它的一些符号,是否包含它们要取决于在编译内核时使用了哪些命令参数。如果 用户不太清楚内核是怎样编译的,可以查看文件 / u s r / i n c l u d e / l i n u x / c o n f i g . h。 • _ _SMP_ _— 对称多处理。如果编译内核的目的是为了支持对称多处理,在编译时就 需要定义这个符号 (即使内核只是在一个 C P U上运行也需要定义它 )。当然,如果用户使 用对称多处理,那么还需要完成其它一些任务 (参见第1 2章)。 • C O N F I G _ M O D V E R S I O N S— 如果C O N F I G _ M O D V E R S I O N S可用,那么在编译内核模 块时就需要定义它,并且包含头文件 / u s r / i n c l u d e / l i n u x / m o d v e r s i o n s . h。还可以用代码自 身来完成这个任务。 完成了以上这些任务以后,剩下唯一要做的事就是切换到根用户下 (你不是以r o o t身份编 译内核模块的吧?别玩什么惊险动作哟! ),然后根据自己的需要插入或删除 h e l l o模块。在执 行完i n s m o d命令以后,可以看到新的内核模块在 / p r o c / m o d u l e s中。 顺便提一下,M a k e f i l e建议用户不要从X执行i n s m o d命令的原因在于,当内核有个消息需 要使用p r i n t k命令打印出来时,内核会把该消息发送给控制台。当用户没有使用 X时,该消息 146 第二部分 Linux 内核模块编程指南 下载 将发送到用户正在使用的虚拟终端 (用户可以用A l t - F < n >来选择当前终端),然后用户就可以看 到这个消息了。而另一方面,当用户使用 X时,存在两种可能性。一种情况是用户用命令 xterm -C打开了一个控制台,这时输出将被发送到那个控制台;另一种情况是用户没有打开控 制台,这时输出将送往虚拟终端 7— 被X所“覆盖”的一个虚拟终端。 当用户的内核不太稳定时,没有使用 X的用户更有可能取得调试信息。如果没有使用 X, p r i n t k将直接从内核把调试消息发送到控制台。而另一方面,在X中p r i n t k的消息将被送给一个用 户模式的进程(xterm -C)。当那个进程获得C P U时间时,它将把该消息传送给X服务器进程。然后, 当X服务器获得C P U时间时,它将显示该消息— 但是一个不稳定的内核通常意味着系统将要崩 溃或者重新启动,所以用户不希望推迟错误信息显示的时间,因为该信息可能会向用户解释什 么地方出了问题,如果显示的时刻晚于系统崩溃或重启的时刻,用户将会错过这个重要的信息。 1.2 多重文件内核模块 有时候在多个源文件间划分内核模块是很有意义的。这时用户需要完成下面三件任务: 1) 除了一个源文件以外,在其它所有源文件中加入一行 #define _ _ NO_VERSION_ _。这 点很重要,因为m o d u l e . h中通常会包含有k e r n e l _ v e r s i o n的定义( k e r n e l _ v e r s i o n是一个全局变量, 它表明该模块是为哪个内核版本所编译的 )。如果用户需要v e r s i o n . h文件,那么用户必须自己 把它包含在源文件中,因为在定义了 _ _NO_VERSION_ _的情况下,m o d u l e . h是不会为用户 完成这个任务的。 2) 像平常一样编译所有的源文件。 3) 把所有的对象文件组合进一个文件中。在 x 8 6下,可以使用命令: ld -m elf_i386 -r -o〈模块名称〉.o (第一个源文件).o (第二个源文件) . o来完成这个任务。 下面是这种内核模块的一个例子。 第1章 Hello,World 147下载 148 第二部分 Linux 内核模块编程指南 下载 下载 第2章 字符设备文件 我们现在就可以吹牛说自己是内核程序员了。虽然我们所写的内核模块还什么也干不了, 但我们仍然为自己感到骄傲,简直可以称得上趾高气扬。但是,有时候在某种程度上我们也 会感到缺少点什么,简单的模块并不是太有趣。 内核模块主要通过两种方法与进程打交道。一种方法是通过设备文件 (例如在目录/ d e v中 的文件),另一种方法是使用 p r o c文件系统。既然编写内核模块的主要原因之一就是支持某些 类型的硬件设备,那么就让我们从设备文件开始吧。 设备文件最初的用途是使进程与内核中的设备驱动程序通信,并且通过设备驱动程序再 与物理设备(调制解调器、终端等等)通信。下面我们要讲述实现这一任务的方法。 每个设备驱动程序都被赋予一个主编号,主要用于负责某几种类型的硬件。可以在 / p r o c / d e v i c e s中找到驱动程序以及它们对应的主编号的列表。由设备驱动程序管理的每个物理 设备都被赋予一个从编号。这些设备中的每一个,不管是否真正安装在计算机系统上,都将 对应一个特殊的文件,该文件称为设备文件,所有的设备文件都包含在目录 / d e v中。 例如,如果执行命令 ls -l /dev/hd[ab]*,用户将可以看到与一个计算机相连接的所有的 I D E硬件分区。注意,所有的这些硬盘分区都使用同一个主编号: 3,但是从编号却各不相同。 需要强调的是,这里假设用户使用的是 P C体系结构。我并不知道在其它体系结构上运行的 L i n u x的设备是怎么样的。 在安装了系统以后,所有的设备文件都由命令 m k n o d创建出来。从技术的角度上讲,并 没有什么特别的原因一定要把这些设备文件放在目录 / d e v中,这只不过是一个有用的传统习 惯而已。如果读者创建设备文件的目的只不过是为了试试看,就像本章的练习一样,那么把 该设备文件放置在编译内核模块的目录中可能会更有意义一些。 设备一般分为两种类型:字符设备和块设备。它们的区别在于块设备具有一个请求缓冲 区,所以块设备可以选择按照何种顺序来响应这些请求。这对于存储设备来说是很重要的。 在存储设备中,读或写相邻的扇区速度要快一些,而读写相互之间离得较远的扇区则要慢得 多。另一个区别在于块设备只能以成块的形式接收输入和返回输出 (块的大小根据设备类型的 变化而有所不同),而字符设备则可以随心所欲地使用任意数目的字节。当前大多数设备都是 字符设备,因为它们既不需要某种形式的缓冲,也不需要按照固定的块大小来进行操作。如 果想知道某个设备文件对应的是块设备还是字符设备,用户可以执行命令 ls -l,查看一下该命 令的输出中的第一个字符,如果第一个字符是“ b”,则对应的是块设备;如果是“ c”,则对 应的是字符设备。 模块分为两个独立的部分:模块部分和设备驱动程序部分。前者用于注册设备。函数 i n i t _ m o d u l e调用m o d u l e _ r e g i s t e r _ c h r d e v,把该设备驱动程序加入到内核的字符设备驱动程序 表中,它还会返回供驱动程序所使用的主编号。函数 c l e a n u p _ m o d u l e则取消该设备的注册。 注册某设备和取消它的注册是这两个函数最基本的功能。内核中的东西并不是按照它们 自己的意愿主动开始运行的,就像进程一样,而是由进程通过系统调用来调用,或者由硬件 设备通过中断来调用,或者由内核的其它部分调用 (只需调用特定的函数),它们才会执行。因 此,如果用户往内核中加入了代码,就必须把它作为某种特定类型事件的处理程序进行注 册;而在删除这些代码时,用户必须取消它的注册。 设备驱动程序一般是由四个 d e v i c e _ < a c t i o n >函数所组成的,如果用户需要处理具有对应 主编号的设备文件,就可以调用这四个函数。通过 f i l e _ o p e r a t i o n s结构F o p s内核可以知道调用 哪些函数。因为该结构的值是在注册设备时给定的,它包含了指向这四个函数的指针。 在这里我们还需要记住的一点是:无论如何不能乱删内核模块。原因在于如果设备文件 是由进程打开的,而我们删去了该内核模块,那么使用该文件就将导致对正确的函数 (读/写) 原来所处的存储位置的调用。如果我们走运,那里没有装入什么其它的代码,那我们至多得 到一些难看的错误信息,而如果我们不走运,在原来的同一位置已经装入了另一个内核模块, 这就意味着跳转到了内核中另一个函数的中间,这样做的后果是不堪设想的,起码不会是令 人愉快的。 一般来说,如果用户不愿意让某件事发生,可以让执行这件事的函数返回一个错误代码 (一个负数)。而对c l e a n u p _ m o d u l e来说这是不可能的,因为它是一个 v o i d函数。一旦调用了 c l e a n u p _ m o d u l e,这个模块就死了。然而,还有一个计数器记录了有多少个其它的内核模块 正在使用该内核模块,这个计数器称为引用计数器 (就是位于文件/ p r o c / m o d u l e s信息行中的最 后那个数值 )。如果这个数值不为零, r m m o d将失败。模块的引用计数值可以从变量 m o d _ u s e _ c o u n t _ 中 得 到 。 因 为 有 些 宏 是 专 门 为 处 理 这 个 变 量 而 定 义 的 ( 如 M O D _ I N C _ U S E _ C O U N T和M O D _ D E C _ U S E _ C O U N T ),我们宁愿使用这些宏,也不愿直接对 m o d _ u s e _ c o u n t _进行操作,这样一来,如果将来实现方法有所变化,我们也会很安全。 150 第二部分 Linux 内核模块编程指南 下载 第2章 字符设备文件 151下载 152 第二部分 Linux 内核模块编程指南 下载 第2章 字符设备文件 153下载 154 第二部分 Linux 内核模块编程指南 下载 第2章 字符设备文件 155下载 多内核版本源文件 系统调用是内核提供给进程的主要接口,它并不随着内核版本的变化而有所改变。当然 156 第二部分 Linux 内核模块编程指南 下载 可能会加入新的系统调用,但是老的系统调用是永远不会改变的。这主要是为了提供向后兼 容性的需要— 新的内核版本不应该使原来工作正常的进程出现问题。在大多数情况下,设 备文件也应该保持不变。另一方面,内核里面的内部接口则可以变化,并且也确实随着内核 版本的变化而改变了。 L i n u x内核的版本可以划分为稳定版本 ( n . <偶数> . m )和开发版本( n . <奇数> . m )两种。开发版 本中包括所有最新最酷的思想,当然其中也可能有一些将来会被认为是馊主意的错误,有些 将会在下一个版本中重新实现。因此,用户不能认为在这些版本之间接口也会保持一致 (这就 是我为什么懒得在本书中介绍它们的原因,这需要做大量的工作,而且很快就会过时被淘汰 )。 另一方面,在稳定的版本中,我们可以无视错误修正版本 (带数字m的版本)而认为接口是保持 不变的。 这个M P G版本包含对L i n u x内核版本2 . 0 . x和版本2 . 2 . x的支持。因为这两个版本之间存在差 异,这就要求用户根据内核版本号来进行条件编译。为了做到这一点,可以使用宏 L I N U X _ V E R S I O N _ C O D E。在内核版本a . b . c中,该宏的值将会是21 6a + 28b + c。为了获取特定内 核版本的值,我们可以使用宏 K E R N E L _ V E R S I O N。在2 . 0 . 3 5中该宏没有定义,如果需要的话 我们可以自己定义它。 第2章 字符设备文件 157下载 下载 第3章 /proc文件系统 在L i n u x中,内核和内核模块还可以通过另一种方法把信息发送给进程,这种方法就是 /proc 文件系统。最初/proc 文件系统是为了可以轻松访问有关进程的信息而设计的 (这就是它 的名称的由来),现在每一个内核部分只要有些信息需要报告,都可以使用 /proc 文件系统,例 如/proc /modules包含一个模块的列表,/proc /meminfo包含有关内存使用的统计信息。 使用/proc 文件系统的方法其实与使用设备驱动程序的方法是非常类似的— 用户需要创 建一个结构,该结构包含了 / p r o c文件所需要的所有信息,包括指向任意处理程序函数的指针 (在我们本章的例子中只有一个处理程序函数,当有人试图读 / p r o c文件时将调用这个函数 )。 然后,i n i t _ m o d u l e将向内核注册这个结构,而 c l e a n u p _ m o d u l e将取消它的注册。 在程序中之所以需要使用 p r o c _ r e g i s t e r _ d y n a m i c,是因为我们不想事先判断文件所使用的 索引节点编号,而是让内核去决定,这样可以避免编号冲突。一般文件系统都是位于磁盘上 的,而不是仅仅存在于内存中 ( / p r o c是存在于内存中的 ),在那种情况下,索引节点编号是一 个指向某个磁盘位置的指针,在那个位置上存放了该文件的索引节点 (简写为i n o d e )。索引节 点包含该文件的有关信息,例如文件的访问权限,以及指向某个或者某些磁盘位置的指针, 在这个或者这些磁盘位置中,存放着文件的数据。 因为在文件打开或者关闭时该模块不会被调用,所以我们无需在该模块中使用 M O D _ I N C _ U S E _ C O U N T和M O D _ D E C _ U S E _ C O U N T。如果文件打开以后模块被删除了,没 有任何措施可以避免这一后果。在下一章中,我们将学习到一种比较难于实现,但却相对方 便的方法,可以用于处理/ p r o c文件,我们也可以使用那个方法来防止这个问题。 第3章 /proc文件系统 159下载 160 第二部分 Linux 内核模块编程指南 下载 第3章 /proc文件系统 161下载 下载 第4章 把/proc用于输入 到目前为止,可以通过两种方法从内核模块产生输出:我们可以注册一个设备驱动程序, 并使用m k n o d命令创建一个设备文件;我们还可以创建一个 / p r o c文件。这样内核模块就可以 告诉我们各种各样的信息。现在唯一的问题是我们没有办法来回答它。如果要把输入信息发 送给内核模块,第一个方法就是把这些信息写回到 / p r o c文件中。 因为编写p r o c文件系统的主要目的是为了让内核可以把它的状态报告给进程,对于输入 并没有提供相应的特别措施。结构 p r o c _ d i r _ e n t r y中并不包含指向输入函数的指针,它只包含 指向输出函数的指针。如果需要输入,为了把信息写到 / p r o c文件中,用户需要使用标准的文 件系统机制。 L i n u x为文件系统注册提供了一个标准的机制。因为每个文件系统都必须具有自己的函数 专门用于处于索引节点和文件操作,所以 L i n u x提供了一个特殊的结构i n o d e _ o p e r a t i o n s,该结 构存放指向所有这些函数的指针,其中包含一个指向结构 f i l e _ o p e r a t i o n s的指针。在/ p r o c中, 无论何时注册一个新文件,用户都可以指定使用哪个 i n o d e _ o p e r a t i o n s结构来访问它。这就是 我们所使用的机制。结构 i n o d e _ o p e r a t i o n s包含指向结构 f i l e _ o p e r a t i o n s的指针,而结构 f i l e _ o p e r a t i o n s又包含指向m o d u l e _ i n p u t和m o d u l e _ o u t p u t函数的指针。 注意 在内核中读和写的标准角色是互换的,读函数用于输出,而写函数则用于输入, 记住这点很重要。之所以会这样,是因为读和写实际上是站在用户的观点来说的—如 果一个进程从内核读信息,内核需要做的是输出这些信息,而如果进程向内核写信息, 内核当然会把它当作输入来接收。 这里另外一个有趣的地方是 m o d u l e _ p e r m i s s i o n函数。只要进程试图对/ p r o c文件干点什么, 这个函数就将被调用,它可以判断是允许对文件进行访问,还是拒绝这次访问。目前这种判 断还只是基于操作本身以及当前所使用的 u i d来作出(当前所使用的 u i d可以从 c u r r e n t得到, c u r r e n t是一个指针,指向包含有关当前运行进程的信息结构 ),但是函数m o d u l e _ p e r m i s s i o n还 可以基于用户所选择的任意条件来作出允许或是拒绝访问的判断,例如其它还有什么进程正 在使用这个文件、日期和时间、或者我们最近接收到的输入。 在程序中我们之所以使用p u t _ u s e r和g e t _ u s e r,主要是因为L i n u x内存是分段的(在I n t e l体系 结构下;有些其它的处理器可能会有所不同 )。这就意味着一个指针并不能指向内存中的某个 唯一的位置,而只能指向一个内存段。为了能够使用指针,用户必须知道它指向的是哪个内 存段。内核只对应一个内存段,且每个进程都对应一个内存段。 进程所能访问的唯一的内存段就是它自己的内存段,所以在写将要当作进程来运行的常 规程序时,程序员不需要考虑有关分段的问题,而当用户编写内核模块时,通常用户需要访 问内核的内存段,该内存段是由系统自动处理的。然而,如果内存缓冲区中的内容需要在当 前运行的进程和内核之间传送,内核函数将会接收到一个指针,该指针指向进程段中的内存 缓冲区。宏p u t _ u s e r和g e t _ u s e r使用户可以访问那块内存。 第4章 把 /proc用于输入 163下载 164 第二部分 Linux 内核模块编程指南 下载 第4章 把 /proc用于输入 165下载 166 第二部分 Linux 内核模块编程指南 下载 第4章 把 /proc用于输入 167下载 168 第二部分 Linux 内核模块编程指南 下载 第4章 把 /proc用于输入 169下载 下载 第5章 把设备文件用于输入 设备文件一般代表物理设备,而大多数物理设备既可用于输出,也可用于输入,所以 L i n u x必须提供一些机制,以便内核中的设备驱动程序可以从进程获得输出信息,并把它发送 到设备。要做到这一点,可以为输出打开设备文件,并且向它写信息,就像写普通的文件一 样。在下面的例子中,这些任务是由 d e v i c e _ w r i t e完成的。 当然仅有上面这种方法是不够的。假设用户有一个与调制解调器相连接的串行口 (即使用 户拥有的是一个内置式的调制解调器,从 C P U角度来看,它还是由一个与调制解调器相连的 串行口来实现的,所以这样的用户也无需过多地苛求自己的想象力 )。用户将会做的最自然的 事情就是使用设备文件来把信息写到调制解调器 (调制解调器命令或者数据将会通过电话线来 传送),并且利用设备文件从调制解调器读信息 (命令的响应或者数据也是通过电话线接收的 )。 然而,这会带来一个很明显的问题:如果用户需要与串行口本身交换信息的话,用户该怎么 办?例如用户可能发送有关数据发送和接收的速率的值。 在U n i x中,可以使用一个称为 i o c t l的特殊函数来解决这个问题 ( i o c t l是输入输出控制的英 文缩写)。每个设备都有属于自己的 i o c t l命令,可以是读 i o c t l (从进程把信息发送到内核 )、写 i o c t l (把信息返回到进程)、都有或者都没有。调用 i o c t l函数必须带上三个参数:适当的设备文 件的文件描述符, i o c t l编号以及另外一个长整型的参数,用户可以使用这个长整型参数来传 送任何信息。 i o c t l编号是由主设备编号、 i o c t l类型、命令以及参数的类型这几者编码而成。这个 i o c t l编 号通常是由一个头文件中的宏调用 (取决于类型的不同,可以是 _ I O、_ I O R、_ I O W或者 _ I O W R )来创建的。然后,将要使用 i o c t l的程序以及内核模块都必须通过 # i n c l u d e命令包含这 个头文件。前者包含这个头文件是为了生成适当的 i o c t l,而后者是为了能理解它。在下面的 例子中,头文件的名称是c h a r d e v. h,而使用它的程序是i o c t l . c。 如果用户希望在自己的内核模块中使用 i o c t l,最好是接受正式的i o c t l的约定,这样如果偶 尔得到别人的i o c t l,或者如果别人获得了你的 i o c t l,你可以知道那些地方出现了错误。如果读 者想知道更多的信息,可以查询 Documentation/ioctl -number. t x t下的内核源代码树。 第5章 把设备文件用于输入 171下载 172 第二部分 Linux 内核模块编程指南 下载 第5章 把设备文件用于输入 173下载 174 第二部分 Linux 内核模块编程指南 下载 第5章 把设备文件用于输入 175下载 176 第二部分 Linux 内核模块编程指南 下载 第5章 把设备文件用于输入 177下载 178 第二部分 Linux 内核模块编程指南 下载 第5章 把设备文件用于输入 179下载 180 第二部分 Linux 内核模块编程指南 下载 第5章 把设备文件用于输入 181下载 下载 第6章 启 动 参 数 在前面所给出的许多例子中,我们不得不把一些东西硬塞进内核模块中,如 / p r o c文件的 文件名或者设备的主设备编号,这样我们就可以使用该设备的 i o c t l命令。但这是与 U n i x和 L i n u x的宗旨背道而驰的,U n i x和L i n u x提倡编写用户所习惯的易于使用的程序。 在程序或者内核模块开始工作之前,如果希望告诉它一些它所需要的信息,可以使用命 令行参数。如果是内核模块,我们不需要使用 a rg c和a rg v— 相反,我们还有更好的选择。我 们可以在内核模块中定义一些全局变量,然后使用 i n s m o d命令,它将替我们给这些变量赋值。 在下面这个内核模块中,我们定义了两个全局变量: s t r 1和s t r 2。用户所需做的全部工作 就是编译该内核模块,然后运行 insmod str1=xxx str2=yyy。当调用i n i t _ m o d u l e时,s t r 1将指向 字符串“x x x”,而s t r 2将指向字符串“y y y”。 在版本2中,对这些参数不进行类型检查。如果 s t r 1或者s t r 2的第一个字符是一个数字,则 内核将用该整数的值填充这个变量,而不会用指向字符串的指针去填充它。如果用户对此不 太确定,那么就必须亲自去检查一下。 而另一方面,在版本2 . 2中,用户使用宏M A C R O _ PA R M告诉i n s m o d自己希望参数、它的名称 以及类型是什么样的。这就解决了类型问题,并且允许内核模块接受那些以数字开头的字符串。 第6章 启 动 参 数 183下载 184 第二部分 Linux 内核模块编程指南 下载 下载 第7章 系 统 调 用 到现在为止,我们所做过的唯一的工作就是使用一个定义好的内核机制来注册 / p r o c文件 和设备处理程序。如果用户仅仅希望做一个内核程序员份内的工作,例如编写设备驱动程序, 那么以前我们所学的知识已经足够了。但是如果用户想做一些不平凡的事,比如在某些方面, 在某种程度上改变一下系统的行为,那应该怎么办呢?答案是,几乎全部要靠自己。 这就是内核编程之所以危险的原因。在编写下面的例题时,我关掉了系统调用 o p e n。这 将意味着我不能打开任何文件,不能运行任何程序,甚至不能关闭计算机。我只能把电源开 关拔掉。幸运的是,我没有删除掉任何文件。为了保证自己也不丢失任何文件,请读者在执 行i n s m o d和r m m o d之前先运行s y n c。 现在让我们忘记 / p r o c文件,忘记设备文件,他们只不过是无关大雅的细节问题。实现内 核通信机制的“真正”进程是系统调用,它是被所有进程所使用的进程。当某进程向内核请 求服务时(例如打开文件、产生一个新进程、或者请求更多的内存 ),它所使用的机制就是系统 调用。如果用户希望以一种有趣的方式改变内核的行为,也需要依靠系统调用。顺便说一下, 如果用户想知道程序使用的是哪个系统调用,可以运行命令 strace 。 一般来说,进程是不能访问内核的。它不能访问内核存储,它也不能调用内核函数。 C P U的硬件保证了这一点(那就是为什么称之为“保护模式”的原因 )。系统调用是这条通用规 则的一个特例。在进行系统调用时,进程以适当的值填充注册程序,然后调用一条特殊的指 令,而这条指令是跳转到以前定义好的内核中的某个位置 (当然,用户进程可以读那个位置, 但却不能对它进行写的操作 )。在Intel CPU下,以上任务是通过中断0 x 8 0来完成的。硬件知道 一旦跳转到这个位置,用户的进程就不再是在受限制的用户模式下运行了,而是作为操作系 统内核来运行— 于是用户就被允许干所有他想干的事。 进程可以跳转到的那个内核中的位置称为 s y s t e m _ c a l l。那个位置上的过程检查系统调用 编号(系统调用编号可以告诉内核进程所请求的是什么服务 )。然后,该过程查看系统调用表 ( s y s _ c a l l _ t a b l e ),找出想要调用的内核函数的地址,然后调用那个函数。在函数返回之后,该 过程还要做一些系统检查工作,然后再返回到进程 (或者如果进程时间已用完,则返回到另一 个进程)。如果读者想读这段代码,可以查看源文件 a r c h / < a r c h i t e c t u r e > / k e r n e l / e n t r y. S,它就在 E N T RY ( s y s t e m _ c a l l )那一行的后面。 这样看来,如果我们想要改变某个系统调用的工作方式,我们需要编写自己的函数来实 现它(通常是加入一点自己的代码,然后再调用原来的函数 ),然后改变s y s _ c a l l _ t a b l e表中的指 针,使它指向我们的函数。因为我们的函数将来可能会被删除掉,而我们不想使系统处于一 个不稳定的状态,所以必须用 c l e a n u p _ m o d u l e使s y s _ c a l l _ t a b l e表恢复到它的原始状态,这是很 重要的。 本章的源代码就是这样一个内核模块的例子。我们想要“侦听”某个特定的用户。无论 何时,只要那个用户一打开某个文件,程序就会用 p r i n t k打印出一个消息。为了做到这一点, 我们用自己的函数代替了用来打开文件的那个系统调用。我们的函数称为 o u r _ s y s _ o p e n。该函 数查看当前进程的 u i d (用户的I D ),如果它就是我们想要侦听的 u i d,它就调用p r i n t k,显示出 将要打开的文件的名称。接下来,不管当前进程的 u i d是不是想要侦听的u i d,该函数都使用同 样的参数调用原来的o p e n函数,真正地打开那个文件。 i n i t _ m o d u l e函数替换s y s _ c a l l _ t a b l e表中相应的位置,并把原来的指针存放在一个变量 中;而c l e a n u p _ m o d u l e函数则使用那个变量把一切都恢复成原来正常的状态。这种方法是具 有一定的危险性的,因为可能有两个内核模块都修改同一个系统调用。现在假设有两个内核 模块A和B。A模块打开文件的系统调用是 A _ o p e n,而B的系统调用是 B _ o p e n。当把A插入到 内核中时,系统调用被换成了 A _ o p e n,它在被调用时将会调用原来的 s y s _ o p e n。接下来,当B 被插入到内核中时,系统调用将被替换成 B _ o p e n,它在被调用时,将会调用它自以为是原始 系统调用的那个系统调用,即 A _ o p e n。 现在假设B先被删除,那么一切都将正常— 系统调用将被恢复成 A _ o p e n,而A _ o p e n会 调用原始的系统调用。然而,如果 A先被删除然后B被删除,系统将会崩溃。删除 A时系统调 用被恢复成原始的 sys_open, B_open将被忽略。接着,当删除 B时,B将把系统调用恢复成它 自认为是原始的系统调用,即 A _ o p e n,而A _ o p e n已经不再位于内存中了。乍一看上去,好象 用户可以检查系统调用是不是打开文件函数,如果是就不做任何修改 (这样在删除B时它就不 会改变系统调用 ),似乎这样可以避免问题的发生。但这样做将会导致一个更为严重的问题。 当A被删除时,它看到系统调用已经被改变为 B _ o p e n,而不再指向A _ o p e n,所以A在从内存 中删除前不会将系统调用恢复为 s y s _ o p e n。不幸的是, B _ o p e n仍将试图调用 A _ o p e n,而 A _ o p e n已不在内存中了,这样,甚至还不到删除 B时系统就将崩溃。 我认为有两种方法可以解决这个问题。第一个方法是把系统调用恢复为原始的值: s y s _ o p e n。不幸的是,s y s _ o p e n不是/ p r o c / k s y m s中内核系统表的一部分,所以我们不能访问它。 另一个方法是一旦装入了模块,马上设立一个引用计数器,以防止根用户把它 r m m o d掉。对 于产品模块来说,这样做是很好的,但却不适合于作为教学的例子— 这就是我没有在这里 实现它的原因。 186 第二部分 Linux 内核模块编程指南 下载 第7章 系 统 调 用 187下载 188 第二部分 Linux 内核模块编程指南 下载 第7章 系 统 调 用 189下载 下载 第8章 阻 塞 处 理 如果有人想让你做一些目前无法做到的事,你会怎么处理呢?如果打扰你的是某个人的 话,你可以说的唯一的一句话是:“现在不行,我很忙,你走吧!”但是如果是进程让内核模 块做一些目前它无法处理的事,内核模块却可以有另一种处理方法。内核可以让进程睡眠, 直到能够为它服务为止。毕竟,随时都会有进程被内核置为睡眠状态或者唤醒 (这就是多个进 程看上去好象同时在一个C P U上运行的道理)。 本章的内核模块就是这样的一个例子。该文件 (名为/ p r o c / s l e e p )每次只能被一个进程所打 开。如果文件早已经被打开,内核模块将调用 m o d u l e _ i n t e r r u p t i b l e _ s l e e p _ o n。这个函数把任 务的状态改为TA S K _ I N T E R R U P T I B L E (任务是内核数据结构,它包含着有关它所处的进程和 系统调用的信息,如果存在系统调用的话 ),把它加入到Wa i t Q当中,这就意味着任务在被唤 醒之前将不会运行。Wa i t Q是等待访问文件的任务队列。然后,函数调用上下文调度程度,切 换到另一个拥有C P U时间的进程。 在进程结束了文件操作以后,进程将关闭文件,并调用 m o d u l e _ c l o s e。该函数将唤醒队列 中的所有进程(没有只唤醒一个进程的机制 ),然后函数返回,刚刚关闭文件的那个进程就可以 继续运行了。如果那个进程的时间片用完了,则调度程度将会及时判断出这一点,并将 C P U 的控制权交给另一个进程。最后,等待队列中的某个进程将会被调度程序授予 C P U的控制权。 该进程将从紧接着调用 m o d u l e _ i n t e r r u p t i b l e _ s l e e p _ o n后面的那个点开始执行。然后它会设置 一个全局变量,告诉其它所有进程文件依然打开着,然后该进程将继续执行。当其它进程获 得C P U时间片时,它们将看到那个全局变量,于是继续睡眠。 更为有趣的是,并不是只有 m o d u l e _ c l o s e才能唤醒那些等待访问文件的进程。一个信号, 例如C t r l + C ( S I G I N T )也可以唤醒进程。在那种情况下,我们希望立刻用 E I N T R返回。这是很 重要的,只有这样用户才能在进程接收到文件之前杀死那个进程。 还需要记住一点,有时候进程并不想睡眠;它们希望或者立刻拿到它们想要的东西,或 者直接告诉它们这是不可能的。这样的进程在打开文件时使用标志 O _ N O N B L O C K。内核在 进行该操作时,将会返回错误代码 E A G A I N来作响应,否则该操作就将阻塞,就像下面例子 中的 打开文件操作一样。本章的源目录 中有一个程序 c a t _ n o b l o c k ,可以用于带 O _ N O N B L O C K标志打开一个文件。 第8章 阻 塞 处 理 191下载 192 第二部分 Linux 内核模块编程指南 下载 第8章 阻 塞 处 理 193下载 194 第二部分 Linux 内核模块编程指南 下载 第8章 阻 塞 处 理 195下载 196 第二部分 Linux 内核模块编程指南 下载 第8章 阻 塞 处 理 197下载 198 第二部分 Linux 内核模块编程指南 下载 下载 第9章 替换printk 在第1章,我曾经提到过 X编程和内核模块编程不能混为一谈。这句话在开发内核模块时 是正确的。但是在实际应用中,用户可能希望向运行 t t y命令的那个模块发送消息。在释放内 核模块以后,这对于识别错误是非常重要的。因为该内核模块将会被所有使用 t t y命令的模块 所使用。 通过使用c u r r e n t可以做到这一点, c u r r e n t是一个指向当前正在运行的任务的指针,通过 使用它可以获得当前任务的 t t y结构。然后查看t t y结构,可以找到指向写字符串的函数的指针, 我们就是用这个指针向t t y写字符串的。 200 第二部分 Linux 内核模块编程指南 下载 第9章 替换printk 201下载 下载 第10章 任 务 调 度 常常有一些“内务处理”任务需要我们定时或者经常去做。如果任务是由进程去完成的, 我们可以把它放在c r o n t a b文件中。如果任务要由内核模块来完成,那么我们将面临两种选择。 第一种方案是把一个进程放在 c r o n t a b文件中,该进程在需要的时候通过系统调用唤醒模块。 例如,通过打开一个文件来做到这一点。第三种方案需要在 c r o n t a b中运行一个新进程,把一 个新的可执行程序读入内存,而这一切都只是为了唤醒一个早已经位于内存中的内核模块这 样做效率是非常低的。 除了以上这两种方法以外,我们还可以创建一个函数,在每次时钟中断时调用一次那个 函数。为了做到这一点,需要创建一个任务,存放在结构 t q _ s t r u c t中,而该结构将存放指向函 数的指针。接着,我们使用 q u e u e _ t a s k把那个任务放置到一个称为 t q _ t i m e r的任务列表中,该 任务列表中的任务都将在下次时钟中断时执行。由于我们希望该函数能继续执行下去,无论 该函数何时被调用,我们一定要把它放回到 t q _ t i m e r中,这样在下一次时钟中断时它还可以执 行。 在这里还需要记住一点。当使用 r m m o d命令删除一个模块时,首先需要检查它的引用计 数器值。如果计数器的值为 0,则调用m o d u l e _ c l e a n u p。然后,该模块以及它的所有函数就被 从内存中删除掉了。没有人会记得检查一下看看时钟的任务列表中是否碰巧包含了一个指向 这些函数中某个函数的指针,而该函数已经不再是可用的了。很长一段时间以后 (这是从计算 机的角度来说的,从人的角度来看这段时间不值一提,有可能比百分之一秒还短 ),内核发生 了一次时钟中断,并试图调用任务列表中的函数。不幸的是,该函数早已经不在那里了。在 大多数情况下,该函数原来所在的内存页还没有被使用,这时用户得到的将是一段难看的错 误信息。但如果那个内存位置已经存放了一些新的其它的代码,事情就变得非常糟糕了。不 幸的是,我们还没有一种简便的方法可以从任务列表中取消一个任务的注册。 由于c l e a n u p _ m o d u l e不能返回错误代码 (它是一个v o i d函数),解决的方法是根本不让它返 回,相反,它调用 s l e e p _ o n或者m o d u l e _ s l e e p _ o n,把r m m o d进程置为睡眠状态。而在此之前, 它会设置一个全局变量,通知那些将要在时钟中断时调用的函数停止连接。然后,在下一次 时钟中断发生时, r m m o d进程将被唤醒,我们的函数已经不在队列中了,这时就可以安全地 删除模块。 第10章 任 务 调 度 203下载 204 第二部分 Linux 内核模块编程指南 下载 第10章 任 务 调 度 205下载 206 第二部分 Linux 内核模块编程指南 下载 下载 第11章 中断处理程序 除了第 1 0章以外,到目前为止我们在内核中所做的所有工作都是为了响应进程的请求, 或者是处理一个特殊的文件,发送 i o c t l,或者是发出一个系统调用。但是内核的任务并不仅 仅是为了响应进程请求。另一个任务也是同样重要的,那就是内核还需要和与机器相连接的 硬件进行通信。 在C P U和计算机的其它硬件之间有两种相互交互的方式。第一种是 C P U向硬件发出命令, 另一种是硬件需要告诉 C P U一些事情。第二种方式称之为中断,相比较而言,中断要难实现 得多,这是因为它需要在硬件方便的时候进行处理,而不是在 C P U方便的时候去处理。一般 来说,每个硬件设备都会有一个数量相当少的 R A M,如果在可以读它们的信息的时候用户没 有去读,则这些信息将丢失。 在L i n u x下,硬件中断称为 I R Q (即Interrupt Request的简称,中断请求 )。I R Q分为两种类 型:短的和长的,短I R Q是指需要的时间周期非常短,在这段时间内,机器的其它部分将被阻 塞,并且不再处理其它的中断。而长 I R Q是指需要的时间相对长一些,在这段时间内也可能会 发生别的中断(但是同一个设备上不会再产生中断 )。如果有可能的话,中断处理程序还是处理 长I R Q要好一些。 当C P U接收到一个中断时,它将停下手中所有的工作 (除非它正在处理一个更为重要的中 断,在那种情况下,只有处理完那个更重要的中断以后, C P U才会去处理这个中断 ),在栈中 保存某些特定的参数,并调用中断处理程序。这就意味着在中断处理程序内部有些特定的事 情是不允许的,因为系统处于一个未知的状态下。为了解决这个问题,中断处理程序应该做 那些需要立刻去做的事情,通常是从硬件读一些信息或者向硬件发送一些信息,在稍后的某 时刻再调度去做新信息的处理工作 (这称为“底半处理”)并返回。内核必须保证尽快调用底半 处理— 在进行底半处理工作时,内核模块中允许做的所有工作都可以做。 要实现这一点,可以调用 r e q u e s t _ i r q,这样一来,在接收到相关 I R Q时,就可以调用用户 的中断处理程序 (在I n t e l平台上共有1 6种I R Q )。r e q u e s t _ i r q接收I R Q编号、函数名称、标志、 / p r o c / i n t e r r u p t s的名称以及传送给中断处理函数的一个参数。这里提到的标志包括 S A _ S H I R Q 和S A _ I N T E R R U P T,前者表明用户愿意与其它中断处理程序分享 I R Q (通常是因为同一个 I R Q 上有多个硬件设备 );而后者表明这是个高速中断。只有当这个 I R Q上还没有处理程序,或者 两者都愿意共享这个I R Q时,r e q u e s t _ i r q函数才会成功。 接下来,我们从中断处理程序内部与硬件进行通信,并使用 t q _ i m m e d i a t e 和 m a r k _ b h ( B H _ I M M E D I AT E )调用q u e u e _ t a s k _ i r q,以调度底半处理。在版本 2中我们不能使用标 准的q u e u e _ t a s k,这是因为中断有可能正好发生在其它的 q u e u e _ t a s k中间。我们之所以需要使 用m a r k _ b h,是因为以前版本的 L i n u x只有一个由3 2个底半处理所组成的队列,而现在它们中 的一个( B H _ I M M E D I AT E )已经被用作底半处理的链接列表,这是为那些没有得到分配给它们 的底半处理入口的驱动程序所准备的。 Intel体系结构的键盘 警告 本章余下的内容完全是与Intel相关的。如果读者不是在Intel平台上运行,这些内 容将不能正常工作。读者甚至不要去编译这些代码。 在写本章的示例代码时我遇到了一个问题。一方面,为了让所给出的例子能有用,它必 须可以在所有人的计算机上运行,且能得到有意义的结果。但是另一方面,内核中早已经为 所有的通用设备准备了设备驱动程序,而那些设备驱动程序与我将要编写的驱动程序是不能 共存的。最后我找到了解决的办法,就是为键盘中断写驱动程序,并且首先关闭常规的键盘 中断驱动程序。因为它被定义成内核源文件 (特别是指d r i v e r s / c h a r / k e y b o a r d . c )中的静态符号, 所以没有办法恢复它。如果用户珍惜自己的文件系统的话,在执行 i n s m o d命令插入这个代码 以前,请先在另一个终端上执行 sleep 120;reboot。 该代码把它自己绑定在 IRQ 1上,在I n t e l体系结构下,I R Q 1是控制键盘的I R Q。这样,当 它接收到键盘中断时,它读键盘的状态 (在程序中使用inb (0x64)来实现),并查看代码,该代 码是由键盘所返回的值。然后在内核认为适合的时候,它立刻运行 g o t _ c h a r,该函数将给出所 使用的键的代码 (即查看到的代码的前七位 ),并给出该键是被按下 (如果第八位为 0 )还是被松 开(如果第八位为1 )。 208 第二部分 Linux 内核模块编程指南 下载 第11章 中断处理程序 209下载 210 第二部分 Linux 内核模块编程指南 下载 下载 第12章 对称多处理 要提高硬件的性能,最简单的 (同时也是最便宜的)方法是把多个C P U放到一个主板上。实 现这一点,可以有两种方法。一是使不同的 C P U执行不同的任务(即非对称多处理),或者使这 些C P U并行运行,执行同样的任务 (即对称多处理,简写为S M P )。进行非对称多处理非常需要 对计算机将要执行的任务有特别的了解,而在像 L i n u x这样的操作系统中,这是不可能的。另 一方面,对称多处理相对要容易实现一些。这里所说的“相对容易”就是相对的意思— 并 不是指它真的容易实现。在对称多处理环境中, C P U共享同一个内存,这样做的后果是一个 C P U中运行的代码可能会影响另一个 C P U所使用的内存。用户不再可以肯定自己在前面一行 中设置了值的那个变量仍然保持着那个值— 另一个C P U可能在用户不注意的时候对那个变 量进行了处理。很显然,要编出这样的程序是不可能的。 在进程编程时,一般来说上面这个问题就不是什么问题了,因为进程在某个时刻一般是 只在一个C P U上运行的。而另一方面,内核则可以被运行在不同 C P U上的不同进程所调用。 在版本2 . 0 . x中,这个也不是什么问题,因为整个内核就是一个大的自旋锁 ( s p i n l o c k )。这 意味着如果某个C P U在内核中而另一个C P U试图进入内核(假设是由于系统调用),则后到的那 个C P U必须等待,直到第一个C P U处理完,这使得Linux SMP比较安全,但是效率却相当低。 在版本2 . 2 . x中,几个C P U可以同时位于内核中,这是模块编程人员需要留意的地方。我 已经就S M P的问题向其它高手求助了,希望在本书的下一个版本中将会包含更多的信息。 下载 第13章 常 见 错 误 在读者踌躇满志地准备动手编写内核模块以前,我还要提醒大家注意一些事情。如果是 因为我没有警告过你而发生了不愉快的情况,请把你遇到的问题告诉我。我将把你买此书所 付的钱全部还给你。 1) 使用标准库 不能这样做,在内核模块中用户只能使用内核函数,也就是可以在 / p r o c / k s y m s中找到的那些函数。 2) 关闭中断 用户可能需要在短时间内暂时关闭中断,那没什么问题,但是事后如果忘 了打开它们,系统将会瘫痪,用户将不得不把电源拔掉。 3) 无视危险的存在 可能不需要提醒读者,但是最后我还是要说,只是为了以防万一。 下载 附录A 2.0和2.2之间的差异 实际上我对整个内核了解得并不是很透彻,没有透彻到能够列出所有变化的地步。在转 换本书例子的过程中 (或者更确切地说是对 Emmanuel Papirakis所做的转换进行修改 ),我遇到 了下面的差异,我把它们全部列举了出来,以便帮助模块编程人员 (特别是那些曾经学习过本 书的前一版本,并熟悉我所使用的技术的读者 )转换到新的版本。 希望进行转换工作的用户还可以访问如下网址: h t t p : / / w w w. a t n f . c s i r o . a u / ~ rg o o c h / l i n u x / d o c s / p o r t i n g _ t o _ 2 . 2 . h t m l . 1) asm/uaccess.h 如果需要使用p u t _ u s e r或者g e t _ u s e r,用户必须用# i n c l u d e包含这个文件。 2) get_user 在版本2 . 2中,g e t _ u s e r既可以接收指向用户内存的指针,也可以接收内核内 存中的变量,以便填充用户信息。之所以这样,是因为在版本 2 . 2中g e t _ u s e r可以一次读两个 或四个字节,如果我们所读的变量是两个字节或四个字节长的话, g e t _ u s e r可以读入它。 3) file_operations 该结构已经在o p e n函数和c l o s e函数之间加入了一个刷新函数。 4) file_operations中的c l o s e函数 在版本2 . 2中,c l o s e函数返回一个整数,所以它可以失 败。 5) file_operations中的r e a d和w r i t e函数 这两个函数的头部已经改变了。在版本 2 . 2中,它 们不再返回整数,而是返回一个 s s i z e _ t类型的值,它们的参数列表也不同了。索引节点不再 作为一个参数,但同时却加入了文件中的偏移量作为参数。 6) proc_register_dynamic 该函数已经不再存在了。取而代之的是,用户可以调用 p r o c _ r e g i s t e r并把结构的索引结点字段置为 0。 7) 信号 在任务结构中的信号不再是 3 2位长的整数值,而是变成了由 _ N S I G _ W O R D S整数 组成的一个数组。 8) queue_task_irq 即使用户希望从中断处理程序内部调度任务来执行,他也应该使用 q u e u e _ t a s k,而不要使用q u e u e _ t a s k _ i r q。 9) 模块参数 用户不再是只把模块参数定义为全局变量了。在 2 . 2中,用户必须同时使用 M O D U L E _ PA R M来定义它们的类型。这是一个很大的改进,因为它允许模块可以接收以数字 开头的字符串参数,而不会搞混淆。 10) 对称多处理 内核不再是一个大的自旋锁了,这就意味着内核模块在使用 S M P时必须 小心。 下载 附录B 其 他 资 源 如果笔者愿意的话,可以轻易地在本书中再加入几章。可以加入一章介绍如何创建新的 文件系统,或者介绍如何增加新的协议栈 (但这是不必要的— 因为读者很难找到L i n u x所不支 持的协议栈)。当然还可以加入一些内核机制的解释,而这些机制我们并没有接触过,例如引 导机制或者磁盘接口。 然而,笔者没有这么做,因为笔者写这本书的目的是为了探索内核模块编程的奥秘,希 望教给用户一些内核模块编程的通用技术。对于那些对内核编程有着强烈兴趣的读者,笔者 建议他们阅读如下网址的内核资源列表: http://jungla.dit.upm.es/~jmseyas /linux/kernel / h a c k e r s - d o c s . h t m l。正如L i n u s所说的那样,学习内核的最佳方法就是自己阅读代码。 如果读者希望得到更多的短内核模块的例子,我建议阅读 P h r a c k杂志,即使用户对安全 性不是太感兴趣,作为一个程序员也应该培养这方面的兴趣。 P h r a c k杂志上的内核模块都是 一些很好的例子,介绍了用户可以在内核中所做的有关工作。而且这些例子都很短,读者不 用费太大的劲就可以弄懂它们。 希望我能帮助读者成为一个更好的程序员,或者至少培养了在这方面的兴趣。如果读者 写出了有用的内核模块,希望也能按照 G P L把它们出版出来,这样我也可以使用它们。 下载 附录C 给出你的评价 这是一本“自由”的书。在 G N U公共许可证所规定的内容以外,读者不受任何限制。如 果读者想为这本书做点什么,我有以下几点建议: • 给我寄一张明信片,地址是: Ori Pomerantz Apt. #1032 2355 N Hwy 360 Grand Prairie, TX 75050 U S A 如果希望收到我的致谢回函,请在明信片上写清你的电子邮箱地址。 • 给自由软件组织捐献一些钱,或者一些时间 (更佳)。编写一个程序或者写作一本书,并 按照G P L的条款出版,教别人怎样使用自由软件,例如 L i n u x或者P e r l。 • 向别人解释一下自私与社会生活是格格不入的,与帮助其它人是格格不入的。我很高兴 我写了这本书,并且我相信出版这本书将来一定会获得回报。同时,我写这本书是为了 帮助读者(如果我做到了的话 )。记住,快乐的人常常比不快乐的人对自己更有用,而有 才华的人常常比低能儿要有帮助的多。 • 高兴起来。如果我将遇见你,而且你是个愉快的人,这次会面将会给我留下美好的印象, 而且愉快的个性也会使你变得对我更有用。
还剩70页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 15 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

zxhjxyg

贡献于2012-06-05

下载需要 15 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf