网络驱动程序中的同步和通知

每当两个执行线程共享可以同时访问的资源时(无论是在单处理器计算机中还是在对称多处理器 (SMP) 计算机上),都需要同步它们。 例如,在单处理器计算机上,如果一个驱动程序函数正在访问共享资源,并且被在更高的 IRQL(如 ISR)上运行的另一个函数中断,则必须保护共享资源,以防止争用条件使资源处于不确定状态。 在 SMP 计算机上,两个线程可以在不同的处理器上同时运行,并尝试修改相同的数据。 此类访问必须同步。

NDIS 提供旋转锁,可用于在在同一 IRQL 上运行的线程之间同步对共享资源的访问。 当两个共享资源的线程在不同的 IRQL 上运行时,NDIS 提供了一种机制,用于暂时提高较低 IRQL 代码的 IRQL,以便可以序列化对共享资源的访问。

当线程依赖于线程外部发生的事件时,该线程依赖于通知。 例如,驱动程序可能需要在经过一段时间后收到通知,以便它可以检查其设备。 或者,网络接口卡 (NIC) 驱动程序可能必须执行定期操作,例如轮询。 计时器提供此类机制。

事件提供了一种机制,两个执行线程可以使用该机制来同步操作。 例如,微型端口驱动程序可以通过写入设备来测试 NIC 上的中断。 驱动程序必须等待中断,以通知驱动程序操作成功。 可以使用事件在等待中断完成的线程和处理中断的线程之间同步操作。

本主题中的以下小节介绍了这些 NDIS 机制。

自旋锁

旋转锁提供了一种同步机制,用于保护在单处理器或多处理器计算机中在 IRQL > PASSIVE_LEVEL运行的内核模式线程共享的资源。 旋转锁处理在 SMP 计算机上并发运行的各种执行线程之间的同步。 线程在访问受保护的资源之前获取旋转锁。 旋转锁保留任何线程,但保留旋转锁的线程无法使用资源。 在 SMP 计算机上,等待旋转锁循环的线程尝试获取旋转锁,直到持有该锁的线程释放该锁。

旋转锁的另一个特征是关联的 IRQL。 尝试获取旋转锁会暂时将请求线程的 IRQL 提升到与旋转锁关联的 IRQL。 这可以防止同一处理器上所有较低 IRQL 线程抢占执行线程。 在同一处理器上,在更高的 IRQL 上运行的线程可能会抢占执行线程,但这些线程无法获取旋转锁,因为它的 IRQL 较低。 因此,在线程获取旋转锁后,在释放该旋转锁之前,任何其他线程都无法获取该旋转锁。 编写良好的网络驱动程序可最大程度地减少旋转锁的保留时间。

旋转锁的典型用途是保护队列。 例如,微型端口驱动程序 send 函数 MiniportSendNetBufferLists 可能会将协议驱动程序传递给它的数据包排队。 由于其他驱动程序函数也使用此队列, 因此 MiniportSendNetBufferLists 必须使用旋转锁保护队列,以便一次只能有一个线程可以操作链接或内容。 MiniportSendNetBufferLists 获取旋转锁,将数据包添加到队列,然后释放旋转锁。 使用旋转锁可确保在将数据包安全地添加到队列时,持有锁的线程是唯一修改队列链接的线程。 当微型端口驱动程序将数据包从队列中移出时,此类访问受同一个旋转锁的保护。 运行修改队列头或组成队列的任何链接字段的指令时,驱动程序必须使用旋转锁保护队列。

驱动程序必须注意不要过度保护队列。 例如,驱动程序可以执行某些操作 (例如,在数据包的网络驱动程序保留字段中填写包含长度) 的字段,然后再将数据包排队。 驱动程序可以在受旋转锁保护的代码区域外部执行此操作,但必须在排队数据包之前执行此操作。 数据包在队列中且正在运行的线程释放旋转锁后,驱动程序必须假定其他线程可以立即取消对数据包的排队。

避免旋转锁问题

为了避免可能出现的死锁,NDIS 驱动程序应在调用 Ndis Xxx Spinlock 函数以外的 NDIS 函数之前释放所有 NDIS 旋转锁。 如果 NDIS 驱动程序不符合此要求,可能会出现死锁,如下所示:

  1. 包含 NDIS 旋转锁 A 的线程 1 调用尝试通过调用 NdisAcquireSpinLock 函数获取 NDIS 旋转锁 B 的 NdisXxx 函数。

  2. 包含 NDIS 旋转锁 B 的线程 2 调用尝试通过调用 NdisAcquireSpinLock 函数获取 NDIS 旋转锁 A 的 Ndis Xxx 函数。

  3. 线程 1 和线程 2(各自等待另一个释放其旋转锁)变为死锁。

Microsoft Windows 操作系统不会限制网络驱动程序同时持有多个旋转锁。 但是,如果驱动程序的一个部分在按住旋转锁 B 时尝试获取旋转锁 A,而另一个部分在持有旋转锁 A 时尝试获取旋转锁 B,则会导致死锁。 如果获取多个旋转锁,则驱动程序应通过强制实施获取顺序来避免死锁。 也就是说,如果驱动程序强制在旋转锁 B 之前获取旋转锁 A,则不会发生上述情况。

获取旋转锁会将 IRQL 提升为DISPATCH_LEVEL并将旧 IRQL 存储在旋转锁中。 释放旋转锁会将 IRQL 设置为存储在旋转锁中的值。 由于 NDIS 有时会在PASSIVE_LEVEL进入驱动程序,因此以下代码顺序可能会出现问题:

NdisAcquireSpinLock(A);
NdisAcquireSpinLock(B);
NdisReleaseSpinLock(A);
NdisReleaseSpinLock(B);

由于以下原因,驱动程序不应访问此序列中的旋转锁:

  • 在释放旋转锁 A 和释放旋转锁 B 之间,代码在 PASSIVE_LEVEL 而不是DISPATCH_LEVEL运行,并受到不适当的中断。

  • 释放旋转锁 B 后,代码在DISPATCH_LEVEL这可能会导致调用方在以后出现IRQL_NOT_LESS_OR_EQUAL停止错误。

使用旋转锁会影响性能,一般情况下,驱动程序不应使用多个旋转锁。 有时,通常不同的函数 (例如,发送和接收函数) 具有次要重叠,可以使用两个旋转锁。 使用多个旋转锁可能是一个值得权衡的权衡,以便允许这两个函数在单独的处理器上独立运行。

计时器

计时器用于轮询或超时操作。 驱动程序创建一个计时器,并将函数与计时器相关联。 当计时器中指定的时间段过期时,将调用关联的函数。 计时器可以是一次性的,也可以是定期的。 设置定期计时器后,它将在每个时间段到期时继续触发,直到明确清除。 每次触发时,都必须重置一次性计时器。

计时器通过调用 NdisAllocateTimerObject 创建和初始化,并通过调用 NdisSetTimerObject 进行设置。 如果使用非长期计时器,则必须通过调用 NdisSetTimerObject 重置该计时器。 通过调用 NdisCancelTimerObject 清除计时器。

事件

事件用于同步两个执行线程之间的操作。 事件由驱动程序分配,并通过调用 NdisInitializeEvent 进行初始化。 在 IRQL = PASSIVE_LEVEL运行的线程调用 NdisWaitEvent 以将自身置于等待状态。 当驱动程序线程等待某个事件时,它会指定等待的最长时间以及要等待的事件。 当调用 NdisSetEvent 导致事件发出信号时,或者当指定的最大等待时间间隔过期时(以先发生者为准)时,满足线程的等待。

通常,事件由调用 NdisSetEvent 的合作线程设置。 事件在创建时未对齐,必须设置事件才能发出等待线程的信号。 在调用 NdisResetEvent 之前,事件将一直发出信号。

网络驱动程序中的多处理器支持