|
|
|
|
|
|

目录页 | 上一页 | 下一页

(LDD) Ch12-加载块设备驱动程序(转载)

发信人: Altmayer (alt), 信区: GNULinux
标  题: (LDD) Ch12-加载块设备驱动程序(转载)
发信站: 饮水思源 (2001年12月13日08:57:39 星期四), 站内信件
 
【 以下文字转载自 UNIXpost 讨论区 】
【 原文由 altmayer.bbs@bbs.nju.edu.cn, 所发表 】
 
【 以下文字转载自 altmayer 的信箱 】
 
第十二章 加载块设备驱动程序
 
 
 
正如在第一章“Linux核心简介”中“设备与模块的分类”中所概述的一样,Unix的设备
驱动程序并不仅限于字符设备。本章就来介绍一下第二大类的设备驱动程序——块设备
驱动程序。所谓面向块的设备是指数据传输是以块为单位的(例如软盘和硬盘),这里
硬件的块一般被称作“扇区(Sector)”。而名词“块”常用来指软件上的概念:驱动
程序常常使用1KB大小的块,即使扇区大小为512字节。
 
在这一章,我们将来构造一个全特征的块设备驱动程序sbull(Simple Block Utility
for Loading Localities)。这个驱动程序与scull类似,也是使用计算机的内存作为硬
件设备。换句话说,它是一个RAM-disk的驱动程序。sbull可以在任何Linux计算机上执
行(不过我只在有限的几个平台上作过测试)。

行(不过我只在有限的几个平台上作过测试)。
 
注册驱动程序
 
和字符设备驱动程序类似,核心里的块设备驱动程序也是由一个主设备号来标识。用来
对其进行注册和取消注册的函数是:
 
int register_blkdev(unsigned int major, const char*name, struct
file_operations *fops)
 
int unregister_blkdev(unsigned int major, const char*name);
 
参数的含义与字符设备驱动程序一样,对主设备号的动态赋值也类似。因此,一个sbull
设备与scull一样将自己注册:
 
result=register_blkdev(sbull_major,“sbull”,$sbull_fops);
 
if(result<0){
 
      printk(KERN_WARNING“sbull:can’t get major %d\n”,sbull_major);
 
      return result;
 

 
      }
 
if (sbull_major==0)  sbull_major=result;     /*dynamic*/
 
major=sbull_major;                       /*Use “major”later on to save
typing*/
 
register_blkdev 的fops参数与我们在字符设备驱动程序中使用的类似,为read,write
以及fsync的操作并不要求针对某个驱动程序。通用函数block_read, block_write及blo
ck_fsync被用来代替任何针对某个驱动程序的函数。另外,check_media_change和reval
idate对块设备驱动程序也有意义,二者都在sbull_fops中定义。
 
在sbull中使用的fops结构如下:
 
(代码236)
 
通用的读写操作被用来获得较高的性能。通过数据缓冲获得加速,这在字符设备驱动程
序重中是没有的。块设备驱动程序可以被缓冲是因为它们的数据服从于计算机的文件层
次结构,任何应用程序都无法直接访问,而字符设备驱动程序则不是这样。
 
不过,当缓冲的高速缓存不能满足一个读请求或当一个待处理的写操作要刷新到物理磁
盘上时,驱动程序必须被调用来进行真正的数据传送。fops结构除了read和write外,并

盘上时,驱动程序必须被调用来进行真正的数据传送。fops结构除了read和write外,并
不带有入口点,因此,必须要一个额外的结构blk_dev_struct来发出对实际数据传送的
请求。
 
这个结构在<linux/blkdev.h>定义,它有几个域,但只有第一个域需被驱动程序设置。
下面是这个结构在核心2.0中的定义。
 
(代码237)
 
当核心需要为sbull设备产生一个I/O操作时,它便调用函数blk_dev[sbull_major].requ
est_fn。因此这个模块的初始化函数须设置这个域使其指向它自己的请求函数。这个结
构中的其它域只供核心函数或宏进行内部使用;你不必在你的代码段中显式地使用它们

 
一个块设备驱动程序模块与核心的关系见图12-1。
 
除了blk_dev还有几个数组带有块设备驱动程序的信息。这些数组一般由主设备号(有时
也用次设备号)进行索引。它们在drivers/block/ll_rw_block.c中被声明和描述。
 
int blk_size[][];
 
这个数组由主设备号和次设备号索引。它以KB为单位描述了每个设备的大小。如果blk_s
ize[major]是NULL,则不对这个设备的大小进行检查(也就是说,核心可能要求数据传

ize[major]是NULL,则不对这个设备的大小进行检查(也就是说,核心可能要求数据传
送通过end_of_device)。
 
int blksize_size[][];
 
被每个设备所使用的块的大小,以字节为单位。与上一个数组类似,这个二维数组也是
由主设备号和次设备号索引。如果blksize_size[major]是一个空指针,那么便假设其块
大小为BLOCK_SIZE(目前是1KB)。块大小必须是2的幂,因为核心使用移位操作将偏移
量转换为块号。
 
int hardsect_size[][];
 
与其它的一样,这个数据结构也是由主设备号和次设备号索引。硬件扇区的缺省大小为5
12字节。直到包括2.0.X版本为止,可变扇区大小仍未真正支持,因为一些核心代码仍旧
假设扇区大小为半KB。不过很可能在2.2版本中会真正实现可变扇区大小。
 
int read_ahead[];
 
这个数组由主设备号索引,它定义了一个文件被顺序读取时,核心可以提前读取多少扇
区。在进程请求数据之前将其读出可以改善系统的性能及总的吞吐率。慢速设备最好指
定一个较大的提前读的值,而一个快速设备则可以在较小的提前读的值下工作的很好。
这个提前读的值越大缓冲高速缓存则需要越多的内存。每个主设备号有一个提前读的值
,它对所有次设备号有效。这个值可以通过驱动程序的ioctl方法来改变;硬盘驱动程序

,它对所有次设备号有效。这个值可以通过驱动程序的ioctl方法来改变;硬盘驱动程序
一般设为8个扇区,对应着4KB。
 
sbull设备允许在加载时设置这些值,它们作用于示例驱动程序的所有次设备号。在sbul
l中变量名和它们的缺省值为:
 
size=2048(KB)
 
由sbull生成的每个ramdisk占两兆字节,正如系统的缺省值。
 
hardsect=512(B)
 
sbull扇区大小是常用的半KB值。改变hardsect的值是不允许的。
 
如前所述,其它的扇区大小并不被支持。如果你一定要改它,可以将sbull/sbull.c中的
安全检查去掉。不过请做好发生严重的内存崩溃的危险的准备。除非在你尝试时,已经
加上了对可变扇区大小的支持。
 
rahead=2(扇区)
 
因为ramdisk是一个快速设备,所以这个缺省提前读的值比较小。
 
sbull设备也允许你选择一个设备个数进行安装。devs是设备个数,缺省设为2,表明缺

sbull设备也允许你选择一个设备个数进行安装。devs是设备个数,缺省设为2,表明缺
省内存使用量为4兆——2个大小为2MB的盘。
 
sbull设备的init_module的实现如下(不含主设备号的注册和错误恢复):
 
(代码239)
 
相应的清除函数如下所示:
 
(代码240)
 
这里,调用fsync_dev是必须的,用以清除核心保存在不同高速缓存中的对设备的所有引
用。事实上,fsync_dev是运行在block--_fsync之后的引擎,它是块设备的fsync“方法
”。
 
头文件 blk.h
 
由于块设备驱动程序的绝大部分是设备无关的,核心的开发者通过把大部分相同的代码放
在一个头文件<linux/blk.h>中,来试图简化驱动程序的代码。因此,每个块设备驱动程
序都必须包含这个头文件,在<linux/blk.h>中定义的最重要的函数是end_request,它
被声明为static(静态)的。让它成为静态的,使得不同驱动程序可有一个正确定义的e
nd_request,而不需要每个都写自己的实现。
 

 
在Linux1.2中,这个头文件应该用<linux/../../drivers/block/blk.h>来包含。原因在
于当时还不支持自定义的块设备驱动程序,而这个头文件最初位于drivers/block源码目
录下。
 
实际上,blk.h相当不寻常,比如它定义了几个基于符号MAJOR_NR的符号,而MAJOR_NR必
须由驱动程序在它包含这个头文件之前声明。这里,我们再次看到blk.h在设计时并没有
真正考虑自定义驱动程序。
 
看看blk.h,你会发现几个设备相关的符号是按照MAJOR_NR的值声明的,也就是说MAJOR_
NR应该提前知道。然而,如果主设备号是动态赋值的,驱动程序无法预知其值,因此也
就不能正确定义MAJOR_NR。如果MAJOR_NR未定义,blk.h就不能设定一些在end_request
中使用的宏。因此,为了让自定义驱动程序从通用的end_request函数受益,从而避免重
新实现它,驱动程序必须在包含blk.h之前定义MAJOR_NR和其它几个符号。
 
下面的列表描述了一些必须提前定义的<linux/blk.h>中的符号。列表结尾给出了sbull
中使用的代码。
 
MAJOR_NR
 
这个符号用来访问一些数组,特别是blk-_dev和blksize-_size。自定义驱动程序(如sb
ull)不能给这个符号赋一个常量值,可以将其定义(#define)为一个存有主设备号的
变量。对sbull而言,它是sbull-_major。

变量。对sbull而言,它是sbull-_major。
 
DEVICE_NAME
 
       被生成的设备名。这个字符串用来从end_request中打印错误信息。
 
DEVICE_NR(kdev_t device)
 
这个符号用来从kdev_t设备号中抽取物理设备的序号。这个宏的值可以是MINOR(device)
或别的表达式。这要依据给设备或分区分配次设备号的常规方式而定。对同一个物理设
备上的所有分区,这个宏应返回同一个设备号——也就是说,DEVICE_NR表达的是磁盘号
,而不是分区号。这个符号被用来声明CURRENT_DEV,它在request_fn中用来确定被一个
传送请求访问的硬件设备的次设备号,可分区设备将在后面“可分区设备”一节中介绍

 
DEVICE_INTR
 
这个符号用来声明一个指向当前下半部处理程序的指针变量。宏SET_INTR(intr)和CLEAR
_INTR用来给这个变量赋值。当设备可以发出具有不同含义的中断时,使用多个处理程序
是很方便的。这个主题将在后面“中断驱动的块设备驱动程序”一节中讨论。
 
TIMEOUT_VALUE
 

 
DEBICE_TIMEOUT
 
TIMEOUT_VALUE以记数的方式表达超时,这个超时的值与老计时器之一(特别地指计时器
号DEVICE_TIMEOUT)相关联。一个驱动程序可以在数据传送时间太长时,通过调用一个
回调函数来检测错误条件。不过,由于老计时器由一个预赋值的计时器静态数组组成(
见第六章“时间流”中“核心计时器”一节),一个自定义的驱动程序不能使用它们。
我在sbull中对这两个符号都未定义,而是用一个新的计时器实现超时。
 
DEBICE_NO_RANDOM
 
在缺省情况下,函数end_request对系统熵值(即所有随机性的总量)有所贡献,这被/d
ev/random所使用。如果一个设备不能对随机设备贡献显著的熵值,DEVICE_NO_RANDOM应
被定义。/dev/random在第九章的“安装中断处理程序”中进行了介绍,SA_SAMPLE_RAND
OM也在那儿做了解释。
 
DEVICE_OFF(kdev_t device)
 
end_request函数在结束时调用这个宏。例如在软盘驱动程序中,它调用一个函数,这个
函数负责更新用来控制马达停转的一个计时器。如果设备没有被关掉,那么串DEVICE_OF
F可以被定义为空。sbull不使用DEVICE_OFF。
 
DEVICE_ON(kdev_t device)

DEVICE_ON(kdev_t device)
 
DEVICE_REQUEST
 
这些函数实际上并未在Linux的头文件中使用,所以驱动程序并不需要定义它们。大多数
官方的Linux设备驱动程序声明这些符号并在内部使用它们,但我在sbull里并没有使用
它。
 
sbull驱动程序以如下的方式声明这些符号:
 
(代码242)
 
头文件blk.h用上面列出的这些宏定义了一些可以由驱动程序使用的额外的宏,我将在后
续章节里对之进行介绍。
 
处理请求
 
系统性能的方式排序。这些联结表中的请求被传递个驱动程序的请求函数,由它对链接
表中的每个请求执行如下的任务:
 
l       检查当前请求的有效性。这个工作由在blk.h中定义的宏INIT_REQUEST完成。
 
l       进行实际的数据传送。用变量CURRENT(实际上是个宏)可以获得发出请求的一

l       进行实际的数据传送。用变量CURRENT(实际上是个宏)可以获得发出请求的一
些细节。CURRENT是一个指向结构request的指针,我将在下节介绍这个结构的域。
 
l       清除当前的请求。这个操作由静态函数end_request完成,函数的代码在blk.h
中。驱动程序向这个函数传递一个参数,即成功时为1,失败时为0。当end_request以参
数0调用时,一个“I/O error”消息会被发给系统日志(通过printk)。
 
l       循环回至开始,消化下一个请求。可以按照程序员的喜好使用一个goto或是一
个for(;;),或者while(1)。
 
实践中,请求函数的代码如下构造:
 
(代码243)
 
尽管这段代码除了打印消息外什么都没有做,运行这个函数可以对数据传送的基本设计
有一个很好的了解。到此为止,代码中唯一不清楚的地方是CURRENT的确切含义及它的域
,这个我将在下一节介绍。
 
我的第一个sbull实现只包含了所示的空代码。我意在一个“不存在”的设备上构造一个
文件系统,并使用它一会儿,只要数据仍在缓冲高速缓存中。在运行一个象这样罗嗦的
请求函数时,看看系统日志能帮助你理解缓冲高速缓存是如何工作的。
 
在编译时,定义符号SBULL_EMPTY_REQUEST,那么这个空且罗嗦的函数可以在sbull 中运

在编译时,定义符号SBULL_EMPTY_REQUEST,那么这个空且罗嗦的函数可以在sbull 中运
行。如果你想理解核心是如何处理不同块大小的,你可以在insmod命令行上实验blksize
=。这个空的请求函数通过打印每个请求的细节揭示了内部核心的工作情况。你或许也可
以试试hardsect=,但目前它被关闭了,因为比较危险。(见本章开始时的“注册驱动程
序”)。
 
请求函数的代码并不显式地调用return(),因为当列表中的待处理请求耗尽时,INIT-_RE
QUEST会替你完成这个工作。
 
执行实际的数据传送
 
为了给sbull构造一个可以工作的数据传送,让我们先来看看核心是如何在结构request
中描述一个请求的。这个结构在<linux/blkdev.h>中定义。通过访问CURRENT的域,驱动
程序可以得到所有为在缓冲高速缓存的物理块设备之间传送数据所需要的信息。
 
CURRENT是用来访问当前请求(即被首先服务的那个请求)。正如你可能猜到的,CURREN
T是blk_dev[MAJOR_NR].current_request的缩短形式。
 
下面这些当前请求的域包含了请求函数的有用信息:
 
kdev_t rq_dev;
 
请求所访问的设备。有本驱动程序所管理的所以设备均被使用同一个请求函数。一个请

请求所访问的设备。有本驱动程序所管理的所以设备均被使用同一个请求函数。一个请
求函数处理所有的次设备号;rq_dev可以被用来取得被操作的次设备。尽管Linux1.2称
这个域为dev,你仍然可以通过宏CURRENT_DEV来访问这个域。CURRENT_DEV在我们所讨论
的所有版本的核心中是可移植的。
 
int cmd;
 
这个域是READ或WRITE。
 
unsigned long sector;
 
请求指向的第一个扇区。
 
unsigned long current_nr_sectors;
 
unsigned long nr_sectors;
 
当前请求的扇区数(大小)。驱动程序应该引用current_nr_sectors,而应该忽略nr_sec
tors(列在这里只是为了完整)。请看下一节“集簇请求”以获得更多的细节。
 
char *buffer
 
缓冲高速缓存中的域。如果cmd==READ,就是写数据的位置;如果cmd==WRITE,就是读数

缓冲高速缓存中的域。如果cmd==READ,就是写数据的位置;如果cmd==WRITE,就是读数
据的位置。
 
struct buffer_head *bh
 
这个结构描述了这个请求列表中的第一个缓冲区。我们将在“集簇请求”中用到这个域

 
在这个结构中还有其它的一些域,但它们基本上是核心内部使用的,驱动程序并不期望
使用它们。
 
sbull中可工作的请求函数的实现如下所示。在下面的代码中sbull-_devices与scull_de
vice类似。我们在第三章字符设备驱动程序的“打开方法”中介绍过scull_devices。
 
(代码245)
 
由于sbull只是个RAM盘,所以它的“数据传送”简化为一个memcpy调用。这个函数唯一
“奇怪”的特征是条件语句中限制只能报告最多5个错误。这样做的目的是为了防止系统
日志被太多的信息搞乱,因为end-_request(0)在请求失败时已打印了“I/O error”的
消息。静态计数器是限制消息报告的标准做法,在核心中被多次用到。
 
集簇请求
 

 
上面请求函数中每次循环迭代都传送几个扇区——按照数据的使用,一般情况下,相当
于一个块的“数据”量。例如,交换一次执行PAGE_SIZE大小的数据,而在ext2文件系统
中就是传送1KB的块。
 
尽管在I/O中最方便的数据大小是一个块,但如果把相邻块的读或写集簇起来,你会获得
很高的性能改善。在这个意义上,“相邻”指的是在硬盘上块的位置,而“连续”则指
连续的内存区域。
 
将相邻块集簇有两个好处。首先,集簇加速了传送(例如,软盘驱动程序将相邻的块组
合在一起,一次传送一个磁道的数据)。另外,它还能通过避免分配冗余的request结构
来节省核心中的内存。
 
如果你愿意,也可以完全忽略集簇。上面给出的框架请求函数在没有集簇的情况下可以
完全正确地工作。不过,如果你想利用集簇,你需要更加仔细地研究struct_request的
内部。
 
不幸的是,我所知道的所有的核心(至少到2.1.51)都不能为自定义驱动程序进行集簇
,而只对象SCSI和IDE这类内部驱动程序使用。如果你对核心的内部不感兴趣,你可以跳
过本节的其余部分。不过,集簇将来还可能在模块中实现,它是通过减少相邻扇区的请
求延迟来提高数据传送性能的一个有趣的途径。
 
在我描述驱动程序如何利用集簇请求之前,让我们先来看看当一个请求被排队时发生了

在我描述驱动程序如何利用集簇请求之前,让我们先来看看当一个请求被排队时发生了
什么。
 
当核心请求数据块传送时,它扫描目标设备的活动请求链表。当一个新块在盘上与一个
已经被请求的块相邻时,它就被集簇到第一个块上。当前已存在的请求便被扩大了而不
是增加一个新请求。
 
不幸的是,磁盘上相邻的两个数据缓冲区在内存中并不一定相邻。这个发现,外加上需
要有效地管理缓冲高速缓存,导致创建一个buffer_head结构。一个buffer_head和一个
数据缓冲相关联。
 
因此,一个“集簇”的请求,就是一个指向buffer_head的结构链表的request_struct结
构。end_request函数负责这个问题,这就是为什么前面给出的请求函数可以独立于集簇
而工作。换句话说,end_request要么清除当前请求并准备为下一个服务,要么准备处理
同一个请求中的下一个缓冲区。因此,集簇对不关心它的设备驱动程序是透明的,上面
的sbull函数就是一个例子。
 
一个驱动程序可能希望通过在它的request_fn函数中每次循环时处理整个缓冲区头链表
的办法来从集簇中获益。为了做到这一点,驱动程序应该指向CURRENT->current_nr_sec
tors(这个域我在上面的sbull_request中已经用过)和CURRENT->nr_sectors,它包含
了集簇在“当前”buffer_heads列表中的相邻扇区的数目。
 
当前缓冲区头是CURRENT->bh,而数据块是CURRENT->bh->b_data。后一个指针为了象sbu

当前缓冲区头是CURRENT->bh,而数据块是CURRENT->bh->b_data。后一个指针为了象sbu
ll一类忽略集簇的驱动程序缓冲在CURRENT->buffer中。
 
请求集簇在drivers/block/ll_rw_block.c的函数make_request中实现。不过,如上所说
,集簇只对几个驱动程序有效(软驱,IDE,和SCSI),以其主设备号为准。我曾通过以
major=34装载sbull看到过集簇是如何工作的,因为34是IDE3_MAJOR,而我的系统中没有
第三个IDE控制器。
 
下面列表总结了当扫描一个集簇请求时应做的事项。bh是被处理的缓冲区头——列表的
第一项。对列表中的每个缓冲区头,驱动程序要完成下面一系列操作:
 
l        传送位于地址bh->b_data,大小为bh->b_size字节数据块。数据传送的方向通
常由CURRENT->cmd指出。
 
l        从列表中找出下一个缓冲区头:bh->b_request。接着通过将b_request置为0
,把刚传送过的缓冲区从列表中摘下。b_reqnext指向你刚找出的新缓冲区。
 
l        通过调用mark_buffer_uptodate(bh,1),unlock_buffer(bh),告诉核心你已
完成对上个缓冲区的操作。这些调用保证缓冲高速缓存保持正确,不致有错误指向的指
针。mark_buffer_uptodate中参数“1”表示传送成功,若传送失败,则换为0。
 
l        循环回到开始,传送下一个相邻块。
 

 
当你做完了集簇请求,CURRENT->bh必须被更新以指向“已经被处理但未被解锁”的第一
个缓冲区。如果列表中所有的缓冲区都已被处理和解锁,CURRENT->bh可被置为NULL。
 
此时,驱动程序可以调用end_request。如果CURRENT->bh是有效的,那么这个函数在转
到下一个缓冲之前对其进行解锁——这是非集簇操作所发生的情况,此时由end_request
照管所有的事情。如果指针为空,这个函数直接转到下一个请求。
 
全功能的集簇实现出现在driver/block/floppy.c,而要求的所有操作出现在blk.h的end
_request中。floppy.c和blk.h都不容易理解,不过建议先从后者开始。
 
安装(Mounting)是如何工作的
 
块设备与字符设备及一般文件的不同在于它们可以被安装到计算机的文件系统上。这与
一般的访问方式不同。一般的访问方式通过结构file进行,这个结构与特定的进程相关
联,并且只在open到close之间存在。当一个文件系统被安装后,没有进程拥有一个filp

 
当核心把一个设备安装到文件系统上,它调用一般的open方法来访问驱动程序。然而,
这种情况下open的参数filp是个虚的变量,几乎只是为了占个地方,它唯一有意义的域
是f_mode。其它域含任意值并不使用。f_mode的值是告诉驱动程序设备是以只读(f_mod
e==FMODE_READ)还是读写(f_mode==(FMODE_READ|FMOD_WRITE))方式被安装。使用
一个虚变量而不是file结构的原因是因为实际的结构file在进程结束时将被释放,而被

一个虚变量而不是file结构的原因是因为实际的结构file在进程结束时将被释放,而被
安装的文件系统在mount命令完成后仍然存在。
 
在安装时,驱动程序唯一调用的是open方法。当磁盘被安装后,核心调用设备中的read
和write 方法(被映射到request_fn)来管理文件系统中的文件。驱动程序并不知道req
uest_fn服务的是一个进程(象fsck)还是核心中的文件系统层。
 
至于umount,它只是刷新缓冲高速缓存并调用驱动程序的release(close)方法。由于
没有有意义的filp可以传递给fop->realse,核心使用NULL。
 
因此,当你实现release时,你应将驱动程序设为能处理为NULL的filp指针。不然,如果
你用了filp,你可能运行mkfs和fsck,它们都使用filp来访问设备,你也可能mount这个
设备,但umount将无法运行,原因就是NULL指针。
 
由于一个块设备驱动程序的release实现不能用filp->private_data来访问设备信息,它
采用inode->i_rdev来区分设备。这里是release的sbull实现:
 
(代码249)
 
其它的驱动程序函数并不关心filp问题,因为它们与安装的文件系统无关。例如,一个
显示地open这个设备的进程只发出ioctl。
 
ioctl方法

ioctl方法
 
如字符设备一样,块设备也可以通过ioctl系统调用进行操作。两者之间相对不一样的地
方在于块设备驱动程序有大量驱动程序都要支持的ioctl命令。
 
块设备驱动程序经常要处理的命令如下所示,它们在<linux/fs.h>中被声明。
 
BLKGETSIZE
 
获取当前设备的大小,以扇区数表示。由系统调用传递的 数值arg是一个指向long数值
的指针,用来将大小拷贝到一个用户空间的变量中。这个ioctl命令可以被mkfs用来获知
产生的文件系统的大小。
 
BLKFLSBUF
 
字面上的意思是“刷新缓冲区”。这个命令的实现对每个设备都是一样的,我们将在后
面整个ioctl方法的示例代码中给出来。
 
BLKRAGET
 
用来为设备取得当前提前读的值。当前数值应该用在参数arg中传递给ioctl的指针写进
一个long类型的用户空间变量。
 

 
BLKRASET
 
设置提前读的值。用户进程在arg中传递这个新值。
 
BLKRRPART
 
重读分区表。这个命令只对可分区设备有意义,将在后面“可分区设备”中介绍。
 
BLKROSET
 
BLKROGET
 
这些命令用来改变和检查设备的只读标志。因为代码是设备无关的,它们由宏RO_IOCTLS
(kdev_tdev,unsigned long where)来实现。这个宏在blk.h中定义。
 
HDIO_GETGEO
 
在<linux/hdreg.h>中定义,用来获得磁盘的几何参数。这个参数应被写入用户空间的结
构hd_geometry中,它也在hdreg.h中定义。sbull显示了这个命令的一般实现。
 
HDIO_GETGEO是<linux/hdreg.h>中定义的一系列HDIO命令中最常用的一个。感兴趣的读
者可以查看ide.c和hd.c以获得这些命令的更多信息。

者可以查看ide.c和hd.c以获得这些命令的更多信息。
 
这里列出的这些命令的一个主要缺点是它们是以“老”方法定义的(是第五章“增强的
字符设备驱动程序操作”中“选择ioctl命令”一节),因此无法使用位域的宏来减化代
码——每个命令要实现它自己的verify_area。不过,如果一个驱动程序需要定义它自己
的命令来利用设备的一些特殊特点,你可以自由地使用“新”方法来定义命令。
 
sbull设备只支持上面的通用命令,因为实现设备特定的命令与实现字符设备驱动程序的
命令没有什么不同。sbull的ioctl实现如下所示,它将有助于你理解上面列出的命令。
 
(代码250)
 
(代码251)
 
函数开始的PDEBUG语句被留出,这样当你编译这个模块时,你可以打开调试(debugging
)来看看设备上调用了哪个ioctl命令。
 
例如,对于显示的ioctl命令,你可以在sbull上使用fdisk。下面是在我自己系统上的一
个示例执行过程:
 
(代码252 1#)
 
在会话过程中下面的消息出现在我的系统日志中:

在会话过程中下面的消息出现在我的系统日志中:
 
(代码252 2#)
 
第一个ioctl是HDIO_GETGEO,它在fdisk启动时被调用;第二个是BLKRRPART。对后一个
命令的sbull实现仅仅是调用一下revalidate函数,它则在打印输出中打印最后的消息(
见本章后面的“revalidate”)。
 
可拆卸的设备
 
在我们讨论字符设备驱动程序时,我们忽略了fops结构中的最后两个文件操作,因为它
们只是为可拆卸块设备而设的。现在是看看它们的时候了。sbull并不真是可拆卸的,但
它假装是,因此它实现了这些方法。
 
我所说的操作是check_media-_change和revalidate。前者用来发现设备自上次访问以来
是否改变过,后者则在磁盘变动之后重新初始化驱动程序的状态。
 
至于sbull,与设备相联的数据区在使用计数下降为零后半分钟要释放。待这个设备处于
未安装状态(或关闭状态)足够长的时间以模拟一次磁盘的改变,下一次对设备的访问
分配一个新的内存区域。
 
这一类的“时间到期”通过一个核心计数器来实现。
 

 
check_media_change
这个检查函数接收到kev_t做为一个确定设备的参数。如果介质被改变了返回值为1,否
则为0。如果一块设备驱动程序不支持可拆卸设备,可以通过置fops->check_media_chan
ge为NULL来避免这个声明函数。
 
有趣的是要注意,当一个设备是可拆卸的,但却无法判断它是否改变了,这时,返回1是
个安全选择。事实上,IDE驱动程序在处理可 鹦洞排淌 就是这么做的。
 
sbull的实现是这样的,当由于计数器超时,设备已经从内存中删除时就返回1,如果数
据仍然有效则返回0。如果设置了调试,它同时向系统日志打印一条消息,这样用户就可
以检查核心什么时候调用了这个方法。
 
(代码253 1#)
 
revalidate
 
这个有效化函数是在检测到一个磁盘的改变时被调用。它也被在核心的2.1版中实现的各
种stat系统调用。返回值目前不做使用;为安全起见,返回0表示成功,出错时返回一个
负的错误代码。
 
revalidate执行的动作是设备特定的,但revalidate通常更新一些内部状态信息以反映
新的设备。

新的设备。
 
在sbull中,revalidate方法在没有一个有效区域的情况下试图分配一块新的数据区域。
 
 
(代码253  2#)
 
(代码254  1#)
 
特别注意
 
当可拆卸设备已经打开时,驱动程序也应该检查是否有磁盘的改变;在mount时核心自动
调用它的check-_disk_change函数,但在open时,并不这样做。
 
不过,有些程序直接访问磁盘数据而不安装这个设备,fsck,mcopy和fdisk都是这类程
序的例子。如果驱动程序在内存中保存可拆卸设备的状态信息,它应在设备第一次打开
时调用check_disk_change函数。这个核心函数还要依赖驱动程序方法(check_media_ch
ange和revalidate),因此在open里不须实现任何特别的东西。
 
这里是open的sbull实现,它关注了发生磁盘改变的情况:
 
(代码254  2#)
 

 
在驱动程序中不需对磁盘的改变做任何别的。如果一个磁盘被改变了,而它的打开计数
大于零,那么数据会被破坏。防止这种情况发生的唯一方法是让利用在物理上支持的设
备使用计数控制门锁。open和close可以在合适的时候关闭或打开锁。
 
可分区设备
 
如果你想用fdisk生成分区,你会发现它们有一些问题。fdisk程序称这些分区为/dev/sb
ull01,/dev/sbull02以 此类推,但文件系统上并不存在这些名字。的确,基本的sbull
设备是一个字节阵列,不存在提供访问数据区域的子区域的入口点,因此想对sbull进行
分区是行不通的。
 
为了能对设备分区,我们必须给每个物理设备分配几个次设备号。一个数字用来访问整
个设备(如/dev/hda),其它的用来访问不同的分区(如/dev/hda1)。由于fdisk产生
分区名的办法是在全盘设备名后加一个数字后缀,我们将在后面的块设备驱动程序中遵
循同样的命名规则。
 
在本节中我将要介绍的设备叫spull,因此它是一个“简单的可分区工具(Simple
Partitionable Utility)”。这个设备位于spull目录,完全与sbull无关,尽管它们共
享很多代码。
 
在字符设备驱动程序scull中,不同的次设备号可以实现不同的行为,因此一个驱动程序
可以显示几种不同的实现。而按照次设备号区分块设备是不可行的,这就是为什么sbull

可以显示几种不同的实现。而按照次设备号区分块设备是不可行的,这就是为什么sbull
和spull被分离开。这种无能为力是块设备驱动程序的一个基本特征,因为几个数据结构
和宏只是作为主设备号的函数定义的。
 
关于移植,需要注意的是可分区模块不能被加载到核心的1.2版,因为符号resetup_one_
dev(在本节后面介绍)没有被引出到模块。在对SCSI盘的支持模块化之前,没有人会考
虑可分区的模块。
 
我要介绍的设备结点被称做pd,表示“可分区磁盘(partitionable disk)”。四个完
整的设备(又称“单元”)被称做/dev/pda直到/dev/pdd;每个设备最多支持15个分区
。次设备号有下面的含义:低四位表示分区号(0为完整的设备),高四位表示单元号。
这个规则在源文件中由下面的宏表达:
 
(代码255)
 
普通硬盘
 
每个可分区设备需要知道它是如何分区的。这个信息可以从分区表中得到。初始化进程
的一部分包括解码分区表,并更新内部数据结构以反映分区信息。
 
这个解码并不容易。不过幸运的是,核心提供可被所有块设备驱动程序使用的“普通硬
盘”支持,它显著地减少了处理分区驱动程序的代码。这个普通支持的另一个好处是驱
动程序的作者不必理解分区是如何完成的,而不需要修改驱动程序的代码就可以在核心

动程序的作者不必理解分区是如何完成的,而不需要修改驱动程序的代码就可以在核心
中支持新的分区方式。
 
想要支持分区的块设备驱动程序要包含<linux/genhd.h>,并声明结构gendisk。所有这
样的结构被组织在一个链表中,它的头是全局指针gendisk_head。
 
在我们进行下一步之前,让我们先看看结构gendisk的域。你为了利用普通设备支持就需
要理解它们。
 
int major
 
确定这个结构所指的设备驱动程序的主设备号。
 
const char*major_name
 
属于这个主设备号的设备的基本名。每个设备名是通过在这个名字后为每个单元加一个
字母并为每个分区加一个数字得到。例如,“hd”是用来构成/dev/hda1和/dev/hda3的
基本名。基本名最多5个字符长,因为add_partition在一个8字节的缓冲区中构造全名,
它要附加上一个确定单元的字母,分区号和一个终止符‘\0’。spull所用的名字是pd(
“可分区磁盘(partitionable disk)”)。
 
int minor_shift
 

 
从设备的次设备号中获取驱动器号要进行移位的次数。在spull中这个数是4。这个域中
的值应与宏DEVICE_NR(device)中的定义一致(见本章前面的“头文件blk.h”)。spull
中的宏扩展为device>>4。
 
int max_p
 
分区的最大数目。在我们的例子中,max_p1是16,或更一般地,是<<minor_shift。
 
int max-_nr
 
单元的最大数目。在spull中,这个数字是4。单元最大数目在移位minor_shift次后的结
果应匹配次设备号的可能的范围,目前是0-255。IDE驱动程序可以同时支持很多驱动器
和每一个驱动器很多分区,因为它注册了几个主设备号,从而绕过了次设备号范围小的
问题。
 
void(*init)(struct gendisk*)
 
驱动程序的初始化函数,它在初始化设备后和分区检查执行前被调用。我将在下面介绍
这个函数更多的细节。
 
struct hd_struct *part
 

 
设备的解码后的分区表。驱动程序用这一项确定通过每个次设备号哪些范围的磁盘扇区
是可以访问的。大多数驱动程序实现max_nr<<minor_shift个结构的静态数值,并负责数
组的分配和释放。在核心解码分区表之前驱动程序应将数组初始化为零。
 
int *sizes
 
这个域指向一个整数数组。这个数组保持着与blk_size同样的信息。驱动程序负责分配
和释放该数据区域。注意设备的分区检查把这个指针拷贝到blk_size,因此处理可分区
设备的驱动程序不必分配这后一个数组。
 
int nr_real
 
存在的真实设备(单元)的个数。这个数字必须小于等于max_nr。
 
void *real_devices
 
这个指针被那些需要保存一些额外私有信息的驱动程序内部使用(这与filp->private_d
ata类似)。
 
void struct gendisk *next
 
在普通硬盘列表中的一根链。

在普通硬盘列表中的一根链。
 
分区检查的设计最适合那些直接链入核心映象的驱动程序,因此我将从介绍核心代码的
基本结构开始。以后我将介绍spull模块处理它的分区的方法。
 
核心中的分区检测
 
在引导时,init/main.c调用了各种各样的初始化函数。其中一个是start_kernel,它通
过调用device_setup来初始化所有的驱动程序。这个函数又调用blk_dev_init,接着检
查所有注册的普通硬盘的分区信息。任何一个块设备驱动程序,如果它找到至少一个它
的设备,就将这个驱动程序的genhd结构注册到核心列表中,这样它的分区便可以被正确
地检测出来。
 
因此,一个可分区的驱动程序应该声明它自己的结构genhd。这个结构看起来如下:
 
(代码258)
 
于是,在这个驱动程序的初始化函数中,这个结构被排队在可分区设备的主列表中。
 
被链入核心的驱动程序的初始化函数与init_module等价,即使它被调用的方式不同。这
个函数一定包含如下两行,它们用来将结构排队:
 
my_gendisk.next=gendisk_head;

my_gendisk.next=gendisk_head;
 
gendisk_head=my_gendisk;
 
通过将结构插入链表,这简单的两行便是驱动程序入口点为所有的分区正确地识别和配
置所需要的所有内容。
 
额外的设置通过my_geninit完成。在上面的例子中,这个函数填充“单元数”域来反映
计算机系统的实际硬件设置。在my_geninit结束后,gendisk.c为所有的盘(单元)执行
实际的分区检测。你可以看到系统启动时被检测的分区,因为gendisk.c在系统控制台上
打印分区检查Partition check:,后面跟随它在可得的普通硬盘上找到的所有分区。
 
你可以修改前面的代码,推迟my_sizes和my_partitions的分配直到my_geninit函数。这
可以节省少量的核心内存,因为这些数组可以小到nr_real<<minor_shift,而竟态数组
则必须为max_nr<<minor_shift字节长。不过,典型的数值是每个物理单元节省几百个字
节。
 
模块中的分区检测
 
一个模块化的驱动程序和链接到核心的驱动程序的区别在于它无法受益于集体中的初始
化。相反,它需要处理它自己的设置。由于没有为模块的两步初始化,所以spull的gend
isk结构在它的init函数指针中有一个NULL指针:
 

 
(代码259 1#)
 
同时也不必在普通硬盘的全局链表里注册gendisk结构。
 
通过引出函数resetup_one_dev,文件gendisk.c被准备用来处理象模块需要一类“晚的
”初始化。resetup_one_dev为单个物理设备扫描分区。其原型是:
 
boid resetup_one_dev(struct gendisk *dev,int drive);
 
从这个函数名字你可以看出来它是要改变一个设备的设置信息。这个函数被设计为由ioc
tl里BLKRRPART实现调用,但他也可以被用来完成一个模块的初始设置。
 
当一个模块被初始化后,它应该为每个它将要访问的物理设备调用resetup_one_dev,从
而将分区信息贮存my_gendisk->part中。分区信息会被设备的request_fn函数使用。
 
在spull中,init_module函数除了通常的指令外还包含了下面的代码。它分配分区检测
所需的数组并初始化数组中完整磁盘的项目。
 
(代码259 2#)
 
(代码260 1#)
 

 
有趣的是注意到resetup_one_dev通过重复调用下面函数打印分区信息:
 
printk(“%s:”,disk_name(hd,minor,buf));
 
这就是为什么spull要打印一个引导串。它意味着要为塞进系统日志的信息增加一些上下
文。
 
当一个可分区的模块被卸载时,驱动程序应该通过为每个支持的主/次对调用fsync_dev
来安排所有的分区刷新。而且,如果结构gendisk被插在全局链表中,它应该被删除——
注意spull并未自己插入它,原因上面提到过。
 
spull的清除函数是:
 
(代码260 2#)
 
(代码261)
 
使用Initrd进行分区检测
 
如果你想从一个设备上安装你的根文件系统,而这个设备的驱动程序只有模块化的形式
,你就必须使用由现代Linux核心提供的Initrd工具。我不想在这里介绍Initrd,这一小
节是针对那些了解Initrd并想知道它是如何影响块设备驱动程序的读者的。

 
使用Initrd进行分区检测
 
如果你想从一个设备上安装你的根文件系统,而这个设备的驱动程序只有模块化的形式
,你就必须使用由现代Linux核心提供的Initrd工具。我不想在这里介绍Initrd,这一小
节是针对那些了解Initrd并想知道它是如何影响块设备驱动程序的读者的。
 
当你用Initrd引导一个核心时,它会在安装真正的根文件系统之前建立一个暂时的运行
环境。模块通常是从被用作临时根文件系统的ramdisk中装载。
 
 
--
※ 来源:.华南网木棉站 bbs.gznet.edu.cn.[FROM: 202.38.196.234]
--
※ 转寄:.华南网木棉站 bbs.gznet.edu.cn.[FROM: 211.80.41.106]
--
※ 转寄:.华南网木棉站 bbs.gznet.edu.cn.[FROM: 211.80.41.106]
--
※ 转载:.南京大学小百合站 bbs.nju.edu.cn.[FROM: 211.80.41.106]
--
※ 转载:·饮水思源 bbs.sjtu.edu.cn·[FROM: 211.80.41.106]

目录页 | 上一页 | 下一页

|
|
|
|
|
|
Copyright©Edward Wang 2000 - 2001
NewB工作室,北京