驱动串行化IO的理解
一、对于IoStartParket的理解:
VOID NTAPI IoStartPacket(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp,
IN PULONG Key, IN PDRIVER_CANCEL CancelFunction)
{
BOOLEAN Stat;
KIRQL OldIrql, CancelIrql;
/* Raise to dispatch level */
KeRaiseIrql(DISPATCH_LEVEL, &OldIrql);
/* Check if we should acquire the cancel lock */
if (CancelFunction)
{ //如果给定了一个“取消函数”
/* Acquire and set it */
IoAcquireCancelSpinLock(&CancelIrql);
Irp->CancelRoutine = CancelFunction;
}
/* Check if we have a key */
if (Key)
{ //对于加了锁的文件片段
/* Insert by key */
Stat = KeInsertByKeyDeviceQueue(&DeviceObject->DeviceQueue,
&Irp->Tail.Overlay.DeviceQueueEntry, *Key);
}
else
{ //对于一般的文件或设备
/* Insert without a key */
Stat = KeInsertDeviceQueue(&DeviceObject->DeviceQueue,
&Irp->Tail.Overlay.DeviceQueueEntry);
}
/* Check if this was a first insert */
if (!Stat)
{ //这是针对目标设备对象的第一个IRP,可以启动
/* Set the IRP */
DeviceObject->CurrentIrp = Irp;
/* Check if this is a cancelable packet */
if (CancelFunction)
{
/* Check if the caller requested no cancellation */
if (IoGetDevObjExtension(DeviceObject)->StartIoFlags &
DOE_SIO_NO_CANCEL)
{
/* He did, so remove the cancel routine */
Irp->CancelRoutine = NULL;
}
/* Release the cancel lock */
IoReleaseCancelSpinLock(OldIrql);
}
/* Call the Start I/O function */ //调用该设备对象所属驱动的I/O启动函数
DeviceObject->DriverObject->DriverStartIo(DeviceObject, Irp);
}
else
{ //目标设备对象的I/O操作已被启动但尚未完成,这是后续到达的IRP
/* The packet was inserted... check if we have a cancel function */
if (CancelFunction)
{ //如果给定了取消函数
/* Check if the IRP got cancelled */
if (Irp->Cancel)
{ //而且要取消
/* Set the cancel IRQL, clear the currnet cancel routine and call ours */
Irp->CancelIrql = CancelIrql;
Irp->CancelRoutine = NULL;
CancelFunction(DeviceObject, Irp); //调用给定的取消函数
}
else
{ //不取消
/* Otherwise, release the lock */
IoReleaseCancelSpinLock(CancelIrql);
}
}
}
/* Return back to previous IRQL */
KeLowerIrql(OldIrql);
}
BOOLEAN NTAPI KeInsertDeviceQueue(IN PKDEVICE_QUEUE DeviceQueue,
IN PKDEVICE_QUEUE_ENTRY DeviceQueueEntry)
{
KLOCK_QUEUE_HANDLE DeviceLock;
BOOLEAN Inserted;
ASSERT_DEVICE_QUEUE(DeviceQueue);
/* Lock the queue */
KiAcquireDeviceQueueLock(DeviceQueue, &DeviceLock);
/* Check if it''''''''s not busy */
if (!DeviceQueue->Busy)
{ //目标设备对象空闲,不需要挂入队列,可以直接执行启动函数
/* Set it as busy */
Inserted = FALSE;
DeviceQueue->Busy = TRUE; //但是现在目标设备对象已经不再空闲
}
else
{ //目标设备对象已经执行过启动函数但尚未完成,需要挂入队列
/* Insert it into the list */
Inserted = TRUE;
InsertTailList(&DeviceQueue->DeviceListHead,
&DeviceQueueEntry->DeviceListEntry);
}
/* Set the Insert state into the entry */
DeviceQueueEntry->Inserted = Inserted;
/* Release the lock */
KiReleaseDeviceQueueLock(&DeviceLock);
/* Return the state */
return Inserted; //返回TRUE表示已排入队列,FALSE表示可以继续操作
}
VOID OnCancel(PDEVICE_OBJECT fdo, PIRP Irp)
{
if (fdo->CurrentIrp == Irp)
{
KIRQL oldirql = Irp->CancelIrql;
IoReleaseCancelSpinLock(DISPATCH_LEVEL);
IoStartNextPacket(fdo, TRUE);
KeLowerIrql(oldirql);
}
else
{
KeRemoveEntryDeviceQueue(&fdo->DeviceQueue, &Irp->Tail.Overlay.DeviceQueueEntry);
IoReleaseCancelSpinLock(Irp->CancelIrql);
}
CompleteRequest(Irp, STATUS_CANCELLED, 0);
}
(一)先看下以下伪代码:
应用端:
AThread
{
ReadFile(...);
}
BThread
{
ReadFile(...);
}
Main()
{
CreateThread(AThread);
CreateThread(BThread);
IOCannel();
}
内核端:
OnRead
{
IOStartParket(...);
}
OnCancel()
{
}
(二)伪代码说明:
AThread调用ReadFile,然后调用内核的OnRead,OnRead中调用IOStartParket=》线程1
BThread调用ReadFile,然后调用内核的OnRead,OnRead中调用IOStartParket=》线程2
MainThread调用IOCannel,然后调用内核OnCancel=》线程3
总共那个三个线程。
(三)线程1的IoStartParket面临以下两种线程的资源竞争:
(1)其他的IOStartParket线程(如线程2)
解释:IN PDRIVER_CANCEL CancelFunction,指明了是否支持Iocancel,如果不支持,则不考虑线程3.
IoStartPacket可以简写为下面函数体。
VOID NTAPI IoStartPacket(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp,
IN PULONG Key, IN PDRIVER_CANCEL CancelFunction)
{
BOOLEAN Stat;
KIRQL OldIrql, CancelIrql;
/* Raise to dispatch level */
KeRaiseIrql(DISPATCH_LEVEL, &OldIrql);
/* Check if we have a key */
if (Key)
{ //对于加了锁的文件片段
/* Insert by key */
Stat = KeInsertByKeyDeviceQueue(&DeviceObject->DeviceQueue,
&Irp->Tail.Overlay.DeviceQueueEntry, *Key);
}
else
{ //对于一般的文件或设备
/* Insert without a key */
Stat = KeInsertDeviceQueue(&DeviceObject->DeviceQueue,
&Irp->Tail.Overlay.DeviceQueueEntry);
}
/* Check if this was a first insert */
if (!Stat)
{ //这是针对目标设备对象的第一个IRP,可以启动
/* Set the IRP */
DeviceObject->CurrentIrp = Irp;
/* Call the Start I/O function */ //调用该设备对象所属驱动的I/O启动函数
DeviceObject->DriverObject->DriverStartIo(DeviceObject, Irp);
}
/* Return back to previous IRQL */
KeLowerIrql(OldIrql);
}
KeRaiseIrql(DISPATCH_LEVEL, &OldIrql);这一行提高Irql的优先级为DISPATCH_LEVEL,
它的作用是在单个CPU的环境下避免同时调用IoStartPacket,但其实对于多CPU的计算机来说,
它是无法避免同时调用IoStartPacke的,那怎么办呢?
其实在KeInsertDeviceQueue中加了自旋锁,此时同时调用IoStartPacket也不会有问题,
因为,如果有一个线程在调用KeInsertDeviceQueue,那另外一个线程肯定是在等待,当第一个线程
退出KeInsertDeviceQueue后,第二个线程进入KeInsertDeviceQueue,此时会把它的请求插入请求列表中。
因为第二个线程成功把请求插入列表,那么它就不会执行DriverStartIo例程。
只要第一个线程没有调用KeRemoveEntryDeviceQueue,以后再有其他线程的请求,都会放入请求列表中。
最后的结果是只有第一个线程会持续运行,因为第一个线程执行DriverStartIo例程快到结束时,
会调用IOStartNextPacket来执行下一请求,IOStartNextPacket中就调用了KeRemoveEntryDeviceQueue函数。
那有没可能IOStartNextPacket在调用KeRemoveEntryDeviceQueue后,来不及取出第二个请求,
此时又来了一个请求执行了DriverStartIo例程,以致和IOStartNextPacket执行的DriverStartIo冲突呢?
这不可能,因为IOStartNextPacke中表示设备忙的标志位不会被改变,除非此时下个请求已经不存在。
只要表示设备忙的标志位处于忙状态,那新来的请求只会被插到请求列表中。如果下个请求不存在,此时
IOStartNextPacke调用了KeRemoveEntryDeviceQueue函数把设备置为空闲状态,马上返回。
此时如果有新的请求进来,就可以直接调用DriverStartIo,不用插到请求列表中。
(2)IoCannelFunction(如线程3)
解释:系统后台维护了一个Cancel自旋锁。再调用Cancel例程之前,系统会先获取该自旋锁,
在Cancel例程获取自旋锁以后,IoStartPacket只能在两个地方等待:
一个是在KeInsertDeviceQueue之前,一个在DriverStartIo例程入口处。
所以,如果要支持IoCancel,DriverStartIo一定要在入口处获取Cancel自旋锁,见下函数体:
VOID DriverStartIo(PDEVICE_OBJECT fdo, PIRP Irp)
{
KIRQL oldirql;
IoAcquireCancelSpinLock(&oldirql);
if (Irp != fdo->CurrentIrp || Irp->Cancel)
{
IoReleaseCancelSpinLock(oldirql);
return;
}
else
{
IoSetCancelRoutine(Irp, NULL);
IoReleaseCancelSpinLock(oldirql);
}
...(如IOStartNextPacket)
}
如果IoCancel完毕,IoStartPacket取消等待,继续运行,
如果IoStartPacket在KeInsertDeviceQueue之前等待,那他会把新来的请求入队或者直接调用
DriverStartIo,这要根据DriverStartIo=>IOStartNextPacket执行的结果,IOStartNextPacket
中如果还有请求,则IoStartPacket请求入队;否则,IoStartPacket直接调用DriverStartIo。
如果IoStartPacket在DriverStartIo例程入口处等待,则往下执行,如果发现当前的请求被取消,
则释放掉自旋锁,直接执行下一个请求。
以上如果对IoStartPacket还有疑问,建议看Walter Oney的Windows Driver Model。