字数 3129,阅读大约需 16 分钟
第八章:块设备驱动
块设备是以固定大小数据块(通常1KB)为单位进行读写的设备,典型代表是硬盘、软盘和虚拟磁盘。Linux 0.12实现了完整的块设备驱动框架,包括统一的请求队列机制、硬盘驱动、软盘驱动和RAM盘驱动。块设备驱动通过异步请求队列实现高效的磁盘I/O,为文件系统提供了可靠的数据存储服务。
8.1 块设备驱动框架
块设备的核心是请求队列(request queue)机制。request结构描述一个I/O请求,包含设备号、命令(READ/WRITE)、起始扇区号、缓冲块指针等信息。系统为每个块设备类型维护一个请求队列,当前正在处理的请求称为当前请求。
ll_rw_block()是块设备I/O的入口函数。它创建一个request,插入设备的请求队列,然后调用设备的request_fn启动I/O操作。make_request()函数实现请求排队策略:它首先检查是否有相邻的请求可以合并(电梯算法),如果有就合并请求以减少磁盘寻道时间;否则分配新的request并加入队列。
块设备中断处理程序在I/O完成时被调用,它取出当前请求,调用end_request()标记请求完成并唤醒等待进程,然后从队列取下一个请求继续处理。这种请求队列机制将同步的文件系统操作转换为异步的设备I/O,大大提高了系统效率。
request请求结构
request结构描述一个块设备I/O请求,记录了目标设备、读写命令、起始扇区、缓冲区指针、等待进程等信息。系统维护一个全局请求池(最多32个),通过next指针组织为链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /* include/linux/fs.h - 块设备请求结构 */struct request { int dev; /* 设备号(-1=空闲) */ int cmd; /* READ=0 / WRITE=1 */ int errors; /* 错误重试计数 */ unsigned long sector; /* 起始扇区号(512字节/扇区) */ unsigned long nr_sectors; /* 剩余扇区数 */ char * buffer; /* 数据缓冲区(指向buffer_head->b_data) */struct task_struct * waiting; /* 等待I/O完成的进程 */struct buffer_head * bh; /* 对应的缓冲块头 */struct request * next; /* 队列链表指针 */};#define NR_REQUEST 32struct request request[NR_REQUEST]; /* 全局请求池 */
blk_dev_struct设备表
每种块设备类型有一个blk_dev_struct项,包含该设备的请求处理函数和当前正在处理的请求指针。系统通过主设备号索引blk_dev数组找到对应设备。
1 2 3 4 5 6 7 8 /* kernel/blk_drv/blk.h - 块设备表 */struct blk_dev_struct { void (*request_fn)(void); /* 请求处理函数 */struct request * current_request; /* 当前请求 */};struct blk_dev_struct blk_dev[7]; /* 索引=主设备号 *//* DEV_MEM=1, DEV_FD=2, DEV_HD=3, DEV_TTYX=4, DEV_TTY=5, DEV_LP=6 */
ll_rw_block块读写入口
ll_rw_block()是文件系统调用块设备的统一入口。它通过make_request()创建请求并插入设备的请求队列:先尝试与队列中相邻请求合并(减少寻道),否则分配新请求按电梯算法(按扇区号排序)插入队列。队列为空时立即启动I/O。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 /* kernel/blk_drv/ll_rw_blk.c - 块读写入口 */void ll_rw_block(int rw, struct buffer_head * bh){ unsigned int major = MAJOR(bh->b_dev); if (major >= 7 || !blk_dev[major].request_fn) return; /* 设备不存在 */ make_request(major, rw, bh);}static void make_request(int major, int rw, struct buffer_head * bh){struct request * req; lock_buffer(bh); /* 尝试与相邻请求合并 */ if (rw == READ && (req = find_adjacent_request(major, bh))) { req->bh = bh; req->nr_sectors += 2; /* 1KB块=2扇区 */ return; } /* 从全局请求池分配空闲项(dev=-1) */ /* ... 若无空闲则sleep_on等待 ... */ req->dev = bh->b_dev; req->cmd = rw; req->sector = bh->b_blocknr * 2; /* 块号→扇区号 */ req->nr_sectors = 2; req->buffer = bh->b_data; req->bh = bh; req->next = NULL; add_request(major, req); /* 按电梯算法插入队列 */}/* 电梯算法插入:按扇区号排序,减少磁头移动 */static void add_request(int major, struct request * req){struct request * tmp = blk_dev[major].current_request; if (!tmp) { blk_dev[major].current_request = req; (blk_dev[major].request_fn)(); /* 队列为空,立即启动I/O */ return; } /* 按扇区号升序找到插入位置 */ while (tmp->next) { if (IN_ORDER(tmp, req) && IN_ORDER(req, tmp->next)) break; tmp = tmp->next; } req->next = tmp->next; tmp->next = req;}
blk_dev_struct数组定义所有块设备的request_fn函数指针。blk_dev_init()初始化块设备,将所有请求队列头指针设为NULL。设备驱动在初始化时分配请求数组并设置request_fn。
1 2 3 4 5 6 7 8 9 10 11 ┌────────────────────────────┐ │ │ ▼ │ bread() ──▶ make_request ──▶ request_fn ──▶ 硬件 ──▶ 中断 (请求入队) (启动I/O) 执行 (完成) ▲ │ │ │ └──── 队列非空 ────┘ │ ▼ 设备空闲
8.2 硬盘驱动
硬盘驱动实现在hd.c中,支持标准的AT接口硬盘(IDE/ATA)。hd_init()初始化硬盘,设置IRQ 14中断处理程序hd_interrupt,从CMOS读取硬盘参数(柱面数、磁头数、扇区数),计算硬盘容量。
do_hd_request()是硬盘的request_fn函数。它从请求队列取当前请求,计算扇区号对应的柱面、磁头、扇区号(CHS地址),然后向硬盘控制器的I/O端口发送命令:写柱面号到0x1F3/0x1F4,写磁头号和驱动器号到0x1F6,写扇区数到0x1F2,写命令(READ/WRITE)到0x1F7。硬盘控制器开始执行命令,CPU返回继续其他工作。
硬盘完成操作后发出IRQ 14中断,hd_interrupt()被调用。它检查硬盘状态寄存器(0x1F7),如果有错误则重试。读操作时,从数据端口0x1F0读取512字节到缓冲区;写操作时,向数据端口写入512字节。一个请求可能包含多个扇区,hd_interrupt()循环处理直到所有扇区完成,然后调用end_request()完成请求并启动下一个请求。
硬盘驱动使用中断驱动而非轮询方式,CPU在等待硬盘时可以运行其他进程,充分利用了CPU资源。这种异步I/O模型是现代操作系统的标准做法。
hd_init硬盘初始化
hd_init()从CMOS读取硬盘几何参数(柱面/磁头/扇区数),注册IRQ14中断处理程序,并将硬盘请求处理函数挂载到块设备表中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 /* kernel/blk_drv/hd.c - 硬盘驱动初始化 */#define HD_DATA 0x1F0 /* 数据寄存器 */#define HD_NSECTOR 0x1F2 /* 扇区数寄存器 */#define HD_SECTOR 0x1F3 /* 扇区号 */#define HD_LCYL 0x1F4 /* 柱面低8位 */#define HD_HCYL 0x1F5 /* 柱面高8位 */#define HD_CURRENT 0x1F6 /* 驱动器/磁头 */#define HD_STATUS 0x1F7 /* 状态寄存器 */#define WIN_READ 0x20 /* ATA读命令 */#define WIN_WRITE 0x30 /* ATA写命令 */struct hd_i_struct { int head, sect, cyl, wpcom, lzone, ctl;};struct hd_i_struct hd_info[2]; /* 最多2块硬盘 */void hd_init(void){ /* 从CMOS 0x12读取硬盘类型,再读取详细参数(cyl/head/sect) */ /* ... */ set_intr_gate(0x2E, &hd_interrupt); /* IRQ14 */ outb(inb_p(0xA1) & 0xBF, 0xA1); /* 解除IRQ14屏蔽 */ blk_dev[3].request_fn = do_hd_request; /* DEV_HD=3 */}
do_hd_request硬盘请求处理
do_hd_request()从请求队列取出当前请求,将线性扇区号转换为CHS地址(柱面/磁头/扇区),然后向ATA控制器端口依次写入参数和命令。命令发出后硬盘异步执行,完成后触发IRQ14中断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 /* kernel/blk_drv/hd.c - 硬盘请求处理 */void do_hd_request(void){struct request * req = blk_dev[3].current_request; if (!req) return; /* 线性扇区号→CHS地址 */ int dev = MINOR(req->dev) >> 6; int block = req->sector; int sect = block % hd_info[dev].sect + 1; block /= hd_info[dev].sect; int head = block % hd_info[dev].head; int cyl = block / hd_info[dev].head; while (inb_p(HD_STATUS) & 0x80) ; /* 等待BUSY清零 */ outb_p(0xA0 | (dev << 4) | head, HD_CURRENT); /* 选择驱动器+磁头 */ outb_p(req->nr_sectors, HD_NSECTOR); outb_p(sect, HD_SECTOR); outb_p(cyl, HD_LCYL); outb_p(cyl >> 8, HD_HCYL); outb_p((req->cmd == READ) ? WIN_READ : WIN_WRITE, HD_STATUS); /* 硬盘开始工作,完成后触发IRQ14中断 */}
hd_interrupt硬盘中断处理
硬盘中断处理程序检查状态寄存器,若出错则重试;否则根据读写方向通过数据端口传输512字节数据,更新请求状态。所有扇区传输完毕后调用end_request()完成请求并启动下一个。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /* kernel/blk_drv/hd.c - 硬盘中断处理 */void hd_interrupt(void){struct request * req = blk_dev[3].current_request; int stat = inb_p(HD_STATUS); if (stat & 0x01) { /* 出错 */ req->errors++; if (req->errors > MAX_ERRORS) end_request(0); do_hd_request(); /* 重试 */ return; } /* 传输数据 */ if (req->cmd == READ) port_read(HD_DATA, req->buffer, 256); /* 256字=512字节 */ else port_write(HD_DATA, req->buffer, 256); req->buffer += 512; req->sector++; if (--req->nr_sectors > 0) return; /* 还有扇区,等待下次中断 */ end_request(1); /* 全部完成 */ do_hd_request(); /* 处理下一个请求 */}void end_request(int uptodate){struct request * req = blk_dev[3].current_request; req->bh->b_uptodate = uptodate; req->bh->b_dirt = 0; unlock_buffer(req->bh); if (req->waiting) wake_up(&req->waiting); blk_dev[3].current_request = req->next; /* 移到下一请求 */ req->dev = -1; /* 标记空闲 */ wake_up(&wait_for_request);}
8.3 软盘驱动
软盘驱动实现在floppy.c中,比硬盘驱动复杂得多,因为软盘介质容易出错,驱动器马达需要手动控制,而且软盘可以随时更换。floppy_init()初始化软盘,设置IRQ 6中断处理程序floppy_interrupt。
do_fd_request()是软盘的request_fn函数。它首先通过floppy_on()启动软驱马达,等待马达加速到工作转速(约500ms),然后向软盘控制器发送FDC命令。软盘使用复杂的命令协议,需要发送多字节命令和参数,通过状态寄存器握手。
软盘操作容易失败(介质损坏、磁盘未插入等),驱动实现了重试机制:失败后重新校准磁头、复位控制器,最多重试三次。更换软盘后,驱动通过检测磁盘更换信号(disk change line)使旧缓冲区失效。定时器在马达空闲几秒后自动关闭马达以节省能源和减少磨损。
8.4 虚拟磁盘(RAM盘)
RAM盘将一块物理内存模拟成块设备,提供极快的I/O速度。rd_init()初始化RAM盘,分配指定大小的内存(通过多次调用get_free_page())作为RAM盘数据。
do_rd_request()处理RAM盘请求。由于RAM盘数据在内存中,无需真正的I/O操作,do_rd_request()直接在RAM盘内存和缓冲区之间复制数据即可,然后立即调用end_request()完成请求。RAM盘没有中断,所有操作都是同步完成的。
RAM盘常用作根文件系统。在系统启动时,引导加载器将根文件系统镜像加载到RAM盘,内核挂载RAM盘作为根文件系统。这种方式启动快速,不依赖真正的硬盘驱动,常用于安装程序和紧急恢复系统。
下面是三种不同类型块设备的处理流程对比图:
1 2 3 4 5 6 7 8 硬盘 ──▶ ATA命令 ──▶ 等待 ──▶ hd_interrupt (慢速, 中断驱动) 软盘 ──▶ 启动马达 ──▶ FDC命令 ──▶ 等待 ──▶ floppy_interrupt (更慢, 机械延迟大) RAM盘 ──▶ 内存复制 ──▶ 立即完成 (快速, 无中断)
8.5 块设备缓冲与同步
块设备与文件系统之间通过缓冲区高速缓存(buffer cache)交互。文件系统调用bread()读取块,bread()通过ll_rw_block()向块设备发起异步读请求,然后睡眠等待。块设备中断处理程序完成读操作后,唤醒等待进程,bread()返回缓冲块。
写操作通过bwrite()实现,它标记缓冲块为脏(b_dirt=1),然后返回。后台定期(或sync系统调用触发)扫描脏缓冲块,调用ll_rw_block()异步写入设备。这种延迟写策略提高了性能,但需要定期sync以防数据丢失。
块设备驱动还实现了磁盘分区支持。hd_init()读取主引导记录(MBR)的分区表,为每个分区设置起始扇区和大小。文件系统操作分区时,块设备驱动自动加上分区起始偏移量,实现透明的分区访问。
通过本章的学习,我们理解了Linux 0.12的块设备驱动实现:从统一的请求队列框架到硬盘、软盘、RAM盘的具体驱动,以及块设备缓冲和同步机制。块设备驱动是文件系统的基础,为数据持久化提供了可靠保障。
8.6 参考资料
本章内容基于以下资料编写:
ATA/IDE规范
• ATA/ATAPI-4 Specification - 描述了IDE/ATA硬盘接口规范 • 82077AA FDC Datasheet - Intel软盘控制器芯片数据手册
Linux 0.12源代码文件
• kernel/blk_drv/ll_rw_blk.c - ll_rw_block、make_request、end_request请求队列管理 • kernel/blk_drv/hd.c - 硬盘驱动、do_hd_request、hd_interrupt • kernel/blk_drv/floppy.c - 软盘驱动、do_fd_request、floppy_interrupt • kernel/blk_drv/ramdisk.c - RAM盘驱动、rd_init、do_rd_request • kernel/blk_drv/blk.h - 块设备驱动公共头文件 • include/linux/fs.h - request结构定义 • include/linux/hdreg.h - 硬盘寄存器和命令常量 • include/linux/fdreg.h - 软盘控制器寄存器和命令常量
"工欲善其事,必先利其器。" —— 《论语》
夜雨聆风