为什把这个方法单独拿出来说,因为别的专题再把这个方法加进去,文章就太长了。偷懒~😁
这个方法对应着硬盘的操作,相对应的对虚拟盘操作就是do_rd_request,对软盘得操作就是do_fd_request,所谓得操作就是读和写。进程先在自己的空间(用户空间)修改数据后,同步到缓冲区,然后对该缓冲区加锁,进程开始等待,然后内核将请求加到请求队列里面,每次执行请求,先计算好扇区号、磁头号、柱面号,给硬盘控制器发送命令,硬盘处理好后发出中断,内核处理中断,解锁缓冲区,唤醒进程。
关于硬盘版本号
设备号306
这是Linux老式的硬盘命名方式,具体值的含义如下:
设备号=主设备号*256 +次设备号(也即dev_no = (major<<8) + minor),其中主设备号:1-内存,2-磁盘,3-硬盘,4-ttyx,5-tty,6-并行口,7-非命名管道。
硬盘的逻辑设备号(主设备号为3)
逻辑设备号 | 对应设备文件 | 说明 |
---|---|---|
0x300 | /dev/hd0 | 代表整个第1个硬盘 |
0x301 | /dev/hd1 | 表示第1个硬盘的第1个分区 |
0x302 | /dev/hd2 | 表示第1个硬盘的第2个分区 |
0x303 | /dev/hd3 | 表示第1个硬盘的第3个分区 |
0x304 | /dev/hd4 | 表示第1个硬盘的第4个分区 |
0x305 | /dev/hd5 | 代表整个第2个硬盘 |
0x306 | /dev/hd6 | 表示第2个硬盘的第1个分区 |
0x307 | /dev/hd7 | 表示第2个硬盘的第2个分区 |
0x308 | /dev/hd8 | 表示第2个硬盘的第3个分区 |
0x309 | /dev/hd9 | 表示第2个硬盘的第4个分区 |
硬盘初始化
kernel/blk_drv/hd.c
void hd_init(void)
{
blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST; // do_hd_request()
set_intr_gate(0x2E,&hd_interrupt);
outb_p(inb_p(0x21)&0xfb,0x21); // 复位接联的主8259A int2的屏蔽位
outb(inb_p(0xA1)&0xbf,0xA1); // 复位硬盘中断请求屏蔽位(在从片上)
}
这里讲的是硬盘,所以MAJOR_NR
就是3
,DEVICE_REQUEST
就是do_hd_request
do_hd_request
所有对硬盘的请求,都会经过该函数,请求可以分为读与写,先计算号请求的扇区号、磁头号、柱面号,把缓冲区的数据写进硬盘控制器数据寄存器端口HD_DATA。
void do_hd_request(void)
{
int i,r = 0;
unsigned int block,dev;
unsigned int sec,head,cyl; //扇区、磁头、柱面
unsigned int nsect;
INIT_REQUEST; //检测请求项的合法性,若已没有请求项则退出
dev = MINOR(CURRENT->dev); // 取设备号中的子设备号,子设备号即是硬盘上的分区号
block = CURRENT->sector; // 请求的起始扇
//如果子设备号不存在
//因为1块大小是2个扇区,一次操作就是2个扇区,扇区号不能大于分区中最后倒数第二个扇区号
if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) {
end_request(0);
goto repeat; // 跳去检测请求项的合法性
}
block += hd[dev].start_sect; //加上本分区的起始扇区号
dev /= 5; // 此时的dev代表的是硬盘号,0就是第一个硬盘,1就是第二个硬盘
//计算扇区的读取参数,磁头号head、柱面号cyl、扇区号sec
__asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
"r" (hd_info[dev].sect));
__asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
"r" (hd_info[dev].head));
sec++;
nsect = CURRENT->nr_sectors; // 欲读/写的扇区数
if (reset) { //如果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;
}
if (CURRENT->cmd == WRITE) {
hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
//循环读取状态寄存器信息并判断请求服务标志DRQ_STAT 是否置位。DRQ_STAT 是硬盘状态寄存器的请求服务位,表示驱动器已经准备好在主机和
//数据端口之间传输一个字或一个字节的数据。
for(i=0 ; i<3000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++)
/* nothing */ ;
if (!r) { //如果超时驱动器仍未准备好,重设该驱动器
bad_rw_intr();
goto repeat;
}
port_write(HD_DATA,CURRENT->buffer,256); //向硬盘控制器数据寄存器端口HD_DATA 写入1 个扇区的数据
} else if (CURRENT->cmd == READ) {
//如果当前请求是读硬盘扇区,则向硬盘控制器发送读扇区命令。
hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);
} else
panic("unknown hd-command");
}
NR_HD
表示硬盘的个数,子版本号每次增加5个数就表示一个硬盘,所以dev >= 5*NR_HD
就表示找不到该设备(硬盘), block+2 > hd[dev].nr_sects
的意思如图
divl
被除数 A 默认存放在 AX 中(16位以内)或 AX 和 DX 中(32位,DX存放高16位,AX存放低16位),如果除数 B 是8位,那么除法的结果AL保存商,AH保存余数,如果除数 B 是16位,那么除法的结果 AX保存商,DX保存余数。
-
block=柱面号* 磁头数 * 扇区数+磁头号 * 扇区数+扇区号
-
扇区号=block/扇区数的余数,所得商记为tmp=柱面号 * 磁头数+磁头号;
-
磁头号=tmp/磁头数的余数,所得的商就是柱面号。 第一个asm语句把余数即扇区号放入edx,所以sec=扇区号,所得的商放在eax中,所以block=tmp; 第二个asm语句把余数即磁头号放入edx,所以head=磁头号,所得的商放在eax中,所以cyl=柱面号。
kernel/blk_drv/blk.h
#define CURRENT (blk_dev[MAJOR_NR].current_request)
#define INIT_REQUEST \
repeat: \
if (!CURRENT) \
return; \
if (MAJOR(CURRENT->dev) != MAJOR_NR) \
panic(DEVICE_NAME ": request list destroyed"); \
if (CURRENT->bh) { \
if (!CURRENT->bh->b_lock) \
panic(DEVICE_NAME ": block not locked"); \
}
hd_out
调用参数:drive - 硬盘号(0-1); nsect - 读写扇区,sect - 起始扇区; head - 磁头号,cyl - 柱面号; cmd - 命令码
static int controller_ready(void)
{
int retries=10000;
// 读硬盘控制器状态寄存器端口HD_STATUS(0x1f7),并循环检测驱动器就绪比特位和控制器忙位
//如果返回值为0,则表示等待超时出错,否则OK
while (--retries && (inb_p(HD_STATUS)&0xc0)!=0x40);
return (retries);
}
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");
if (drive>1 || head>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);
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); //命令:硬盘控制命令
}
#define outb_p(value,port) \ //向端口 port 写值 value
__asm__ ("outb %%al,%%dx\n" \
"\tjmp 1f\n" \
"1:\tjmp 1f\n" \
"1:"::"a" (value),"d" (port)) //这里的1标号是为了延时
向硬盘发送指令完成了,接下来就要等待了,硬盘准备好数据后会发出中断信号,就会被hd_interrupt
处理。
hd_interrupt
kernel/system_call.s
硬盘中断处理程序,do_hd
表示的是一个函数,如果是读硬盘就是read_intr(),如果是读硬盘就是write_intr()。
hd_interrupt:
pushl %eax
pushl %ecx
pushl %edx
push %ds
push %es
push %fs
movl $0x10,%eax //转入内核态
mov %ax,%ds
mov %ax,%es
movl $0x17,%eax
mov %ax,%fs
# 由于初始化中断控制芯片时没有采用自动EOI,所以这里需要发指令结束该硬件中断。
movb $0x20,%al
outb %al,$0xA0 # EOI to interrupt controller #1
jmp 1f # give port chance to breathe
1: jmp 1f
# do_hd定义为一个函数指针,将被赋值read_intr()或write_intr()函数地址。放到edx
# 寄存器后就将do_hd指针变量置为NULL。然后测试得到的函数指针,若该指针为空,则
# 赋予该指针指向C函数unexpected_hd_interrupt(),以处理未知硬盘中断。
1: xorl %edx,%edx
xchgl do_hd,%edx
testl %edx,%edx # 测试函数指针是否为NULL
jne 1f # 若空,则使指针指向C函数unexpected_hd_interrup().
movl $unexpected_hd_interrupt,%edx
1: outb %al,$0x20 # 送主8259A中断控制器EOI命令(结束硬件中断)
call *%edx # "interesting" way of handling intr.
pop %fs # 上句调用do_hd指向C函数
pop %es
pop %ds
popl %edx
popl %ecx
popl %eax
iret
write_intr
write_intr
检查有没有把数据全部写到磁盘中,如果没有继续写,写完后,等中断到来又会调用write_intr
kernel/blk_drv/hd.c
// 读端口port,共读nr 字(2个字节),保存在buf 中
#define port_read(port,buf,nr) \
__asm__("cld;rep;insw"::"d" (port),"D" (buf),"c" (nr))
//写端口port,共写nr 字(2个字节),从buf 中取数据
#define port_write(port,buf,nr) \
__asm__("cld;rep;outsw"::"d" (port),"S" (buf),"c" (nr))
// 检测硬盘执行命令后的状态
//读取状态寄存器中的命令执行结果状态。返回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);
}
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;
port_write(HD_DATA,CURRENT->buffer,256); 再向数据寄存器端口写256 字
return; // 返回等待硬盘再次完成写操作后的中断处理
}
end_request(1); //若全部扇区数据已经写完,则处理请求结束
do_hd_request(); // 执行其它硬盘请求操作
}
static void bad_rw_intr(void)
{
if (++CURRENT->errors >= MAX_ERRORS) //如果读扇区时的出错次数大于或等于7 次时
end_request(0); //则结束请求并唤醒等待该请求的进程,而且对应缓冲区更新标志复位(没有更新
if (CURRENT->errors > MAX_ERRORS/2) //如果读一扇区时的出错次数已经大于3 次则要求执行复位硬盘控制器操作
reset = 1;
}
include/asm/io.h
#define inb_p(port) ({ \ // 读取端口port的值
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al\n" \
"\tjmp 1f\n" \
"1:\tjmp 1f\n" \
"1:":"=a" (_v):"d" (port)); \
_v; \
})
end_request
解锁缓冲区,释放请求项,唤醒等待的进程,处理下一个请求下项。 kernel/blk_drv/blk.h
static inline void end_request(int uptodate)
{
DEVICE_OFF(CURRENT->dev); //关闭设备
if (CURRENT->bh) {
CURRENT->bh->b_uptodate = uptodate; //缓冲区更新标志
unlock_buffer(CURRENT->bh); //解锁缓冲区
}
if (!uptodate) { //如果更新标志为0 则显示设备错误信息
printk(DEVICE_NAME " I/O error\n\r");
printk("dev %04x, block %d\n\r",CURRENT->dev,
CURRENT->bh->b_blocknr);
}
wake_up(&CURRENT->waiting); //唤醒等待该请求项的进程
wake_up(&wait_for_request); //唤醒等待请求的进程
CURRENT->dev = -1; //释放该请求项
CURRENT = CURRENT->next; //从请求链表中删除该请求项,并且当前请求项指针指向下一个请求项
}