当内核尝试从硬盘驱动器读取块时,它会发送一个软件中断,该中断将由设备驱动程序处理。如果设备驱动程序通过工作队列将处理请求的工作分成上半部分和下半部分,那么内核如何知道在下半部分完成之前数据不可用?
换句话说,内核如何知道驱动程序尚未获取所需的块并复制到提供的缓冲区中?
显然,如果内核期望数据在上半部分完成执行并返回后随时可用,那么它可能会读取垃圾数据。
当内核尝试从硬盘驱动器读取块时,它会发送一个软件中断,该中断将由设备驱动程序处理。如果设备驱动程序通过工作队列将处理请求的工作分成上半部分和下半部分,那么内核如何知道在下半部分完成之前数据不可用?
换句话说,内核如何知道驱动程序尚未获取所需的块并复制到提供的缓冲区中?
显然,如果内核期望数据在上半部分完成执行并返回后随时可用,那么它可能会读取垃圾数据。
自 Linux 诞生以来,块设备驱动程序 API 已经改变了几次,但今天它基本上看起来像下面这样。
初始化函数调用blk_init_queue
,传递请求回调和该队列的可选锁:
struct request_queue *q;
q = blk_init_queue(my_request_cb, &my_dev->lock);
my_request_cb
是一个回调,它将处理该块设备的所有 I/O。当内核块驱动层决定时, I/O 请求将被推入此队列并被my_request_cb
调用以一个接一个地处理它们。然后将此队列添加到磁盘:
struct gendisk *disk;
disk->queue = q;
然后将磁盘添加到系统中:
add_disk(disk);
具有其他信息,disk
如主编号、第一个次编号和其他文件操作(open
、release
等ioctl
,但在字符设备中没有read
和没有类似)。write
现在,my_request_cb
可以随时调用,不一定会从在块设备上启动读/写的进程的上下文中调用。这个调用是由内核异步的。
这个函数是这样声明的:
static void my_request_cb(struct request_queue *q);
该队列q
包含对该块设备的请求的有序列表。然后该函数可以查看下一个请求 ( blk_fetch_request(q)
)。要将请求标记为已完成,它将调用blk_end_request_all
(存在其他变体,视情况而定)。
这就是我回答您的问题的地方:内核知道特定块设备请求在其驱动程序调用blk_end_request_all
或针对该请求的类似函数时完成。驱动程序不必在 内结束请求my_request_cb
:例如,它可以启动 DMA 传输、重新排队请求、忽略其他请求,并且只有在断言完成 DMA 传输的中断时才结束它,有效地告诉内核这个特定的读/写操作就完成了。
LDD3/第 16 章可以提供帮助,但自 2.6 以来有些事情发生了变化。