设备驱动程序的生命周期😮💨
设备驱动程序的完整工作流程
第零步:驱动的初始化与注册 (在系统启动或模块加载时)
在任何I/O操作发生前,驱动程序必须先让内核“认识”它。
-
加载与初始化: 系统启动时或管理员加载模块时,内核会运行驱动程序的
init()
初始化函数。 -
探测硬件: 驱动程序会探测总线,检查它所负责的硬件设备是否存在且工作正常。
-
注册接口: 这是最关键的一步。驱动程序向内核注册一个包含一系列函数指针的结构体(在Linux中称为
file_operations
)。-
作用: 这相当于驱动程序向内核立下字据:“尊敬的内核,我是硬盘驱动。以后只要有针对硬盘的
open
,read
,write
,ioctl
等操作请求,请调用我这里的my_disk_open
,my_disk_read
,my_disk_write
等函数地址。” -
从此,内核就知道该如何将上层的通用I/O请求转发给这个具体的驱动程序了。
-
以下流程是一个具体的read
请求到达后的生命周期
第一步:接收上层请求(“Top Half”的开始)
-
一个进程发起
read
系统调用,经过设备无关的I/O层处理后,内核确定需要从磁盘读取某个物理块。 -
设备无关层会调用在第零步中注册的函数指针,即调用磁盘驱动程序的
my_disk_read()
函数,并将请求的块号、目标内存缓冲区地址等信息作为参数传入。 -
驱动程序代码开始执行。 这部分由系统调用直接触发,在进程的上下文中运行,我们称之为驱动的**“上半部 (Top Half)”**。
第二步:参数校验与设备状态检查
-
驱动程序首先会检查上层传来的参数是否合法。例如,请求的块号是否超出了磁盘的容量范围?
-
接着,检查硬件设备当前是否正忙于处理上一个请求。如果设备忙,新的请求可能会被放入一个请求队列中等待。如果设备空闲,则继续。
第三步:对硬件编程(核心翻译工作)
这是驱动程序的核心价值所在。它需要将上层“读逻辑块X”这样抽象的命令,翻译成磁盘控制器能听懂的“电子语言”。
-
准备DMA传输: 驱动程序向内核申请一块用于DMA(直接内存访问)的内存缓冲区,并获取其物理地址。
-
设置控制器寄存器: 驱动程序通过特定的I/O端口地址,向磁盘控制器的硬件寄存器中写入一系列值,例如:
-
目标内存地址寄存器: 写入DMA缓冲区的物理地址。
-
扇区计数寄存器: 写入要读取的扇区数量(例如一个块包含8个扇区,就写入8)。
-
LBA寄存器: 写入要读取的逻辑块地址(LBA)。
-
命令寄存器: 最后,向命令寄存器写入**“开始读”**的命令码。
-
第四步:阻塞当前进程
-
向命令寄存器写入命令后,硬件就开始了漫长的物理操作(寻道、旋转、读取)。这个过程可能耗时数毫秒,CPU不能在此空等。
-
驱动程序会调用内核的调度器函数,将发起此次I/O请求的进程状态设置为**“阻塞(Blocked)”或“等待(Waiting)”**,并将其放入等待队列。
-
然后,驱动程序的
my_disk_read()
函数返回,将CPU控制权交还给调度器。调度器会选择另一个处于“就绪”状态的进程投入运行。
(至此,上半部工作完成,CPU已转去处理其他任务,静待硬件佳音)
第五步:处理硬件中断(“Bottom Half”的开始)
-
磁盘硬件完成了数据读取,并通过DMA控制器将数据成功写入到了第三步中指定的内存DMA缓冲区。
-
为了通知任务完成,磁盘控制器会向CPU发送一个中断信号。
-
CPU无论正在做什么,都会立即暂停,保存当前现场,然后根据中断向量表,跳转到该中断对应的中断服务例程(ISR)——这正是磁盘驱动程序预先注册好的中断处理函数。
-
这部分在中断上下文中运行的代码,我们称之为驱动的**“下半部 (Bottom Half)”**。
第六步:中断善后与唤醒进程
中断处理程序(下半部)必须执行得非常快。它通常会做以下几件事:
-
读取状态: 从磁盘控制器的状态寄存器中读取I/O操作的结果,检查是否发生了错误。
-
数据处理: 如果有错误,则记录错误信息。如果成功,则通知上层数据已准备好。
-
唤醒进程: 调用内核函数,将在第四步中被阻塞的那个进程从等待队列中移出,将其状态改为**“就绪(Ready)”**,并放入就绪队列,等待调度器再次垂青。
-
返回: 从中断处理程序返回。CPU恢复之前被中断的现场,继续执行。
第七步:请求完成与返回用户
-
当调度器再次选择运行我们那个刚刚被唤醒的进程时,它的执行会从当初
my_disk_read()
函数中被阻塞的地方继续。 -
此时,驱动程序代码知道I/O已经完成,它会进行一些清理工作。
-
数据此时已经位于内核的DMA缓冲区中,设备无关层会负责将其从内核缓冲区复制到用户最初指定的缓冲区
buf
中。 -
最后,整个系统调用完成,返回到用户态,用户程序从
read()
函数调用处继续执行,并在buf
中得到了它想要的数据。