窃槽文言文:标题:LINUX块设备分析

来源:百度文库 编辑:偶看新闻 时间:2024/05/03 10:24:55






操作系统报告

LINUX块设备分析








一九九九年六月十日


LINUX块设备分析
学号:9811537 提交者:沈昶
计算机应用专业98硕 sc@icad.zju.edu.cn

[摘要] 在本文中,首先概括了块设备在OS中的位置和工作原理,接着在分析各个与块设备相关的多个数据结构以及相互之间的联系。文章从对块设备管理的角度进行分析,依次讲了缓冲区管理、请求管理、中断管理,最后以CD_ROM为例,分析了块设备的初始化和对CD_ROM的操作。
块设备概述
块设备的特点是:对设备的读写是以块为单位的(块的大小由具体的设备决定,并不一定是Linux定义的1024字节),并且对设备的访问是随机的。与其他多数的操作系统相似,Linux对设备的访问是通过文件系统实现的。因此文件系统与设备驱动有着密切的关系。

图1
上图为设备驱动与文件系统之间的关系简图。有关文件系统的详细情况在此不多做分析,下面只对在文件系统中与设备驱动有关的数据结构和函数调用作一个介绍。
Linux通过VFS(virtual file system)管理各种不同类型的文件系统,如Ext2,Msdos等。对每一个被安装上(Mount)文件系统,VFS用VFS Superblock表示,结构如下:
/*include/linux/fs.h*/
struct super_block {
kdev_t s_dev; //该文件系统所在设备的设备标志符
unsigned long s_blocksize; //该文件系统中块所含的字节数
unsigned char s_blocksize_bits;
unsigned char s_lock;
unsigned char s_rd_only;
unsigned char s_dirt;
struct file_system_type *s_type;//指明该文件系统的结构
struct super_operations *s_op; /*指向该文件系统的超级块程序入口,包括VFS用以读写Inode和Superblock的函数。*/
struct dquot_operations *dq_op;
unsigned long s_flags;
unsigned long s_magic;
unsigned long s_time;
struct inode * s_covered; //指向一个目录Inode,该目录是文件系统安装上的目录
struct inode * s_mounted; //指向该文件系统中的第一个Inode
struct wait_queue * s_wait;
union {
struct minix_sb_info minix_sb;
struct ext_sb_info ext_sb;
struct ext2_sb_info ext2_sb;
struct hpfs_sb_info hpfs_sb;
struct msdos_sb_info msdos_sb;
struct isofs_sb_info isofs_sb;
struct nfs_sb_info nfs_sb;
struct xiafs_sb_info xiafs_sb;
struct sysv_sb_info sysv_sb;
struct affs_sb_info affs_sb;
struct ufs_sb_info ufs_sb;
void *generic_sbp;
} u;
};
同样,对设备上的每一个文件,VFS用数据结构inode表示如下:
/*include/linux/fs.h*/

struct inode {
kdev_t i_dev; //该文件所在设备的设备标志符
unsigned long i_ino; //文件的inode号,在文件系统中有唯一性
umode_t i_mode;
nlink_t i_nlink;
uid_t i_uid; //文件所属用户的ID
gid_t i_gid; //文件所属用户所在组的ID
kdev_t i_rdev;
off_t i_size;
time_t i_atime;
time_t i_mtime;
time_t i_ctime;
unsigned long i_blksize;
unsigned long i_blocks;
unsigned long i_version;
unsigned long i_nrpages;
struct semaphore i_sem;
struct inode_operations *i_op;
struct super_block *i_sb;
struct wait_queue *i_wait;
struct file_lock *i_flock;
struct vm_area_struct *i_mmap;
struct page *i_pages;
struct dquot *i_dquot[MAXQUOTAS];
struct inode *i_next, *i_prev;
struct inode *i_hash_next, *i_hash_prev;
struct inode *i_bound_to, *i_bound_by;
struct inode *i_mount;
unsigned long i_count; /* needs to be > (address_space * tasks)>>pagebits */
unsigned short i_flags;
unsigned short i_writecount;
unsigned char i_lock;
unsigned char i_dirt;
unsigned char i_pipe;
unsigned char i_sock;
unsigned char i_seek;
unsigned char i_update;
unsigned char i_condemned;
union {
struct pipe_inode_info pipe_i;
struct minix_inode_info minix_i;
struct ext_inode_info ext_i;
struct ext2_inode_info ext2_i;
struct hpfs_inode_info hpfs_i;
struct msdos_inode_info msdos_i;
struct umsdos_inode_info umsdos_i;
struct iso_inode_info isofs_i;
struct nfs_inode_info nfs_i;
struct xiafs_inode_info xiafs_i;
struct sysv_inode_info sysv_i;
struct affs_inode_info affs_i;
struct ufs_inode_info ufs_i;
struct socket socket_i;
void * generic_ip;
} u;
};/*
从以上两个文件系统的关键的数据结构中可以看到,通过文件系统和文件的属性描述块,可以找到该文件系统或文件所在的设备。对文件的访问,VFS通过唯一的设备号实现对特定设备的低层函数调用。
对于块设备,不用亲自编写read()和write()函数,而是用VFS提供的通用函数block_write()和block_read(),即在数据结构file_operations中域read和write的值是block_read和block_write。Block_read和bolck_write(src\fs\block_dev.c)参与文件系统缓冲区管理。
缓冲区管理
文件系统通过请求实现向块设备读写数据块。所有的块数据读写请求以数据结构buffer_head的形式通过标准的核心调用交给块设备驱动。Buffer_head的结构如下:
/*include\linux\fs.h*/
struct buffer_head {
/* First cache line: */
unsigned long b_blocknr; /* block number */
kdev_t b_dev; /* device (B_FREE = free) */
kdev_t b_rdev; /* Real device */
unsigned long b_rsector; /* Real buffer location on disk */
struct buffer_head * b_next; /* Hash queue list */
struct buffer_head * b_this_page; /* circular list of buffers in one page */

/* Second cache line: */
unsigned long b_state; /* buffer state bitmap (see above) */
struct buffer_head * b_next_free;
unsigned int b_count; /* users using this block */
unsigned long b_size; /* block size */

/* Non-performance-critical data follows. */
char * b_data; /* pointer to data block (1024 bytes) */
unsigned int b_list; /* List that this buffer appears */
unsigned long b_flushtime; /* Time when this (dirty) buffer
* should be written */
unsigned long b_lru_time; /* Time when this buffer was 
* last used. */
struct wait_queue * b_wait;
struct buffer_head * b_prev; /* doubly linked list of hash-queue */
struct buffer_head * b_prev_free; /* doubly linked list of buffers */
struct buffer_head * b_reqnext; /* request queue */
};

为了加速对物理设备的访问速度,Linux将块缓冲区放在Cache内,块缓冲区是由buffer_head连成的链表结构。主要由两部分组成。第一部分是空闲的buffer_head组成的链表,按块大小的不同分类组成不同的链表,Linux目前支持的512、1024、2048、4096和8192字节。第二部分是正在用的块,块以Hash_table的形式组织,具有相同hash索引的缓冲块连在一起,hash索引根据设备标志符和该数据块的块号得到。如图2。同时将同一状态的缓冲区块用LRU算法连在一起,系统这样做的目的是便于对同一状态的所有缓冲区块统一操作。对缓冲区的各个链表定义如下:
//contained in fs\buffer.c
static struct buffer_head ** hash_table;
static struct buffer_head * lru_list[NR_LIST] = {NULL, };
static struct buffer_head * free_list[NR_SIZES] = {NULL, };

static struct buffer_head * unused_list = NULL;
static struct buffer_head * reuse_list = NULL;

图2

当文件系统需要从物理块设备读数据块时,它首先在Cache中查找是否有相应的数据块。如果没有,则先从具有相同块大小的空闲块队列中取出一块;或者在Cache中有相应的数据块,但没有更新过,这时文件系统将向设备驱动发请求从设备中读相应的数据块。
和其他类型的Cache一样,缓冲区Cache也需要维护,以使其更有效并能在不同的设备之间公平的分配Cache。Linux用bdflush管理缓冲区Cache。Bdflush是一个核心守护进程,当系统有太多的“脏”缓冲区时;或者缓冲区中的数据必须被写出时,它动态对此作出反应。它在系统启动时驻入内存,大部分时间Bdflush处于睡眠状态;当“脏”的缓冲区数量达到足够大时,Bdflush自动向设备发出请求。
请求管理(Strategy routines)
所有对块设备的读写都是通过strategy routines完成,strategy routines向文件系统提供了对块设备访问的统一的界面。strategy routines并不带参数,也不返回任何结果。它是一组策略,知道I/O请求队列的情况,知道如何访问设备,读取数据块。它在被调用时需要屏蔽中断,以免引起竞争,在返回前必须打开中断开关。我们将讨论strategy routines的具体实现。
Linux有一张表blkdevs用以维护所有被登记的块设备,表单元的数据结构是device_struct。
/*src\fs\devices.c*/
struct device_struct {
const char * name; //设备名
struct file_operations * fops; //文件操作结构
};
static struct device_struct blkdevs[MAX_BLKDEV] = {
{ NULL, NULL },
};
blkdevs以设备的主设备号作下标,当做文件访问时,对不同类型的块设备上的文件的访问采用的方法也不同。数据结构device_struct为不同类型的块设备访问提供了统一的界面。当块设备登记上后,device_struct中的域fops指向该块设备的访问操作函数。
向量表blkdevs为文件系统访问块设备提供了一致的界面;同样,对于缓冲区Cache,块设备也要向其提供一致的界面。向量表blk_dev解决了这一问题。
/*src\include\linux\blkdev.h*/
struct blk_dev_struct {
void (*request_fn)(void); /*请求处理函数指针,在系统初启时由每一种设备的初始化函数分别进行初始化。请求处理函数是写设备驱动程序的重要一环,设备驱动程序在此函数中通过outb向位于I/O空间中的设备命令寄存器发出命令,以后通过中断接受请求的处理结果;*/
struct request * current_request; /*指向当前正在处理的请求。每个设备拥有一个请求队列,该请求队列由plug维持,当系统准备处理请求时,设备对应的请求队列就被插上去(令current_request=&plug),请求处理过程由unplug_device()完成。*/
struct request plug; /*plug的引入是LINUX2.0版本与以前版本的一个不同之处,plug主要被用于异步提前读写操作,在这种情况下,由于没有特别的请求,为了提高系统性能,需要等发送完所有的提前读写请求才开始进行请求处理,即unplug_device。*/
struct tq_struct plug_tq; //维持了设备对应的任务队列
};
/*src\drivers\block\ll_rw_blk.c*/
struct blk_dev_struct blk_dev[MAX_BLKDEV]; /* initialized by blk_dev_init() */
数据结构blk_dev_struct的说明请参见上面注释部分。blk_dev是一个一维数组,数组下标对应块设备的主设备号,因此系统中的每一种块设备都唯一对应了一个blk_dev_struct结构,用来维护与该块设备相关的请求信息。我们可以用下图3表示出块设备请求队列的数据结构示意图3:

从上面与块设备有关的数据结构中可看到,对块设备的访问是以发请求的方式进行的,请求通过数据结构request表示:
/*src\include\linux\blk_dev.h*/
struct request {
volatile int rq_status; /*表示请求的状态,可能的取值范围是RQ_INACTIVE、RQ_ACTIVE、 RQ_SCSI_BUSY、RQ_SCSI_DONE、RQ_SCSI_DISCONNECTING;*/
kdev_t rq_dev; /*是该请求对应的设备号,kdev_t是unsigned short类型,高8位是主设备号,低8位是从设备号,每一请求都针对一个设备发出的;*/
int cmd; //表示该请求对应的命令,取READ或WRITE;
int errors;
unsigned long sector; //每一扇区的字节数
unsigned long nr_sectors; //每一扇区的扇区数
unsigned long current_nr_sectors; //当前的扇区数;
char * buffer; /*存放buffer_head.b_data值,表示发出请求的数据存取地址;*/
struct semaphore * sem; /*一个信号量,用来保证设备读写的原语操作,仅当sem=0时才能处理该请求;*/
struct buffer_head * bh; //读写缓冲区的头指针
struct buffer_head * bhtail; //读写缓冲区的尾指针
struct request * next; //指向下一个请求
};

每一个对块设备的请求用数据结构request表示,对同一块设备的不同请求按elevator算法排序。对不同块设备的所有请求都放在请求数组all_requests中,定义如下:
/*src\drivers\blokc\ll_rw_blk.c*/
static struct request all_requests[NR_REQUEST];
请求数组all_requests实际上是一个请求缓冲池,请求的释放与申请都是针对这个缓冲池进行。请求结构里的next指针用来联结各个请求,形成请求队列。系统中的每一种设备都拥有自己的请求队列,这个队列由current_request和plug共同维护。
了解了与请求管理有关的主要数据结构后,让我们来看看系统是如何实现请求管理的。首先调用宏INIT_REQUEST,以确定请求队列正常无误,请求队列通过函数add_request()得到。然后调用函数end_request(1),将当前请求从请求队列取下,然后用wake_up()唤醒别的进程。以下是在请求管理中用道的函数。
static void make_request(int major,int rw, struct buffer_head * bh)
本函数用来申请一个请求缓冲区,调用者需要提供主设备号major,操作命令cmd(READ or WRITE)以及读写缓冲区bh。make_request先检查当前设备是否有current_request,如果current_request=NULL,就调用plug_device()以减少分配请求失败的可能。接着,make_request以不等待方式申请一个请求缓冲区,如果申请不成功,则以等待方式申请。等待方式申请时,通过run_task_queue()来重新调度I/O任务,从而获得其他I/O任务结束后释放的缓冲区。当缓冲区申请到后,就用add_request()它添加到设备的请求队列中。添加原则是:如果current_request=NULL,就令current_request指向当前申请到的请求缓冲区,同时调用请求结构里的request_fn()开始处理请求;要是current_request!=NULL,就把当前请求根据一定规则放到current_request维护的请求队列中,插入规则按主设备号从低到高、读写扇区数从少到多进行。在上述缓冲区申请过程中,如果进行的是提前读写请求,则系统在不等待方式下缓冲区申请失败时立即返回;
void ll_rw_block(int rw, int nr, struct buffer_head * bh[])
这个函数是请求管理的对外接口,它封装了make_request()。文件系统可以用它来发出同一设备多个数据块的读写请求。函数的参数很简单,rw是读写命令,nr是数据缓冲区的个数,bh是数据缓冲区列表;
void unplug_device(void * data)
本函数的功能已在前面作了论述,其主要功能是处理current_request指向的请求队列。该函数的地址在系统初始化时被赋给dev->plug_tq.routine,因此每次设备对应的I/O任务被唤醒时,系统都会回调本函数,处理current_request累积起来的请求队列。除此外,设备驱动程序也在适当的地方调用本函数。函数参数是一个void指针data,data实际上指向设备对应的blk_dev_struct;
int blk_dev_init(void)
本函数功能很简单,主要完成块设备初始化,即通过调用各种设备的初始化函数完成blk_dev的初始化。
以上讲的是块设备的读写请求管理。相关的源程序主要有:
drivers/block/ll_rw_block.c
include/linux/blk.h
include/linux/blkdev.h
从图1中可以看到,读写请求管理位于较中断管理为高的层次。那么,系统又是如何处理读写请求的呢?即dev->request_fn完成的功能是什么呢?这一切都涉及到设备的中断管理。
事实上,dev->request_fn是在各设备驱动程序内初始化的,系统的每一种设备都有它自己的请求处理函数。
中断管理
与中断管理有关的数据类型是:
struct irqaction {
void (*handler)(int, void *, struct pt_regs *);
unsigned long flags;
unsigned long mask;
const char *name;
void *dev_id;
struct irqaction *next;
};
handler指向设备的中断响应函数,它在系统初始化时被置入。当中断发生时,系统自动调用该函数;
flags指示了中断类型,如SA_INTERRUPT等等;
mask是中断的屏蔽字;
name是设备名;
dev_id是与设备相关的数据类型,中断响应函数可以根据需要将它转化所需的数据指针,从而达到访问系统数据的功能;
next指向下一个irqaction。
系统声明了一个irqaction类型的静态指针数组:
static struct irqaction *irq_action[16];
由于中断数目有限,且很少更新(只在系统初始化时进行),所以系统在初始化时,采用kmalloc从系统堆中分配内存给每一个irq_action指针,通过next指针将它们连成一个队列。下面先给出与中断管理有关的主要函数,然后对系统的中断初始化以及中断流程作一阐述。
与中断管理有关的主要函数有:
int request_irq(unsigned int irq, 
void (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags, 
const char * devname,
void *dev_id)
本函数分配一个中断向量。irq是中断向量号,小于15;handler是中断处理函数,保存在中断对应的irq_action中。handler由各驱动程序单独编写,例如IDE使用函数void ide_intr(int irq,void *device_id,struct pt_regs *regs)。
int setup_x86_irq(int irq, struct irqaction * new)
本函数调用set_intr_gate把irq对应的中断向量函数装入******。中断向量函数分为三种:interrupt、fast_interrupt和bad_interrupt。interrupt属于完整的中断处理函数,它处理所有的工作(包括信号处理),处理时不屏蔽中断,因此适用于中断频率较低的设备,如键盘、定时器。fast_interrupt用来处理简单的中断,它通常是原语操作,屏蔽所有中断;bad_interrupt在没有其它处理处理函数时使用。上述interrupt、fast_interrupt和bad_interrupt是通过宏扩展生成的汇编代码,它们封装调用了C语言的do_IRQ、do_fast_IRQ。bad_interrupt不做特殊处理。在do_IRQ、do_fast_IRQ中,中断所对应的irq_action[irq]->handler被调用,从而完成了中断处理过程。
unsigned long probe_irq_on (void)
本函数返回将要进行检测的中断源(以位图表示),同时使能这些中断。
void init_IRQ(void)
本函数完成中断的初始化工作(由hwif_init调用)。除了IRQ2、IRQ13,其他的irq_action都初始化为NULL指针。IRQ2用于中断级连,可以用它来扩展中断控制器。IRQ13在SMP主板上被用于处理器间中断,在其他主板上,该中断则用于算术错误中断。对所有的中断响应函数,系统都初始化为bad_interrupt。因此,各设备驱动程序应从自己的需要出发,通过request_irq来重新设置中断处理函数以及中断响应函数。
当系统初启时,系统调用各驱动程序的初始化程序。驱动程序先检测可能的中断向量,然后调用request_irq(),传入中断处理函数指针(如ide_intr)。在request_irq()中,系统通过kmalloc从系统堆中分配一块内存给irq_action[intr],把输入参数填入irqaction结构,然后调用setup_x86_irq初始化中断向量。
下面以IDE为例对LINUX的中断初始化作一示例性说明:
init_ide_data ();
probe_for_hwifs (); /* 检查已知的IDE中断向量 */
probe_hwif (); /* 检查所有可能的中断向量 */
hwif_init ();
register_blkdev(); /* 在文件系统中登记本设备 */
init_irq (); /* 调用request_irq */
init_gendisk();
当中断发生时,系统根据中断号查找对应的中断处理函数。在IDE的驱动程序中,这一函数被命名为ide_intr();ide_intr()需要一个void*类型的参数,它实际上指向一块参数区,更改参数区内的数据可以实现多种途径的命令传送方式。例如要从IDE中读一块数据,我们可以设置cmd域以及其他相关的域。因此当中断完成后,它知道该向谁汇报处理结果,因为我们为它保存了中断前的处理环境。
与上述中断管理相关的源程序主要有:
include/linux/interrupt.h
include/asm-i386/irq.h
arch/i386/kernel/irq.c
drivers/block/ide.c
应该看到,中断管理和读写请求管理是密不可分的,它们一个共同的实现机制是利用回调函数:前者通过BI/OS中断响应机制进行回调,后者通过系统任务调度进行回调。中断回调函数在更大程度上依赖于设备的种类,根据设备的不同,设备命令寄存器发出不同的命令。
IDE CD_ROM设备
Linux系统中最常用的磁盘结口是IDE接口,即Integrated Disk Electronic。每个IDE控制器可以支持两个磁盘设备,一个为主设备,一个为辅设备。IDE设备的速度比SCSI设备速度低,但是比SCSI设备更便宜。在Linux系统初始化时,系统将登记IDE控制器,而不是IDE磁盘设备。基本(primary)的IDE控制器的设备标志号是3,第二(secondary)IDE控制器的设备标志号是22。因此,联系到前面的,IDE设备在blk_dev和blkdevs向量表中的下表为3或22。文件系统对IDE设备上的文件的访问操作,系统都会通过设备标志号做索引指向IDE设备。IDE子系统将通过从设备号将请求交给具体的设备。不同的IDE设备对请求的处理也有不同,本源代码分析报告以IDE CD_ROM为列,分析IDE设备的初始化,以及设备驱动的各种功能。
CD_ROM现在是PC机最常用的设备之一,在Linux中,也提供了多种支持CD_ROM的设备驱动。CD_ROM主要有两种类型IDE接口、SCSI接口,这里主要讨论IDE接口。市场上有多种品牌的CD_ROM,各个开发商对各自的CD_ROM实现的功能有不同,同时对同一功能,不同的CD_ROM实现的方法也有不同。Linux提供了标准的IDE接口的CD_ROM驱动(driver\block\ide-cd.c),只要是与ATAPI 1.2标准兼容的CD_ROM,该驱动都能用。有些CD_ROM生产商即提供了与ATAPI兼容的,也提供了自己特有的驱动。如果设备使用自己特有的设备驱动,则IDE-CD驱动就不工作。生产商提供的自己特有的驱动在drivers\cdrom\目录下。
IDE-CD的初始化
在系统初始化IDE设备时,系统首先在CMOS中察看有关的IDE设备的信息。CMOS存储器存有有关IDE控制器和磁盘设备的情况。Linux根据获得的信息对IDE设备进行初始化。初始化工作有以下内容。
初始化IDE控制器和登记IDE设备 ide_hwif_t
此项工作由函数init_ide_data()完成(drivers\block\ide.c),init_ide_data为IDE设备设置缺省值,完成表ide_hwifs的初始化。在此需特别说明的是init_ide_date仅在首次被调用时起作用,以后的调用不起任何作用,不会修改任何数据。init_ide_date一般在驱动初始化时被调用,但也可以早在核心启动时的命令分析(parse_options())时被调用,正因为init_ide_data()被调用时刻的不确定性,导致在init_ide_data运行时刻无法确定其他核心模块是否正在运行,故在init_ide_data中不能依靠其他核心模块,如内存的定位(memory allocation)。这种做法很不合理,如果采用别的方法,我们可以动态的确定有关的数据结构的位置,如此比较节省内存资源。
init_ide_data()在初始化时刻的被调用关系如下:


//contained in init\main.c
Start_kernel()
{
char * command_line;
….
….
setup_arch(&command_line, &memory_start, &memory_end); //获取命令行
….
….
….
parse_options(command_line)//命令行分析
/*{Parse_options(char* line)
{….
Checksetup(line)
/*Checksetup(char* line)
{
…..
…..
#ifdef CONFIG_BLK_DEV_IDE 
/* ide driver needs the basic string, rather than pre-processed values */
//如果命令行有有关IDE的信息,则初始化IDE设备。
if (!strncmp(line,"ide",3) || (!strncmp(line,"hd",2) && line[2] != '='))
{
ide_setup(line);//在该函数中调用init_ide_data()
return 1;
}
#endif
……
……
}
*/
//if it's an environment variable 
……
}
*/


kernel_thread(init, NULL, 0);


}
以上是系统核心初始化函数start_kernel()有关IDE设备初始化的函数调用,代码中”….”表示省略。可以看到,parse_options()中调用了init_ide_data(),同时在最后kernel_thread()执行了init()子进程,而在init()中也调用了init_ide_data()。这并不存在重复调用的情况,原因已在前面说明。Init()中有关IDE设备初始化的代码如下:
//contained in init\main.c
static int init(void * unused)
{ ….
….
….
setup()//为系统调用,contained in init\entry.s
/*sys_setup() //contained in fs\filesystems.c]
device_setup() //contained in block\genhd.c
blk_dev_init() //contained in block\ll_rw_blk.c
ide_init() // contained in block\ide.c该函数中调用init_ide_data()
*/
….
….
}
从系统核心初始化的部分源代码中可以看到,IDE设备的初始化一般是在init()子进程中完成的,但也有可能早于init()而在parse_options中完成,则主要取决于命令行内容。但是无论在何处完成,只有第一次调用有效。
接着,我们来讨论init_ide_data()具体进行那些初始化工作。Init_ide_data()的主要源代码如下:
/*contained in drivers\block\ide.c
static void init_ide_data (void)
{
unsigned int index;
static unsigned long magic_cookie = MAGIC_COOKIE;

if (magic_cookie != MAGIC_COOKIE)
return; /* already initialized */
magic_cookie = 0; //保证仅被调用一次。

for (index = 0; index < MAX_HWIFS; ++index)
init_hwif_data(index); //初始化数据结构。

idebus_parameter = 0;
system_bus_speed = 0;
}
从以上源代码可知,系统通过申明静态变量magic_cookie来确保设备初始化只进行一次。
对IDE设备的初始化工作在函数init_hwif_data()中进行。函数init_hwif_data通过参数index对表de_hwifs进行初始化,具体初始化IDE控制器(IDE Controler), 对具体IDE设备并在此完成。表de_hwifs的结构如下:
//contained in drivers\block\ide.h
typedef struct hwif_s {
struct hwif_s *next; /* for linked-list in ide_hwgroup_t */
void *hwgroup; /* actually (ide_hwgroup_t *) */
unsigned short io_base; /* base io port addr */
unsigned short ctl_port; /* usually io_base+0x206 */
ide_drive_t drives[MAX_DRIVES]; /* drive info */
struct gendisk *gd; /* gendisk structure */
ide_tuneproc_t *tuneproc; /* routine to tune PIO mode for drives */
#if defined(CONFIG_BLK_DEV_HT6560B) || defined(CONFIG_BLK_DEV_PROMISE)
ide_selectproc_t *selectproc; /* tweaks hardware to select drive */
#endif
ide_dmaproc_t *dmaproc; /* dma read/write/abort routine */
unsigned long *dmatable; /* dma physical region descriptor table */
unsigned short dma_base; /* base addr for dma ports (triton) */
byte irq; /* our irq number */
byte major; /* our major number */
char name[5]; /* name of interface, eg. "ide0" */
byte index; /* 0 for ide0; 1 for ide1; ... */
hwif_chipset_t chipset; /* sub-module for tuning.. */
unsigned noprobe : 1; /* don't probe for this interface */
unsigned present : 1; /* this interface exists */
unsigned serialized : 1; /* serialized operation with mate hwif */
unsigned sharing_irq: 1; /* 1 = sharing irq with another hwif */
#ifdef CONFIG_BLK_DEV_PROMISE
unsigned is_promise2: 1; /* 2nd i/f on promise DC4030 */
#endif /* CONFIG_BLK_DEV_PROMISE */
#if (DISK_RECOVERY_TIME > 0)
unsigned long last_time; /* time when previous rq was done */
#endif
#ifdef CONFIG_BLK_DEV_IDECD
struct request request_sense_request; /* from ide-cd.c */
struct packet_command request_sense_pc; /* from ide-cd.c */
#endif /* CONFIG_BLK_DEV_IDECD */
#ifdef CONFIG_BLK_DEV_IDETAPE
ide_drive_t *tape_drive; /* Pointer to the tape on this interface */
#endif /* CONFIG_BLK_DEV_IDETAPE */
} ide_hwif_t;

//contained in driver\block\ide.c
ide_hwif_t ide_hwifs[MAX_HWIFS];
从数据结构ide_hwif_t中可看到域 drives[[MAX_DRIVES],这表drives是记录该IDE控制器下的设备的。ide_drive_t的结构如下:
typedef struct ide_drive_s {
special_t special; /* special action flags */
unsigned present : 1; /* drive is physically present */
unsigned noprobe : 1; /* from: hdx=noprobe */
unsigned keep_settings : 1; /* restore settings after drive reset */
unsigned busy : 1; /* currently doing revalidate_disk() */
unsigned removable : 1; /* 1 if need to do check_media_change */
unsigned using_dma : 1; /* disk is using dma for read/write */
unsigned forced_geom : 1; /* 1 if hdx=c,h,s was given at boot */
unsigned unmask : 1; /* flag: okay to unmask other irqs */
unsigned no_unmask : 1; /* disallow setting unmask bit */
unsigned no_io_32bit : 1; /* disallow enabling 32bit I/O */
unsigned nobios : 1; /* flag: do not probe bios for drive */
unsigned slow : 1; /* flag: slow data port */
unsigned autotune : 2; /* 1=autotune, 2=noautotune, 0=default */
#if FAKE_FDISK_FOR_EZDRIVE
unsigned remap_0_to_1 : 1; /* flag: partitioned with ezdrive */
#endif /* FAKE_FDISK_FOR_EZDRIVE */
unsigned no_geom : 1; /* flag: do not set geometry */
ide_media_t media; /* disk, cdrom, tape, floppy */
select_t select; /* basic drive/head select reg value */
byte ctl; /* "normal" value for IDE_CONTROL_REG */
byte ready_stat; /* min status value for drive ready */
byte mult_count; /* current multiple sector setting */
byte mult_req; /* requested multiple sector setting */
byte tune_req; /* requested drive tuning setting */
byte io_32bit; /* 0=16-bit, 1=32-bit, 2/3=32bit+sync */
byte bad_wstat; /* used for ignoring WRERR_STAT */
byte sect0; /* offset of first sector for DM6:DDO */
byte usage; /* current "open()" count for drive */
byte head; /* "real" number of heads */
byte sect; /* "real" sectors per track */
byte bios_head; /* BIOS/fdisk/LILO number of heads */
byte bios_sect; /* BIOS/fdisk/LILO sectors per track */
unsigned short bios_cyl; /* BIOS/fdisk/LILO number of cyls */
unsigned short cyl; /* "real" number of cyls */
void *hwif; /* actually (ide_hwif_t *) */
struct wait_queue *wqueue; /* used to wait for drive in open() */
struct hd_driveid *id; /* drive model identification info */
struct hd_struct *part; /* drive partition table */
char name[4]; /* drive name, such as "hda" */
#ifdef CONFIG_BLK_DEV_IDECD
struct cdrom_info cdrom_info; /* for ide-cd.c */
#endif /* CONFIG_BLK_DEV_IDECD */
#ifdef CONFIG_BLK_DEV_IDETAPE
idetape_tape_t tape; /* for ide-tape.c */
#endif /* CONFIG_BLK_DEV_IDETAPE */
#ifdef CONFIG_BLK_DEV_IDEFLOPPY
void *floppy; /* for ide-floppy.c */
#endif /* CONFIG_BLK_DEV_IDEFLOPPY */
#ifdef CONFIG_BLK_DEV_IDESCSI
void *scsi; /* for ide-scsi.c */
#endif /* CONFIG_BLK_DEV_IDESCSI */
} ide_drive_t;
在函数init_ide_data()中,我们看到初始化工作是对最基本的IDE有关的参数进行设置,对特定的IDE设备如硬盘、光驱并没有进行初始化。我们看到在结构ide_drive_t和ide_hwif_t中有好多与光驱有关的域,对光驱的具体初始化将在后面讨论到。
接下来,我们以IDE CD_ROM为例讨论具体IDE设备的初始化。我们已经知道设备的初始化工作大多在init子进程中完成,主要在函数device_setup()进行。以下是device_setup()的源代码:
/*contained in drivers\block\genhd.c*/
void device_setup(void)
{
extern void console_map_init(void);
struct gendisk *p;
int nr=0;

chr_dev_init();
blk_dev_init();
sti();
#ifdef CONFIG_SCSI
scsi_dev_init();
#endif
#ifdef CONFIG_INET
net_dev_init();
#endif
console_map_init();

for (p = gendisk_head ; p ; p=p->next) {
setup_dev(p);
nr += p->nr_real;
}
#ifdef CONFIG_BLK_DEV_RAM
#ifdef CONFIG_BLK_DEV_INITRD
if (initrd_start && mount_initrd) initrd_load();
else
#endif
rd_load();
#endif
}
我们主要关心函数blk_dev_init()和setup_dev();blk_dev_init()对系统的各种块设备进行初始化,如硬盘、软驱、光驱等。函数ide_init()便是对IDE设备初始化,前面讲到在ide_init中调用init_ide_data()初始化IDE控制器,现在讲在init_ide_data()执行完毕后,调用函数hwif_init(),完成IDE设备的部分参数设置,操作的数据对象是表ide_hwifs,块设备表blk_dev和blkdevs。主要是初始化表bik_dev,blkdevs,并为设备分配请求入口地址,和中断号。如按惯例,将14分配给主IDE控制器,将15分配给从IDE控制器。函数register_blkdev()完成对表blkdevs的初始化。相关的赋值语句如下:
if (hwif->irq == HD_IRQ && hwif->io_base != HD_DATA) {
printk("%s: CANNOT SHARE IRQ WITH OLD HARDDISK DRIVER (hd.c)\n", hwif->name);
return (hwif->present = 0);
}
…….
if (register_blkdev (hwif->major, hwif->name, &ide_fops)) {
printk("%s: UNABLE TO GET MAJOR NUMBER %d\n", hwif->name, hwif->major);
……..
blk_dev[hwif->major].request_fn = rfn;

此外,在hwif_init()中,还调用函数init_gendisk(),初始化通用磁盘gendisk。即为ide_hwifs中的每个单元在系统空间申请一个gendisk数据结构。为以后登记分区表做准备。gendisk的数据结构如下:
//condtained in drivers\block\hd.c
struct gendisk {
int major; /* major number of driver */
const char *major_name; /* name of major driver */
int minor_shift; /* number of times minor is shifted to
get real minor */
int max_p; /* maximum partitions per device */
int max_nr; /* maximum number of real devices */

void (*init)(struct gendisk *); /* Initialization called before we do our thing */
struct hd_struct *part; /* partition table */
int *sizes; /* device size in blocks, copied to blk_size[] */
int nr_real; /* number of real devices */

void *real_devices; /* internal use */
struct gendisk *next;
};
init_gendisk()主要的语句如下:
struct gendisk *gd, **gdp;
gd = kmalloc (sizeof(struct gendisk), GFP_KERNEL);
gd->sizes = kmalloc (minors * sizeof(int), GFP_KERNEL);
gd->part = kmalloc (minors * sizeof(struct hd_struct), GFP_KERNEL);
bs = kmalloc (minors*sizeof(int), GFP_KERNEL); gd->major = hwif->major; /* our major device number */
gd->major_name = IDE_MAJOR_NAME; /* treated special in genhd.c */
gd->minor_shift = PARTN_BITS; /* num bits for partitions */
gd->max_p = 1< gd->max_nr = units; /* max num real drives */
gd->nr_real = units; /* current num real drives */
gd->init = ide_geninit; /* initialization function */
gd->real_devices= hwif; /* ptr to internal data */
gd->next = NULL; /* linked list of major devs */
for (gdp = &gendisk_head; *gdp; gdp = &((*gdp)->next)) ;
hwif->gd = *gdp = gd; /* link onto tail of list */
其中语句gd->init = ide_geninit表示初始函数指向函数ide_geninit,该函数将在后面进行分区检测时被调用。对各种设备的初始化完毕后,接着进行setup_dev(),主要是对gendisk表的每个单元所代表的设备的分区情况进行判断。并对设备进行详细的设置。Setup_dev()中有如下一句:
dev->init(dev);
其实此init函数指向的是ide_geninit。在ide_geninit中,系统对不同设备采用与之相适应的设置函数。对于CD_ROM,调用函数ide_cdrom_setup()进行设置,语句如下:
#ifdef CONFIG_BLK_DEV_IDECD
if (drive->present && drive->media == ide_cdrom)
ide_cdrom_setup(drive);
#endif /* CONFIG_BLK_DEV_IDECD */
在ide_cdrom_setup()中,系统主要对与IDE_CDROM相关的控制信息进行更详细的设置。设置的内容存在结构ide_cd_config_flags和ide_cd_stat_flags中,为了减少数据结构ide_drive_t的存储空间,系统挪用了域’bios_sect’和’bios_head’的地址空间,因为域’bios_sect’和’bios_head’对CD_ROM来说是不会被用到的。宏定义CDROM_CONFIG_FLAGS(drive)和CDROM_STATE_FLAGS(drive)实现了他们自间的转换。下面是ide_cd_config_flags和ide_cd_stat_flags的数据结构:
//contained in drivers\block\ide_cd.c
struct ide_cd_config_flags {
__u8 drq_interrupt : 1; /* Device sends an interrupt when ready
for a packet command. */
__u8 no_doorlock : 1; /* Drive cannot lock the door. */
#if ! STANDARD_ATAPI
__u8 old_readcd : 1; /* Drive uses old READ CD opcode. */
__u8 playmsf_as_bcd : 1; /* PLAYMSF command takes BCD args. */
__u8 tocaddr_as_bcd : 1; /* TOC addresses are in BCD. */
__u8 toctracks_as_bcd : 1; /* TOC track numbers are in BCD. */
__u8 subchan_as_bcd : 1; /* Subchannel info is in BCD. */
#endif /* not STANDARD_ATAPI */
__u8 reserved : 1;
};
#define CDROM_CONFIG_FLAGS(drive) ((struct ide_cd_config_flags *)&((drive)->bios_sect))


/* State flags. These give information about the current state of the
drive, and will change during normal operation. */
struct ide_cd_state_flags {
__u8 media_changed : 1; /* Driver has noticed a media change. */
__u8 toc_valid : 1; /* Saved TOC information is current. */
__u8 door_locked : 1; /* We think that the drive door is locked. */
__u8 eject_on_close: 1; /* Drive should eject when device is closed. */
__u8 sanyo_slot : 2; /* Sanyo 3 CD changer support */
__u8 reserved : 2;
};
#define CDROM_STATE_FLAGS(drive) ((struct ide_cd_state_flags *)&((drive)->bios_head))

至此,有关IDE设备的初始化工作已经全部完毕,与设备有关的各种表结构也已经设置完毕。

对CD_ROM的访问操作
分析ide_cd.c下的各个函数的功能,以及和其他函数之间的调用关系。首先是对CD_ROM的读写数据。函数cdrom_in_bytes()是读数据,函数cdrom_out_bytes()是写数据。实现缓冲区与CD_ROM之间的数据交换。这两个函数分别调用了input_ide_data()和output_ide_data()函数。input_ide_data()是通用的从IDE设备读数据的函数,output_ide_data()是通用的往IDE设备写数据的函数。即不仅是访问IDE CD_ROM用到这两个函数,而且访问其他IDE设备如软驱,也用到这两个函数。这两个函数提供了底层的访问IDE设备的功能。
我们知道了用于缓冲区与CD_ROM之间直接进行数据交换的函数,同时在请求管理部分中讲到,文件系统对块设备的访问并非是直接对设备进行的,而是通过缓冲区实现的。于是下面讨论文件系统访问CD_ROM上的文件时,各个层次间函数的调用关系。
首先在前面提到的系统核心初始化过程中,函数hwif_init()为CD_ROM分配了中断号,同时也指定了请求响应的列程。hwif_init()中通过以下语句实现。
//为CD_ROM分配了中断号
if (!(hwif->irq = default_irqs[h])) {
printk("%s: DISABLED, NO IRQ\n", hwif->name);
return (hwif->present = 0);
//指定了请求响应的列程
switch (hwif->major) {
case IDE0_MAJOR: rfn = &do_ide0_request; break;
#if MAX_HWIFS > 1
case IDE1_MAJOR: rfn = &do_ide1_request; break;
#endif
#if MAX_HWIFS > 2
case IDE2_MAJOR: rfn = &do_ide2_request; break;
#endif
#if MAX_HWIFS > 3
case IDE3_MAJOR: rfn = &do_ide3_request; break;
#endif
default:
printk("%s: request_fn NOT DEFINED\n", hwif->name);
return (hwif->present = 0);
} blk_dev[hwif->major].request_fn = rfn;
…….

图4

跟踪函数do_ide*_request(),不难发现对CD_ROM请求的具体实现。各函数之间的关系如图4。下面介绍主要函数的意义:
do_hwgroup_request()//contained in drivers\block\ide.c
do_hwgroup_request()首先屏蔽该设备所有可能发生的中断,避免竞争,然后调用ide_do_request() 。
ide_do_request() //contained in drivers\block\ide.c
函数首先用cli()屏蔽中断标志位。然后调用do_request()。

do_request() //contained in drivers\block\ide.c
do_request()对新的I/O请求做初始化。Do_request()带参数ide_hwif_t *hwif,根据&hwif->drives[unit]->media的不同,进入特定的请求处理例程,当&hwif->drives[unit]->media为ide_cdrom时,进入CD_ROM请求例程ide_do_rw_cdrom。
ide_do_rw_cdrom() //contained in drivers\block\ide_cd.c
本函数定义如下:
void ide_do_rw_cdrom (ide_drive_t *drive, unsigned long block)
{
struct request *rq = HWGROUP(drive)->rq;

if (rq -> cmd == PACKET_COMMAND || rq -> cmd == REQUEST_SENSE_COMMAND)
cdrom_do_packet_command (drive);
else if (rq -> cmd == RESET_DRIVE_COMMAND) {
cdrom_end_request (1, drive);
ide_do_reset (drive);
return;
} else if (rq -> cmd != READ) {
printk ("ide-cd: bad cmd %d\n", rq -> cmd);
cdrom_end_request (0, drive);
} else
cdrom_start_read (drive, block);
}
函数根据请求内容的不同,即rq->cmd的不同,执行相应的驱动函数。当rq->cmd为PACKET_COMMAND或REQUEST_SENSE_COMMAND时,执行cdrom_do_packet_command();当rq->cmd为READ时,执行cdrom_start_read()。在cdrom_do_packet_command()和cdrom_start_read ()中都激发了一个重要的函数:cdrom_transfer_packet_command(),该函数的参数定义如下:
static int cdrom_transfer_packet_command (ide_drive_t *drive,
char *cmd_buf, int cmd_len,
ide_handler_t *handler)

该函数发一个包命令(packet command)给设备,设备在参数drive中注明,包命令用参数CMD_BUF和CMD_LEN表示。参数HANDLER是中断句柄,当包命令完成时,HANDLER将被调用。在函数cdrom_start_read()中cdrom_transfer_packet_command()被调用的形式如下:
(void) cdrom_transfer_packet_command (drive, pc.c, sizeof (pc.c),
&cdrom_read_intr);
在函数cdrom_do_packet_command()中cdrom_transfer_packet_command()被调用的形式如下:
cdrom_transfer_packet_command (drive, pc->c,
sizeof (pc->c), &cdrom_pc_intr);
以cdrom_start_read()为例,探讨系统访问块设备所采取的策略。以下为cdrom_start_read()的伪代码:
/*contained in drivers\block\ide_cd.c
* Start a read request from the CD-ROM.
*/
static void cdrom_start_read (ide_drive_t *drive, unsigned int block)
{
//获取drive的请求数据
struct request *rq = HWGROUP(drive)->rq;
int minor = MINOR (rq->rq_dev);
如果请求是针对一个分区的,则使请求指向分区的绝对地址。
/* We may be retrying this request after an error. Fix up
any weirdness which might be present in the request packet. */
恢复可能被部分改变的请求结构rq,restore_request (rq);

根据请求,在缓冲区内确认是否能满足,即先在缓冲区内查找请求所需的数据。如果请求被满足,则结束该请求cdrom_end_request (1, drive),返回;如果没有被满足,则继续。
if (cdrom_read_from_buffer (drive))
return;
如果缓冲区无法满足请求,则将读请求送到设备,当请求完成后,执行相应的中断例程
/* Start sending the read request to the drive. */
cdrom_start_packet_command (drive, 32768,
cdrom_start_read_continuation);
}

IDE_CD的打开和关闭
打开设备的操作是ide_cdrom_open(),函数的内容很简单,如果是第一次打开,则检测CD_ROM的状态,否则不做任何事情。关闭设备的操作是ide_cdrom_release(),主要的操作是释放与该设备有关的内存空间。
IDE_CD的ioctl操作
IDE CD_ROM的ioctl操作内容较多。通过函数ide_cdrom_ioctl()实现,主要有以下操作:
CDROMEJECT //弹出
CDROMCLOSETRAY //关闭托盘
CDROMEJECT_SW
CDROMPAUSE //暂停
CDROMRESUME
CDROMSTART
CDROMSTOP
CDROMREADTOCHDR
CDROMREADTOCENTRY
CDROMVOLCTRL
CDROMVOLREAD
CDROMMULTISESSION
CDROMREADRAW

总结
至此我们对IDE 块设备做了较全面的探讨。在分析过程中,我们首先感到到Linux系统的庞大,有点无从着手;在认真阅读了有关的文档资料后,逐渐对源代码有了了解;随着分析的不断深入,体会到作者写程序的精彩之处,兴趣也不断增强,获益匪浅。软件产业在迅速的发展,向渗透的领域也将越来越广泛,OS的小型化,专业化代表了其中的一种趋势。我觉得通过简化现有LINUX的核心,开发支持特定设备(如车载设备,掌上设备)小LINUX内核,将大有市场。这就需要我们熟悉LINUX设备驱动的实现。本文对LINUX块设备做了初步的探讨,为以后更深层次的了解LINUX设备做了些基础工作。
参考文献
[1].David A. Rusling,“The Linux Kernel”Version 0.8-3,1999
[2].Alessandro Rubini,“Linux Device Drivers”,O’Reily&Associates,USA,1998
[3].Michael K. Johnson,“Writing Linux Device Drivers”,DECUS '95 in Washington,1995
[4].Michael K. Johnson and Others,“Linux Kernel Hackers' Guide”,1998
[5].Ori Pomerantz,“Linux Kernel Module Programming Guide”,1998