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

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

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

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

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

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

自旋锁

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

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

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

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

避免旋转锁问题

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

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

  2. 保留 NDIS 旋转锁 B 的线程 2 调用 NdisXxx 函数,该函数通过调用 NdisAcquireSpinLock 函数来尝试获取 NDIS 旋转锁 A。

  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 之前,事件保持信号。

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