第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 命令参数来编译它们。还有一点需要注意,在编译所有内核模块时,都将需要定义好某些特定的符号。
linux内核文件放在哪• _ _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.件,那么用户必须自己把它包含在源文件中,因为在定义了_ _NO_VERSION_ _的情况下,m o d u l e.h是不会为用户完成这个任务的。
2) 像平常一样编译所有的源文件。
3) 把所有的对象文件组合进一个文件中。在x 86下,可以使用命令:
ld -m elf_i386 -r -o〈模块名称〉.o (第一个源文件).o (第二个源文件) .o来完成这个任务。
下面是这种内核模块的一个例子。
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则取消该设备的注册。
注册某设备和取消它的注册是这两个函数最基本的功能。内核中的东西并不是按照它们自己的意愿主动开始运行的,就像进程一样,而是由进程通过系统调用来调用,或者由硬件
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论