【Linux 0.11】第九章 块设备驱动程序

752 阅读50分钟

在这里插入图片描述

块设备驱动管理相关代码:

文件名称位置功能
blk.hlinux-0.12\kernel\blk_drv块设备专用头文件
ll_rw_blk.clinux-0.12\kernel\blk_drv其它程序访问块设备的接口程序
hd.clinux-0.12\kernel\blk_drv硬盘驱动程序
ramdisk.clinux-0.12\kernel\blk_drv内存虚拟盘驱动程序
floppy.clinux-0.12\kernel\blk_drv软盘驱动程序

Linux 0.11 内核中的主设备号

主设备号类型说明请求项操作函数
0NULL
1块/字符ram,虚拟盘,内存设备do_rd_request()
2fd,软驱设备do_fd_request()
3hd,硬盘设备do_hd_request()
4字符ttyx 设备NULL
5字符tty 设备NULL
6字符lp 打印机设备NULL

Linux 0.11 内核主要支持硬盘、软盘和虚拟盘三种块设备。

9.1 总体功能

对硬盘和软盘设备上数据的读写操作是通过中断处理程序进行的。内核每次读写的数据量以一个逻辑块(1KB)为单位,块设备控制器以扇区(512B)为单位。在处理过程中,使用读写请求项等待队列来顺序缓冲一次读写多个逻辑块的操作。

当进程需要从硬盘上读取逻辑块时,会向缓冲区管理程序提出申请,缓冲区管理程序会在缓冲区中寻找该块是否存在,如果存在则将缓冲区块头指针返回给申请进程,若缓冲区中不存在该块,则缓冲区管理程序调用低级块读写函数ll_rw_block(),向块设备驱动程序发送请求。

在这里插入图片描述

9.1.1 块设备管理的数据结构——块设备请求项和请求队列

对于各种块设备,内核使用一张块设备表来管理,每种块设备都在块设备表中占有一项。

struct blk_dev_struct {
	void (*request_fn)(void);	// 请求项操作的函数指针
	struct request * current_request;	// 当前请求项指针
};
extern struct blk_dev_struct blk_dev[NR_BLK_DEV];	// 块设备表(数组)NR_BLK_DEV=7
  • 第一个字段——函数指针:用于操作相应块设备的请求项,对于硬盘驱动程序,对应的是do_hd_request()。
  • 第二个字段——当前请求项结构指针:用于指明被块设备目前正在处理的请求项。

当内核发出一个块设备读写或其他操作请求时,ll_rw_block()函数会根据参数指明的操作命令和数据缓冲块头中的设备号,利用对应的请求项操作函数 do_XX_request() 建立一个块设备请求项,利用电梯调度算法构造请求项队列。

struct request {
	int dev;		// 使用的设备号(-1表示空闲)
	int cmd;		// 命令(read或write)
	int errors;		// 操作时产生的错误次数
	unsigned long sector;	// 起始扇区(1块=2扇区)
	unsigned long nr_sectors;	// 读/写扇区数
	char * buffer;	// 数据缓冲区
	struct task_struct * waiting;	// 任务等待操作执行完成的地方
	struct buffer_head * bh;	// 缓冲区头指针
	struct request * next;	// 指向下一请求项
};
extern struct request request[NR_REQUEST];	// 请求项数组(NR_REQUEST=32)

每个块设备的当前请求指针与请求项数组中该设备的请求项链表共同构成了该设备的请求队列,项与项之间形成链表,所有请求项只有32项,所有块设备共用32个请求项。

采用数组+链表的目的:

  1. 利用请求项的数组结构在搜索空闲请求块时提高效率。
  2. 满足电梯算法插入请求项操作。

在这里插入图片描述

建立写操作时的空闲项搜索范围限制在整个请求项数组的前2/3范围内,剩下的部分留给读操作建立请求项使用。

在这里插入图片描述

9.1.2 块设备访问调度处理流程

在向硬盘控制器发送操作命令前,先对读/写磁盘扇区数据的顺序进行排序(I/O调度程序处理),使得请求项访问的磁盘扇区数据块都尽量依次顺序进行操作,并非按照请求项收到的顺序发送给块设备进行处理。

电梯算法:磁头向一个方向移动,要么一直向盘片圆心方向移动,要么反方向向盘片边缘移动。

写盘操作

在这里插入图片描述

写数据过程中:cpu 发送的写命令是hd_out(),允许写信号DRQ是控制器状态寄存器的数据请求服务标志,当控制器把数据全部写入驱动器(或发送错误)后,产生中断信号,调用预置C函数(write_intr())来检查是否还有数据要写,如果有,则再把一个扇区的数据送入驱动器,再次等待引发的中断。

如果所有数据均已写入驱动器,则C函数就执行本次写盘结束后的处理工作:唤醒等待该请求项有关数据的相关进程、唤醒等待请求项的进程、释放当前请求项并从链表中删除该请求项以及释放锁定的相关缓冲区。再调用请求项操作函数去执行下一个读/写盘请求项。

读盘操作

在这里插入图片描述

读数据过程:cpu 向控制器发送参数,控制器按照要求从驱动器中读入数据到自己的缓冲区,向 cpu 产生中断,执行预设置 C 函数(read_intr())将控制器中的数据送入系统缓冲区,cpu 继续等待中断信号。

预置的C函数首先把控制器缓冲区中一个扇区的数据放到系统的缓冲区中,调整系统缓冲区中当前写入位置,然后递减需读的扇区数量。如果还有数据要读,则继续等待控制器发出下一个中断信号。

对于虚拟设备,由于不涉及与外部设备之间的同步操作,当前请求项对虚拟设备的读写操作完全在do_rd_request()中实现。

各系统间调度流程:

在这里插入图片描述

9.2 硬盘初始化程序 setup.S

linux/boot/setup.S

setup.S是一个操作系统加载程序,主要作用是利用中断从BIOS中读取机器系统数据,将这些数据保存到0x90000开始的位置,内存地址0x90080和0x90090保存了两块硬盘的参数表,如果没有第二块硬盘则清空。

INITSEG  = DEF_INITSEG  ! 0x9000
! 取第1个硬盘信息(复制硬盘参数表)
	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x41]	! 取中断向量0x41的值,即内存4*0x41=0x104处的值(第1个硬盘参数表的首地址) -> ds:si
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0080	! 硬盘参数表目的地址 0x9000:0x0080 -> es:di
	mov	cx,#0x10	! 表的长度为16B
	rep
	movsb

! 取第2个硬盘信息
	mov	ax,#0x0000
	mov	ds,ax
	lds	si,[4*0x46]	! 取中断向量0x46的值 第2个硬盘的首地址 ds:si
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0090 ! 目的地址:0x9000:0x0090 -> es:di
	mov	cx,#0x10
	rep
	movsb

! 检查是否存在第2块硬盘
	mov	ax,#0x01500	! 功能号ah=0x15
	mov	dl,#0x81	! 驱动器号
	int	0x13
	jc	no_disk1
	cmp	ah,#3
	je	is_disk1
no_disk1:
	mov	ax,#INITSEG
	mov	es,ax
	mov	di,#0x0090
	mov	cx,#0x10
	mov	ax,#0x00
	rep
	stosb
is_disk1:

代码流程:

  1. 复制第一块硬盘参数表内容到内存。中断0x41处向量值保存了硬盘参数表的首地址(4B),复制BIOS 中第一块硬盘参数表的内容到内存0x90080处。代码第5行,取出了0x41处4个字节的内容,即第一块硬盘参数表的地址保存到[ds:si]。代码第6~12行所示,设置目的地址[es:di]=0x9000:0x0080,传输字节数(16字节)。
  2. 复制第二块硬盘参数表内容到内存。代码14~22行所示,第二块硬盘参数表的地址存储在中断0x46处。
  3. 检查是否有第二块硬盘,如果没有则把第二个表清零。如代码2539行所示,代码2530行实现了判断是否是硬盘的功能,25和26行,传入了功能号ah=0x15和驱动器号dl=0x81,以选择第2块硬盘。然后调用0x13中断取类型功能。代码29行,根据保存在ah中的类型码,判断是否为3(硬盘类型)来确定是否存在第二块硬盘。代码31~39行,执行了第2个硬盘表清零操作,清空16字节的硬盘参数表,代码第28行,jc 命令的原因是:int13的出口参数CF=1——操作失败,AH=状态代码,否则,AH=00H — 未安装驱动器。

BIOS 使用int13调用取盘类型功能

  • 参数1:ah(功能号,表明读取盘类型)=0x15;
  • 参数2:dl(驱动器号):0x80-第1个硬盘,0x81-第2个硬盘。

返回结果 ah(类型码):00-没有这个盘CF置位;01-是软驱,没有change-line支持;02-是软驱(或其它可移动设备),有change-line支持;03-是硬盘。

9.3 接口程序 ll_rw_blk.c

graph TD
A[blk-dev-init] --> B[ll-rw-block]
A --> C[ll-rw-page]
B --> D[make-request]
C --> E[add-request]
D --> E

9.3.1 功能描述

==为其它设备创建块设备读写请求项,并插入到指定设备请求队列中。==

该程序主要用于执行底层块设备读/写操作,是本章所有块设备(硬盘、软盘和 Ram 虚拟盘)与系统其他部分之间的接口程序。通过调用该程序的低级块读写函数 ll_rw_block(),系统中的其他程序可以异步读写块设备中的数据。实际的读写操作是由设备的请求项处理函数 request_fn()完成(对于硬盘操作——do_hd_request()、对于软盘操作——do_fd_request()、对于虚拟盘操作——do_rd_request())。

根据下图执行流程,ll_rw_block()针对一个块设备建立起一个请求项,并通过测试块设备的当前请求项指针为空而确定设备空闲时,就会设置该新建的请求项,并直接调用request_fn()对该请求项进行操作。否则就会使用电梯调度算法将新建的请求项插入到该设备的请求项链表中等待处理。而当request_fn()结束对一个请求项的处理,就会把该请求项从链表中删除。在处理每个请求项时,通过中断方式进行。

在这里插入图片描述

9.3.2

9.3.3 块设备初始化函数

linux/kernel/blk_drv/ll_rw_blk.c/blk_dev_init()

该程序主要由main.c进行调用,完成对请求数组**request[NR_REQUEST]**进行初始化,将所有的请求项置为空闲(-1)。

struct request request[NR_REQUEST];	// 请求项数组队列 NR_REQUEST=32
void blk_dev_init(void)	// 初始化请求项
{
	int i;
	for (i=0 ; i<NR_REQUEST ; i++) {
		request[i].dev = -1;	// 表示该设备空闲
		request[i].next = NULL;
	}
}
// 请求队列中,请求项的结构 blk.c
struct request {
	int dev;		/* -1 if no request */
	int cmd;		/* READ orc WRITE */ // READ(0) WRITE(1)
	int errors; //操作时产生错误的次数
	unsigned long sector;   // 起始扇区
	unsigned long nr_sectors;   // 读/写扇区数
	char * buffer;  // 数据缓冲区
    struct task_struct * waiting;   // 任务等待请求完成操作的队列
	struct buffer_head * bh;    // 缓冲区头指针
	struct request * next;  // 指向下一请求项
};

9.3.4 块设备驱动程序与系统的接口函数

linux/kernel/blk_drv/ll_rw_blk.c/ll_rw_block()

低级块读写函数。该函数通常是在fs/buffer.c程序中被调用,其主要功能是创建块设备读写请求项并插入到指定块设备请求队列中。实际的读写操作则是由设备的request_fn()完成。

void ll_rw_block(int rw, struct buffer_head * bh)	// 接口函数 检查参数 调用 make_request
{
	unsigned int major;	// 主设备号
	if ((major=MAJOR(bh->b_dev)) >= NR_BLK_DEV ||	// NR_BLK_DEV=7
	!(blk_dev[major].request_fn)) {	// 判断主设备号是否存在以及该设备的请求操作函数是否存在
		printk("Trying to read nonexistent block-device\n\r");
		return;
	}
	make_request(major,rw,bh);
}

函数输入:rw -- READ、READA、WRITE、WRITEA,bh -- 数据缓冲块头指针。

第4~5行,判断设备主设备号是否存在或者该设备的请求操作函数不存在,如果是则显示出错信息,否则创建请求项并插入请求队列。

// 数据缓冲块头指针定义 Fs.h 文件系统
struct buffer_head {
	char * b_data;			/* pointer to data block (1024 bytes) */	// 指针
	unsigned long b_blocknr;	/* block number */	// 块号
	unsigned short b_dev;		/* device (0 = free) */	// 数据源的设备号
	unsigned char b_uptodate;	// 更新标志:表示数据是否已更新
	unsigned char b_dirt;		/* 0-clean,1-dirty */	// 修改标志:0:未修改,1:已修改
	unsigned char b_count;		/* users using this block */	// 使用的用户数
	unsigned char b_lock;		/* 0 - ok, 1 -locked */	// 缓冲区是否被锁定
	struct task_struct * b_wait;	// 指向等待该缓冲区解锁的任务
	struct buffer_head * b_prev;	// hash 队列上前一块(这四个指针用于缓冲区管理)
	struct buffer_head * b_next;	// hash 队列上下一块
	struct buffer_head * b_prev_free;	// 空闲表上前一块
	struct buffer_head * b_next_free;	// 空闲表上下一块
};
// 块设备数组
struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
	{ NULL, NULL },		/* no_dev */
	{ NULL, NULL },		/* dev mem */
	{ NULL, NULL },		/* dev fd */
	{ NULL, NULL },		/* dev hd */
	{ NULL, NULL },		/* dev ttyx */
	{ NULL, NULL },		/* dev tty */
	{ NULL, NULL }		/* dev lp */
};
struct task_struct {	// 调度程序头文件 sched.h 中
/* these are hardcoded - don't touch */
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter;
	long priority;
	long signal;
	struct sigaction sigaction[32];
	long blocked;	/* bitmap of masked signals */
/* various fields */
	int exit_code;
	unsigned long start_code,end_code,end_data,brk,start_stack;
	long pid,pgrp,session,leader;
	int	groups[NGROUPS];
	/* 
	 * pointers to parent process, youngest child, younger sibling,
	 * older sibling, respectively.  (p->father can be replaced with 
	 * p->p_pptr->pid)
	 */
	struct task_struct	*p_pptr, *p_cptr, *p_ysptr, *p_osptr;
	unsigned short uid,euid,suid;
	unsigned short gid,egid,sgid;
	unsigned long timeout,alarm;
	long utime,stime,cutime,cstime,start_time;
	struct rlimit rlim[RLIM_NLIMITS]; 
	unsigned int flags;	/* per process flags, defined below */
	unsigned short used_math;
/* file system info */
	int tty;		/* -1 if no tty, so it must be signed */
	unsigned short umask;
	struct m_inode * pwd;
	struct m_inode * root;
	struct m_inode * executable;
	struct m_inode * library;
	unsigned long close_on_exec;
	struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];
/* tss for this task */
	struct tss_struct tss;
};
struct blk_dev_struct {
	void (*request_fn)(void); 
	struct request * current_request; 
};

9.3.5 低级页面读写函数

linux/kernel/blk_drv/ll_rw_blk.c/ ll_rw_page()

以页面(4K)为单位访问块设备数据,每次读写8个扇区。

将请求项建立完毕后,调用add_request()把它添加到请求队列中,然后直接调用调度函数让当前进程睡眠等待页面从交换设备中读入。

struct task_struct * wait_for_request = NULL;	// 用于请求数组不空闲时,进程的临时等待处 ll_rw_page
void ll_rw_page(int rw, int dev, int page, char * buffer)
{
	struct request * req;
	unsigned int major = MAJOR(dev);
	// 检查参数
    // 检查主设备号以及设备的请求操作函数是否存在
	if (major >= NR_BLK_DEV || !(blk_dev[major].request_fn)) {
		printk("Trying to read nonexistent block-device\n\r");
		return;
	}
    // 检查参数命令是否是 READ 或者 WRITE
	if (rw!=READ && rw!=WRITE)
		panic("Bad block dev command, must be R/W");
	// 建立请求项
repeat:
	req = request+NR_REQUEST;	// 将指针指向队列尾部
	while (--req >= request)
		if (req->dev<0)	// 表示该项空闲
			break;
	if (req < request) {
		sleep_on(&wait_for_request);	// 睡眠,过会再查看请求队列
		goto repeat;
	}
// 向空闲请求项中填写请求信息, 并将其加入队列中
/* fill up the request-info, and add it to the queue */
	req->dev = dev;	// 设备号
	req->cmd = rw;	// 命令(READ/WRITE)
	req->errors = 0;	// 读写操作错误计数
	req->sector = page<<3;	// 起始读扇区 swap_nr
	req->nr_sectors = 8;	// 读写扇区数 8 块
	req->buffer = buffer;	// 数据缓冲区
	req->waiting = current;	// 当前进程进入该请求等待队列
	req->bh = NULL;	// 无缓冲块头指针(不用高速缓冲)
	req->next = NULL;	// 下一个请求项指针
	current->state = TASK_UNINTERRUPTIBLE;	// 置为不可中断状态
	add_request(major+blk_dev,req);	// 将请求项加入队列中
	schedule();	// 因为需要对交换设备读/写8个扇区,需要花较长时间,所以将当前进程进行睡眠等待
}

函数执行流程:

  1. 检查参数。代码8~14行,如果设备主设备号不存在或者该设备的请求操作函数不存在,显示出错信息,退出;如果参数给出的命令既不是READ或WRITE,表示内核程序出错。
  2. 建立请求项。代码16~24行,从请求数组中寻址空闲项(从后往前寻址),如果没有则睡眠等等。
  3. 向空闲请求项中填写请求信息。向请求项中填写相关信息(代码27~35行),代码第36行,把当前进程置为不可中断睡眠状态,代码第37行,将请求项加入队列中,因为需要对交换设备读/写8个扇区,需要花较长时间,所以将当前进程进行睡眠等待。
struct task_struct {
/* these are hardcoded - don't touch */
	long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter;
	long priority;
	long signal;
	struct sigaction sigaction[32];
	long blocked;	/* bitmap of masked signals */
/* various fields */
	int exit_code;
	unsigned long start_code,end_code,end_data,brk,start_stack;
	long pid,pgrp,session,leader;
	int	groups[NGROUPS];
	/* 
	 * pointers to parent process, youngest child, younger sibling,
	 * older sibling, respectively.  (p->father can be replaced with 
	 * p->p_pptr->pid)
	 */
	struct task_struct	*p_pptr, *p_cptr, *p_ysptr, *p_osptr;
	unsigned short uid,euid,suid;
	unsigned short gid,egid,sgid;
	unsigned long timeout,alarm;
	long utime,stime,cutime,cstime,start_time;
	struct rlimit rlim[RLIM_NLIMITS]; 
	unsigned int flags;	/* per process flags, defined below */
	unsigned short used_math;
/* file system info */
	int tty;		/* -1 if no tty, so it must be signed */
	unsigned short umask;
	struct m_inode * pwd;
	struct m_inode * root;
	struct m_inode * executable;
	struct m_inode * library;
	unsigned long close_on_exec;
	struct file * filp[NR_OPEN];
/* ldt for this task 0 - zero 1 - cs 2 - ds&ss */
	struct desc_struct ldt[3];
/* tss for this task */
	struct tss_struct tss;
};
void sleep_on(struct task_struct **p)
{
	__sleep_on(p,TASK_UNINTERRUPTIBLE);
}

9.3.6 关于缓冲块的两个操作

linux/kernel/blk_drv/ll_rw_blk.c

关于缓冲块的两个操作分别为锁定指定缓冲块以及释放锁定的缓冲区。

// 锁定指定缓冲块
static inline void lock_buffer(struct buffer_head * bh)
{
	cli();	// 禁止中断
	while (bh->b_lock)	// 如果缓冲区已被锁定则睡眠,直到缓冲区解锁
		sleep_on(&bh->b_wait);
	bh->b_lock=1;	// 立刻锁定该缓冲区
	sti();	// 开启中断
}
// 释放(解锁)锁定的缓冲区
static inline void unlock_buffer(struct buffer_head * bh)
{
	if (!bh->b_lock)	// 该缓冲区没有被锁定
		printk("ll_rw_block.c: buffer not locked\n\r");
	bh->b_lock = 0;	// 解锁
	wake_up(&bh->b_wait);	// 唤醒等待该缓冲区的任务
}

9.3.7 创建请求项并插入请求队列

linux/kernel/blk_drv/ll_rw_blk.c

add_request():把已经设置好的请求项 req 添加到指定设备的请求项链表中。如果该设备的当前请求项指针为空,则可以设置 req 为当前请求项并立刻调用设备请求项处理函数,否则就把 req 请求项插入到该请求项链表中。

make_request():创建请求项。

// 向链表中加入请求项
static void add_request(struct blk_dev_struct * dev, struct request * req) // dev:指定块设备结构指针;req:已经设置好的请求项
{
	struct request * tmp;
	req->next = NULL;	// 置空请求项中的下一请求项指针
	cli();	// 关中断
	if (req->bh)
		req->bh->b_dirt = 0;	// 清缓冲区"脏"标志
	if (!(tmp = dev->current_request)) {// 指定设备dev当前无请求项 =0:表示该设备没有请求项 本次是第一个请求项
		dev->current_request = req;	// 将块设备当前请求指针直接指向该请求项
		sti();	// 开中断
		(dev->request_fn)();	// 立即执行请求函数 硬盘是 do_hd_request()
		return;
	}
	for ( ; tmp->next ; tmp=tmp->next) {// 如果当前指定设备dev忙,有空闲请求项在处理,则将当前请求项插入请求链表中 for 循环中的判断语句用于把req所指请求项与请求队列中已有的请求项作比较,找出req插入该队列的正确位置(电梯调度算法)
		if (!req->bh)
			if (tmp->next->bh)
				break;  // 读页优先
			else
				continue;
		if ((IN_ORDER(tmp,req) ||
		    !IN_ORDER(tmp,tmp->next)) &&
		    IN_ORDER(req,tmp->next))
			break;
        /*
         if (tmp > req && req > tmp->next) break;
         elif (tmp <= tmp->next  && req > tmp->next ) break;
         else continue;
        */
	}
	req->next=tmp->next;
	tmp->next=req;
	sti();
}
// 创建请求项并插入请求队列
static void make_request(int major,int rw, struct buffer_head * bh) //主设备号;指定命令;存放数据的缓冲区头指针
{
	struct request * req;
	int rw_ahead;

/* WRITEA/READA is special case - it is not really needed, so if the */
/* buffer is locked, we just forget about it, else it's a normal read */
	if (rw_ahead = (rw == READA || rw == WRITEA)) { // rw_ahead 预读写标志 68行,放弃提前命令
		if (bh->b_lock)	// 指定的缓冲区正在使用,已被上锁,放弃预读写请求
			return;
		if (rw == READA)
			rw = READ;
		else
			rw = WRITE;
	}
	if (rw!=READ && rw!=WRITE)	// 命令不是 READ/WRITE
		panic("Bad block dev command, must be R/W/RA/WA");
	lock_buffer(bh);
	if ((rw == WRITE && !bh->b_dirt) || (rw == READ && bh->b_uptodate)) {// 向块设备写入一个未修改过的缓冲块,从块设备上读取一个已经和缓冲区内容同步了的块??
		unlock_buffer(bh);
		return;
	}
repeat:
/* we don't allow the write-requests to fill up the queue completely:
 * we want some room for reads: they take precedence. The last third
 * of the requests are only for reads.
 */
	if (rw == READ)
		req = request+NR_REQUEST;	// 读请求, 指针指向队列尾部 ll_rw_blk.c struct request request[NR_REQUEST];
	else
		req = request+((NR_REQUEST*2)/3);	// 对于写请求, 指针指向队列2/3处
/* find an empty request */
	while (--req >= request)	// 探寻一个空请求项
		if (req->dev<0)
			break;
/* if none found, sleep on new requests: check for rw_ahead */
	if (req < request) {	// 已搜索到头
		if (rw_ahead) {	// 若是提前读/写请求, 则退出
			unlock_buffer(bh);
			return;
		}
		sleep_on(&wait_for_request);	// 睡眠等待
		goto repeat;
	}
/* fill up the request-info, and add it to the queue */
	req->dev = bh->b_dev;	// 设备号
	req->cmd = rw;	// 命令
	req->errors=0;	// 操作时产生的错误次数
	req->sector = bh->b_blocknr<<1;	// 起始扇区。块号转换成扇区号。
	req->nr_sectors = 2;	// 本请求项需要读写的扇区数 扇区数为2
	req->buffer = bh->b_data;	// 请求项缓冲区指针指向需读写的数据缓冲区
	req->waiting = NULL;	// 任务等待操作执行完成的地方
	req->bh = bh;	// 缓冲块头指针
	req->next = NULL;	// 指向下一请求项
	add_request(major+blk_dev,req);	// 将请求项加入队列中(blk_dev[major],req)
}

向链表中加入请求项——add_request()

参数:dev —— 指定块设备结构指针;req —— 已经设置好的请求项结构指针。

在这里插入图片描述

函数执行流程:

  1. 设置req相关参数。如代码5~8行所示,置空请求项的下一项请求项指针,关中断并清除请求项相关缓冲区脏标志。
  2. 如果设备空闲。如代码9~14行所示,根据设备号的current_request属性查看当前设备是否有请求项。如果没有请求项,表示设备空闲,此刻将块设备当前指针直接指向该请求项,立刻执行相应设备的请求函数。
  3. 如果设备忙。如代码15~25行所示,就利用电梯调度算法寻找请求项在请求链表中插入的最佳位置。如果判断出欲插入请求项的缓冲块头指针空,即没有缓冲块,那么就需要找一个项,其已经有可用的缓冲块,因此若当前插入位置处的空闲项缓冲块头指针不空,就选择这个位置。
// blk.h
// 电梯算法:读操作在写操作之前进行
// s1, s2 为request请求项指针,根据请求项中的cmd、dev、sector来判断两个请求项的前后排序
#define IN_ORDER(s1,s2) \
((s1)->cmd<(s2)->cmd || (s1)->cmd==(s2)->cmd && \
((s1)->dev < (s2)->dev || ((s1)->dev == (s2)->dev && \
(s1)->sector < (s2)->sector)))

创建请求项——make_request()

参数:major —— 主设备号;rw —— 指定命令;bh —— 存放数据的缓冲区头指针。

函数执行流程:

  1. 检查参数信息。代码38~52行,对于READA和WRITEA两个命令,当指定的缓冲区正在使用而已被上锁时,放弃预读/写请求。否则作为普通的READ/WRITE命令处理。其中rw_ahead为预读写命令标志。
  2. 设置请求项指针位置。代码58~61行,对于读命令请求,我们直接从队列末尾开始搜索,而对于写请求从队列2/3处向队列头处搜索空项填入。
  3. 寻找空闲请求项。代码62~74行,搜索一个空请求项,若无空闲项则睡眠等待,之后再次遍历查找空闲请求项,若是提前读/写请求,则退出。
  4. 向空闲项中填写信息,将请求项加入队列中。如代码76~85行所示。
// Fs.h
struct buffer_head {
	char * b_data;			/* pointer to data block (1024 bytes) */
	unsigned long b_blocknr;	/* block number */
	unsigned short b_dev;		/* device (0 = free) */
	unsigned char b_uptodate;
	unsigned char b_dirt;		/* 0-clean,1-dirty */
	unsigned char b_count;		/* users using this block */
	unsigned char b_lock;		/* 0 - ok, 1 -locked */
	struct task_struct * b_wait;
	struct buffer_head * b_prev;
	struct buffer_head * b_next;
	struct buffer_head * b_prev_free;
	struct buffer_head * b_next_free;
};

9.4 块设备头文件 blk.h

kernel/blk_drv/blk.h

有关硬盘等块设备参数的头文件。定义了请求等待队列中请求项的数据结构 request,用宏语句定义了电梯搜索算法,并对内核目前支持的虚拟盘,软盘和硬盘三种块设备。

#ifndef _BLK_H
#define _BLK_H
#define NR_BLK_DEV	7   // 块设备类型数量
#define NR_REQUEST	32  // 请求队列中所包含的项数
// 请求队列中,请求项的结构
struct request {
	int dev;		/* -1 if no request */
	int cmd;		/* READ or WRITE */ // READ(0) WRITE(1)
	int errors; //操作时产生错误的次数
	unsigned long sector;   // 起始扇区
	unsigned long nr_sectors;   // 读/写扇区数
	char * buffer;  // 数据缓冲区
    struct task_struct * waiting;   // 任务等待请求完成操作的队列
	struct buffer_head * bh;    // 缓冲区头指针
	struct request * next;  // 指向下一请求项
};
// 电梯算法,读操作比写操作更加严格
#define IN_ORDER(s1,s2) \
((s1)->cmd<(s2)->cmd || (s1)->cmd==(s2)->cmd && \
((s1)->dev < (s2)->dev || ((s1)->dev == (s2)->dev && \
(s1)->sector < (s2)->sector)))
//  块设备处理结构
struct blk_dev_struct {
	void (*request_fn)(void);   // 请求处理函数指针
	struct request * current_request;   // 当前处理的请求结构
};
// 块设备表,每种块设备占用一项,共7项。
extern struct blk_dev_struct blk_dev[NR_BLK_DEV];
// 请求项数组,共 32 项
extern struct request request[NR_REQUEST];
// 等待空闲请求项的进程队列头指针
extern struct task_struct * wait_for_request;
// 一个块设备上数据块总数指针数组。每个指针项指向指定主设备号的总块数数组hd_sizes[]。该总块数数组每一项对应一个子设备所拥有的数据块总数(1块=1KB)
extern int * blk_size[NR_BLK_DEV];
// 程序使用的主设备号
#ifdef MAJOR_NR

#if (MAJOR_NR == 1)
/* ram disk */
#define DEVICE_NAME "ramdisk"   // 设备名称("内存虚拟盘")
#define DEVICE_REQUEST do_rd_request    // 设备请求项处理函数
#define DEVICE_NR(device) ((device) & 7)    // 子设备号(0~7)
#define DEVICE_ON(device)   // 开启设备
#define DEVICE_OFF(device)  // 关闭设备

#elif (MAJOR_NR == 2)
/* floppy */
#define DEVICE_NAME "floppy"    // 设备名称("软盘驱动器")
#define DEVICE_INTR do_floppy   // 设备中断处理函数
#define DEVICE_REQUEST do_fd_request    // 设备请求项处理函数
#define DEVICE_NR(device) ((device) & 3)    // 子设备号(0-3)
#define DEVICE_ON(device) floppy_on(DEVICE_NR(device))  // 开启设备宏
#define DEVICE_OFF(device) floppy_off(DEVICE_NR(device))    // 关闭设备宏

#elif (MAJOR_NR == 3)
/* harddisk */
#define DEVICE_NAME "harddisk"  // 设备名称("硬盘")
#define DEVICE_INTR do_hd   // 设备中断处理函数
#define DEVICE_TIMEOUT hd_timeout   // 设备超时值
#define DEVICE_REQUEST do_hd_request    // 设备请求项处理函数
#define DEVICE_NR(device) (MINOR(device)/5) // 硬盘设备号(0-1)
#define DEVICE_ON(device)   // 一开机硬盘就总是运转着
#define DEVICE_OFF(device)

#elif
/* unknown blk device */
#error "unknown blk device" // 编译预处理阶段显示出错信息 "未知块设备"

#endif

#define CURRENT (blk_dev[MAJOR_NR].current_request) // 指定设备号的当前请求结构项指针
#define CURRENT_DEV DEVICE_NR(CURRENT->dev) // 当前请求项 CURRENT 中设备号

#ifdef DEVICE_INTR   // 设备中断处理符号常数,把它声明为一个函数指针
void (*DEVICE_INTR)(void) = NULL;
#endif
#ifdef DEVICE_TIMEOUT   // 设备超时符号,定义同名变量,令其值等于0,并定义 SET_INTR() 宏
int DEVICE_TIMEOUT = 0;
#define SET_INTR(x) (DEVICE_INTR = (x),DEVICE_TIMEOUT = 200)
#else
#define SET_INTR(x) (DEVICE_INTR = (x))
#endif
static void (DEVICE_REQUEST)(void); // 声明设备请求符号常数 DEVICE_REGUEST 是一个静态函数指针

// 解锁指定的缓冲块
extern inline void unlock_buffer(struct buffer_head * bh)
{
	if (!bh->b_lock)
		printk(DEVICE_NAME ": free buffer being unlocked\n");
	bh->b_lock=0;
	wake_up(&bh->b_wait);
}

// 解锁请求处理宏
extern inline void end_request(int uptodate)
{
	DEVICE_OFF(CURRENT->dev);   // 关闭设备
	if (CURRENT->bh) {  // CURRENT 为当前请求结构项指针
		CURRENT->bh->b_uptodate = uptodate; // 置更新标志
		unlock_buffer(CURRENT->bh); // 解锁缓冲区
	}
	if (!uptodate) {    // 此次请求项操作失败
		printk(DEVICE_NAME " I/O error\n\r");   // 显示相关块设备IO出错信息
		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;    // 指向下一个请求项
}blk.h

#ifdef DEVICE_TIMEOUT   // 设备超时符号常量
#define CLEAR_DEVICE_TIMEOUT DEVICE_TIMEOUT = 0;
#else
#define CLEAR_DEVICE_TIMEOUT
#endif

#ifdef DEVICE_INTR  // 设备中断符号常量
#define CLEAR_DEVICE_INTR DEVICE_INTR = 0;
#else
#define CLEAR_DEVICE_INTR
#endif
// 定义初始化请求项宏
/*该宏对于当前请求项进行一些有效性判断:如果设备当前请求项为空,表示本设备目前已无需要处理的请求项。于是略做扫尾工作就退出相应函数。否则,如果当前请求项中设备的主设备号不等于驱动程序定义的主设备号,说明请求项队列乱掉了,于是内核显示出错信息并停机。否则若请求项中用的缓冲块没有被锁定,也说明内核程序出了问题,于是也显示出错信息并停机。*/
#define INIT_REQUEST \
repeat: \
	if (!CURRENT) {\
		CLEAR_DEVICE_INTR \
		CLEAR_DEVICE_TIMEOUT \
		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"); \
	}

#endif

#endif

9.5 硬盘控制器驱动程序 hd.c

hd.c 程序是硬盘控制器驱动程序,提供硬盘控制器和块设备的读写操作,以及硬盘的初始化处理。

相关程序函数:

  • 初始化硬盘 —— sys_setup()
    • 设置硬盘驱动器参数。
    • 读取硬盘分区表。
    • 把引导盘上的虚拟盘根文件系统加载到内存虚拟盘。
  • 设置硬盘所用数据结构信息 —— hd_init()
    • 设置硬盘控制器中断描述符。
    • 复位硬盘控制器中屏蔽码。
  • 向硬盘控制器发送命令 —— hd_out()
    • 带有预设置中断过程调用的函数指针。
    • 向CPU发出中断请求信号,执行硬盘中断处理过程。
  • 处理硬盘当前请求项 —— do_hd_request()
    • 判断当前请求项是否存在。
    • 检查设备号和盘起始扇区的合法性。
    • 根据请求项计算请求数据的磁盘磁道号、磁头号和柱面号。
    • 如果复位标志被设置,硬盘执行复位操作。
    • 如果重新校正标志被设置,向控制器发送硬盘重新校正命令,并在发送之前预先设置好该命令引发的中断中需要执行的C函数(recal_intr()),并退出。
    • 如果当前请求项是写操作,首先设置硬盘控制器调用的C函数为write_intr(),向控制器发送写操作的命令参数块,并循环查询控制器的状态寄存器,以判断请求服务标志(DRQ)是否置位。若该标志置位,则表示控制器已“同意”接受数据,于是接着就把请求项所指缓冲区中的数据写入控制器的数据缓冲区中。若循环查询超时后该标志仍未置位,则说明此次操作失败。于是调用bad_rw_intr()函数,根据处理当前请求项发生的出错次数来确定是否放弃继续处理当前请求项,还是需要设置复位标志以继续重新处理当前请求项。
    • 如果当前请求项是读操作,则设置硬盘控制器调用C函数read_intr(),并向控制器发送读盘操作命令。
  • 硬盘中断处理过程中调用的C函数,如read_intr()、write_intr()、bad_rw_intr()、recal_intr()
    • 控制器写操作完成后调用的函数 —— write_intr()
      • 调用 win_result() 读取状态寄存器,判断错误是否发生。
      • 若有错误,调用 bad_rw_intr() 判断是否需要放弃此请求项,是否需要设置复位标志。
      • 若无错误,判断是否把此请求要求的所有数据写盘了,如果还有数据,则调用 port_write() 函数再把一个扇区的数据写入控制器缓冲区。
      • 如果数据已经全部写盘,调用 end_request() 函数处理结束事宜:唤醒等待本请求项完成的进程、唤醒等待空闲请求项的进程、设置当前请求项所指缓冲区数据已更新标志、释放当前请求项(从块设备链表中删除该项)。
      • 重新调用 do_hd_request() 函数。
    • 控制器读操作完成后调用的函数 —— read_intr()
      • 调用 win_result() 读取状态寄存器,判断错误是否发生。
      • 若有错误,读盘错误则调用 bad_rw_intr() 判断是否需要放弃此请求项,是否需要设置复位标志。
      • 若无错误,使用 port_read() 函数从控制器缓冲区把一个扇区的数据复制到请求项指定的缓冲区中。
      • 若还有数据要读,退出,等待下一个中断的到来。
      • 调用 end_request() 函数来处理当前请求项的结束事宜:唤醒等待本请求项完成的进程、唤醒等待空闲请求项的进程、设置当前请求项所指缓冲区数据已更新标志、释放当前请求项(从块设备链表中删除该项)。
      • 重新调用 do_hd_request() 函数。
  • 硬盘控制器操作辅助函数,如controler_ready()、drive_busy()、win_result()、hd_out()、reset_controler()

读硬盘数据操作时的时序关系:

在这里插入图片描述

写硬盘数据操作时的时序关系:

在这里插入图片描述

hd.c 中相关函数类型:

在这里插入图片描述

9.5.1

9.5.2 初始化硬盘和设置硬盘所用数据结构信息

linux/kernel/blk_drv/hd.c/hd_init()

硬盘系统初始化。

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);	// 复位主片上接联引脚屏蔽位 允许从片发出中断请求信号
	outb(inb_p(0xA1)&0xbf,0xA1);	// 复位从片上硬盘中断请求屏蔽位 允许硬盘控制器发送中断请求信号
}

函数流程:

  1. 代码第4行,hd_intrupt(kernel/sys_call.s第235行是其中断处理过程地址),硬盘中断号为int 0x2E,对应8259A芯片的中断请求信号 IRQ14,中断描述符表IDT内中断门描述符设置宏set_intr_gate()在include/asm/system.h中实现。
  2. 代码第5行,复位接联的主8259A int2的屏蔽位,允许从片发出中断请求信号。
  3. 代码第6行,复位从片上硬盘中断请求屏蔽位,允许硬盘控制器发送中断请求信号。
// blk.h 块设备处理结构
struct blk_dev_struct {
	void (*request_fn)(void);   // 请求处理函数指针
	struct request * current_request;   // 当前处理的请求结构
};
// System.h
#define set_intr_gate(n,addr) \
	_set_gate(&idt[n],14,0,addr)
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
	"movw %0,%%dx\n\t" \
	"movl %%eax,%1\n\t" \
	"movl %%edx,%2" \
	: \
	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
	"o" (*((char *) (gate_addr))), \
	"o" (*(4+(char *) (gate_addr))), \
	"d" ((char *) (addr)),"a" (0x00080000))
// io.h
#define outb(value,port) \
__asm__ ("outb %%al,%%dx"::"a" (value),"d" (port))
#define outb_p(value,port) \
__asm__ ("outb %%al,%%dx\n" \
		"\tjmp 1f\n" \
		"1:\tjmp 1f\n" \
		"1:"::"a" (value),"d" (port))

linux/kernel/blk_drv/hd.c/sys_setup()

系统设置函数。主要功能是读取 CMOS 硬盘参数表的信息,用于设置硬盘分区结构hd,并尝试加载RAM虚拟盘和根文件系统。

硬盘参数表

在这里插入图片描述 在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

graph LR
A((sys-setup)) --> B[1. 硬盘信息数组 hd-info]
A --> C[2. 两个硬盘整体参数 hd0]
A --> D[3. 判断硬盘是否是有第二块硬盘]
A --> E[4. 设置 hd1-4]
A --> F[5. 统计每个分区中的数据块总数 hd0-4]
A --> G[6. 在内存虚拟盘中加载根文件映像]
A --> H[7. 交换设备初始化]
A --> I[8. 安装根文件系统]
#define MAX_HD		2	// 系统支持的最多硬盘数
int sys_setup(void * BIOS)	// init/main.c init 子程序设置为指向硬盘参数表结构指针
{
	static int callable = 1;	// 限制本函数只使用1次
	int i,drive;
	unsigned char cmos_disks;
	struct partition *p;
	struct buffer_head * bh;

	if (!callable)
		return -1;
	callable = 0;
#ifndef HD_TYPE	// 如果没有定义 HD_TYPE,则读取。 include/linux/config.h 文件中定义了符号常数 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指向下一表
	}
	if (hd_info[1].cyl)
		NR_HD=2;	// 硬盘数设置为2
	else
		NR_HD=1;
#endif
	for (i=0 ; i<NR_HD ; i++) {// 设置硬盘分区结构数组 项0和项5分别表示两个硬盘的整体参数
		hd[i*5].start_sect = 0;	// 硬盘起始扇区号
		hd[i*5].nr_sects = hd_info[i].head*
				hd_info[i].sect*hd_info[i].cyl;	// 硬盘总扇区数
	}
	if ((cmos_disks = CMOS_READ(0x12)) & 0xf0)	// 从CMOS偏移地址0x12处读出硬盘类型字节,低半字节存放着第二块硬盘的类型值
		if (cmos_disks & 0x0f)	// 低半字节不为0,表示系统中有2块硬盘
			NR_HD = 2;
		else
			NR_HD = 1;
	else
		NR_HD = 0;	// 非AT兼容硬盘
	for (i = NR_HD ; i < 2 ; i++) {	// 两个硬盘数据结构清零
		hd[i*5].start_sect = 0;
		hd[i*5].nr_sects = 0;
	}
	for (drive=0 ; drive<NR_HD ; drive++) {// 读取每个硬盘上第一个数据块中第一扇区中的此硬盘各个分区信息
		if (!(bh = bread(0x300 + drive*5,0))) { // bread()-读块函数 0x300、0x305 两个硬盘的设备号,0:所需读取的块号 从硬盘读到缓冲区
			printk("Unable to read partition table of drive %d\n\r",
				drive);
			panic("");
		}
		if (bh->b_data[510] != 0x55 || (unsigned char)// 判断硬盘第1个扇区最后两个字节
		    bh->b_data[511] != 0xAA) {
			printk("Bad partition table on drive %d\n\r",drive);
			panic("");
		}
		p = 0x1BE + (void *)bh->b_data;	// 每个硬盘第一扇区中位于偏移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);	// 释放为存放硬盘数据块而申请的缓冲区
	}
	for (i=0 ; i<5*MAX_HD ; i++) // MAX_HD=2
		hd_sizes[i] = hd[i].nr_sects>>1 ;	// 统计每个分区中的数据块总数 static int hd_sizes[5*MAX_HD] = {0, };
	blk_size[MAJOR_NR] = hd_sizes;	// 设备数据块总数指针数组的本设备项指向该数组
	if (NR_HD)
		printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
	rd_load();	// 在系统内存虚拟盘中加载启动盘包含的根文件系统映像 ramdisk.c
	init_swapping();	// 交换设备初始化 swap.c
	mount_root();	// 安装根文件系统 super.c (Fs)
	return (0);
}

输入参数:BIOS 是由初始化程序 init/main.c 中 init 子程序设置为指向硬盘参数表结构的指针。该硬盘参数表结构包含2个硬盘参数表的内容,是从内存0x90080处复制而来的。 执行流程:

  1. 设置 callable 标志。如代码9~11行所示,该函数只能被调用一次。

  2. 设置硬盘信息数组hd_fo[]。如果在include/linux/config.h中定义了 HD_TYPE,则hd_info[]数据已经设置完毕了。如果没有定义,如代码13~27行所示,设置hd_info数据。首先设置hd_info初始化内容,如代码23~26行所示,取BIOS硬盘参数表信息时,因为如果只有一个硬盘,就会将对应第2个硬盘的16字节全部清零,因此,判断第2个硬盘柱面数是否为0就可以直到是否有第2个硬盘。

  3. 设置硬盘分区结构数组hd[]。如代码28~32行所示,数组的项0和项5分别表示两个硬盘的整体参数,而项1-9和6-9分别表示两个硬盘的4个分区的参数。

  4. 第1个驱动器参数存放在CMOS字节0x12处的高半字节中,第2个存放在低半字节中。该4位字节信息可以是驱动器类型,也可能是0xf——表示使用CMOS中0x19字节作为驱动器1的8位类型字节,使用CMOS中0x1A字节作为驱动器2的类型字节。

    确定系统中所含硬盘个数。如代码33~43行所示,检查CMOS_READ相关字节,判断硬盘个数,如果硬盘不是AT控制器兼容的,将两个硬盘数据结构全清零,如果硬盘数为1,则将第2个硬盘的参数清零。

  5. 读取每个硬盘上第1个扇区中的分区表信息。设置分区结构数组hd[]中硬盘分区的信息。如代码44~61行所示,利用读块函数bread()读硬盘第1个数据块(第一个参数分别是两个硬盘的设备号,第二个参数是所需读取的块号),若读取成功,数据会被存放在缓冲块bh数据区中,若缓冲块指针为0,则说明读操作失败,显示出错信息并停机。我们根据硬盘第一个扇区最后两个字节是否是0xAA55来判断扇区中数据的有效性。若有效,则将硬盘分区表信息存入硬盘分区结构数组hd[]中。最后释放bh缓冲区。

  6. 统计每个分区中的数据块总数。如代码62~64行所示,将每个分区中的数据块总数保存在总数据块数组hd_size[]中,然后让设备数据块总数指针数组的本设备项指向该数组。

  7. 后续调用。如代码65~69行所示,在系统内存虚拟盘中加载启动盘中包含的根文件系统映像;对交换设备初始化;安装根文件系统。

// hdreg.h
struct partition {
	unsigned char boot_ind;		/* 0x80 - active (unused) */
	unsigned char head;		/* ? */
	unsigned char sector;		/* ? */
	unsigned char cyl;		/* ? */
	unsigned char sys_ind;		/* ? */
	unsigned char end_head;		/* ? */
	unsigned char end_sector;	/* ? */
	unsigned char end_cyl;		/* ? */
	unsigned int start_sect;	/* starting sector counting from 0 */
	unsigned int nr_sects;		/* nr of sectors in partition */
};
// buffer.c
struct buffer_head * bread(int dev,int block)
{
	struct buffer_head * bh;

	if (!(bh=getblk(dev,block)))
		panic("bread: getblk returned NULL\n");
	if (bh->b_uptodate)
		return bh;
	ll_rw_block(READ,bh);
	wait_on_buffer(bh);
	if (bh->b_uptodate)
		return bh;
	brelse(bh);
	return NULL;
}
// hd.c 读取CMOS参数宏函数
#define CMOS_READ(addr) ({ \	
outb_p(0x80|addr,0x70); \	// 0x70 是写端口号 0x80|addr是要读的CMOS内存地址
inb_p(0x71); \	// 0x71 是读端口号
})
// io.h
#define outb(value,port) \
__asm__ ("outb %%al,%%dx"::"a" (value),"d" (port))	// io 驱动器访问指令
#define inb(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al":"=a" (_v):"d" (port)); \
_v; \
})
#define outb_p(value,port) \
__asm__ ("outb %%al,%%dx\n" \
		"\tjmp 1f\n" \
		"1:\tjmp 1f\n" \
		"1:"::"a" (value),"d" (port))
#define inb_p(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al\n" \
	"\tjmp 1f\n" \
	"1:\tjmp 1f\n" \
	"1:":"=a" (_v):"d" (port)); \
_v; \
})
// 定义硬盘分区结构。给出每个分区从硬盘0道开始算起的物理起始扇区号和分区扇区总数。其中5的倍数处的项(包括hd[0])代表整个硬盘的参数 hd.c
static struct hd_struct {
	long start_sect;	// 物理起始扇区号
	long nr_sects;	// 分区扇区总数
} hd[5*MAX_HD]={{0,0},};
// hdreg.h
struct partition {
	unsigned char boot_ind;		/* 0x80 - active (unused) */
	unsigned char head;		/* ? */
	unsigned char sector;		/* ? */
	unsigned char cyl;		/* ? */
	unsigned char sys_ind;		/* ? */
	unsigned char end_head;		/* ? */
	unsigned char end_sector;	/* ? */
	unsigned char end_cyl;		/* ? */
	unsigned int start_sect;	/* starting sector counting from 0 */
	unsigned int nr_sects;		/* nr of sectors in partition */
};
// 硬盘中断处理程序代码 sys_call.s
_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
	movb $0x20,%al
	outb %al,$0xA0		# EOI to interrupt controller #1
	jmp 1f			# give port chance to breathe
1:	jmp 1f
1:	xorl %edx,%edx
	movl %edx,_hd_timeout
	xchgl _do_hd,%edx
	testl %edx,%edx
	jne 1f
	movl $_unexpected_hd_interrupt,%edx
1:	outb %al,$0x20
	call *%edx		# "interesting" way of handling intr.
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret
// ll_rw_blk.c
int * blk_size[NR_BLK_DEV] = { NULL, NULL, };
// hd.c
static int hd_sizes[5*MAX_HD] = {0, };

在这里插入图片描述

9.5.3 向硬盘控制器发送命令

linux/kernel/blk_drv/hd.c/hd_out()

graph LR
A[hd-out] --> B[给磁盘控制器发送命令]

向硬盘控制器发送命令块。

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))	// 硬盘号;读写扇区数;起始扇区;磁头号;柱面号;命令码;硬盘中断处理程序中将调用的C处理函数指针
{
	register int port asm("dx");	// 定义局部寄存器变量并放在指定寄存器dx中
	if (drive>1 || head>15)	// 启动器号和磁头号
		panic("Trying to write bad sector");
	if (!controller_ready())	// 等待驱动器就绪
		panic("HD controller not ready");
	SET_INTR(intr_addr);	// do_hd=intr_addr 在中断中被调用,intr_addr() 为硬盘中断处理程序中将调用的C处理函数指针
	outb_p(hd_info[drive].ctl,HD_CMD);	// 向控制寄存器输出控制字节 #define HD_CMD		0x3f6
	port=HD_DATA;	// 置 dx 为数据寄存器端口 #define HD_DATA		0x1f0	/* _CTL when writing */
	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);	// 硬盘控制命令
}

函数执行流程:

  1. 检查参数有效性。如代码6~9行所示,检查驱动器号是否为0或者1、磁头号是否小于等于15,否则程序不支持,如果参数无效,则出错停机。调用 controller_ready() 等待驱动器就绪,如果等待一段时间后仍未就绪,则出错,停机。
  2. 设置硬盘中断发生时将调用的C函数指针do_hd。如代码10行所示。
  3. 向硬盘控制器命令端口发送控制字节,如代码11行所示,建立指定硬盘的控制方式。
  4. 向控制器端口发送7字节的参数命令块,如代码13~19行所示。

9.5.4 处理硬盘当前请求项

linux/kernel/blk_drv/hd.c/do_hd_request()

graph LR
A[do-hd-request] --> B[1. 请求项中的内容转换为具体硬盘信息]
A --> C[2. 复位控制器 校正硬盘]
A --> D[3. 执行写/读命令]

硬盘读写请求操作。根据设备当前请求项中的设备号和起始扇区号信息首先计算得到对应硬盘上的柱面号、当前磁道中扇区号、磁头号数据,然后再根据请求项中的命令对硬盘发送相应读/写命令。

void do_hd_request(void)
{
	int i,r;
	unsigned int block,dev;
	unsigned int sec,head,cyl;
	unsigned int nsect;
	INIT_REQUEST;	// 检测请求项合法性
	dev = MINOR(CURRENT->dev);	// 从请求项中取设备号中子设备号
	block = CURRENT->sector;	// 从请求项中去请求的起始扇区号
	if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) {
		end_request(0);
		goto repeat;	// blk.h INIT_REQUEST
	}
	block += hd[dev].start_sect;	// 需要读写的块对应到整个硬盘的绝对扇区号block,加上了对应分区的起始扇区号
	dev /= 5;	// 对应硬盘号
	__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) {	// static int reset = 0; 复位标志。当发生读写错误时会设置该标志并调用相关复位函数
		recalibrate = 1;	// 重新校正标志
		reset_hd();	// 向硬盘控制器发送复位命令,发送硬盘控制器命令建立驱动器参数
		return;
	}
	if (recalibrate) {	// static int recalibrate = 0; 重新校正标志。当设置了该标志,程序中会调用recal_intr()以将磁头移动到0柱面
		recalibrate = 0;
		hd_out(dev,hd_info[CURRENT_DEV].sect,0,0,0,
			WIN_RESTORE,&recal_intr);	// 执行寻道操作,让处于任何地方的磁头移动到0柱面
		return;
	}	
	if (CURRENT->cmd == WRITE) {
		hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);	// 发送写命令
		for(i=0 ; i<10000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++)// 循环读取状态寄存器信息
			/* nothing */ ;
		if (!r) {
			bad_rw_intr();	// 写硬盘失败
			goto repeat;
		}
		port_write(HD_DATA,CURRENT->buffer,256);	// 发送1个扇区数据 256(字)
	} else if (CURRENT->cmd == READ) {
		hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);	// 读命令
	} else
		panic("unknown hd-command");
}

函数执行流程:

  1. 检测参数合法性。如代码7~15行所示,代码第7行,检测请求项的合法性,若请求队列中没有请求项则退出(blk.h,含有标号 repeat),代码8~9行,取设备号中的子设备号(硬盘上各个分区)以及设备当前请求项中的起始扇区号,代码10~13行,判断子设备号是否存在以及起始扇区是否大于分区扇区数-2(因为一次要求读写一块数据(2个函数),所以请求的扇区号不能大于分区中最后倒数第二个扇区号)。

  2. 求出绝对扇区号和硬盘号。如代码14~15行,加上子设备号对应分区的起始扇区号,子设备号除以5得到对应的硬盘号。

  3. 求解扇区号、柱面号和磁头号。如代码16~19行所示。

    扇区号 / 每磁道扇区数 = 总磁道数(所有磁头面)··· 当前磁道上扇区号(sec) 总磁道数 / 磁盘总磁头数 = 柱面号(cyl) ··· 当前磁头号(head) 代码20~21行,调整磁道上的扇区号和预读/写的扇区数

  4. 检测硬盘控制器和硬盘复位情况。首先复位控制器状态,如代码22~26行所示,重新校正标志,然后置位重新校正标志,重新校正硬盘,让磁头移动到0柱面。

  5. 向硬盘控制器发送I/O操作信息。如代码33~45行所示,如果是写扇区命令,向硬盘控制器发送写命令,然后循环读取状态寄存器,判断请求服务标志(DRQ_STAT)是否置位,若没有置位,则跳转执行出错处理,若可以写入数据,则调用port_write()函数向硬盘控制器数据寄存器端口HD_DATA写入1个扇区的数据(第41行,256指的是内存字,512字节)。如果是读命令,如代码43行所示,向硬盘控制器发送读扇区命令。

// blk.h
#define INIT_REQUEST \
repeat: \
	if (!CURRENT) {\
		CLEAR_DEVICE_INTR \
		CLEAR_DEVICE_TIMEOUT \
		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"); \
	}
// blk.h
// 解锁请求处理宏
extern inline void end_request(int uptodate)
{
	DEVICE_OFF(CURRENT->dev);   // 关闭设备
	if (CURRENT->bh) {  // CURRENT 为当前请求结构项指针
		CURRENT->bh->b_uptodate = uptodate; // 置更新标志
		unlock_buffer(CURRENT->bh); // 解锁缓冲区
	}
	if (!uptodate) {    // 此次请求项操作失败
		printk(DEVICE_NAME " I/O error\n\r");   // 显示相关块设备IO出错信息
		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;    // 指向下一个请求项
}
// 读端口嵌入汇编宏 读端口port,共读nr字,保存在buf中
#define port_read(port,buf,nr) \
__asm__("cld;rep;insw"::"d" (port),"D" (buf),"c" (nr):"cx","di")
// 写端口嵌入汇编宏 写到端口port,共写nr字(32位),从buf中取数据
#define port_write(port,buf,nr) \
__asm__("cld;rep;outsw"::"d" (port),"S" (buf),"c" (nr):"cx","si")

9.5.5 硬盘中断处理过程中调用的 C 函数

linux/kernel/blk_drv/hd.c/bad_rw_intr()

读写硬盘失败处理调用函数。

static void bad_rw_intr(void)
{
	if (++CURRENT->errors >= MAX_ERRORS) // MAX_ERRORS=7 hd.c
		end_request(0);	// 结束当前请求项并唤醒等待该请求的进程
	if (CURRENT->errors > MAX_ERRORS/2)
		reset = 1;	// 复位硬盘控制器
}

函数执行流程:

  1. 如果读扇区时出错次数大于等于7,则结束当前请求项并唤醒等待该请求的进程。
  2. 如果读扇区时出错次数大于等于3,设置复位标志,表示数据没有更新。

linux/kernel/blk_drv/hd.c/read_intr()

读扇区中断调用函数。该函数将在硬盘读命令结束时引发的硬盘中断过程中被调用。在读命令执行后硬盘控制器就会产生硬盘中断请求信号,并执行中断处理程序。此时中断处理程序中调用的C函数指针do_hd已经指向read_intr(),因此会在一次读扇区操作完成(或出错)后就执行该函数。

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) {	// 所需读取的扇区数还未完,则再置硬盘调用C函数指针为read_intr()
		SET_INTR(&read_intr);	// 置硬盘调用C函数指针
		return;
	}
	end_request(1);
	do_hd_request();
}

函数执行流程:

  1. 若控制器忙、读写错误或命令执行出错。如代码3~6行所示,调用 bad_rw_intr() 函数执行读写硬盘失败处理,然后再次请求硬盘作复位处理。
  2. 若读操作没有出错。如代码8~17行所示,从数据寄存器端口把1个扇区的数据读到请求项的缓冲区中,并且递减请求项所需读取的扇区数值,若递减后不等于0,表示本项请求还有数据没有取完,于是再次置中断调用C函数指针do_hd为read_intr()并直接返回,等待硬盘在读出另1个扇区数据后发出中断并再次调用本函数。
  3. 如代码16~17行所示,将本次请求项的全部扇区数据读完后,调用 end_quest() 函数处理请求项结束事宜,再次调用 do_hd_request() 函数,处理其他硬盘请求项。
// 读端口嵌入汇编宏 读端口port,共读nr字,保存在buf中
#define port_read(port,buf,nr) \
__asm__("cld;rep;insw"::"d" (port),"D" (buf),"c" (nr):"cx","di")

linux/kernel/blk_drv/hd.c/write_intr()

写扇区中断调用函数。该函数将在硬盘写命令结束时引发的硬盘中断过程中被调用。在写命令执行后硬盘控制器就会产生硬盘中断请求信号,并执行中断处理程序。此时中断处理程序中调用的C函数指针do_hd已经指向write_intr(),因此会在一次读扇区操作完成(或出错)后就执行该函数。

static void write_intr(void)
{
	if (win_result()) {	// 硬盘控制器返回错误信息
		bad_rw_intr();	// 首先进行硬盘读写失败处理
		do_hd_request();	// 再次请求硬盘作相应处理
		return;	
	}
	if (--CURRENT->nr_sectors) {	// 若还有扇区要写
		CURRENT->sector++;	// 当前请求扇区扇区号+1
		CURRENT->buffer += 512;	// 调整请求缓冲区指针
		SET_INTR(&write_intr);	// do_hd 置函数指针为 write_intr()
		port_write(HD_DATA,CURRENT->buffer,256);	// 写256字
		return;
	}
	end_request(1);
	do_hd_request();
}
// 写端口嵌入汇编宏 写到端口port,共写nr字,从buf中取数据
#define port_write(port,buf,nr) \
__asm__("cld;rep;outsw"::"d" (port),"S" (buf),"c" (nr):"cx","si")

linux/kernel/blk_drv/hd.c/recal_intr()

硬盘中断服务程序中调用的重新复位函数。 如果硬盘控制器返回错误信息,则函数首先进行硬盘读写失败处理,然后请求硬盘作相应复位处理。

static void recal_intr(void)
{
	if (win_result())
		bad_rw_intr();
	do_hd_request();
}

9.5.6 硬盘控制器操作辅助函数

函数名称作用备注
int controller_ready(void)判断并循环等待硬盘控制器就绪。返回剩余等待次数
int win_result(void)检测硬盘执行命令后的状态,读取状态寄存器中的命令执行结果。0:正常
1:错误
int drive_busy(void)等待硬盘就绪。0:就绪
1:等待超时
void reset_controller(void)重新校正硬盘控制器。
void reset_hd(void)硬盘复位操作。
void unexpected_hd_interrupt(void)硬盘意外中断调用的默认函数。
void hd_times_out(void)硬盘操作超时处理函数。
graph LR
B[win-result]
C[drive-busy]
D[reset-controller]
E[reset-hd]
F[unexpected-hd-interrupt]
G[hd-times-out]

D --> C

E -- reset=1 --> D
E -- reset=0 --> B
B --> 1(bad-rw-intr)
E -- i --> 2(hd-out)
E --> 3(do-hd-request)

F --> 3(do-hd-request)

G --> 3(do-hd-request)

linux/kernel/blk_drv/hd.c/controller_ready()

判断并循环等待硬盘控制器就绪。

static int controller_ready(void)
{
	int retries = 100000;	// 循环等待次数
	while (--retries && (inb_p(HD_STATUS)&0xc0)!=0x40);	// 驱动器就绪(置位)控制器忙(复位)HD_STATUS-硬盘控制器状态寄存器端口
	return (retries);	// 返回值不为0说明等待时间期限内控制器回到空闲状态
}

(inb_p(HD_STATUS)&0xc0)!=0x40:读硬盘控制器状态寄存器端口 HD_STATUS,循环检测其中的驱动器就绪比特位(位6)是否为1,控制器忙位(位7)是否为0。 如果返回值 retries 为0,则表示等待控制器空闲的时间已经超时而发送错误;若不为0则说明等待(循环)时间期限内控制器回到空闲状态。

在这里插入图片描述

linux/kernel/blk_drv/hd.c/win_result()

检测硬盘执行命令后的状态,读取状态寄存器中的命令执行结果。

static int win_result(void)
{
	int i=inb_p(HD_STATUS);	// 读状态信息
	if ((i & (BUSY_STAT | READY_STAT | WRERR_STAT | SEEK_STAT | ERR_STAT)) // 1111 0001
		== (READY_STAT | SEEK_STAT)) // 0101 0000
		return(0); /* ok */
	if (i&1) i=inb(HD_ERROR);	// 若 ERR_STAT 为1,则读出错寄存器 #define HD_ERROR	0x1f1
	return (1);
}
// hdreg.h HD_STATUS
#define ERR_STAT	0x01	// 命令执行错误
#define INDEX_STAT	0x02	// 收到索引
#define ECC_STAT	0x04	/* Corrected error */ // ECC 校验错
#define DRQ_STAT	0x08	// 请求服务
#define SEEK_STAT	0x10	// 寻道结束
#define WRERR_STAT	0x20	// 驱动器故障
#define READY_STAT	0x40	// 驱动器就绪
#define BUSY_STAT	0x80	// 控制器忙碌
// HD_ERROR
// 执行控制器诊断命令	执行其他命令
// 0x01: 无错误	  数据标志丢失
// 0x02: 控制器出错	磁道0错
// 0x03: 扇区缓冲区错
// 0x04: ECC部件错	  命令放弃
// 0x05: 控制处理器错
// 0x10:			ID未找到
// 0x40:			ECC错误
// 0x80:			 坏扇区

linux/kernel/blk_drv/hd.c/drive_busy()

等待硬盘就绪。

static int drive_busy(void)
{
	unsigned int i;
	unsigned char c;
	for (i = 0; i < 50000; i++) {
		c = inb_p(HD_STATUS);	// 取主控制器状态字节
		c &= (BUSY_STAT | READY_STAT | SEEK_STAT);
		if (c == (READY_STAT | SEEK_STAT))	// 就绪或寻道结束标志置位,表示硬盘就绪
			return 0;
	}
	printk("HD controller times out\n\r");	// 等待超时,显示信息
	return(1);
}

函数执行流程: 循环读取控制器的主状态寄存器 HD_STATUS,检测忙位、就绪位和寻道结束位。若只有就绪位和寻道结束标志位为1,表示硬盘就绪,返回0,否则表示循环结束时等待超时。显示警告信息,返回1。

linux/kernel/blk_drv/hd.c/reset_controller()

graph LR
D[reset-controller] --> C[driver-busy]

重新校正硬盘控制器。

static void reset_controller(void)
{
	int	i;
	outb(4,HD_CMD);	// 向控制寄存器端口HD_CMD发送复位(4)控制字节 #define HD_CMD		0x3f6
	for(i = 0; i < 1000; 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);
}

代码流程:

  1. 代码第5行,循环空操作等待的目的是让控制器执行复位操作。
  2. 代码第6行,向控制器端口发送的字节为允许重试、重读。
  3. 代码7~10行,等待硬盘就绪,如果超时,显示忙警告信息。读取错误寄存器内容,如果不等于1,则显示硬盘控制器复位失败信息。

linux/kernel/blk_drv/hd.c/reset_hd()

graph LR
E[reset-hd] -- reset=1 --> D[reset-controller]
E -- reset=0 --> B[win-result]
B --> 1[bad-rw-intr]
E -- i --> 2[hd-out]
2 --> 3[do-hd-request]

硬盘复位操作。

static void reset_hd(void)
{
	static int i;
repeat:
	if (reset) {
		reset = 0;
		i = -1;
		reset_controller();	// 复位硬盘控制器
	} else if (win_result()) {	// 判断复位硬盘控制器命令执行是否正常
		bad_rw_intr();	// 统计出错次数,再确定是否再次设置reset标志
		if (reset)
			goto repeat;
	}
	i++;
	if (i < NR_HD) {
		hd_out(i,hd_info[i].sect,hd_info[i].sect,hd_info[i].head-1,
			hd_info[i].cyl,WIN_SPECIFY,&reset_hd);// 给每块硬盘都建立驱动器参数命令
	} else
		do_hd_request();
}

函数执行流程:

  1. 复位硬盘控制器。如代码5~8行所示,将复位标志清零后,执行复位硬盘控制器操作。向第i个硬盘向控制器发送“建立驱动器参数”命令,如代码16~17行所示,会发出硬盘中断信号,再次调用本函数,直到系统中NR_HD个硬盘都已经正常执行了发送的命令,调用do_hd_request()函数开始对请求项进行处理,如代码19行所示。
  2. 当再次调用本函数执行的时候,由于reset为0,故代码会执行第9行的语句,判断命令执行是否正常,若发生错误,则调用bad_rw_intr()函数来确定是否再次设置reset标志,如果设置了reset标志,则跳转到repeat重新执行本函数。

linux/kernel/blk_drv/hd.c/unexpected_hd_interrupt()

graph LR
F[unexpected-hd-interrupt] --> 1[do-hd-request]

硬盘意外中断调用的默认函数。在发生意外硬盘中断时中断处理程序中调用的默认C处理函数。当被调用函数指针为NULL时就会调用该函数。设置复位标志reset,调用请求项函数go_hd_request(),在其中执行复位处理操作。

void unexpected_hd_interrupt(void) // 发送意外硬盘中断时中断处理程序调用的函数
{
	printk("Unexpected HD interrupt\n\r");
	reset = 1;
	do_hd_request();
}

linux/kernel/blk_drv/hd.c/hd_times_out()

graph LR
G[hd-times-out] --> 1[do-hd-request]

硬盘操作超时处理函数。do_timer()函数会调用本函数来设置复位标志reset,并调用do_hd_request()执行复位处理。

void hd_times_out(void)
{
	if (!CURRENT)	// 无请求项
		return;
	printk("HD timeout");
	if (++CURRENT->errors >= MAX_ERRORS)
		end_request(0);	// 请求项中执行过程中的出错次数已经大于设定值
	SET_INTR(NULL);	// 失败形式结束将中断过程中调用的C函数置空
	reset = 1;	// 设置复位标志
	do_hd_request(); // 执行复位操作
}

函数执行流程:

  1. 判断当前是否有请求项处理。如代码3~4行所示,如果没有请求项要处理,则无超时可言,直接返回。
  2. 判断当前请求项执行过程中,如代码6~7行所示,出错次数是否已经大于设定值,如果是,则结束本次请求项的处理。
  3. 将中断过程中调用的C函数指针 do_hd 置空,并设置复位标志 reset ,调用 do_hd_request() 中执行复位操作。

9.6 内存虚拟盘驱动程序 ramdisk.c

linux/kernel/blk_drv/ramdisk.c

此文件是内存虚拟盘驱动程序,虚拟盘设备是一种利用物理内存来模拟实际磁盘存储数据的方式。优点是:提高了磁盘数据的读写速度,缺点是:当系统奔溃时,虚拟盘中的所有数据将全部消失。内核初始化程序会在内存中划出一块指定大小的内存区域用于存放虚拟盘数据。

符号RAMDISK定义在linux/Makefile文件中,内核初始化程序会在内存中划出一块指定大小的内存区域用于存放虚拟盘数据。虚拟盘容量等于RAMDISK的值,若RAMDISK=512,则表示虚拟盘大小为512KB,虚拟盘在物理内存中所处的具体位置在内核初始化阶段确定(init/main.c)位于内核高速缓冲区和主内存区之间,如图所示。

在这里插入图片描述

对虚拟盘设备的读写访问操作与块设备的访问方式相似,由于不牵涉与外部控制器或设备进行同步操作,所以数据在系统与设备之间的传送只需要执行内存数据块复制操作。

在这里插入图片描述

ramdisk.c 包含3个函数。

  • rd_init():由init/main.c程序调用,确定虚拟盘在物理内存中的具体位置和大小。
  • do_rd_request():虚拟盘设备的请求项操作函数。
  • rd_load():虚拟盘根文件系统加载函数。检测启动盘上的第256磁盘块开始处是否存在一个根文件系统,从磁盘的第257块中读取根文件系统超级块,若成功,就把该根文件映像文件读到内存虚拟盘中,作为根文件系统使用,从而就可以使用一张集成了根文件系统的启动盘来引导系统到shell命令提示符状态,把根文件系统设备标志 ROOT_DEV 设置为虚拟盘设备,否则退出该函数。
#include <string.h>
#include <linux/config.h>
#include <linux/sched.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <asm/system.h>
#include <asm/segment.h>
#include <asm/memory.h>
#define MAJOR_NR 1
#include "blk.h"
char	*rd_start;	// 虚拟盘在内存中的开始地址
int	rd_length = 0;	// 虚拟盘所占内存大小
void do_rd_request(void)	//在低级块设备接口函数 ll_rw_block() 建立起虚拟盘的请求项并添加到rd的链表中,就会调用该函数对 rd 当前请求项进行处理
{
	int	len;
	char	*addr;
	INIT_REQUEST;
	addr = rd_start + (CURRENT->sector << 9);	// 计算请求项中虚拟盘起始扇区在物理内存中对应的地址 addr 和占用的内存字节值 len CURRENT:blk_dev[MAJOR_NR].current_request
	len = CURRENT->nr_sectors << 9;
	if ((MINOR(CURRENT->dev) != 1) || (addr+len > rd_start+rd_length)) {	// 标号不为 1(软盘就1个分区?) ,或大于虚拟盘末尾,结束请求项
		end_request(0);
		goto repeat;
	}
	if (CURRENT-> cmd == WRITE) {	// 将缓冲区的内容赋值到地址 addr 处,长度为len
		(void ) memcpy(addr,
			      CURRENT->buffer,
			      len);
	} else if (CURRENT->cmd == READ) {
		(void) memcpy(CURRENT->buffer, 
			      addr,
			      len);
	} else
		panic("unknown ramdisk-command");
	end_request(1);
	goto repeat;
}
long rd_init(long mem_start, int length)	// 返回内存虚拟盘ramdisk所需的内存量 length=RAMDISK*1024
{
	int	i;
	char	*cp;
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;	// do_rd_request()
	rd_start = (char *) mem_start;	// 16MB内存系统,其值为4MB 虚拟盘在物理内存中的起始地址
	rd_length = length;	// 虚拟盘区域长度值 占用字节长度值
	cp = rd_start;	// 盘区清零 对整个虚拟盘清零
	for (i=0; i < length; i++)
		*cp++ = '\0';
	return(length); // 保留给虚拟盘的内存容量
}
void rd_load(void)	// 尝试加载根文件系统到虚拟盘
{
	struct buffer_head *bh;	// 高速缓冲块头文件
	struct super_block	s;	// 文件超级块结构
	int		block = 256;	// 高速缓冲块头指针
	int		i = 1;
	int		nblocks;	// 文件系统盘块总数
	char		*cp;		/* Move pointer */
	if (!rd_length)
		return;
	printk("Ram disk: %d bytes, starting at 0x%x\n", rd_length,
		(int) rd_start);
	if (MAJOR(ROOT_DEV) != 2)	// 是否是软盘
		return;
	bh = breada(ROOT_DEV,block+1,block,block+2,-1);	// 读根文件系统的基本参数,从软盘块256+1、256和256+2
	if (!bh) {
		printk("Disk error while looking for ramdisk!\n");
		return;
	}
	*((struct d_super_block *) &s) = *((struct d_super_block *) bh->b_data);
	brelse(bh);
	if (s.s_magic != SUPER_MAGIC)	// 说明是非MINIX文件系统
		return;
	nblocks = s.s_nzones << s.s_log_zone_size;
	if (nblocks > (rd_length >> BLOCK_SIZE_BITS)) {	// 文件系统中数据块总数大于内存虚拟盘所能容纳的块数的情况
		printk("Ram disk image too big!  (%d blocks, %d avail)\n", 
			nblocks, rd_length >> BLOCK_SIZE_BITS);
		return;
	}
	printk("Loading %d bytes into ram disk... 0000k", 
		nblocks << BLOCK_SIZE_BITS);
	cp = rd_start;
	while (nblocks) {	// 循环操作将磁盘上根文件系统映像文件加载到虚拟盘上
		if (nblocks > 2) 
			bh = breada(ROOT_DEV, block, block+1, block+2, -1);
		else
			bh = bread(ROOT_DEV, block);
		if (!bh) {
			printk("I/O error on block %d, aborting load\n", 
				block);
			return;
		}
		(void) memcpy(cp, bh->b_data, BLOCK_SIZE);
		brelse(bh);
		printk("\010\010\010\010\010%4dk",i);
		cp += BLOCK_SIZE;
		block++;
		nblocks--;
		i++;
	}
	printk("\010\010\010\010\010done \n");
	ROOT_DEV=0x0101;	// 目前根文件系统设备号修改成虚拟盘的设备号0x0101
}

9.5.1 处理内存虚拟盘当前请求项

linux/kernel/blk_drv/ramdisk.c/do_rd_request()

虚拟盘当前请求项操作函数。 在低级块设备接口函数ll_rw_block()建立起虚拟盘的请求项并添加到rd的链表之中后,调用该函数对rd当前请求项进行处理。 函数执行流程:

  1. 检查请求项合理性。代码17~23行所示,代码第18、19行,计算请求项处理的虚拟盘中起始扇区在物理内存中对应的地址addr和占用的内存字节长度值len。左移9位的目的是 sector* 512,换算成字节值。CURRENT被定义在blk_dev[MAJOR_NR].current_request中。如代码20~23行,判断当前请求项中子设备号是否为1,对应起始位置是否大于虚拟盘末尾,如果是则结束该请求项,然后跳转到repeat来处理下一个虚拟盘请求项。
  2. 然后进行读写操作。代码24~35行所示,根据读写命令,将请求项中缓冲区的内容复制到地址addr处,长度为len字节。代码第34行,置更新标志,继续处理下一请求项。

9.5.2 内存虚拟盘初始化

linux/kernel/blk_drv/ramdisk.c/rd_init()

虚拟盘初始化函数,返回内存虚拟盘ramdisk所需的内存量。 函数输入:

  • mem_start:主内存区起始位置。
  • length:RAMDISK* 1024(字节)。 函数执行流程:
  1. 设置虚拟盘设备请求项处理函数指针。如代码41行所示,将请求项处理函数指针指向do_rd_request()。
  2. 设置参数值。如代码42~43行所示,确定虚拟盘在物理内存中的起始地址、占用字节长度值。
  3. 虚拟盘区清零。读代码44~46行所示。

9.5.3 加载根文件系统

linux/kernel/blk_drv/ramdisk.c/rd_load()

如果根文件系统设备是 ramdisk 的话,就调用该函数加载,该函数尝试把根文件系统加载到虚拟盘中。该函数在 hd.c 中被调用。(1 磁盘块=1024B)

函数执行流程:

  1. 检查虚拟盘的有效性和完整性。如代码57~62行所示,依次检查了 ramdisk 长度是否为0,是否是软盘设备。
  2. 读根文件系统的基本参数。如代码63~71行所示,breada() 函数用于从磁盘上读取指定的数据块,并标出还需要读的块,然后返回含有数据块的缓冲区指针,如果该指针为 NULL,则表明数据块不可读,然后把缓冲区中的磁盘超级块复制到 s 变量中,并释放缓冲区,再对超级块的有效性进行判断,如代码70~71行,判断超级块中文件系统魔数,判断加载的数据块是否为 MINIX 文件系统。
  3. 尝试将根文件系统读入到内存虚拟盘中。依据文件系统中的数据块总数和内存虚拟盘所能容纳的块数二者的数量关系。如代码72~77行所示,对于一个文件系统来说,其超级块结构的 s_nzones 字段中保存着总逻辑块数,一个逻辑块中含有的数据块总数则由字段 s_log_zone_size 指定。因此文件系统中的数据块总数 nblocks 就等于逻辑块总数*2^(每区数据块数),判断文件系统中数据块总数与内存虚拟盘所能容纳的块数的情况。
  4. 加载数据块信息。如代码78~98行所示,显示加载数据块信息,让 cp 指向内存虚拟盘起始处。进行循环操作将磁盘上根文件系统映像文件加载到虚拟盘上。在操作过程中,如果一次需要加载的盘快数大于2块,采用超前预读函数,否则单块读取。如果出现 I/O 错误,放弃加载过程返回。将读取的磁盘块使用 memcp() 函数从高速缓冲区中复制到内存虚拟盘相应位置处,同时显示已加载的块数。
  5. 当根文件系统加载完毕后,如代码99~100行所示,显示“done”信息,将目前根文件系统设备号修改成虚拟盘的设备号 0x0101。