设备驱动程序的生命周期😮‍💨

设备驱动程序的完整工作流程

第零步:驱动的初始化与注册 (在系统启动或模块加载时)

在任何I/O操作发生前,驱动程序必须先让内核“认识”它。

  1. 加载与初始化: 系统启动时或管理员加载模块时,内核会运行驱动程序的init()初始化函数。

  2. 探测硬件: 驱动程序会探测总线,检查它所负责的硬件设备是否存在且工作正常。

  3. 注册接口: 这是最关键的一步。驱动程序向内核注册一个包含一系列函数指针的结构体(在Linux中称为file_operations)。

    • 作用: 这相当于驱动程序向内核立下字据:“尊敬的内核,我是硬盘驱动。以后只要有针对硬盘的open, read, write, ioctl等操作请求,请调用我这里的my_disk_open, my_disk_read, my_disk_write等函数地址。”

    • 从此,内核就知道该如何将上层的通用I/O请求转发给这个具体的驱动程序了。


以下流程是一个具体的read请求到达后的生命周期

第一步:接收上层请求(“Top Half”的开始)

  1. 一个进程发起read系统调用,经过设备无关的I/O层处理后,内核确定需要从磁盘读取某个物理块。

  2. 设备无关层会调用在第零步中注册的函数指针,即调用磁盘驱动程序的my_disk_read()函数,并将请求的块号、目标内存缓冲区地址等信息作为参数传入。

  3. 驱动程序代码开始执行。 这部分由系统调用直接触发,在进程的上下文中运行,我们称之为驱动的**“上半部 (Top Half)”**。

第二步:参数校验与设备状态检查

  1. 驱动程序首先会检查上层传来的参数是否合法。例如,请求的块号是否超出了磁盘的容量范围?

  2. 接着,检查硬件设备当前是否正忙于处理上一个请求。如果设备忙,新的请求可能会被放入一个请求队列中等待。如果设备空闲,则继续。

第三步:对硬件编程(核心翻译工作)

这是驱动程序的核心价值所在。它需要将上层“读逻辑块X”这样抽象的命令,翻译成磁盘控制器能听懂的“电子语言”。

  1. 准备DMA传输: 驱动程序向内核申请一块用于DMA(直接内存访问)的内存缓冲区,并获取其物理地址。

  2. 设置控制器寄存器: 驱动程序通过特定的I/O端口地址,向磁盘控制器的硬件寄存器中写入一系列值,例如:

    • 目标内存地址寄存器: 写入DMA缓冲区的物理地址。

    • 扇区计数寄存器: 写入要读取的扇区数量(例如一个块包含8个扇区,就写入8)。

    • LBA寄存器: 写入要读取的逻辑块地址(LBA)。

    • 命令寄存器: 最后,向命令寄存器写入**“开始读”**的命令码。

第四步:阻塞当前进程

(至此,上半部工作完成,CPU已转去处理其他任务,静待硬件佳音)


第五步:处理硬件中断(“Bottom Half”的开始)

  1. 磁盘硬件完成了数据读取,并通过DMA控制器将数据成功写入到了第三步中指定的内存DMA缓冲区。

  2. 为了通知任务完成,磁盘控制器会向CPU发送一个中断信号

  3. CPU无论正在做什么,都会立即暂停,保存当前现场,然后根据中断向量表,跳转到该中断对应的中断服务例程(ISR)——这正是磁盘驱动程序预先注册好的中断处理函数。

  4. 这部分在中断上下文中运行的代码,我们称之为驱动的**“下半部 (Bottom Half)”**。

第六步:中断善后与唤醒进程

中断处理程序(下半部)必须执行得非常快。它通常会做以下几件事:

  1. 读取状态: 从磁盘控制器的状态寄存器中读取I/O操作的结果,检查是否发生了错误。

  2. 数据处理: 如果有错误,则记录错误信息。如果成功,则通知上层数据已准备好。

  3. 唤醒进程: 调用内核函数,将在第四步中被阻塞的那个进程从等待队列中移出,将其状态改为**“就绪(Ready)”**,并放入就绪队列,等待调度器再次垂青。

  4. 返回: 从中断处理程序返回。CPU恢复之前被中断的现场,继续执行。

第七步:请求完成与返回用户