📄 driver.txt
字号:
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
</head>
<blockquote>
<blockquote>
<blockquote>
<div align="center">
<center>
<PRE><b><font color="#FF0000" size="5">MiniNurse(MN)驱动程序及所涉之内核代码的分析
</font></b> </PRE>
</center>
</div>
<PRE> 据称,在庞大的Linux内核源代码中,各种驱动程序占据了超过85%的容量。
本文会让这个数字继续增长。本文会按照下面的顺序逐步展开:
* <a name="概念中的驱动程序">概念中的驱动程序</a>
* <a name="Linux内核模块(LKM)与驱动程序以及一个范例模块">Linux内核模块(LKM)与驱动程序以及一个范例模块</a>
* <a name="一个范例性应用程序">一个范例性应用程序</a>
* <a name="MN的驱动程序">MN的驱动程序</a>
* <a name="第一次跟踪内核">第一次跟踪内核</a>
* <a name="更多内核设施和机制">更多内核设施和机制</a>
* <a name="一些讨论">一些讨论</a>
</PRE>
<PRE><b><font color="#0000FF" size="3"><a href="#概念中的驱动程序">— 概念中的驱动程序 —</a></font></b></PRE>
<PRE> 当人们向计算机插入甚至在购买一个新设备的时候,总会考虑一件事情:是
否能够同时获得其驱动程序。然而人们却很少考虑当他们使用着某个图形前
端或者守护进程时发生的一些鲜为人知的事实。让我们先做一个实验:</PRE>
<PRE> 1.cat /proc/interrupts 这个文件记录了到上次为止每个已注册中断源
收到的中断总数;
2.看看你有哪些设备,我们分别以键盘、鼠标和声卡为例;
3.键盘:用最少的办法重复刚才的命令(上箭头+回车),观察键盘中断
值的变化,6次中断!你还可以编写一个小的程序,每隔一秒钟读这个
文件一次,在每个一秒中内测试不同的键所产生的中断次数。
4.鼠标:测试的例子是一款USB鼠标,因此只能观察USB主控的中断情况。
用小拇指轻轻碰一下鼠标,让它产生不到1cm的位移。这次产生了17
个中断。
5.声卡:放一秒钟你最喜欢的那首歌曲,然后自己看看去。</PRE>
<PRE> 这个实验从一个很小的方面揭示了事情往往并不是想象的那么简单。然而它
还没有说明是什么使得一位普通的字处理软件使用者不用因为他所产生的天
文数字的中断次数而感到内疚?
是他所使用的字处理软件吗?不。中断是应该被尽量避免的概念,在现代操
作系统里,它甚至不能出现在用户空间代码里,这不符合设计哲学并且有显
而易见的安全问题。那么对这些的处理显然隐藏在操作系统中。
</PRE>
<PRE> 在教科书里可以看到无论是宏核还是微核操作系统都具备的几个经典模块,
如:进程调度、内存管理、文件系统等等。这些经典组件构筑了操作系统的
核心,或者站在调用的角度看这些组件实现了大部分基础设施。
同样工作在处理器特权级模式下的其它一些代码通过调用基础设施(在微核
中通过IPC)负责具体中断等资源的解释和响应。这些代码被称为驱动程序。
并不是所有的驱动程序都和一个物理上存在的设备对应,部分驱动为更高层
模块提供对底层的抽象服务,比如包过滤防火墙的实现;有些驱动提供一个
纯软设备,比如/dev/null。
</PRE>
<PRE> 在这里有必要说明一个核心的概念:文件系统,在Unix及其变种里,文件系
统处于很高的地位(这里说的文件系统并不是ext?/reiserfs等具体的文件系
统格式)。文件系统并不是硬件驱动程序,相反它是软件的驱动程序,它将
底层数据结构映射到高层数据结构,决定文件名长度以及目录项中所保存的
信息等等。文件系统凌驾于某个实际的磁盘操作之上,抽象出读、写等操作
和相关的数据结构。设备驱动程序(和其它所有的东西一样)也以文件的形式
出现在文件系统上,用户可以(只能)通过这些文件操纵设备,方法和打开一
个普通的文本文件是完全一致的。
</PRE>
<PRE> 围绕着文件系统,从另一个方面观察驱动程序的作用。在现代操作系统中,
对外的唯一接口被称为系统调用。核外程序员通常使用各种例程、库甚至其
它应用软件提供的二次开发接口编写他们自己的软件。在各式各样的库例程
以及应用程序自身就包含了大量的系统调用。为了论述方便,这里举一实例:
系统调用 ssize_t write (int filedes,
const void *buff,
size_t nbytes);
前面说过,使用write对支持的设备操作和用它写磁盘文件的方法是一样的。
一个磁盘文件和一台打印机的差距是如此之大,为什么同样的函数完成了截
然不同的操作?原因是操作系统“重载”了write“方法”。没错,这是两
个面向对象术语,尽管*nix没有使用OOP方法编译,但确实很自然地使用了
这个方法。重载后的write就是由各自的驱动程序实现的。这样看来,驱动
程序实现了部分系统调用。</PRE>
<PRE> 最后,从设计艺术的角度观察驱动程序。“机制”与“策略”的分离一直都
是隐藏在*nix设计背后最好的思想之一。这使得软件精简、幽雅从而导致健
壮性、柔性。我们可以用Gimp处理出漂亮的图片然后用打印机打印,也可以
用一张动力强劲的图形加速卡导演一场精彩的Quake大战。那么Gimp的代码
和打印机驱动代码/Quake代码和显卡驱动代码有何区别?从设计角度看,驱
动程序实现机制,而应用软件部署策略。好的系统设计是综合各方面因素后
的折中。
</PRE>
<PRE><b><font color="#0000FF" size="3"><a href="#Linux内核模块(LKM)与驱动程序以及一个范例模块">— Linux内核模块(LKM)与驱动程序以及一个范例模块 —</a></font></b></PRE>
<PRE> Linux内核属于宏内核,所有的模块包括驱动程序最终被连入一个单一的核
心,以一个唯一的内核“进程”运行。传统的宏核UNIX,每当增加一个新功
能都需要重新编译一次内核然后用新内核重新引导计算机(据说在某大型机
上编译一次Linux内核只需要数秒钟,但在我的机器上则至少需要20分钟左
右)。越来越多的驱动加在宏核中,对于x86这样有设计缺陷的平台也是个麻
烦并且浪费内存。不仅如此,传统宏核对新增代码的调试也是比较困难的。</PRE>
<PRE> 现代Unix之一:Linux提供了一种新的机制—可加载内核模块。允许模块在
被使用时才连入内核,不再使用时还可以被移走。多数驱动程序在允许配置
内核时选择被编译成模块或是直接编译进内核。</PRE>
<PRE> Linux和其它Unix变种一样拥有三类驱动程序:字符设备驱动、块设备驱动
和网络设备驱动。绝大多数驱动按照惯例把与之对应的设备文件放在/dev目
录中。用ls -l可以看到这些文件的详细信息,比如:
ls -l /dev/MiniNurse
会显示类似如下的信息(取决与你的计算机):
crw-rw-r-- 1 root root 8, 1 Jan 1 1970 /dev/MiniNurse
这行文字说明了一些重要的信息:/dev/MiniNurse是一个字符设备,它的主
设备号是8,次设备号是1。主设备号决定了驱动程序入口地址,在内核文档
中为多数已知设备分配了主设备,不能随意使用它们。然而MN的驱动使用了
一个新的机制,程序员不用再为主设备号烦恼,这个机制还带来了很多其它
的好处,后面我们会看到。次设备号通常内核放交给驱动程序自行使用,典
型的用法是在一个可重入的驱动中让不同的次设备号代表不同的设备实例。</PRE>
<PRE> 编写一个能够运行的模块比你能够想象的还要简单,但是一个真正能用的驱
动却需要具备对设备的充分理解和优化。下面是一个示例模块:</PRE>
<PRE> 1 <linux/module.h></PRE>
<PRE> 2 int init_module(void)
3 {printk("<1>Hi! This is kernel talk to you.\n");return 0;}</PRE>
<PRE> 4 void cleanup_module(void)
5 {printk("<1>Bey! Welcome to be a Hacker.\n");}</PRE>
<PRE> 6 MODULE_LICENSE("GPL");</PRE>
<PRE> 第一行包含了内核模块所需要的一些宏/数据结构等等。第二和第四行的函
数分别是模块的入口和出口,对于入口函数如果返回值非负,内核的模块管理
就会认为模块安装时有误。模块被加载和移除时执行这两个函数。可以看到
这个模块没有完成任何实际的功能,仅仅在被加载和移除时叫唤一声。编译
这模块使用编译指令"-D__KERNEL__ -DMODULE -O -Wall -c",编译成功后
会得到一个.o结尾的文件,这就是模块的二进制代码了。用
insmod/rmmod/lsmod可以管理它。
</PRE>
<PRE><font size="3" color="#0000FF"><b><a href="#一个范例性应用程序">— 一个范例性应用程序 —</a></b></font></PRE>
<PRE> 在看到这篇文章时,驱动和应用程序也许已经更新。因此确定手头的版本是
否与行文时相同:</PRE>
<PRE> usb-mininurse.c 233行 版本0.1.1
usb-mininurse.h 118行 版本0.1
usb-mn-ioctls.h 15行 (此文件被连接到应用程序代码目录,并与应用
程序共享)</PRE>
<PRE> flashLED.c 65行
turnLED.c 56行</PRE>
<PRE> 首先从终端用户的角度观察,仅以turnLED为例,flashLED与此绝类,不论。
在实际使用之前必须先编译。对于驱动程序,有一个Makefile,直接make就
可以了。对于范例应用程序:gcc turnLED.c -O3 -o turnLED。直接运行
turnLED会显示其帮助。turnLED需要两个参数。第一个参数表示命令,0表
示熄灭LED,1表示点亮LED;第二个参数为命令的对象,0和1分示左右两个
LED(取决于你的方向),A表示两个同时。比如,turnLED 1 A命令会同时点
亮两个LED;turnLED 0 1 会熄灭其中的一个。</PRE>
<PRE> 现在站在核外程序员的角度,打开turnLED的源代码。</PRE>
<PRE> 2 #include "usb-mn-ioctls.h"
这个头文件包含了三个宏:</PRE>
<PRE> #define MN_IOC_MAGIC 0x25
#define MN_LEDon IOR (MN_IOC_MAGIC,0,__u16)
#define MN_LEDoff IOW (MN_IOC_MAGIC,1,__u16)
这些宏的意义会在后面介绍。</PRE>
<PRE> 3 #include <fcntl.h>
这个头文件包含了打开文件时所必须的模式参数。</PRE>
<PRE> 4 #include <sys/ioctl.h>
这个头文件包含了下面要使用到的ioctl系统调用。</PRE>
<PRE> 5 #include <asm/types.h>
asm这个目录是根据平台连接到不同的目录,我的机器是PIII-C,所以asm指
向asm-i386。types.h包含了后面用到的__u16数据类型。__u16是这样被定
义的:
typedef unsigned short __u16
types.h的注释里指出使用双下划线前坠的目的是让用户空间代码使用时不
会产生命名冲突。Linux使用这个技术来保证在不同的处理器平台下,当需
要使用精确长度数据类型时使用同一个名字。例如当某个应用需要64位的无
符号整型时,可以使用u64(核外空间是__u64)来声明,在i386平台下u64被
定义为unsigned long long型,而64位的Power芯片平台下则是unsigned
long型。</PRE>
<PRE> 进入主函数首先是参数检查和语法说明,turnLED的设计很简单没有使用GNU
get_opt库,所以只能用0/1这样粗糙的方式控制。</PRE>
<PRE> 22 fd=open("/dev/MiniNurse",O_WRONLY);
23 if (fd<0) {
24 printf("CanNOT open Device.\n");
25 exit(-1);
26 }</PRE>
<PRE> 正如前面说到的,打开设备文件并返回文件描述符的操作与对一个普通文本
的操作没有任何区别。O_WRONLY标记指明文件以只读方式打开,因为MN固件
尚未实现从设备读的处理。</PRE>
<PRE> 紧跟着的一个switch是对第二个参数的处理,至于其中的0x04/0x08/0x0C,
可以参考固件和MN的连线。</PRE>
<PRE> 对第一个参数的处理包含着重要的语句:ioctl,正是这些语句让MN散发出
迷人的光芒。核外空间ioctl的原型长得比较奇怪:
int ioctl (int fd, int cmd, ...);
省略号表示可变参数,根据cmd的不同这个可变参数的意义/存在与否/参数
类型也不同(不要怀疑你对C语言的掌握,这个参数其实总是存在的,并且有
一个唯一的类型:char *argp)。参数cmd会原装传递给内核,附加的参数会
被作为unsigned long传递。不过要十分注意的是当附加参数是指针时,需
特别处理,原因是内核空间的核外空间使用不同的映射方法;并且也有可能
带来安全问题。MN的这个参数类型是u16,所以暂时不用担心这个问题。</PRE>
<PRE> 回到终端用户身份,再执行一次turnLED,不过这次同时使用一个特殊的程
序:
[root@Home show]# strace ./turnLED 1 A
execve("./turnLED", ["./turnLED", "1", "A"], [/* 53 vars */]) = 0
uname({sys="Linux", node="Home", ...}) = 0
brk(0) = 0x8049854
old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40013000
open("/etc/ld.so.preload", O_RDONLY) = -1 ENOENT (No such file or directory)
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -