到目前为止,内核只能输出到屏幕,无法接收输入,也无法持久化数据。设备驱动是内核与硬件之间的桥梁,没有键盘驱动,内核就是个聋子;没有硬盘驱动,数据重启就消失。
设备驱动的动机
没有设备驱动,操作系统就需要直接操作硬件寄存器,代码难以维护、无法扩展,可移植性也无从谈起。驱动程序将不同设备的硬件接口差异封装为统一的操作接口,通过中断处理响应硬件事件,管理设备共享和并发访问,并通过缓冲、请求队列等机制提高 I/O 效率。
理解了设备驱动的必要性,接下来看如何组织不同类型的驱动。
设备驱动架构
操作系统根据访问方式将设备分为三类:
- 字符设备:按字节流顺序访问,如键盘、鼠标、串口
- 块设备:按固定大小的块访问,支持随机读写,如硬盘、SSD
- 网络设备:通过网络协议栈访问,如网卡
设备抽象层
内核通过统一的 device_t 结构来管理所有设备,无论字符设备还是块设备,都通过同一套接口访问:
typedef struct device { char name[32]; /* 设备名称 */ device_type_t type; /* 设备类型:字符/块/网络 */ int major; /* 主设备号:标识驱动类型 */ int minor; /* 次设备号:标识具体设备实例 */ void *private; /* 设备私有数据 */ int refcount; /* 引用计数 */
/* 操作函数指针 */ int (*open)(struct device *dev); int (*close)(struct device *dev); ssize_t (*read)(struct device *dev, void *buf, size_t count, off_t offset); ssize_t (*write)(struct device *dev, const void *buf, size_t count, off_t offset); int (*ioctl)(struct device *dev, int cmd, void *arg);} device_t;device_t 中的 major 和 minor 字段构成设备号,用于唯一标识设备:主设备号标识设备类型或驱动程序,次设备号标识同类设备中的具体实例。例如 /dev/hda 的 major=3, minor=0(第一块 IDE 硬盘),/dev/keyboard 的 major=1, minor=0(键盘)。
先从最常用的输入设备开始,键盘。键盘是典型的字符设备,数据以字节流形式到达,按字节顺序访问,不支持随机访问,通常不使用缓存,适合交互式输入输出。
字符设备驱动:键盘
键盘是最典型的字符设备,它通过中断向内核报告按键事件。以下是键盘驱动的完整工作流程:
键盘中断处理:
/* 键盘中断处理程序 */void keyboard_isr(interrupt_frame_t *frame){ (void)frame; uint8_t scancode = inb(0x60); /* 从0x60端口读取扫描码 */
/* 处理特殊键(Shift、Ctrl、Alt等) */ switch (scancode) { case SCANCODE_LSHIFT: case SCANCODE_RSHIFT: keyboard_dev.state.shift = 1; return; case SCANCODE_LSHIFT_R: case SCANCODE_RSHIFT_R: keyboard_dev.state.shift = 0; return; case SCANCODE_CAPS: keyboard_dev.state.caps_lock = !keyboard_dev.state.caps_lock; return; }
/* 忽略按键释放码(最高位为1) */ if (scancode & 0x80) { return; }
/* 转换扫描码为ASCII字符 */ char ch = scancode_to_ascii(scancode);
if (ch != 0) { spinlock_acquire(&kb_lock);
/* 检查缓冲区是否满 */ if (keyboard_dev.count < KB_BUFFER_SIZE) { /* 写入环形缓冲区 */ keyboard_dev.buffer[keyboard_dev.tail] = ch; keyboard_dev.tail = (keyboard_dev.tail + 1) % KB_BUFFER_SIZE; keyboard_dev.count++;
/* 唤醒等待的进程 */ sem_signal(&kb_sem); }
spinlock_release(&kb_lock); }}扫描码转换:
键盘发送的是扫描码(scancode),需要转换为ASCII字符。驱动使用两张查找表,分别对应普通模式和Shift模式:
/* 普通模式扫描码表 */static const char scancode_table[] = { 0, 0, '1', '2', '3', '4', '5', '6', /* 0x00-0x07 */ '7', '8', '9', '0', '-', '=', '\b', '\t', /* 0x08-0x0F */ 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', /* 0x10-0x17 */ 'o', 'p', '[', ']', '\n', 0, 'a', 's', /* 0x18-0x1F */ 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', /* 0x20-0x27 */ '\'', '`', 0, '\\', 'z', 'x', 'c', 'v', /* 0x28-0x2F */ 'b', 'n', 'm', ',', '.', '/', 0, '*', /* 0x30-0x37 */ 0, ' ', 0, 0, 0, 0, 0, 0, /* 0x38-0x3F */};
/* 扫描码转ASCII */char scancode_to_ascii(uint8_t scancode){ /* 处理释放码 */ if (scancode & 0x80) { return 0; }
/* 根据Shift状态选择不同的转换表 */ if (keyboard_dev.state.shift) { if (scancode < sizeof(scancode_table_shift)) { char c = scancode_table_shift[scancode]; /* Caps Lock处理:大小写切换 */ if (keyboard_dev.state.caps_lock && c >= 'A' && c <= 'Z') { c += 32; /* 转为小写 */ } else if (keyboard_dev.state.caps_lock && c >= 'a' && c <= 'z') { c -= 32; /* 转为大写 */ } return c; } } else { if (scancode < sizeof(scancode_table)) { char c = scancode_table[scancode]; if (keyboard_dev.state.caps_lock && c >= 'a' && c <= 'z') { c -= 32; /* 转为大写 */ } return c; } }
return 0;}阻塞式读取:
键盘读取使用信号量实现阻塞式I/O:
/* 键盘设备读取(阻塞) */static ssize_t keyboard_read(device_t *dev, void *buf, size_t count, off_t offset){ (void)offset; keyboard_device_t *kb = (keyboard_device_t *)dev; char *p = (char *)buf; size_t read = 0;
while (read < count) { /* 阻塞等待:ISR产生数据后signal */ sem_wait(&kb_sem);
spinlock_acquire(&kb_lock); if (kb->count > 0) { p[read++] = kb->buffer[kb->head]; kb->head = (kb->head + 1) % KB_BUFFER_SIZE; kb->count--; } spinlock_release(&kb_lock); }
return (ssize_t)read;}键盘驱动解决了输入问题,但数据重启就消失。要持久化数据,需要块设备驱动。块设备按固定大小的块访问(通常 512 字节或 4KB),支持随机访问,使用请求队列和缓存,适合存储设备。
块设备驱动:ATA 硬盘
ATA(Advanced Technology Attachment)是常见的硬盘接口标准。驱动程序通过端口I/O与硬盘控制器通信。
ATA寄存器布局:
/* ATA控制器寄存器(主控制器,I/O基地址0x1F0) */#define ATA_REG_DATA 0x00 /* 数据寄存器(16位) */#define ATA_REG_ERROR 0x01 /* 错误寄存器 */#define ATA_REG_SECCOUNT 0x02 /* 扇区计数 */#define ATA_REG_LBA0 0x03 /* LBA低字节 */#define ATA_REG_LBA1 0x04 /* LBA中字节 */#define ATA_REG_LBA2 0x05 /* LBA高字节 */#define ATA_REG_HDDEVSEL 0x06 /* 驱动器/磁头选择 */#define ATA_REG_COMMAND 0x07 /* 命令寄存器 */#define ATA_REG_STATUS 0x07 /* 状态寄存器(与命令寄存器共用) */
/* 控制器基址(主控制器) */#define ATA_PRIMARY_IO 0x1F0#define ATA_PRIMARY_CTRL 0x3F6状态寄存器:
#define ATA_SR_BSY 0x80 /* 忙:设备正在执行命令 */#define ATA_SR_DRDY 0x40 /* 设备就绪 */#define ATA_SR_DF 0x20 /* 设备故障 */#define ATA_SR_DSC 0x10 /* 介质改变 */#define ATA_SR_DRQ 0x08 /* 数据请求:数据可读写 */#define ATA_SR_CORR 0x04 /* 校正数据 */#define ATA_SR_IDX 0x02 /* 索引 */#define ATA_SR_ERR 0x01 /* 错误 */设备检测与识别:
/* 检测ATA设备 */int ata_detect(uint16_t io_base, int is_master, ata_device_t *dev){ uint8_t status; uint16_t identify[256];
/* 初始化结构 */ memset(dev, 0, sizeof(ata_device_t)); dev->io_base = io_base; dev->is_master = is_master;
/* 选择驱动器 */ ata_write_reg(io_base, ATA_REG_HDDEVSEL, ATA_LBA_MODE | (is_master ? 0 : 0x10) | 0xA0);
/* 等待就绪 */ for (volatile int i = 0; i < 10000; i++);
/* 检查状态 */ status = ata_read_reg(io_base, ATA_REG_STATUS); if (status == 0xFF || status == 0x00) { return -1; /* 设备不存在 */ }
/* 发送IDENTIFY命令 */ ata_write_reg(io_base, ATA_REG_COMMAND, ATA_CMD_IDENTIFY);
/* 等待数据就绪 */ if (ata_wait_ready(io_base, 1) != 0) { return -1; }
/* 读取IDENTIFY数据(256个字) */ for (int i = 0; i < 256; i++) { identify[i] = inw(io_base + ATA_REG_DATA); }
/* 解析设备信息 */ dev->lba_count = (uint64_t)identify[60] | ((uint64_t)identify[61] << 16);
/* 检查LBA48支持 */ if (identify[83] & 0x400) { dev->lba48_supported = 1; dev->lba_count = (uint64_t)identify[100] | ((uint64_t)identify[101] << 16) | ((uint64_t)identify[102] << 32) | ((uint64_t)identify[103] << 48); }
/* 获取设备型号 */ for (int i = 0; i < 40; i += 2) { dev->model[i] = identify[27 + i/2] >> 8; dev->model[i + 1] = identify[27 + i/2] & 0xFF; } dev->model[40] = '\0';
return 0;}读取扇区流程:
扇区读取实现:
/* 读取扇区 */int ata_read_sector(ata_device_t *dev, uint32_t lba, void *buf){ if (dev == NULL || buf == NULL) { return -1; }
spinlock_acquire(&ata_lock);
/* 1. 选择驱动器并设置LBA */ ata_write_reg(dev->io_base, ATA_REG_HDDEVSEL, ATA_LBA_MODE | (dev->is_master ? 0 : 0x10) | ((lba >> 24) & 0x0F));
/* 2. 等待设备就绪 */ if (ata_wait_ready(dev->io_base, 0) != 0) { spinlock_release(&ata_lock); return -1; }
/* 3. 设置LBA地址和扇区数 */ ata_write_reg(dev->io_base, ATA_REG_SECCOUNT, 1); ata_write_reg(dev->io_base, ATA_REG_LBA0, lba & 0xFF); ata_write_reg(dev->io_base, ATA_REG_LBA1, (lba >> 8) & 0xFF); ata_write_reg(dev->io_base, ATA_REG_LBA2, (lba >> 16) & 0xFF);
/* 4. 发送读取命令 */ ata_write_reg(dev->io_base, ATA_REG_COMMAND, ATA_CMD_READ_PIO);
/* 5. 等待数据就绪 */ if (ata_wait_ready(dev->io_base, 1) != 0) { spinlock_release(&ata_lock); return -1; }
/* 6. 读取数据(256个16位字 = 512字节) */ uint16_t *data = (uint16_t *)buf; for (int i = 0; i < 256; i++) { data[i] = inw(dev->io_base + ATA_REG_DATA); }
spinlock_release(&ata_lock); return 0;}等待设备就绪:
/* 等待设备就绪(带超时) */static int ata_wait_ready(uint16_t io_base, int check_drq){ uint32_t start = getTick(); uint8_t status;
while (1) { status = ata_read_reg(io_base, ATA_REG_STATUS);
/* 检查错误 */ if (status & ATA_SR_ERR) { return -1; }
/* 检查设备故障 */ if (status & ATA_SR_DF) { return -2; }
/* 检查就绪 */ if ((status & ATA_SR_BSY) == 0) { if (check_drq) { if (status & ATA_SR_DRQ) { return 0; /* 数据可读写 */ } } else { if (status & ATA_SR_DRDY) { return 0; /* 设备就绪 */ } } }
/* 超时检查 */ if ((getTick() - start) * 10 > ATA_TIMEOUT) { return -3; } }}有了键盘和硬盘两种驱动,还需要一个统一的框架来管理它们。
设备注册框架
内核维护一个全局设备表,所有注册的驱动程序都会在其中登记:
#define MAX_DEVICES 32
static device_t *device_table[MAX_DEVICES];static int device_count = 0;static spinlock_t device_lock;
/* 注册设备 */int register_device(device_t *dev){ if (dev == NULL) { return -1; }
spinlock_acquire(&device_lock);
/* 查找空槽 */ for (int i = 0; i < MAX_DEVICES; i++) { if (device_table[i] == NULL) { device_table[i] = dev; dev->refcount = 0; device_count++; spinlock_release(&device_lock);
vga_printf("[Device] Registered: %s (major=%d, minor=%d)\n", dev->name, dev->major, dev->minor); return i; } }
spinlock_release(&device_lock); vga_printf("[Device] ERROR: Device table full!\n"); return -1;}
/* 按设备号查找设备 */device_t *find_device(int major, int minor){ spinlock_acquire(&device_lock);
for (int i = 0; i < MAX_DEVICES; i++) { device_t *dev = device_table[i]; if (dev != NULL && dev->major == major && dev->minor == minor) { spinlock_release(&device_lock); return dev; } }
spinlock_release(&device_lock); return NULL;}设备初始化流程:
代码实现
文件结构
13.kernel-drivers/├── boot/│ ├── mbr.S # MBR引导代码│ └── loader.S # 加载器├── kernel/│ ├── include/│ │ ├── device.h # 设备抽象接口│ │ ├── keyboard.h # 键盘驱动接口│ │ └── ata.h # ATA驱动接口│ ├── drivers/│ │ ├── device.c # 设备框架实现│ │ ├── keyboard.c # 键盘驱动实现│ │ └── ata.c # ATA驱动实现│ └── kernel.c # 内核主函数└── Makefile # 编译配置键盘和 ATA 驱动各自维护私有数据结构:
/* 键盘设备结构 */typedef struct { device_t dev; /* 基础设备结构 */ uint8_t buffer[KB_BUFFER_SIZE]; /* 环形缓冲区 */ uint32_t head; /* 读指针 */ uint32_t tail; /* 写指针 */ uint32_t count; /* 缓冲区数据量 */
struct { int shift; /* Shift键状态 */ int caps_lock; /* Caps Lock状态 */ int ctrl; /* Ctrl键状态 */ int alt; /* Alt键状态 */ } state;} keyboard_device_t;
/* ATA设备结构 */typedef struct { block_device_t bdev; /* 块设备结构 */ uint16_t io_base; /* I/O基地址 */ uint16_t ctrl_base; /* 控制器基址 */ int is_master; /* 是否为主设备 */
uint64_t lba_count; /* LBA扇区总数 */ int lba48_supported; /* 是否支持LBA48 */ char model[41]; /* 设备型号 */} ata_device_t;键盘中断处理流程:
ATA 读取扇区流程:
环形缓冲区实现
/* 键盘缓冲区 */#define KB_BUFFER_SIZE 128
/* 中断处理程序中写入 */if (keyboard_dev.count < KB_BUFFER_SIZE) { keyboard_dev.buffer[keyboard_dev.tail] = ch; keyboard_dev.tail = (keyboard_dev.tail + 1) % KB_BUFFER_SIZE; keyboard_dev.count++; sem_signal(&kb_sem);}
/* 读取函数中读取 */if (kb->count > 0) { p[read++] = kb->buffer[kb->head]; kb->head = (kb->head + 1) % KB_BUFFER_SIZE; kb->count--;}解析:环形缓冲区在中断处理程序(生产者)和读取进程(消费者)之间传递数据。head 和 tail 指针通过取模运算实现循环,避免缓冲区溢出。
状态轮询等待
static int ata_wait_ready(uint16_t io_base, int check_drq){ uint32_t start = getTick(); uint8_t status;
while (1) { status = ata_read_reg(io_base, ATA_REG_STATUS);
if (status & ATA_SR_ERR) { return -1; /* 错误 */ }
if ((status & ATA_SR_BSY) == 0) { if (check_drq) { if (status & ATA_SR_DRQ) { return 0; /* 数据就绪 */ } } else { if (status & ATA_SR_DRDY) { return 0; /* 设备就绪 */ } } }
/* 超时检查 */ if ((getTick() - start) * 10 > ATA_TIMEOUT) { return -3; } }}解析:轮询 ATA 状态寄存器等待设备就绪,使用超时机制避免死锁,检查多种状态标志确保操作正确性。
自旋锁保护
/* 设备注册时的锁保护 */int register_device(device_t *dev){ spinlock_acquire(&device_lock);
for (int i = 0; i < MAX_DEVICES; i++) { if (device_table[i] == NULL) { device_table[i] = dev; /* ... */ spinlock_release(&device_lock); return i; } }
spinlock_release(&device_lock); return -1;}解析:在多 CPU 环境下,设备表访问需要用自旋锁保护,防止竞态条件。自旋锁适用于短临界区,不适合可能导致阻塞的操作。
运行与验证
编译运行
cd 13.kernel-driversmake cleanmake allmake run预期输出
[Device] Framework initialized (max 32 devices)[Keyboard] Driver initialized (IRQ1)[Device] Registered: keyboard (major=1, minor=0)[ATA] Detecting IDE devices... Primary Master: Found Model: QEMU HARDDISK Sectors: 1048576 (512 MB) LBA48: Yes[Device] Registered: hda (major=3, minor=0) Primary Slave: Not found[ATA] Initialization complete测试键盘驱动
在QEMU中按键,应该能在VGA上看到字符输出。驱动会显示按键的扫描码和对应的ASCII字符:
Key pressed: 'a' (scancode=0x1E)Key pressed: 'B' (scancode=0x30, shift)Key pressed: 'C' (scancode=0x2E, caps_lock)测试ATA驱动
驱动会自动检测并识别硬盘,读取MBR(主引导记录)验证磁盘访问:
/* 测试代码:读取MBR */uint8_t mbr[512];block_read(&primary_master.bdev, 0, mbr);
if (mbr[510] == 0x55 && mbr[511] == 0xAA) { vga_printf("MBR signature: 0x55AA (valid)\n");} else { vga_printf("MBR signature: invalid\n");}踩坑记录
1. 键盘无响应
原因:
- 键盘中断(IRQ1)未在IDT中注册
- 中断处理程序未正确发送EOI
- PS/2端口未正确初始化
解决方案:
/* 确保注册中断处理程序 */register_interrupt_handler(INT_IRQ1, keyboard_isr);
/* 检查EOI是否发送 */void keyboard_isr(interrupt_frame_t *frame) { /* ... 处理代码 ... */ outb(PIC1_CMD, PIC_EOI); /* 发送EOI */}2. 硬盘读取失败
原因:
- 等待设备就绪超时
- LBA地址设置错误
- 设备不支持LBA48但尝试使用
解决方案:
/* 增加超时时间 */#define ATA_TIMEOUT 5000 /* 从1000ms增加到5000ms */
/* 检查LBA48支持后再使用 */if (dev->lba48_supported && lba > 0x0FFFFFFF) { /* 使用LBA48模式 */} else { /* 使用LBA28模式 */}3. 数据损坏或缓冲区溢出
原因:
- 环形缓冲区未正确处理满的情况
- 多线程访问未加锁保护
- 读取时缓冲区被覆盖
解决方案:
/* 在写入前检查缓冲区是否满 */if (keyboard_dev.count < KB_BUFFER_SIZE) { /* 安全写入 */} else { vga_printf("[Keyboard] Buffer overflow!\n");}
/* 使用自旋锁保护缓冲区访问 */spinlock_acquire(&kb_lock);/* 读写操作 */spinlock_release(&kb_lock);小结
本章从设备驱动的动机出发,构建了统一的设备抽象层:device_t 结构通过函数指针屏蔽字符设备和块设备的差异,主/次设备号提供唯一标识。键盘驱动展示了中断驱动的 I/O 模型,扫描码到 ASCII 的转换,以及环形缓冲区加信号量的阻塞读取;ATA 硬盘驱动展示了端口 I/O、LBA 寻址、状态轮询和扇区读写;设备注册框架则将两者纳入全局设备表统一管理。有了键盘驱动,内核能接收输入;有了硬盘驱动,内核能读写持久化数据。但按扇区读写硬盘仍然过于原始,下一章将在此基础上构建文件系统,让数据以文件和目录的形式组织起来。
参考
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






