linux内核版本 c 11,linux0.11内核彻底剖析 - hd.c

声明:

参考《linux内核彻底剖析基于linux0.11》--赵炯    节选

块设备驱动

一、功能描述node

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 行)。linux

do_hd_request() 是硬盘请求项的操做函数。其操做流程以下:

--  首先判断当前请求项是否存在,若当前请求项指针为空,则说明目前硬盘块设备已经没有待处理的请求项,所以马上退出程序。这是在宏 INIT_REQUEST 中执行的语句。不然就继续处理当前请求项。

--对当前请求项中指明的设备号和请求的盘起始扇区号的合理性进行验证;

--根据当前请求项提供的信息计算请求数据的磁盘磁道号、磁头号和柱面号;

--若是复位标志( reset )已被设置,则也设置硬盘从新校订标志( recalibrate ),并对硬盘执行复位操做,向控制器从新发送“创建驱动器参数”命令( WIN_SPECIFY )。该命令不会引起硬盘中断;

--若是从新校订标志被置位的话,就向控制器发送硬盘从新校订命令( WIN_RESTORE ),并在发送以前预先设置好该命令引起的中断中须要执行的 C 函数( recal_intr() ),并退出。 recal_intr() 函数的主要做用是:当控制器执行该命令结束并引发中断时,能从新(继续)执行本函数。c++

--若是当前请求项指定是写操做,则首先设置硬盘控制器调用的 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() 函数,以继续处理硬盘设备的其余请求项。

为了能更清晰的看清楚硬盘读写操做的处理过程,咱们能够把这些函数、中断处理过程以及硬盘控制器三者之间的执行时序关系用下图表示出来。数据结构

57d6fbb8bb5a0dcad73107a3376c33ec.png

2d0fca2f12dcfad2cd5f37c3379a8097.png

由以上分析能够看出,本程序中最重要的 4 个函数是 hd_out() 、 do_hd_request() 、 read_intr() 和write_intr() 。理解了这 4 个函数的做用也就理解了硬盘驱动程序的操做过程 ☺ 。less

值得注意的是,在使用hd_out()向硬盘控制器发送了读写或其余命令后,hd_out()函数并不会等待所发命令的执行过程,而是马上返回调用它的程序中,例如 do_hd_request()。而 do_hd_request()函数也马上返回上一级调用它的函数(add_request()),最终返回到调用块设备读写函数ll_rw_block()的其余程序。(例如 fs/buffer.c 的 bread()函数)中去等待块设备IO的完成。函数

2.代码注释

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 // 内核配置头文件。定义键盘语言和硬盘类型(HD_TYPE)可选项。

#include // 调度程序头文件,定义了任务结构 task_struct、初始任务 0 的数据

#include // 文件系统头文件。定义文件表结构(file,buffer_head,m_inode 等)。

#include // 内核头文件。含有一些内核经常使用函数的原形定义。

#include // 硬盘参数头文件。定义访问硬盘寄存器端口,状态码,分区表等信息。

#include // 系统头文件。定义了设置或修改描述符/中断门等的嵌入式汇编宏。

#include // io 头文件。定义硬件端口输入/输出宏汇编语句。

#include // 段操做头文件。定义了有关段寄存器操做的嵌入式汇编函数。

// 必须在 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_TYPE

for (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。

else

NR_HD=1;

#endif

// 设置每一个硬盘的起始扇区号和扇区总数。其中编号 i*5 含义参见本程序后的有关说明。

for (i=0 ; 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 BIOS

compatable with ST-506, and thus showing up in our

BIOS table, but not register compatable, and therefore

not present in CMOS.

Furthurmore, we will assume that our ST-506 drives

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 CMOS

byte 0x12, the second in the low nibble. This will be

either 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;

else

NR_HD = 1;

else

NR_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

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.s

end_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);

} else

panic( "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 头文件。spa

d634f6aa79186e786a2a6126f05d241c.png

下面对各端口寄存器进行详细说明:操作系统

◆数据寄存器( HD_DATA , 0x1f0 )

这是一对 16 位高速 PIO 数据传输器,用于扇区读、写和磁道格式化操做。 CPU 经过该数据寄存器向硬盘写入或从硬盘读出 1 个扇区的数据,也即要使用命令 'rep outsw' 或 'rep insw' 重复读 / 写 cx=256 字。

◆错误寄存器(读) / 写前预补偿寄存器(写)(HD_ERROR , 0x1f1)

在读时,该寄存器存放有 8 位的错误状态。但只有当主状态寄存器 (HD_STATUS , 0x1f7) 的位 0=1时该寄存器中的数据才有效。执行控制器诊断命令时的含义与其余命令时的不一样。见下表所示。

99cebdab00084fc6111baf14d767a10a.png

在写操做时,该寄存器即做为写前预补偿寄存器。它记录写预补偿起始柱面号。对应于与硬盘基本参数表位移 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 表示选择的磁头。见下表 所示。

a9ee3db9e851cc76b9bf59329b8964ba.png

◆主状态寄存器(读) / 命令寄存器(写)( HD_STATUS/HD_COMMAND , 0x1f7)

在读时,对应一个 8 位主状态寄存器。反映硬盘控制器在执行命令先后的操做状态。各位的含义见下表所示。

58e3a315d3f0d97ef57e22b3e703407c.png

当执行写操做时,该端口对应命令寄存器,接受 CPU 发出的硬盘控制命令,共有 8 种命令,见下表所示。其中最后一列用于说明相应命令结束后控制器所采起的动做(引起中断或者什么也不作)。

75054d7a2236d08dfe412c849c9522c1.png

表中命令码字节的低 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处的字节说明相同,见下表所示。

a93d2b4b046f3a5b4c7dd8b435bc7f0b.png

3.2 AT 硬盘控制器编程

在对硬盘控制器进行操做控制时,须要同时发送参数和命令。其命令格式见下表 所示。首先发送6 字节的参数,最后发出 1 字节的命令码。无论什么命令均须要完整输出这 7 字节的命令块,依次写入端口 0x1f1 -- 0x1f7 。一旦命令块寄存器加载,命令就开始执行。

e9f76d2e0a79573165aa05d27ee1ebac.png

首先 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 中断向量中。

bf54963d89589755f7c92328d1811322.png

3.4 硬盘设备号命名方式

硬盘的主设备号是 3 。其余设备的主设备号分别为:

1- 内存 ,2- 磁盘 ,3- 硬盘 ,4-ttyx,5-tty,6- 并行口 ,7- 非命名管道

因为 1 个硬盘中能够存在 1--4 个分区,所以硬盘还依据分区的不一样用次设备号进行指定分区。所以硬盘的逻辑设备号由如下方式构成:

设备号 = 主设备号 *256 +  次设备号

也即 dev_no = (major<<8) + minor

两个硬盘的全部逻辑设备号见下表所示。

9c423a61e40f79860cca5fcb6e71fdd9.png

其中 0x300 和 0x305 并不与哪一个分区对应,而是表明整个硬盘。

从 linux 内核 0.95 版后已经不使用这种烦琐的命名方式,而是使用与如今相同的命名方法了。

3.5 硬盘分区表

为了实现多个操做系统共享硬盘资源,硬盘能够在逻辑上分为 1--4 个分区。每一个分区之间的扇区号是邻接的。分区表由 4 个表项组成,每一个表项由 16 字节组成,对应一个分区的信息,存放有分区的大小和起止的柱面号、磁道号和扇区号,见表 6–12 所示。分区表存放在硬盘的 0 柱面 0 头第 1 个扇区的0x1BE--0x1FD 处。

ec5729b185baaa7b547728ad8c10c4e8.png

硬盘的第 1 个扇区称为主引导扇区( Master Boot Record --MBR ),它除了多包含一个分区表之外,其余与软盘上第一个扇区( boot 扇区)的做用同样,只是它的代码会把本身从 0x7c00 下移到 0x6000 处,腾出 0x7c00处的空间,而后根据分区表中的信息,找出活动分区是哪个,接着把活动分区的第 1 个扇区加载到 0x7c00 处去执行。一个分区从硬盘的哪一个柱面、磁头和扇区开始,都记录在分区表中。所以从分区表中能够知道一个活动分区的第 1 个扇区(即该分区的引导扇区)在硬盘的什么地方。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值