声明:
块设备驱动
1、功能描述
hd.c 程序是硬盘控制器驱动程序,提供对硬盘控制器块设备的读写驱动和硬盘初始化处理。程序中所有函数按照功能不同可分为 5 类:
1) 初始化硬盘和设置硬盘所用数据结构信息的函数,如 sys_setup() 和 hd_init() ;
2) 向硬盘控制器发送命令的函数 hd_out() ;
3) 处理硬盘当前请求项的函数 do_hd_request() ;
4) 硬盘中断处理过程中调用的 C 函数,如 read_intr() 、 write_intr() 、 bad_rw_intr() 和 recal_intr() 。
do_hd_request() 函数也将在 read_intr() 和 write_intr() 中被调用;
5)硬盘控制器操作辅助函数,如 controler_ready() 、 drive_busy() 、 win_result() 、 hd_out() 和
reset_controler() 等。
sys_setup() 函数利用 boot/setup.s 程序提供的信息对系统中所含硬盘驱动器的参数进行了设置。然后读取硬盘分区表,并尝试把启动引导盘上的虚拟盘根文件系统映像文件复制到内存虚拟盘中,若成功则加载虚拟盘中的根文件系统,否则就继续执行普通根文件系统加载操作。
hd_init() 函数用于在内核初始化时设置硬盘控制器中断描述符,并复位硬盘控制器中断屏蔽码,以允许硬盘控制器发送中断请求信号。
hd_out() 是硬盘控制器操作命令发送函数。该函数带有一个中断过程中调用的 C 函数指针参数,在向控制器发送命令之前,它首先使用这个参数预置好中断过程中会调用的函数指针( do_hd ),然后它按照规定的方式依次向硬盘控制器 0x1f0 至 0x1f7 发送命令参数块。除控制器诊断( WIN_DIAGNOSE )和建立驱动器参数( WIN_SPECIFY )两个命令以外,硬盘控制器在接收到任何其他命令并执行了命令以后,都会向 CPU 发出中断请求信号,从而引发系统去执行硬盘中断处理过程(在 system_call.s , 221 行)。
do_hd_request() 是硬盘请求项的操作函数。其操作流程如下:
-- 首先判断当前请求项是否存在,若当前请求项指针为空,则说明目前硬盘块设备已经没有待处理的请求项,因此立刻退出程序。这是在宏 INIT_REQUEST 中执行的语句。否则就继续处理当前请求项。
-- 对当前请求项中指明的设备号和请求的盘起始扇区号的合理性进行验证;
-- 根据当前请求项提供的信息计算请求数据的磁盘磁道号、磁头号和柱面号;
-- 如果复位标志( reset )已被设置,则也设置硬盘重新校正标志( recalibrate ),并对硬盘执行复位操作,向控制器重新发送“建立驱动器参数”命令( WIN_SPECIFY )。该命令不会引发硬盘中断;
-- 如果重新校正标志被置位的话,就向控制器发送硬盘重新校正命令( WIN_RESTORE ),并在发送之前预先设置好该命令引发的中断中需要执行的 C 函数( recal_intr() ),并退出。 recal_intr() 函数的主要作用是:当控制器执行该命令结束并引发中断时,能重新(继续)执行本函数。
-- 如果当前请求项指定是写操作,则首先设置硬盘控制器调用的 C 函数为 write_intr() ,向控制器发送写操作的命令参数块,并循环查询控制器的状态寄存器,以判断请求服务标志( DRQ )是否置位。若该标志置位,则表示控制器已“同意”接收数据,于是接着就把请求项所指缓冲区中的数据写入控制器的数据缓冲区中。若循环查询超时后该标志仍然没有置位,则说明此次操作失败。于是调用 bad_rw_intr() 函数,根据处理当前请求项发生的出错次数来确定是放弃继续当前请求项还是需要设置复位标志,以继续重新处理当前请求项。
-- 如果当前请求项是读操作,则设置硬盘控制器调用的 C 函数为 read_intr() ,并向控制器发送读盘操作命令。
write_intr() 是在当前请求项是写操作时被设置成中断过程调用的 C 函数。控制器完成写盘命令后会立刻向 CPU 发送中断请求信号,于是在控制器写操作完成后就会立刻调用该函数。
该函数首先调用 win_result() 函数,读取控制器的状态寄存器,以判断是否有错误发生。若在写盘操作时发生了错误,则调用 bad_rw_intr() ,根据处理当前请求项发生的出错次数来确定是放弃继续当前请求项还是需要设置复位标志,以继续重新处理当前请求项。若没有发生错误,则根据当前请求项中指明的需写扇区总数,判断是否已经把此请求项要求的所有数据写盘了。若还有数据需要写盘,则再把一个扇区的数据复制到控制器缓冲区中。若数据已经全部写盘,则处理当前请求项的结束事宜:唤醒等待本请求项完成的进程、唤醒等待空闲请求项的进程(若有的话)、设置当前请求项所指缓冲区数据已更新标志、释放当前请求项(从块设备链表中删除该项)。最后继续调用 do_hd_request() 函数,以继续处理硬盘设备的其他请求项。
read_intr() 则是在当前请求项是读操作时被设置成中断过程中调用的 C 函数。控制器在把指定的扇区数据从硬盘驱动器读入自己的缓冲区后,就会立刻发送中断请求信号。而该函数的主要作用就是把控制器中的数据复制到当前请求项指定的缓冲区中。
与 write_intr() 开始的处理方式相同,该函数首先也调用 win_result() 函数,读取控制器的状态寄存器,以判断是否有错误发生。若在读盘时发生了错误,则执行与 write_intr() 同样的处理过程。若没有发生任何错误,则从控制器缓冲区把一个扇区的数据复制到请求项指定的缓冲区中。然后根据当前请求项中指明的欲读扇区总数,判断是否已经读取了所有的数据。若还有数据要读,则退出,以等待下一个中断的到来。若数据已经全部获得,则处理当前请求项的结束事宜:唤醒等待当前请求项完成的进程、唤醒等待空闲请求项的进程(若有的话)、设置当前请求项所指缓冲区数据已更新标志、释放当前请求项(从块设备链表中删除该项)。最后继续调用 do_hd_request() 函数,以继续处理硬盘设备的其他请求项。
为了能更清晰的看清楚硬盘读写操作的处理过程,我们可以把这些函数、中断处理过程以及硬盘控制器三者之间的执行时序关系用下图表示出来。
由以上分析可以看出,本程序中最重要的 4 个函数是 hd_out() 、 do_hd_request() 、 read_intr() 和write_intr() 。理解了这 4 个函数的作用也就理解了硬盘驱动程序的操作过程 ? 。
值得注意的是,在使用hd_out()向硬盘控制器发送了读写或其他命令后,hd_out()函数并不会等待所发命令的执行过程,而是立刻返回调用它的程序中,例如 do_hd_request()。而 do_hd_request()函数也立刻返回上一级调用它的函数(add_request()),最终返回到调用块设备读写函数ll_rw_block()的其他程序。(例如 fs/buffer.c 的 bread()函数)中去等待块设备IO的完成。
linux/kernel/blk_drv/hd.c 程序
/*
* linux/kernel/hd.c
*
* (C) 1991 Linus Torvalds
*//*
* This is the low-level hd interrupt support. It traverses the
* request-list, using interrupts to jump between functions. As
* all the functions are called within interrupts, we may not
* sleep. Special care is recommended.
*
* modified by Drew Eckhardt to check nr of hd's from the CMOS.
*/
/*
* 本程序是底层硬盘中断辅助程序。主要用于扫描请求列表,使用中断在函数之间跳转。
* 由于所有的函数都是在中断里调用的,所以这些函数不可以睡眠。请特别注意。
* 由 Drew Eckhardt 修改,利用 CMOS 信息检测硬盘数。
*/#include <linux/config.h> // 内核配置头文件。定义键盘语言和硬盘类型(HD_TYPE)可选项。
#include <linux/sched.h> // 调度程序头文件,定义了任务结构 task_struct、初始任务 0 的数据
#include <linux/fs.h> // 文件系统头文件。定义文件表结构(file,buffer_head,m_inode 等)。
#include <linux/kernel.h> // 内核头文件。含有一些内核常用函数的原形定义。
#include <linux/hdreg.h> // 硬盘参数头文件。定义访问硬盘寄存器端口,状态码,分区表等信息。
#include <asm/system.h> // 系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。
#include <asm/io.h> // io 头文件。定义硬件端口输入/输出宏汇编语句。
#include <asm/segment.h> // 段操作头文件。定义了有关段寄存器操作的嵌入式汇编函数。// 必须在 blk.h 文件之前定义下面的主设备号常数,因为 blk.h 文件中要用到该常数。
#define MAJOR_NR 3 // 硬盘主设备号是 3。
#include "blk.h" // 块设备头文件。定义请求数据结构、块设备数据结构和宏函数等信息。#define CMOS_READ(addr) ({ \ // 读 CMOS 参数宏函数。
outb_p(0x80|addr,0x70); \ // 0x70是写端口号,0x80|addr是要读的CMOS内存地址
inb_p(0x71); \ // 0x71是读端口号
})/* Max read/write errors/sector */
/* 每个扇区读写操作允许的最多出错次数 */
#define MAX_ERRORS 7 // 读/写一个扇区时允许的最多出错次数。
#define MAX_HD 2 // 系统支持的最多硬盘数。// 重新校正处理函数
// 硬盘中断程序在复位操作时会调用的重新校正函数(287行)。
static void recal_intr(void); static int recalibrate = 1; // 重新校正标志。将磁头移动到 0 柱面。
static int reset = 1; // 复位标志。当发生读写错误时会设置该标志,以复位硬盘和控制器。/*
* This struct defines the HD's and their types.
*/
/* 下面结构定义了硬盘参数及类型 */
// 各字段分别是磁头数、每磁道扇区数、柱面数、写前预补偿柱面号、磁头着陆区柱面号、控制字节。
// 它们的含义请参见程序列表后的说明。
struct hd_i_struct {int head, sect, cyl, wpcom, lzone, ctl;
};
// 如果已经在 include/linux/config.h 头文件中定义了 HD_TYPE,就取其中定义好的参数作为
// hd_info[]的数据。否则,先默认都设为 0 值,在 setup()函数中会进行设置。
#ifdef HD_TYPE
struct hd_i_struct hd_info[] = { HD_TYPE };
#define NR_HD ((sizeof (hd_info))/(sizeof (struct hd_i_struct))) // 计算硬盘个数。
#else
struct hd_i_struct hd_info[] = { {0,0,0,0,0,0},{0,0,0,0,0,0} };
static int NR_HD = 0;
#endif// 定义硬盘分区结构。给出每个分区的物理起始扇区号、分区扇区总数。
// 其中 5 的倍数处的项(例如 hd[0]和 hd[5]等)代表整个硬盘中的参数。
static struct hd_struct {long start_sect;long nr_sects;
} hd[5*MAX_HD]={
{0,0},};// 读端口 port,共读 nr 字,保存在 buf 中。
#define port_read(port,buf,nr) \
__asm__( "cld;rep;insw" :: "d" (port), "D" (buf), "c" (nr): "cx" , "di" )// 写端口 port,共写 nr 字,从 buf 中取数据。
#define port_write(port,buf,nr) \
__asm__( "cld;rep;outsw" :: "d" (port), "S" (buf), "c" (nr): "cx" , "si" )extern void hd_interrupt(void); // 硬盘中断过程(system_call.s,221 行)。
extern void rd_load(void); // 虚拟盘创建加载函数(ramdisk.c,71 行)。/* This may be used only once, enforced by 'static int callable' */
/* 下面该函数只在初始化时被调用一次。用静态变量 callable 作为可调用标志。*/
// 该函数的参数由初始化程序 init/main.c 的 init 子程序设置为指向 0x90080 处,此处存放着 setup.s
// 程序从 BIOS 取得的 2 个硬盘的基本参数表(32 字节)。硬盘参数表信息参见下面列表后的说明。
// 本函数主要功能是读取 CMOS 和硬盘参数表信息,用于设置硬盘分区结构 hd,并加载 RAM 虚拟盘和
// 根文件系统。
int sys_setup(void * BIOS)
{static int callable = 1;int i,drive;unsigned char cmos_disks;struct partition *p;struct buffer_head * bh;// 初始化时 callable=1,当运行该函数时将其设置为 0,使本函数只能执行一次。if (!callable)return -1;callable = 0;// 如果没有在 config.h 中定义硬盘参数,就从 0x90080 处读入。#ifndef HD_TYPEfor (drive=0 ; drive<2 ; drive++) {hd_info[drive].cyl = *(unsigned short *) BIOS; // 柱面数。hd_info[drive].head = *(unsigned char *) (2+BIOS); // 磁头数。hd_info[drive].wpcom = *(unsigned short *) (5+BIOS); // 写前预补偿柱面号。hd_info[drive].ctl = *(unsigned char *) (8+BIOS); // 控制字节。hd_info[drive].lzone = *(unsigned short *) (12+BIOS); // 磁头着陆区柱面号。hd_info[drive].sect = *(unsigned char *) (14+BIOS); // 每磁道扇区数。BIOS += 16; // 每个硬盘的参数表长 16 字节,这里 BIOS 指向下一个表。}// setup.s 程序在取 BIOS 中的硬盘参数表信息时,如果只有 1 个硬盘,就会将对应第 2 个硬盘的// 16 字节全部清零。因此这里只要判断第 2 个硬盘柱面数是否为 0 就可以知道有没有第 2 个硬盘了。if (hd_info[1].cyl)NR_HD=2; // 硬盘数置为 2。elseNR_HD=1;#endif// 设置每个硬盘的起始扇区号和扇区总数。其中编号 i*5 含义参见本程序后的有关说明。for (i=0 ; i<NR_HD ; i++) {hd[i*5].start_sect = 0; // 硬盘起始扇区号。hd[i*5].nr_sects = hd_info[i].head*hd_info[i].sect*hd_info[i].cyl; // 硬盘总扇区数。}/*We querry CMOS about hard disks : it could be that we have a SCSI/ESDI/etc controller that is BIOScompatable with ST-506, and thus showing up in ourBIOS table, but not register compatable, and thereforenot present in CMOS.Furthurmore, we will assume that our ST-506 drives<if any> are the primary drives in the system, and the ones reflected as drive 1 or 2.The first drive is stored in the high nibble of CMOSbyte 0x12, the second in the low nibble. This will beeither a 4 bit drive type or 0xf indicating use byte 0x19 for an 8 bit type, drive 1, 0x1a for drive 2 in CMOS.Needless to say, a non-zero value means we have an AT controller hard disk for that drive.*//** 我们对 CMOS 有关硬盘的信息有些怀疑:可能会出现这样的情况,我们有一块 SCSI/ESDI/等的* 控制器,它是以 ST-506 方式与 BIOS 兼容的,因而会出现在我们的 BIOS 参数表中,但却又不* 是寄存器兼容的,因此这些参数在 CMOS 中又不存在。* 另外,我们假设 ST-506 驱动器(如果有的话)是系统中的基本驱动器,也即以驱动器 1 或 2* 出现的驱动器。* 第 1 个驱动器参数存放在 CMOS 字节 0x12 的高半字节中,第 2 个存放在低半字节中。该 4 位字节* 信息可以是驱动器类型,也可能仅是 0xf。0xf 表示使用 CMOS 中 0x19 字节作为驱动器 1 的 8 位* 类型字节,使用 CMOS 中 0x1A 字节作为驱动器 2 的类型字节。* 总之,一个非零值意味着我们有一个 AT 控制器硬盘兼容的驱动器。*/// 这里根据上述原理来检测硬盘到底是否是 AT 控制器兼容的。有关 CMOS 信息请参见 4.2.3.1 节。if ((cmos_disks = CMOS_READ(0x12)) & 0xf0)if (cmos_disks & 0x0f)NR_HD = 2;elseNR_HD = 1;elseNR_HD = 0;// 若 NR_HD=0,则两个硬盘都不是 AT 控制器兼容的,硬盘数据结构清零。// 若 NR_HD=1,则将第 2 个硬盘的参数清零。for (i = NR_HD ; i < 2 ; i++) {hd[i*5].start_sect = 0;hd[i*5].nr_sects = 0;}// 读取每一个硬盘上第 1 块数据(第 1 个扇区有用),获取其中的分区表信息。// 首先利用函数 bread()读硬盘第 1 块数据(fs/buffer.c,267),参数中的 0x300 是硬盘的主设备号// (参见列表后的说明)。然后根据硬盘头 1 个扇区位置 0x1fe 处的两个字节是否为'55AA'来判断// 该扇区中位于 0x1BE 开始的分区表是否有效。最后将分区表信息放入硬盘分区数据结构 hd 中。for (drive=0 ; drive<NR_HD ; drive++) {if (!(bh = bread(0x300 + drive*5,0))) { // 0x300, 0x305 逻辑设备号。printk( "Unable to read partition table of drive %d\n\r" ,drive);panic( "" );}if (bh->b_data[510] != 0x55 || (unsigned char)bh->b_data[511] != 0xAA) { // 判断硬盘信息有效标志'55AA'。printk( "Bad partition table on drive %d\n\r" ,drive);panic( "" );}p = 0x1BE + (void *)bh->b_data; // 分区表位于硬盘第 1 扇区的 0x1BE 处。for (i=1;i<5;i++,p++) {hd[i+5*drive].start_sect = p->start_sect;hd[i+5*drive].nr_sects = p->nr_sects;}brelse(bh); // 释放为存放硬盘块而申请的内存缓冲区页。}if (NR_HD) // 如果有硬盘存在并且已读入分区表,则打印分区表正常信息。printk( "Partition table%s ok.\n\r" ,(NR_HD>1)? "s" : "" );rd_load(); // 加载(创建)RAMDISK(kernel/blk_drv/ramdisk.c,71)。mount_root(); // 安装根文件系统(fs/super.c,242)。return (0);}判断并循环等待驱动器就绪。
// 读硬盘控制器状态寄存器端口 HD_STATUS(0x1f7),并循环检测驱动器就绪比特位和控制器忙位。
// 如果返回值为 0,则表示等待超时出错,否则 OK。static int controller_ready(void){int retries=10000;while (--retries && (inb_p(HD_STATUS)&0xc0)!=0x40);return (retries); // 返回等待循环的次数。}检测硬盘执行命令后的状态。(win_表示温切斯特硬盘的缩写)
// 读取状态寄存器中的命令执行结果状态。返回 0 表示正常,1 出错。如果执行命令错,
// 则再读错误寄存器 HD_ERROR(0x1f1)。static int win_result(void){int i=inb_p(HD_STATUS); // 取状态信息。if ((i & (BUSY_STAT | READY_STAT | WRERR_STAT | SEEK_STAT | ERR_STAT))== (READY_STAT | SEEK_STAT))return(0); /* ok */if (i&1) i=inb(HD_ERROR); // 若 ERR_STAT 置位,则读取错误寄存器。return (1);}向硬盘控制器发送命令块(参见列表后的说明)。
// 调用参数:drive - 硬盘号(0-1); nsect - 读写扇区数;
// sect - 起始扇区; head - 磁头号;
// cyl - 柱面号; cmd - 命令码(参见控制器命令列表,表 6.3);
// *intr_addr() - 硬盘中断发生时处理程序中将调用的 C 处理函数。static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,unsigned int head,unsigned int cyl,unsigned int cmd,void (*intr_addr)(void)){register int port asm( "dx" ); // port 变量对应寄存器 dx。if (drive>1 || head>15) // 如果驱动器号(0,1)>1 或磁头号>15,则程序不支持。panic( "Trying to write bad sector" );if (!controller_ready()) // 如果等待一段时间后仍未就绪则出错,死机。panic( "HD controller not ready" );do_hd = intr_addr; // do_hd 函数指针将在硬盘中断程序中被调用。outb_p(hd_info[drive].ctl,HD_CMD); // 向控制寄存器(0x3f6)输出控制字节。port=HD_DATA; // 置 dx 为数据寄存器端口(0x1f0)。outb_p(hd_info[drive].wpcom>>2,++port); // 参数:写预补偿柱面号(需除 4)。outb_p(nsect,++port); // 参数:读/写扇区总数。outb_p(sect,++port); // 参数:起始扇区。outb_p(cyl,++port); // 参数:柱面号低 8 位。outb_p(cyl>>8,++port); // 参数:柱面号高 8 位。outb_p(0xA0|(drive<<4)|head,++port); // 参数:驱动器号+磁头号。outb(cmd,++port); // 命令:硬盘控制命令。}等待硬盘就绪。也即循环等待主状态控制器忙标志位复位。若仅有就绪或寻道结束标志
// 置位,则成功,返回 0。若经过一段时间仍为忙,则返回 1。static int drive_busy(void){unsigned int i;for (i = 0; i < 10000; i++) // 循环等待就绪标志位置位。if (READY_STAT == (inb_p(HD_STATUS) & (BUSY_STAT|READY_STAT)))break;i = inb(HD_STATUS); // 再取主控制器状态字节。i &= BUSY_STAT | READY_STAT | SEEK_STAT; // 检测忙位、就绪位和寻道结束位。if (i == READY_STAT | SEEK_STAT) // 若仅有就绪或寻道结束标志,则返回 0。return(0);printk( "HD controller times out\n\r" ); // 否则等待超时,显示信息。并返回 1。return(1);}诊断复位(重新校正)硬盘控制器。static void reset_controller(void){int i;outb(4,HD_CMD); // 向控制寄存器端口发送控制字节(4-复位)。for(i = 0; i < 100; i++) nop(); // 等待一段时间(循环空操作)。outb(hd_info[0].ctl & 0x0f ,HD_CMD); // 再发送正常的控制字节(不禁止重试、重读)。if (drive_busy()) // 若等待硬盘就绪超时,则显示出错信息。printk( "HD-controller still busy\n\r" );if ((i = inb(HD_ERROR)) != 1) // 取错误寄存器,若不等于 1(无错误)则出错。printk( "HD-controller reset failed: %02x\n\r" ,i);}复位硬盘 nr。首先复位(重新校正)硬盘控制器。然后发送硬盘控制器命令“建立驱动器参数”,
// 其中 recal_intr()是在硬盘中断处理程序中调用的重新校正处理函数。static void reset_hd(int nr){reset_controller();hd_out(nr,hd_info[nr].sect,hd_info[nr].sect,hd_info[nr].head-1,hd_info[nr].cyl,WIN_SPECIFY,&recal_intr);}意外硬盘中断调用函数。
// 发生意外硬盘中断时,硬盘中断处理程序中调用的默认 C 处理函数。在被调用函数指针为空时
// 调用该函数。参见(kernel/system_call.s,241 行)。void unexpected_hd_interrupt(void){printk( "Unexpected HD interrupt\n\r" );}读写硬盘失败处理调用函数。static void bad_rw_intr(void){if (++CURRENT->errors >= MAX_ERRORS) // 如果读扇区时的出错次数大于或等于 7 次时,end_request(0); // 则结束请求并唤醒等待该请求的进程,而且// 对应缓冲区更新标志复位(没有更新)。if (CURRENT->errors > MAX_ERRORS/2) // 如果读一扇区时的出错次数已经大于 3 次,reset = 1; // 则要求执行复位硬盘控制器操作。}读操作中断调用函数。将在硬盘读命令结束时引发的中断过程中被调用。
// 该函数首先判断此次读命令操作是否出错。若命令结束后控制器还处于忙状态,或者命令执行错误,
// 则处理硬盘操作失败问题,接着请求硬盘作复位处理并执行其它请求项。
// 如果读命令没有出错,则从数据寄存器端口把一个扇区的数据读到请求项的缓冲区中,并递减请求项
// 所需读取的扇区数值。若递减后不等于 0,表示本项请求还有数据没取完,于是直接返回,等待硬盘
// 在读出另一个扇区数据后的中断。否则表明本请求项所需的所有扇区都已读完,于是处理本次请求项
// 结束事宜。最后再次调用 do_hd_request(),去处理其它硬盘请求项。
// 注意:257 行语句中的 256 是指内存字,也即 512 字节。static void read_intr(void){if (win_result()) { // 若控制器忙、读写错或命令执行错,bad_rw_intr(); // 则进行读写硬盘失败处理do_hd_request(); // 然后再次请求硬盘作相应(复位)处理。return;}port_read(HD_DATA,CURRENT->buffer,256); // 将数据从数据寄存器口读到请求结构缓冲区。CURRENT->errors = 0; // 清出错次数。CURRENT->buffer += 512; // 调整缓冲区指针,指向新的空区。CURRENT->sector++; // 起始扇区号加 1,if (--CURRENT->nr_sectors) { // 如果所需读出的扇区数还没有读完,则do_hd = &read_intr; // 再次置硬盘调用 C 函数指针为 read_intr()return; // 因为硬盘中断处理程序每次调用 do_hd 时} // 都会将该函数指针置空。参见 system_call.send_request(1); // 若全部扇区数据已经读完,则处理请求结束事宜,do_hd_request(); // 执行其它硬盘请求操作。}写扇区中断调用函数。在硬盘中断处理程序中被调用。
// 在写命令执行后,会产生硬盘中断信号,执行硬盘中断处理程序,此时在硬盘中断处理程序中调用的
// C 函数指针 do_hd()已经指向 write_intr(),因此会在写操作完成(或出错)后,执行该函数。static void write_intr(void){if (win_result()) { // 如果硬盘控制器返回错误信息,bad_rw_intr(); // 则首先进行硬盘读写失败处理,do_hd_request(); // 然后再次请求硬盘作相应(复位)处理,return; // 然后返回(也退出了此次硬盘中断)。}if (--CURRENT->nr_sectors) { // 否则将欲写扇区数减 1,若还有扇区要写,则CURRENT->sector++; // 当前请求起始扇区号+1,CURRENT->buffer += 512; // 调整请求缓冲区指针,do_hd = &write_intr; // 置硬盘中断程序调用函数指针为 write_intr(),port_write(HD_DATA,CURRENT->buffer,256); // 再向数据寄存器端口写 256 字。return; // 返回等待硬盘再次完成写操作后的中断处理。}end_request(1); // 若全部扇区数据已经写完,则处理请求结束事宜,do_hd_request(); // 执行其它硬盘请求操作。}硬盘重新校正(复位)中断调用函数。在硬盘中断处理程序中被调用。
// 如果硬盘控制器返回错误信息,则首先进行硬盘读写失败处理,然后请求硬盘作相应(复位)处理。static void recal_intr(void){if (win_result())bad_rw_intr();do_hd_request();}执行硬盘读写请求操作。
// 若请求项是块设备的第 1 个,则块设备当前请求项指针(参见 ll_rw_blk.c,28 行)会直接指向该
// 请求项,并会立刻调用本函数执行读写操作。否则在一个读写操作完成而引发的硬盘中断过程中,
// 若还有请求项需要处理,则也会在中断过程中调用本函数。参见 kernel/system_call.s,221 行。void do_hd_request(void){int i,r;unsigned int block,dev;unsigned int sec,head,cyl;unsigned int nsect;// 检测请求项的合法性,若已没有请求项则退出(参见 blk.h,127)。INIT_REQUEST;// 取设备号中的子设备号(见列表后对硬盘设备号的说明)。子设备号即是硬盘上的分区号。dev = MINOR(CURRENT->dev); // CURRENT 定义为 blk_dev[MAJOR_NR].current_request。block = CURRENT->sector; // 请求的起始扇区。// 如果子设备号不存在或者起始扇区大于该分区扇区数-2,则结束该请求,并跳转到标号 repeat 处// (定义在 INIT_REQUEST 开始处)。因为一次要求读写 2 个扇区(512*2 字节),所以请求的扇区号// 不能大于分区中最后倒数第二个扇区号。if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) {end_request(0);goto repeat; // 该标号在 blk.h 最后面。}// 通过加上本分区的起始扇区号,把将所需读写的块对应到整个硬盘的绝对扇区号上。block += hd[dev].start_sect;dev /= 5; // 此时 dev 代表硬盘号(是第 1 个硬盘(0)还是第 2 个(1))。// 下面嵌入汇编代码用来从硬盘信息结构中根据起始扇区号和每磁道扇区数计算在磁道中的// 扇区号(sec)、所在柱面号(cyl)和磁头号(head)。__asm__( "divl %4" : "=a" (block), "=d" (sec): "" (block), "1" (0),"r" (hd_info[dev].sect));__asm__( "divl %4" : "=a" (cyl), "=d" (head): "" (block), "1" (0),"r" (hd_info[dev].head));sec++;nsect = CURRENT->nr_sectors; // 欲读/写的扇区数。// 如果 reset 标志是置位的,则执行复位操作。复位硬盘和控制器,并置需要重新校正标志,返回。if (reset) {reset = 0;recalibrate = 1;reset_hd(CURRENT_DEV);return;}// 如果重新校正标志(recalibrate)置位,则首先复位该标志,然后向硬盘控制器发送重新校正命令。// 该命令会执行寻道操作,让处于任何地方的磁头移动到 0 柱面。if (recalibrate) {recalibrate = 0;hd_out(dev,hd_info[CURRENT_DEV].sect,0,0,0,WIN_RESTORE,&recal_intr);return;}// 如果当前请求是写扇区操作,则发送写命令,循环读取状态寄存器信息并判断请求服务标志// DRQ_STAT 是否置位。DRQ_STAT 是硬盘状态寄存器的请求服务位,表示驱动器已经准备好在主机和// 数据端口之间传输一个字或一个字节的数据。if (CURRENT->cmd == WRITE) {hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);for(i=0 ; i<3000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++)/* nothing */ ;// 如果请求服务 DRQ 置位则退出循环。若等到循环结束也没有置位,则表示此次写硬盘操作失败,去// 处理下一个硬盘请求。否则向硬盘控制器数据寄存器端口 HD_DATA 写入 1 个扇区的数据。if (!r) {bad_rw_intr();goto repeat; // 该标号在 blk.h 文件最后面,也即跳到 301 行。}port_write(HD_DATA,CURRENT->buffer,256);// 如果当前请求是读硬盘扇区,则向硬盘控制器发送读扇区命令。} else if (CURRENT->cmd == READ) {hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);} elsepanic( "unknown hd-command" );}// 硬盘系统初始化。void hd_init(void){blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; // do_hd_request()。set_intr_gate(0x2E,&hd_interrupt); // 设置硬盘中断门向量 int 0x2E(46)。// hd_interrupt 在(kernel/system_call.s,221)。outb_p(inb_p(0x21)&0xfb,0x21); // 复位接联的主 8259A int2 的屏蔽位,允许从片// 发出中断请求信号。outb(inb_p(0xA1)&0xbf,0xA1); // 复位硬盘的中断请求屏蔽位(在从片上),允许// 硬盘控制器发送中断请求信号。}
3.其他信息
3.1 AT 硬盘接口寄存器
AT 硬盘控制器的编程寄存器端口说明见下表所示。另外请参见 include/linux/hdreg.h 头文件。
下面对各端口寄存器进行详细说明:
◆ 数据寄存器( HD_DATA , 0x1f0 )
这是一对 16 位高速 PIO 数据传输器,用于扇区读、写和磁道格式化操作。 CPU 通过该数据寄存器向硬盘写入或从硬盘读出 1 个扇区的数据,也即要使用命令 'rep outsw' 或 'rep insw' 重复读 / 写 cx=256 字。
◆ 错误寄存器(读) / 写前预补偿寄存器(写)(HD_ERROR , 0x1f1)
在读时,该寄存器存放有 8 位的错误状态。但只有当主状态寄存器 (HD_STATUS , 0x1f7) 的位 0=1时该寄存器中的数据才有效。执行控制器诊断命令时的含义与其他命令时的不同。见下表所示。
在写操作时,该寄存器即作为写前预补偿寄存器。它记录写预补偿起始柱面号。对应于与硬盘基本参数表位移 0x05 处的一个字,需除 4 后输出。
◆扇区数寄存器( HD_NSECTOR , 0x1f2 )
该寄存器存放读、写、检验和格式化命令指定的扇区数。当用于多扇区操作时,每完成 1 扇区的操作该寄存器就自动减 1,直到为 0 。若初值为 0 ,则表示传输最大扇区数 256 。
◆ 扇区号寄存器( HD_SECTOR , 0x1f3 )
该寄存器存放读、写、检验操作命令指定的扇区号。在多扇区操作时,保存的是起始扇区号,而每完成 1 扇区的操作就自动增 1 。
◆ 柱面号寄存器( HD_LCYL , HD_HCYL , 0x1f4 , 0x1f5 )
该两个柱面号寄存器分别存放有柱面号的低 8 位和高 2 位。
◆驱动器 / 磁头寄存器 (HD_CURRENT , 0x1f6)
该寄存器存放有读、写、检验、寻道和格式化命令指定的驱动器和磁头号。其位格式为 101dhhhh 。其中 101 表示采用ECC 校验码和每扇区为 512 字节; d 表示选择的驱动器( 0 或 1 ); hhhh 表示选择的磁头。见下表 所示。
◆主状态寄存器(读) / 命令寄存器(写)( HD_STATUS/HD_COMMAND , 0x1f7)
在读时,对应一个 8 位主状态寄存器。反映硬盘控制器在执行命令前后的操作状态。各位的含义见下表所示。
当执行写操作时,该端口对应命令寄存器,接受 CPU 发出的硬盘控制命令,共有 8 种命令,见下表所示。其中最后一列用于说明相应命令结束后控制器所采取的动作(引发中断或者什么也不做)。
表中命令码字节的低 4 位是附加参数,其含义为:
R 是步进速率。 R=0 ,则步进速率为 35us ; R=1 为 0.5ms ,以此量递增。程序中默认 R=0 。
L 是数据模式。 L=0 表示读 / 写扇区为 512 字节; L=1 表示读 / 写扇区为 512 加 4 字节的 ECC 码。
程序中默认值是 L=0 。
T 是重试模式。 T=0 表示允许重试; T=1 则禁止重试。程序中取 T=0 。
下面分别对这几个命令进行详细说明。
(1) 0x1X -- ( WIN_RESTORE ),驱动器重新校正( Recalibrate )命令
该命令把读 / 写磁头从磁盘上任何位置移动到 0 柱面。当接收到该命令时,驱动器会设置BUSY_STAT 标志并且发出一个0 柱面寻道指令。然后驱动器等待寻道操 作结束,更新状态、复位BUSY_STAT 标志并且产生一个中断。
(2) 0x20 -- ( WIN_READ )可重试读扇区; 0x21 -- 无重试读扇区。
读扇区命令可以从指定扇区开始读取 1 到 256 个扇区。若所指定的命令块中扇区计数为 0 的话,则表示读取 256 个扇区。当驱动器接受了该命令,将会设立BUSY_STAT 标志并且开始执行该命令。对于单个扇区的读取操作,若磁头的磁道位置不对,则驱动器会隐含地执行一次寻道操作。一旦磁头在正确的磁道上,驱动器磁头就会定位到磁道地址场中相应 的标志域( ID 域)上。
对于无重试读扇区命令,若两个索引脉冲发生之前不能正确读取无错的指定 ID 域,则驱动器就会在错误寄存器中给出 ID 没有找到的错误信息。对于可重试读扇区命令,驱动器则会在读 ID 域碰到问题时重试多次。重试的次数由驱动器 厂商设定。
如果驱动器正确地读到了 ID 域,那么它就需要在指定的字节数中识别数据地址标志( DataAddress Mark ),否则就报告数据地址标志没有找到的错误。一旦磁头找到数据地址标志,驱动器就会把数据域中的数据读入扇区缓冲区中。如果发生错误,驱动器就会设置出错比特位、设置DRQ_STAT 并且产生一个中断。不管是否发生错误,驱动器总是会在读扇区后设置 DRQ_STAT 。在命令完成后,命令块寄存器中将含有最后一个所读扇区的柱面号、磁头号和扇区号。
对于多扇区读操作,每当驱动器准备好向主机发送一个扇区的数据时就会设置 DRQ_STAT 、清BUSY_STAT 标志并且产生一个中断。当扇区数据传输结束,驱动器就会复位 DRQ_STAT 和BUSY_STAT 标志,但在最后一个扇区传输完成后会设置 BUSY_STAT 标志。在命令结束后命令块寄存器中将含有最后一个所读扇区的柱面号、磁头号和扇区号。
如果在多扇区读操作中发生了一个不可纠正的错误,读操作将在发生错误的扇区处终止。同样,此时命令块寄存器中将含有该出错扇区的柱面号、磁头号和扇区号。不管错误是否可以被纠正,驱动器都会把数据放入扇区缓冲区中。
(3) 0x30 -- ( WIN_WRITE )可重试写扇区; 0x31 -- 无重试写扇区。
写扇区命令可以从指定扇区开始写 1 到 256 个扇区。若所指定的命令块(见表 6–9 )中扇区计数为 0 的话,则表示要写 256 个扇区。当驱动器接受了该命令,它将设置 DRQ_STAT 并等待扇区缓冲区被添满数据。在开始第一次向扇区缓冲区添入数据时不会产生中断,一旦数据填满驱动器就会复位 DRQ 、设置 BUSY_STAT 标志并且开始执行命令。
对于写一个扇区数据的操作,驱动器会在收到命令时设置 DRQ_STAT 并且等待主机填满扇区缓冲区。一旦数据已被传输,驱动器就会设置 BUSY_STAT 并且复位 DRQ_STAT 。与读扇区操作一样,若磁头的磁道位置不对,则驱动器会隐含地执行一次寻道操作。一旦磁头在正确的磁道上,驱动器磁头就会定位到磁道地址场中相应的标志域( ID 域)上。
如果 ID 域被正确地读出,则扇区缓冲区中的数据包括 ECC 字节就被写到磁盘上。当驱动器处理过扇区后就会清 BUSY_STAT 标志并且产生一个中断。此时主机就可以读取状态寄存器。在命令结束后,命令块寄存器中将含有最后一 个所写扇区的柱面号、磁头号和扇区号。
在多扇区写操作期间,除了对第一个扇区的操作,当驱动器准备好从主机接收一个扇区的数据时就会设置 DRQ_STAT 、清 BUSY_STAT 标志并且产生一个中断。一旦一个扇区传输完毕,驱动器就会复位 DRQ 并设置 BUSY 标志。当最后一个扇区被写到磁盘上后,驱动器就会清掉 BUSY_STAT标志并产生一个中断(此时 DRQ_STAT 已经复位)。在写命令结束后,命令块寄存器中将含有最后一个所写扇区的柱面号、磁头号和扇区号。
如果在多扇区写操作中发生了一个错误,写操作将在发生错误的扇区处终止。同样,此时命令块寄存器中将含有该出错扇区的柱面号、磁头号和扇区号。
(4) 0x40 -- ( WIN_VERIFY )可重试读扇区验证; 0x41 -- 无重试读扇区验证。
该命令的执行过程与读扇区操作相同,但是本命令不会导致驱动器去设置 DRQ_STAT ,并且不会向主机传输数据。当收到读验证命令时,驱动器就会设置 BUSY_STAT 标志。当指定的扇区被验证过后,驱动器就会复位 BUSY_STAT 标志并且产生一个中断。在命令结束后,命令块寄存器中将含有最后一个所验证扇区的柱面号、磁头号和扇区号。
如果在多扇区验证操作中发生了一个错误,验证操作将在发生错误的扇区处终止。同样,此时命令块寄存器中将含有该出错扇区的柱面号、磁头号和扇区号。
(5) 0x50 -- ( WIN_FORMAT )格式化磁道命令。
扇区计数寄存器中指定了磁道地址。当驱动器接受该命令时,它会设置 DRQ_STAT 比特位,然后等待主机填满扇区缓冲区。当缓冲区满后,驱动器就会清 DRQ_STAT 、设置 BUSY_STAT 标志并且开始命令的执行。
(6) 0x60 -- ( WIN_INIT )控制器初始化。
(7) 0x7X -- ( WIN_SEEK )寻道操作。
寻道操作命令将命令块寄存器中所选择的磁头移动到指定的磁道上。当主机发出一个寻道命令时,驱动器会设置BUSY 标志并且产生一个中断。在寻道操作结束之前,驱动器在寻道操作完成之前不会设置 SEEK_STAT ( DSC - 寻道完成)。在驱动器产生一个中断之前寻道操作可能还没有完成。如果在寻道操作进行当中主机又向驱动器发出了一个新命令,那么 BUSY_STAT 将依然处于置位状态,直到寻道结束。然后驱动器才开始执行新的命令。
(8) 0x90 -- ( WIN_DIAGNOSE )驱动器诊断命令。
该命令执行驱动器内部实现的诊断测试过程。驱动器 0 会在收到该命令的 400ns 内设置BUSY_STAT 比特位。
如果系统中含有第 2 个驱动器,即驱动器 1 ,那么两个驱动器都会执行诊断操作。驱动器 0 会等待驱动器 1 执行诊断操作 5 秒钟。如果驱动器 1 诊断操作失败,则驱动器 0 就会在自己的诊断状态中附加 0x80 。如果主机在读取驱动器 0的状态时检测到驱动器 1 的诊断操作失败,它就会设置驱动器 / 磁头寄存器 (0x1f6) 的驱动器选择比特位(位 4 ),然后读取驱动器 1 的状态。如果驱动器 1 通过诊断检测或者驱动器 1 不存在,则驱动器 0 就直接把自己的诊断状态加载到出错寄存器中。
如果驱动器 1 不存在,那么驱动器 0 仅报告自己的诊断结果,并且在复位 BUSY_STAT 比特位后产生一个中断。
(9) 0x91 -- ( WIN_SPECIFY )建立驱动器参数命令。
该命令用于让主机设置多扇区操作时磁头交换和扇区计数循环值。在收到该命令时驱动器会设置 BUSY_STAT 比特位并产生一个中断。该命令仅使用两个寄存器的值。一个是扇区计数寄存器,用于指定扇区数;另一个是驱动器 / 磁头寄存器,用于指定磁头数 -1 ,而驱动器选择比特位(位 4 )则根据具体选择的驱动器来设置。
该命令不会验证所选择的扇区计数值和磁头数。如果这些值无效,驱动器不会报告错误。直到另一个命令使用这些值而导致无效一个访问错误。
◆ 硬盘控制寄存器(写)( HD_CMD , 0x3f6 )
该寄存器是只写的。用于存放硬盘控制字节并控制复位操作。其定义与硬盘基本参数表的位移 0x08处的字节说明相同,见下表所示。
3.2 AT 硬盘控制器编程
在对硬盘控制器进行操作控制时,需要同时发送参数和命令。其命令格式见下表 所示。首先发送6 字节的参数,最后发出 1 字节的命令码。不管什么命令均需要完整输出这 7 字节的命令块,依次写入端口 0x1f1 -- 0x1f7 。一旦命令块寄存器加载,命令就开始执行。
首先 CPU 向控制寄存器端口 (HD_CMD)0x3f6 输出控制字节,建立相应的硬盘控制方式。方式建立后即可按上面顺序发送参数和命令。步骤为:
1. 检测控制器空闲状态: CPU 通过读主状态寄存器,若位 7 ( BUSY_STAT )为 0 ,表示控制器空闲。若在规定时间内控制器一直处于忙状态,则判为超时出错。参见 hd.c 中第 161 行的controller_ready() 函数。
2. 检测驱动器是否就绪: CPU 判断主状态寄存器位 6 ( READY_STAT )是否为 1 来看驱动器是否就绪。为 1 则可输出参数和命令。参见 hd.c 中第 202 行的 drive_busy() 函数。
3. 输出命令块:按顺序输出分别向对应端口输出参数和命令。参见 hd.c 中第 180 行开始的 hd_out()函数。
4. CPU 等待中断产生:命令执行后,由硬盘控制器产生中断请求信号( IRQ14 -- 对应中断 int46 )或置控制器状态为空闲, 表明操作结束或表示请求扇区传输(多扇区读 / 写)。程序 hd.c 中在中断处理过程中调用的函数参见代码 237--293 行。有5 个函数分别对应 5 种情况。
5. 检测操作结果: CPU 再次读主状态寄存器,若位 0 等于 0 则表示命令执行成功,否则失败。若失败则可进一步查询错误寄存器 (HD_ERROR) 取错误码。参见 hd.c 中第 202 行的 win_result() 函数。
3.3 硬盘基本参数表
中断向量表中, int 0x41 的中断向量位置( 4 * 0x41 =0x0000:0x0104 )存放的并不是中断程序的地址而是第一个硬盘的基本参数表,见表 6–10 所示。对于 100% 兼容的 BIOS 来说,这里存放着硬盘参数表阵列的首地址 F000h:E401h 。第二个硬盘的基本参数表入口地址存于 int 0x46 中断向量中。
3.4 硬盘设备号命名方式
硬盘的主设备号是 3 。其他设备的主设备号分别为:
1- 内存 ,2- 磁盘 ,3- 硬盘 ,4-ttyx,5-tty,6- 并行口 ,7- 非命名管道
由于 1 个硬盘中可以存在 1--4 个分区,因此硬盘还依据分区的不同用次设备号进行指定分区。因此硬盘的逻辑设备号由以下方式构成:
设备号 = 主设备号 *256 + 次设备号
也即 dev_no = (major<<8) + minor
两个硬盘的所有逻辑设备号见下表所示。
3.5 硬盘分区表
为了实现多个操作系统共享硬盘资源,硬盘可以在逻辑上分为 1--4 个分区。每个分区之间的扇区号是邻接的。分区表由 4 个表项组成,每个表项由 16 字节组成,对应一个分区的信息,存放有分区的大小和起止的柱面号、磁道号和扇区号,见表 6–12 所示。分区表存放在硬盘的 0 柱面 0 头第 1 个扇区的0x1BE--0x1FD 处。
硬盘的第 1 个扇区称为主引导扇区( Master Boot Record --MBR ),它除了多包含一个分区表以外,其他与软盘上第一个扇区( boot 扇区)的作用一样,只是它的代码会把自己从 0x7c00 下移到 0x6000 处,腾出 0x7c00处的空间,然后根据分区表中的信息,找出活动分区是哪一个,接着把活动分区的第 1 个扇区加载到 0x7c00 处去执行。一个分区从硬盘的哪个柱面、磁头和扇区开始,都记录在分区表中。因此从分区表中可以知道一个活动分区的第 1 个扇区(即该分区的引导扇区)在硬盘的什么地方。