《LINUX内核模块编程指南》由会员分享,可在线阅读,更多相关《LINUX内核模块编程指南(71页珍藏版)》请在金锄头文库上搜索。
1、下载 第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可以为内核的 某些东西
2、注册一个处理程序 或者也可以用自身的代码来取代某个内核函数 通常是先干点别 的什么事 然后再调用原来的函数 函数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 这个符号告诉头文件 这个程序代码将在内核模式下运行 而不要 作为用户进程的一部分来执
3、行 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 对称多处理 如果编译内核的目的是为了
4、支持对称多处理 在编译时就 需要定义这个符号 即使内核只是在一个 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身份编
5、译内核模块的吧 别玩什么惊险动作哟 然后根据自己的需要插入或删除 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 来选择当前终端 然后用户就可以看 到这个消息了 而另一方面 当用户使
6、用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时间时 它将显示该消息 但是一个不稳定的内核通常意味着系统将要崩 溃或者重新
7、启动 所以用户不希望推迟错误信息显示的时间 因为该信息可能会向用户解释什 么地方出了问题 如果显示的时刻晚于系统崩溃或重启的时刻 用户将会错过这个重要的信息 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
8、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章字符设备文件 我们现在就可以吹牛说自己是内核程序员了 虽然我们所写的内核模块还什么也干不了 但我们仍然
9、为自己感到骄傲 简直可以称得上趾高气扬 但是 有时候在某种程度上我们也 会感到缺少点什么 简单的模块并不是太有趣 内核模块主要通过两种方法与进程打交道 一种方法是通过设备文件 例如在目录 d e v中 的文件 另一种方法是使用 p r o c文件系统 既然编写内核模块的主要原因之一就是支持某些 类型的硬件设备 那么就让我们从设备文件开始吧 设备文件最初的用途是使进程与内核中的设备驱动程序通信 并且通过设备驱动程序再 与物理设备 调制解调器 终端等等 通信 下面我们要讲述实现这一任务的方法 每个设备驱动程序都被赋予一个主编号 主要用于负责某几种类型的硬件 可以在 p r o c d e v i
10、c e s中找到驱动程序以及它们对应的主编号的列表 由设备驱动程序管理的每个物理 设备都被赋予一个从编号 这些设备中的每一个 不管是否真正安装在计算机系统上 都将 对应一个特殊的文件 该文件称为设备文件 所有的设备文件都包含在目录 d e v中 例如 如果执行命令 ls l dev hd ab 用户将可以看到与一个计算机相连接的所有的 I D E硬件分区 注意 所有的这些硬盘分区都使用同一个主编号 3 但是从编号却各不相同 需要强调的是 这里假设用户使用的是 P C体系结构 我并不知道在其它体系结构上运行的 L i n u x的设备是怎么样的 在安装了系统以后 所有的设备文件都由命令 m k
11、n o d创建出来 从技术的角度上讲 并 没有什么特别的原因一定要把这些设备文件放在目录 d e v中 这只不过是一个有用的传统习 惯而已 如果读者创建设备文件的目的只不过是为了试试看 就像本章的练习一样 那么把 该设备文件放置在编译内核模块的目录中可能会更有意义一些 设备一般分为两种类型 字符设备和块设备 它们的区别在于块设备具有一个请求缓冲 区 所以块设备可以选择按照何种顺序来响应这些请求 这对于存储设备来说是很重要的 在存储设备中 读或写相邻的扇区速度要快一些 而读写相互之间离得较远的扇区则要慢得 多 另一个区别在于块设备只能以成块的形式接收输入和返回输出 块的大小根据设备类型的 变化而
12、有所不同 而字符设备则可以随心所欲地使用任意数目的字节 当前大多数设备都是 字符设备 因为它们既不需要某种形式的缓冲 也不需要按照固定的块大小来进行操作 如 果想知道某个设备文件对应的是块设备还是字符设备 用户可以执行命令 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 把该设备驱动程序加入到内核的字符设备驱动程序 表中 它还
13、会返回供驱动程序所使用的主编号 函数 c l e a n u p m o d u l e则取消该设备的注册 注册某设备和取消它的注册是这两个函数最基本的功能 内核中的东西并不是按照它们 自己的意愿主动开始运行的 就像进程一样 而是由进程通过系统调用来调用 或者由硬件 设备通过中断来调用 或者由内核的其它部分调用 只需调用特定的函数 它们才会执行 因 此 如果用户往内核中加入了代码 就必须把它作为某种特定类型事件的处理程序进行注 册 而在删除这些代码时 用户必须取消它的注册 设备驱动程序一般是由四个 d e v i c e 函数所组成的 如果用户需要处理具有对应 主编号的设备文件 就可以调用这四
14、个函数 通过 f i l e o p e r a t i o n s结构F o p s内核可以知道调用 哪些函数 因为该结构的值是在注册设备时给定的 它包含了指向这四个函数的指针 在这里我们还需要记住的一点是 无论如何不能乱删内核模块 原因在于如果设备文件 是由进程打开的 而我们删去了该内核模块 那么使用该文件就将导致对正确的函数 读 写 原来所处的存储位置的调用 如果我们走运 那里没有装入什么其它的代码 那我们至多得 到一些难看的错误信息 而如果我们不走运 在原来的同一位置已经装入了另一个内核模块 这就意味着跳转到了内核中另一个函数的中间 这样做的后果是不堪设想的 起码不会是令 人愉快的 一
15、般来说 如果用户不愿意让某件事发生 可以让执行这件事的函数返回一个错误代码 一个负数 而对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 中 得 到 因 为 有 些 宏
16、是 专 门 为 处 理 这 个 变 量 而 定 义 的 如 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