乐于分享
好东西不私藏

《Linux-0.12 源码篇》- 08 块设备驱动

《Linux-0.12 源码篇》- 08 块设备驱动

字数 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 - 软盘控制器寄存器和命令常量

"工欲善其事,必先利其器。" —— 《论语》