4. 块设备驱动框架

块设备与字符设备最本质的区别在于:字符设备以字节流为单位顺序访问,而块设备以“块”为最小传输单位,并且内核会在驱动之上叠加一层缓冲区管理与I/O调度机制。在VxWorks中,块设备驱动并不是直接暴露给应用层,而是通过块设备抽象层(dosFs、cbio、bio)与文件系统对接。理解这一框架,关键在于弄清四个环节:设备如何注册进系统、缓冲区如何被管理、I/O请求如何被调度、驱动接口如何被实现。

一、块设备注册:从设备节点到驱动入口

VxWorks中块设备注册的核心是iosDevAdd()与设备描述结构BLK_DEV。驱动开发者需要先填充一个BLK_DEV结构,其中包含块大小、总块数、读写函数指针、I/O控制函数指针等字段,然后调用iosDevAdd()将其挂入I/O系统设备表。注册完成后,设备名(如/bd0)即可被dosFsDevInit()或cbioDevInit()识别,进而挂载文件系统。

注册时最容易被忽略的是块大小与设备容量的匹配。例如一个NAND Flash分区,物理页为2048字节,若驱动层声明块大小为512字节,则上层文件系统会按512字节发起请求,驱动内部必须自行完成“512字节逻辑块→2048字节物理页”的映射与拼装。注册参数一旦确定,后续所有I/O都以此为基础,因此必须在BLK_DEV初始化阶段就明确。

典型注册代码骨架(示例参数,非实测):
BLK_DEV myBlkDev;
myBlkDev.bd_blkSize   = 512;      /* 逻辑块大小 */
myBlkDev.bd_blkCount  = 204800;   /* 总块数,约100MB */
myBlkDev.bd_read      = myBlkRead;
myBlkDev.bd_write     = myBlkWrite;
myBlkDev.bd_ioctl     = myBlkIoctl;
iosDevAdd(&myBlkDev, "/bd0", 0);

二、缓冲区管理:bio与cbio的两级结构

VxWorks块设备缓冲管理分为两层:cbio(cached block I/O)提供带缓存的块访问,bio(block I/O)提供无缓存的直接块访问。cbio在内存中维护一个缓冲区池,每个缓冲区对应一个逻辑块,并带有“脏位”和“有效位”。当上层发起读请求时,cbio先查缓存,命中则直接返回;未命中则通过bio向驱动发起实际读操作,并将结果填入缓存。

写操作则采用写回(write-back)策略:数据先写入缓存并置脏位,真正落盘由cbioFlush()或缓存替换时触发。这种设计显著减少了对慢速存储介质的访问次数,但也带来一致性风险——掉电时未刷新的脏缓冲区会丢失。因此驱动在实现bd_write时,必须保证单次写操作的原子性,或在ioctl中提供flush语义。

VxWorks块设备驱动框架与I/O数据流 应用层 / 文件系统(dosFs、rawFs) cbio 缓存块I/O层 缓冲区池 脏位/有效位 命中/未命中判断 写回/刷新策略 bio 块I/O层(无缓存直接访问) 将逻辑块请求转换为驱动可识别的块操作 块设备驱动(BLK_DEV) bd_read() bd_write() bd_ioctl() 中断/DMA 物理存储介质(NAND Flash / eMMC / SATA / SD卡)

三、I/O调度:请求合并与顺序化

块设备驱动面对的I/O请求往往不是顺序到达的。文件系统可能同时发起多个分散的读写请求,如果驱动逐个按到达顺序执行,机械寻道或Flash擦写效率会急剧下降。VxWorks的块设备框架允许在bio层实现简单的请求队列与合并:将相邻逻辑块的请求合并为一次较大的传输,或将多个写请求按块号排序后批量提交。

对于NAND Flash这类介质,调度策略还需考虑擦除块边界。例如一个擦除块为128KB(64页×2KB),若写请求跨越了擦除块边界,驱动必须拆分为两次“读-改-写”操作。因此调度器在合并请求时,应以擦除块为上限,避免跨块合并导致额外的擦除开销。实际工程中,常在bd_write入口处维护一个待写请求链表,按块号排序后逐块处理,并在块边界处插入擦除判断。

四、块设备驱动接口实现要点

实现bd_read与bd_write时,函数原型通常为:

STATUS myBlkRead(BLK_DEV *pDev, UINT32 blkNum, UINT32 numBlks, char *pBuf);
STATUS myBlkWrite(BLK_DEV *pDev, UINT32 blkNum, UINT32 numBlks, char *pBuf);

其中blkNum是起始逻辑块号,numBlks是连续块数,pBuf是数据缓冲区。驱动内部需要完成三件事:地址转换(逻辑块号→物理页/扇区地址)、数据搬运(通过DMA或PIO将数据写入控制器FIFO)、状态等待(轮询或中断等待操作完成)。

以NAND Flash为例,一次写操作的实际流程是:计算物理页号→发送写命令→写入列地址与行地址→搬运数据到页寄存器→发送编程命令→等待R/B引脚变高→读取状态寄存器判断是否成功。若失败,还需执行块替换或标记坏块。这些细节都封装在bd_write内部,上层文件系统无需感知。

常见陷阱:在bd_read/bd_write中直接调用可能引起阻塞的taskDelay()或信号量等待,会导致I/O系统在中断上下文或高优先级任务中挂起。正确做法是使用轮询加超时,或将耗时操作放入独立的任务/工作队列中异步完成。

五、注册、缓冲、调度、接口的协同关系

这四个环节并非孤立:注册时声明的块大小决定了缓冲区管理的粒度;缓冲区管理的写回策略决定了调度器看到的请求模式;调度器的合并与排序又直接影响驱动接口中地址转换与数据搬运的效率。一个常见的优化是:在cbio层将多个小写请求累积到一定阈值后再触发一次bd_write,从而减少驱动层实际编程次数。对于写密集型场景,还可以在ioctl中实现FIOSYNC命令,强制刷新所有脏缓冲区,保证数据一致性。

实际调试时,可以通过iosDevShow()查看设备注册状态,通过cbioShow()观察缓冲区命中率与脏块数量,通过bioShow()查看当前挂起的块请求。若发现命中率长期偏低,说明缓冲区池过小或访问模式过于随机;若脏块数量持续增长而不下降,则可能是flush策略未生效或驱动写操作返回了错误状态。这些观察点构成了块设备驱动调试的基本闭环。