linux0.11源码分析-向硬盘写入数据

1,034 阅读6分钟

为什把这个方法单独拿出来说,因为别的专题再把这个方法加进去,文章就太长了。偷懒~😁

这个方法对应着硬盘的操作,相对应的对虚拟盘操作就是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; //从请求链表中删除该请求项,并且当前请求项指针指向下一个请求项
}