《LINUX内核模块编程指南.pdf》由会员分享,可在线阅读,更多相关《LINUX内核模块编程指南.pdf(50页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、下载第1章Hello,World如果第一个程序员是一个山顶洞人,它在山洞壁(第一台计算机)上凿出的第一个程序应该是用羚羊图案构成的一个字符串“Hello,Wo rld”。罗马的编程教科书也应该是以程序“S alut,M undi”开始的。我不知道如果打破这个传统会带来什么后果,至少我还没有勇气去做第一个吃螃蟹的人。内核模块至少必须有两个函数:i nit_module和c leanup_module。第一个函数是在把模块插入内核时调用的;第二个函数则在删除该模块时调用。一般来说,i nit_module可以为内核的某些东西注册一个处理程序,或者也可以用自身的代码来取代某个内核函数(通常是先干点别
2、的什么事,然后再调用原来的函数)。函数 c leanup_module的任务是清除掉i nit_module所做的一切,这样,这个模块就可以安全地卸载了。1.1 内核模块的 Makefiles 文件内核模块并不是一个独立的可执行文件,而是一个对象文件,在运行时内核模块被链接到内核中。因此,应该使用-c命令参数来编译它们。还有一点需要注意,在编译所有内核模块时,都将需要定义好某些特定的符号。?_ _KERNEL_ _这个符号告诉头文件:这个程序代码将在内核模式下运行,而不要作为用户进程的一部分来执行。?MODULE这个符号告诉头文件向内核模块提供正确的定义。?L INUX从技术的角度讲,这个符号
3、不是必需的。然而,如果程序员想要编写一个重要的内核模块,而且这个内核模块需要在多个操作系统上编译,在这种情况下,程序员将会很高兴自己定义了L INUX这个符号。这样一来,在那些依赖于操作系统的部分,这个符号就可以提供条件编译了。还有其它的一些符号,是否包含它们要取决于在编译内核时使用了哪些命令参数。如果用户不太清楚内核是怎样编译的,可以查看文件/usr/include/linux/config.h。?_ _SMP_ _对称多处理。如果编译内核的目的是为了支持对称多处理,在编译时就需要定义这个符号(即使内核只是在一个C PU上运行也需要定义它)。当然,如果用户使用对称多处理,那么还需要完成其它一
4、些任务(参见第 1 2章)。?C ONFIG_MODVERSIONS如果 C ONFIG_MODVERSIONS可用,那么在编译内核模块时就需要定义它,并且包含头文件/usr/include/linux/modversions.h。还可以用代码自身来完成这个任务。完成了以上这些任务以后,剩下唯一要做的事就是切换到根用户下(你不是以 r oot身份编译内核模块的吧?别玩什么惊险动作哟!),然后根据自己的需要插入或删除h ello模块。在执行完 i nsmod命令以后,可以看到新的内核模块在/proc/modules中。顺便提一下,M akefile建议用户不要从 X执行 i nsmod命令的原因
5、在于,当内核有个消息需要使用 p rintk命令打印出来时,内核会把该消息发送给控制台。当用户没有使用X时,该消息146第二部分Linux 内核模块编程指南下载将发送到用户正在使用的虚拟终端(用户可以用 A lt-F来选择当前终端),然后用户就可以看到这个消息了。而另一方面,当用户使用X时,存在两种可能性。一种情况是用户用命令xterm-C 打开了一个控制台,这时输出将被发送到那个控制台;另一种情况是用户没有打开控制台,这时输出将送往虚拟终端7被X所“覆盖”的一个虚拟终端。当用户的内核不太稳定时,没有使用X的用户更有可能取得调试信息。如果没有使用X,p rintk将直接从内核把调试消息发送到控
6、制台。而另一方面,在X中 p rintk的消息将被送给一个用户模式的进程(xterm-C)。当那个进程获得 CPU时间时,它将把该消息传送给 X服务器进程。然后,当 X服务器获得 C PU时间时,它将显示该消息但是一个不稳定的内核通常意味着系统将要崩溃或者重新启动,所以用户不希望推迟错误信息显示的时间,因为该信息可能会向用户解释什么地方出了问题,如果显示的时刻晚于系统崩溃或重启的时刻,用户将会错过这个重要的信息。1.2 多重文件内核模块有时候在多个源文件间划分内核模块是很有意义的。这时用户需要完成下面三件任务:1)除了一个源文件以外,在其它所有源文件中加入一行#define _ _ NO_VE
7、RSION_ _。这点很重要,因为 m odule.h中通常会包含有 k ernel_version的定义(kernel_version是一个全局变量,它表明该模块是为哪个内核版本所编译的)。如果用户需要v ersion.h文件,那么用户必须自己把它包含在源文件中,因为在定义了_ _NO_VERSION_ _的情况下,m odule.h是不会为用户完成这个任务的。2)像平常一样编译所有的源文件。3)把所有的对象文件组合进一个文件中。在x 86下,可以使用命令:ld-m elf_i386-r-o 模块名称.o(第一个源文件).o(第二个源文件).o来完成这个任务。下面是这种内核模块的一个例子。第
8、1章Hello,World 147下载148第二部分Linux 内核模块编程指南下载下载第2章字符设备文件我们现在就可以吹牛说自己是内核程序员了。虽然我们所写的内核模块还什么也干不了,但我们仍然为自己感到骄傲,简直可以称得上趾高气扬。但是,有时候在某种程度上我们也会感到缺少点什么,简单的模块并不是太有趣。内核模块主要通过两种方法与进程打交道。一种方法是通过设备文件(例如在目录/dev中的文件),另一种方法是使用p roc文件系统。既然编写内核模块的主要原因之一就是支持某些类型的硬件设备,那么就让我们从设备文件开始吧。设备文件最初的用途是使进程与内核中的设备驱动程序通信,并且通过设备驱动程序再与
9、物理设备(调制解调器、终端等等)通信。下面我们要讲述实现这一任务的方法。每个设备驱动程序都被赋予一个主编号,主要用于负责某几种类型的硬件。可以在/proc/devices中找到驱动程序以及它们对应的主编号的列表。由设备驱动程序管理的每个物理设备都被赋予一个从编号。这些设备中的每一个,不管是否真正安装在计算机系统上,都将对应一个特殊的文件,该文件称为设备文件,所有的设备文件都包含在目录/dev中。例如,如果执行命令ls-l/dev/hdab*,用户将可以看到与一个计算机相连接的所有的I DE硬件分区。注意,所有的这些硬盘分区都使用同一个主编号:3,但是从编号却各不相同。需要强调的是,这里假设用户
10、使用的是P C体系结构。我并不知道在其它体系结构上运行的L inux的设备是怎么样的。在安装了系统以后,所有的设备文件都由命令m knod创建出来。从技术的角度上讲,并没有什么特别的原因一定要把这些设备文件放在目录/dev中,这只不过是一个有用的传统习惯而已。如果读者创建设备文件的目的只不过是为了试试看,就像本章的练习一样,那么把该设备文件放置在编译内核模块的目录中可能会更有意义一些。设备一般分为两种类型:字符设备和块设备。它们的区别在于块设备具有一个请求缓冲区,所以块设备可以选择按照何种顺序来响应这些请求。这对于存储设备来说是很重要的。在存储设备中,读或写相邻的扇区速度要快一些,而读写相互之
11、间离得较远的扇区则要慢得多。另一个区别在于块设备只能以成块的形式接收输入和返回输出(块的大小根据设备类型的变化而有所不同),而字符设备则可以随心所欲地使用任意数目的字节。当前大多数设备都是字符设备,因为它们既不需要某种形式的缓冲,也不需要按照固定的块大小来进行操作。如果想知道某个设备文件对应的是块设备还是字符设备,用户可以执行命令ls-l,查看一下该命令的输出中的第一个字符,如果第一个字符是“b”,则对应的是块设备;如果是“c”,则对应的是字符设备。模块分为两个独立的部分:模块部分和设备驱动程序部分。前者用于注册设备。函数i nit_module调用 m odule_register_chrd
12、ev,把该设备驱动程序加入到内核的字符设备驱动程序表中,它还会返回供驱动程序所使用的主编号。函数c leanup_module则取消该设备的注册。注册某设备和取消它的注册是这两个函数最基本的功能。内核中的东西并不是按照它们自己的意愿主动开始运行的,就像进程一样,而是由进程通过系统调用来调用,或者由硬件设备通过中断来调用,或者由内核的其它部分调用(只需调用特定的函数),它们才会执行。因此,如果用户往内核中加入了代码,就必须把它作为某种特定类型事件的处理程序进行注册;而在删除这些代码时,用户必须取消它的注册。设备驱动程序一般是由四个d evice_函数所组成的,如果用户需要处理具有对应主编号的设备
13、文件,就可以调用这四个函数。通过f ile_operations结构 F ops内核可以知道调用哪些函数。因为该结构的值是在注册设备时给定的,它包含了指向这四个函数的指针。在这里我们还需要记住的一点是:无论如何不能乱删内核模块。原因在于如果设备文件是由进程打开的,而我们删去了该内核模块,那么使用该文件就将导致对正确的函数(读/写)原来所处的存储位置的调用。如果我们走运,那里没有装入什么其它的代码,那我们至多得到一些难看的错误信息,而如果我们不走运,在原来的同一位置已经装入了另一个内核模块,这就意味着跳转到了内核中另一个函数的中间,这样做的后果是不堪设想的,起码不会是令人愉快的。一般来说,如果用
14、户不愿意让某件事发生,可以让执行这件事的函数返回一个错误代码(一个负数)。而对 c leanup_module来说这是不可能的,因为它是一个v oid函数。一旦调用了c leanup_module,这个模块就死了。然而,还有一个计数器记录了有多少个其它的内核模块正在使用该内核模块,这个计数器称为引用计数器(就是位于文件/proc/modules信息行中的最后那个数值)。如果这个数值不为零,r m m o d 将失败。模块的引用计数值可以从变量m o d _ u s e _ c o u n t _ 中 得 到。因 为 有 些 宏 是 专 门 为 处 理 这 个 变 量 而 定 义 的(如M OD
15、_INC_USE_COUNT和M OD_DEC_USE_COUNT),我们宁愿使用这些宏,也不愿直接对m od_use_count_进行操作,这样一来,如果将来实现方法有所变化,我们也会很安全。150第二部分Linux 内核模块编程指南下载第2章字符设备文件151下载152第二部分Linux 内核模块编程指南下载第2章字符设备文件153下载154第二部分Linux 内核模块编程指南下载第2章字符设备文件155下载多内核版本源文件系统调用是内核提供给进程的主要接口,它并不随着内核版本的变化而有所改变。当然156第二部分Linux 内核模块编程指南下载可能会加入新的系统调用,但是老的系统调用是永远
16、不会改变的。这主要是为了提供向后兼容性的需要新的内核版本不应该使原来工作正常的进程出现问题。在大多数情况下,设备文件也应该保持不变。另一方面,内核里面的内部接口则可以变化,并且也确实随着内核版本的变化而改变了。L inux内核的版本可以划分为稳定版本(n.m)和开发版本(n.m)两种。开发版本中包括所有最新最酷的思想,当然其中也可能有一些将来会被认为是馊主意的错误,有些将会在下一个版本中重新实现。因此,用户不能认为在这些版本之间接口也会保持一致(这就是我为什么懒得在本书中介绍它们的原因,这需要做大量的工作,而且很快就会过时被淘汰)。另一方面,在稳定的版本中,我们可以无视错误修正版本(带数字 m
17、的版本)而认为接口是保持不变的。这个 M PG版本包含对 L inux内核版本 2.0.x和版本 2.2.x的支持。因为这两个版本之间存在差异,这就要求用户根据内核版本号来进行条件编译。为了做到这一点,可以使用宏L INUX_VERSION_CODE。在内核版本 a.b.c中,该宏的值将会是21 6a+28b+c。为了获取特定内核版本的值,我们可以使用宏K ERNEL_VERSION。在2.0.35中该宏没有定义,如果需要的话我们可以自己定义它。第2章字符设备文件157下载下载第3章/proc文件系统在L inux中,内核和内核模块还可以通过另一种方法把信息发送给进程,这种方法就是/proc
18、文件系统。最初/proc 文件系统是为了可以轻松访问有关进程的信息而设计的(这就是它的名称的由来),现在每一个内核部分只要有些信息需要报告,都可以使用/proc 文件系统,例如/proc/modules 包含一个模块的列表,/proc/meminfo 包含有关内存使用的统计信息。使用/proc 文件系统的方法其实与使用设备驱动程序的方法是非常类似的用户需要创建一个结构,该结构包含了/proc文件所需要的所有信息,包括指向任意处理程序函数的指针(在我们本章的例子中只有一个处理程序函数,当有人试图读/proc文件时将调用这个函数)。然后,i nit_module将向内核注册这个结构,而c lean
19、up_module将取消它的注册。在程序中之所以需要使用p roc_register_dynamic,是因为我们不想事先判断文件所使用的索引节点编号,而是让内核去决定,这样可以避免编号冲突。一般文件系统都是位于磁盘上的,而不是仅仅存在于内存中(/proc是存在于内存中的),在那种情况下,索引节点编号是一个指向某个磁盘位置的指针,在那个位置上存放了该文件的索引节点(简写为 i node)。索引节点包含该文件的有关信息,例如文件的访问权限,以及指向某个或者某些磁盘位置的指针,在这个或者这些磁盘位置中,存放着文件的数据。因为在文件打开或者关闭时该模块不会被调用,所以我们无需在该模块中使用M OD_I
20、NC_USE_COUNT和M OD_DEC_USE_COUNT。如果文件打开以后模块被删除了,没有任何措施可以避免这一后果。在下一章中,我们将学习到一种比较难于实现,但却相对方便的方法,可以用于处理/proc文件,我们也可以使用那个方法来防止这个问题。第3章/proc文件系统159下载160第二部分Linux 内核模块编程指南下载第3章/proc文件系统161下载下载第4章把/proc用于输入到目前为止,可以通过两种方法从内核模块产生输出:我们可以注册一个设备驱动程序,并使用 m knod命令创建一个设备文件;我们还可以创建一个/proc文件。这样内核模块就可以告诉我们各种各样的信息。现在唯一
21、的问题是我们没有办法来回答它。如果要把输入信息发送给内核模块,第一个方法就是把这些信息写回到/proc文件中。因为编写 p roc文件系统的主要目的是为了让内核可以把它的状态报告给进程,对于输入并没有提供相应的特别措施。结构p roc_dir_entry中并不包含指向输入函数的指针,它只包含指向输出函数的指针。如果需要输入,为了把信息写到/proc文件中,用户需要使用标准的文件系统机制。L inux为文件系统注册提供了一个标准的机制。因为每个文件系统都必须具有自己的函数专门用于处于索引节点和文件操作,所以L inux提供了一个特殊的结构i node_operations,该结构存放指向所有这些
22、函数的指针,其中包含一个指向结构f ile_operations的指针。在/proc中,无论何时注册一个新文件,用户都可以指定使用哪个i node_operations结构来访问它。这就是我们所使用的机制。结构i node_oper ations包含指向结构f ile_operations的指针,而结构f ile_operations又包含指向 m odule_input和m odule_output函数的指针。注意在内核中读和写的标准角色是互换的,读函数用于输出,而写函数则用于输入,记住这点很重要。之所以会这样,是因为读和写实际上是站在用户的观点来说的如果一个进程从内核读信息,内核需要做的是
23、输出这些信息,而如果进程向内核写信息,内核当然会把它当作输入来接收。这里另外一个有趣的地方是m odule_permission函数。只要进程试图对/proc文件干点什么,这个函数就将被调用,它可以判断是允许对文件进行访问,还是拒绝这次访问。目前这种判断还只是基于操作本身以及当前所使用的u id来作出(当前所使用的u id可以从 c urr ent得到,c urrent是一个指针,指向包含有关当前运行进程的信息结构),但是函数 m odule_permission还可以基于用户所选择的任意条件来作出允许或是拒绝访问的判断,例如其它还有什么进程正在使用这个文件、日期和时间、或者我们最近接收到的输
24、入。在程序中我们之所以使用p ut_user和g et_user,主要是因为 L inux内存是分段的(在I ntel体系结构下;有些其它的处理器可能会有所不同)。这就意味着一个指针并不能指向内存中的某个唯一的位置,而只能指向一个内存段。为了能够使用指针,用户必须知道它指向的是哪个内存段。内核只对应一个内存段,且每个进程都对应一个内存段。进程所能访问的唯一的内存段就是它自己的内存段,所以在写将要当作进程来运行的常规程序时,程序员不需要考虑有关分段的问题,而当用户编写内核模块时,通常用户需要访问内核的内存段,该内存段是由系统自动处理的。然而,如果内存缓冲区中的内容需要在当前运行的进程和内核之间传
25、送,内核函数将会接收到一个指针,该指针指向进程段中的内存缓冲区。宏 p ut_user和g et_user使用户可以访问那块内存。第4章把/proc用于输入163下载164第二部分Linux 内核模块编程指南下载第4章把/proc用于输入165下载166第二部分Linux 内核模块编程指南下载第4章把/proc用于输入167下载168第二部分Linux 内核模块编程指南下载第4章把/proc用于输入169下载下载第5章把设备文件用于输入设备文件一般代表物理设备,而大多数物理设备既可用于输出,也可用于输入,所以L inux必须提供一些机制,以便内核中的设备驱动程序可以从进程获得输出信息,并把它发
26、送到设备。要做到这一点,可以为输出打开设备文件,并且向它写信息,就像写普通的文件一样。在下面的例子中,这些任务是由d evice_write完成的。当然仅有上面这种方法是不够的。假设用户有一个与调制解调器相连接的串行口(即使用户拥有的是一个内置式的调制解调器,从C PU角度来看,它还是由一个与调制解调器相连的串行口来实现的,所以这样的用户也无需过多地苛求自己的想象力)。用户将会做的最自然的事情就是使用设备文件来把信息写到调制解调器(调制解调器命令或者数据将会通过电话线来传送),并且利用设备文件从调制解调器读信息(命令的响应或者数据也是通过电话线接收的)。然而,这会带来一个很明显的问题:如果用户
27、需要与串行口本身交换信息的话,用户该怎么办?例如用户可能发送有关数据发送和接收的速率的值。在U nix中,可以使用一个称为i octl 的特殊函数来解决这个问题(ioctl是输入输出控制的英文缩写)。每个设备都有属于自己的i octl命令,可以是读i octl(从进程把信息发送到内核)、写i octl(把信息返回到进程)、都有或者都没有。调用i octl函数必须带上三个参数:适当的设备文件的文件描述符,i octl编号以及另外一个长整型的参数,用户可以使用这个长整型参数来传送任何信息。i octl编号是由主设备编号、i octl类型、命令以及参数的类型这几者编码而成。这个i octl编号通常是
28、由一个头文件中的宏调用(取决于类型的不同,可以是_ I O、_ IOR、_ I OW或者_ IOWR)来创建的。然后,将要使用i octl的程序以及内核模块都必须通过#include命令包含这个头文件。前者包含这个头文件是为了生成适当的i octl,而后者是为了能理解它。在下面的例子中,头文件的名称是c hardev.h,而使用它的程序是i octl.c。如果用户希望在自己的内核模块中使用i octl,最好是接受正式的i octl的约定,这样如果偶尔得到别人的 i octl,或者如果别人获得了你的i octl,你可以知道那些地方出现了错误。如果读者想知道更多的信息,可以查询Documentat
29、ion/ioctl-number.txt下的内核源代码树。第5章把设备文件用于输入171下载172第二部分Linux 内核模块编程指南下载第5章把设备文件用于输入173下载174第二部分Linux 内核模块编程指南下载第5章把设备文件用于输入175下载176第二部分Linux 内核模块编程指南下载第5章把设备文件用于输入177下载178第二部分Linux 内核模块编程指南下载第5章把设备文件用于输入179下载180第二部分Linux 内核模块编程指南下载第5章把设备文件用于输入181下载下载第6章启 动 参 数在前面所给出的许多例子中,我们不得不把一些东西硬塞进内核模块中,如/proc文件的文
30、件名或者设备的主设备编号,这样我们就可以使用该设备的i octl命令。但这是与U nix和L inux的宗旨背道而驰的,U nix和L inux提倡编写用户所习惯的易于使用的程序。在程序或者内核模块开始工作之前,如果希望告诉它一些它所需要的信息,可以使用命令行参数。如果是内核模块,我们不需要使用a rg c和a rg v相反,我们还有更好的选择。我们可以在内核模块中定义一些全局变量,然后使用i nsmod命令,它将替我们给这些变量赋值。在下面这个内核模块中,我们定义了两个全局变量:s tr1和s tr2。用户所需做的全部工作就是编译该内核模块,然后运行insmod str1=xxx str2=
31、yyy。当调用 i nit_module时,s tr1将指向字符串“x xx”,而 s tr2将指向字符串“y yy”。在版本 2中,对这些参数不进行类型检查。如果s tr1或者 s tr2的第一个字符是一个数字,则内核将用该整数的值填充这个变量,而不会用指向字符串的指针去填充它。如果用户对此不太确定,那么就必须亲自去检查一下。而另一方面,在版本 2.2中,用户使用宏 MACRO_PARM告诉insmod自己希望参数、它的名称以及类型是什么样的。这就解决了类型问题,并且允许内核模块接受那些以数字开头的字符串。第6章启 动 参 数183下载184第二部分Linux 内核模块编程指南下载下载第7章
32、系 统 调 用到现在为止,我们所做过的唯一的工作就是使用一个定义好的内核机制来注册/proc文件和设备处理程序。如果用户仅仅希望做一个内核程序员份内的工作,例如编写设备驱动程序,那么以前我们所学的知识已经足够了。但是如果用户想做一些不平凡的事,比如在某些方面,在某种程度上改变一下系统的行为,那应该怎么办呢?答案是,几乎全部要靠自己。这就是内核编程之所以危险的原因。在编写下面的例题时,我关掉了系统调用o pen。这将意味着我不能打开任何文件,不能运行任何程序,甚至不能关闭计算机。我只能把电源开关拔掉。幸运的是,我没有删除掉任何文件。为了保证自己也不丢失任何文件,请读者在执行 i nsmod和r
33、mmod之前先运行 s ync。现在让我们忘记/proc文件,忘记设备文件,他们只不过是无关大雅的细节问题。实现内核通信机制的“真正”进程是系统调用,它是被所有进程所使用的进程。当某进程向内核请求服务时(例如打开文件、产生一个新进程、或者请求更多的内存),它所使用的机制就是系统调用。如果用户希望以一种有趣的方式改变内核的行为,也需要依靠系统调用。顺便说一下,如果用户想知道程序使用的是哪个系统调用,可以运行命令strace 。一般来说,进程是不能访问内核的。它不能访问内核存储,它也不能调用内核函数。C PU的硬件保证了这一点(那就是为什么称之为“保护模式”的原因)。系统调用是这条通用规则的一个特
34、例。在进行系统调用时,进程以适当的值填充注册程序,然后调用一条特殊的指令,而这条指令是跳转到以前定义好的内核中的某个位置(当然,用户进程可以读那个位置,但却不能对它进行写的操作)。在 Intel CPU 下,以上任务是通过中断0 x80来完成的。硬件知道一旦跳转到这个位置,用户的进程就不再是在受限制的用户模式下运行了,而是作为操作系统内核来运行于是用户就被允许干所有他想干的事。进程可以跳转到的那个内核中的位置称为s ystem_call。那个位置上的过程检查系统调用编号(系统调用编号可以告诉内核进程所请求的是什么服务)。然后,该过程查看系统调用表(sys_call_table),找出想要调用的
35、内核函数的地址,然后调用那个函数。在函数返回之后,该过程还要做一些系统检查工作,然后再返回到进程(或者如果进程时间已用完,则返回到另一个进程)。如果读者想读这段代码,可以查看源文件a rch/kernel/entry.S,它就在E NTRY(system_call)那一行的后面。这样看来,如果我们想要改变某个系统调用的工作方式,我们需要编写自己的函数来实现它(通常是加入一点自己的代码,然后再调用原来的函数),然后改变 s ys_call_table表中的指针,使它指向我们的函数。因为我们的函数将来可能会被删除掉,而我们不想使系统处于一个不稳定的状态,所以必须用c leanup_module使s
36、 ys_call_table表恢复到它的原始状态,这是很重要的。本章的源代码就是这样一个内核模块的例子。我们想要“侦听”某个特定的用户。无论何时,只要那个用户一打开某个文件,程序就会用p rintk打印出一个消息。为了做到这一点,我们用自己的函数代替了用来打开文件的那个系统调用。我们的函数称为o ur_sys_open。该函数查看当前进程的u id(用户的 I D),如果它就是我们想要侦听的u id,它就调用 p rintk,显示出将要打开的文件的名称。接下来,不管当前进程的u id是不是想要侦听的u id,该函数都使用同样的参数调用原来的o pen函数,真正地打开那个文件。i nit_mod
37、ule函数替换 s ys_call_table表中相应的位置,并把原来的指针存放在一个变量中;而 c leanup_module函数则使用那个变量把一切都恢复成原来正常的状态。这种方法是具有一定的危险性的,因为可能有两个内核模块都修改同一个系统调用。现在假设有两个内核模块 A和B。A模块打开文件的系统调用是A _open,而 B的系统调用是 B _open。当把 A插入到内核中时,系统调用被换成了A _open,它在被调用时将会调用原来的s ys_open。接下来,当 B被插入到内核中时,系统调用将被替换成B _open,它在被调用时,将会调用它自以为是原始系统调用的那个系统调用,即A _op
38、en。现在假设 B先被删除,那么一切都将正常系统调用将被恢复成A _open,而 A _open会调用原始的系统调用。然而,如果A先被删除然后 B被删除,系统将会崩溃。删除A时系统调用被恢复成原始的sys_open,B_open 将被忽略。接着,当删除B时,B将把系统调用恢复成它自认为是原始的系统调用,即A _open,而 A _open已经不再位于内存中了。乍一看上去,好象用户可以检查系统调用是不是打开文件函数,如果是就不做任何修改(这样在删除 B时它就不会改变系统调用),似乎这样可以避免问题的发生。但这样做将会导致一个更为严重的问题。当A被删除时,它看到系统调用已经被改变为B _open,
39、而不再指向 A _open,所以 A在从内存中删除前不会将系统调用恢复为s ys_open。不幸的是,B _open仍将试图调用A _open,而A _open已不在内存中了,这样,甚至还不到删除B时系统就将崩溃。我认为有两种方法可以解决这个问题。第一个方法是把系统调用恢复为原始的值:s ys_open。不幸的是,s ys_open不是/proc/ksyms中内核系统表的一部分,所以我们不能访问它。另一个方法是一旦装入了模块,马上设立一个引用计数器,以防止根用户把它r mmod掉。对于产品模块来说,这样做是很好的,但却不适合于作为教学的例子这就是我没有在这里实现它的原因。186第二部分Linu
40、x 内核模块编程指南下载第7章系 统 调 用187下载188第二部分Linux 内核模块编程指南下载第7章系 统 调 用189下载下载第8章阻 塞 处 理如果有人想让你做一些目前无法做到的事,你会怎么处理呢?如果打扰你的是某个人的话,你可以说的唯一的一句话是:“现在不行,我很忙,你走吧!”但是如果是进程让内核模块做一些目前它无法处理的事,内核模块却可以有另一种处理方法。内核可以让进程睡眠,直到能够为它服务为止。毕竟,随时都会有进程被内核置为睡眠状态或者唤醒(这就是多个进程看上去好象同时在一个C PU上运行的道理)。本章的内核模块就是这样的一个例子。该文件(名为/proc/sleep)每次只能被
41、一个进程所打开。如果文件早已经被打开,内核模块将调用m odule_interruptible_sleep_on。这个函数把任务的状态改为 TA SK_INTERRUPTIBLE(任务是内核数据结构,它包含着有关它所处的进程和系统调用的信息,如果存在系统调用的话),把它加入到 Wa itQ当中,这就意味着任务在被唤醒之前将不会运行。Wa itQ是等待访问文件的任务队列。然后,函数调用上下文调度程度,切换到另一个拥有 C PU时间的进程。在进程结束了文件操作以后,进程将关闭文件,并调用m odule_close。该函数将唤醒队列中的所有进程(没有只唤醒一个进程的机制),然后函数返回,刚刚关闭文件
42、的那个进程就可以继续运行了。如果那个进程的时间片用完了,则调度程度将会及时判断出这一点,并将C PU的控制权交给另一个进程。最后,等待队列中的某个进程将会被调度程序授予C PU的控制权。该进程将从紧接着调用m odule_interruptible_sleep_on后面的那个点开始执行。然后它会设置一个全局变量,告诉其它所有进程文件依然打开着,然后该进程将继续执行。当其它进程获得C PU时间片时,它们将看到那个全局变量,于是继续睡眠。更为有趣的是,并不是只有m odule_close才能唤醒那些等待访问文件的进程。一个信号,例如 C trl+C(SIGINT)也可以唤醒进程。在那种情况下,我们
43、希望立刻用E INTR返回。这是很重要的,只有这样用户才能在进程接收到文件之前杀死那个进程。还需要记住一点,有时候进程并不想睡眠;它们希望或者立刻拿到它们想要的东西,或者直接告诉它们这是不可能的。这样的进程在打开文件时使用标志O _NONBLOCK。内核在进行该操作时,将会返回错误代码E AGAIN来作响应,否则该操作就将阻塞,就像下面例子中的 打 开 文 件 操 作 一 样。本 章 的 源 目 录 中有一个程序c a t _ n o b l o c k,可以用于带O _NONBLOCK标志打开一个文件。第8章阻 塞 处 理191下载192第二部分Linux 内核模块编程指南下载第8章阻 塞 处 理193下载194第二部分Linux 内核模块编程指南下载