2.4 Synchronization And Notification

Synchronization is necessary whenever two threads of execution share resources that can be accessed at the same time either in a uniprocessor machine or on an SMP machine. For instance, on a uniprocessor machine, if one driver function is accessing a shared resource, and is interrupted by another function that runs at a higher IRQL, such as an ISR, the shared resource must be protected to prevent race conditions that leave the resource in an indeterminate state. On an SMP machine, two threads could be running simultaneously on different processors and attempting to modify the same data. Such accesses must be synchronized.

NDIS provides spin locks that can be used to synchronize access to shared resources between threads that execute at the same IRQL. When two threads that share a resource execute at different IRQLs, NDIS provides a mechanism for temporarily raising the IRQL of the lower IRQL code so that access to the shared resource can be serialized.

Notification is necessary when a thread is dependent on the occurrence of an event outside of the thread. For instance, a driver might need to be notified when some time period has passed so that it can check its device. Or a NIC driver might have to perform a periodic operation such as polling. Timers provide such a mechanism.

Events provide a mechanism that two threads of execution can use to synchronize operations. For instance, a miniport NIC driver may want to test the interrupt on its NIC by writing to the device. It must wait for an interrupt to notify the driver that the operation was successful. Events can be used to synchronize an operation between the thread waiting for the interrupt to complete and the thread that handles the interrupt.

A description of these NDIS mechanisms follows.

Spin Locks

A spin lock provides a synchronization mechanism for protecting resources shared by kernel-mode threads running at IRQL > PASSIVE_LEVEL in either a uniprocessor or a multiprocessor machine. A spin lock handles synchronization among various threads of execution running concurrently on an SMP machine. A thread acquires a spin lock before accessing protected resources. The spin lock keeps any thread but the one holding the spin lock from using the resource. A thread that is waiting on the spin lock loops attempting to acquire the spin lock until it is released by the thread that holds the lock.

Another characteristic of spin locks is the associated IRQL. Attempted acquisition of a spin lock temporarily raises the IRQL of the requesting thread to the IRQL associated with the spin lock. This prevents all lower IRQL threads on the same processor from preempting the executing thread. Threads running at a higher IRQL can preempt the executing thread, but these threads cannot acquire the spin lock because it has a lower IRQL than they do. Therefore, no other threads on the same processor attempt to acquire the spin lock until it has been both acquired and released by the executing thread. A well-written network driver will minimize the amount of time a spin lock is held.

A typical use for a spin lock is to protect a queue. For example, the miniport send function, MiniportSend, might queue packets passed to it by a protocol driver. Because other driver functions also use this queue, MiniportSend must protect the queue with a spin lock so that only one thread at a time can manipulate the links or contents. MiniportSend acquires the spin lock, adds the packet to the queue and then releases the spin lock. Using a spin lock ensures that the thread holding the lock is the only thread modifying the queue links while the packet is safely added to the queue. When the NIC driver takes the packets off the queue, such an access is protected by the same spin lock. When executing instructions that modify the head of the queue or any of the link fields making up the queue, the driver must protect the queue with a spin lock.

A driver must take care not to overprotect a queue. For example, the driver can perform some operations (for example, filling in a field containing the length) in the network driver-reserved field of a packet before it queues the packet. The driver can do this outside the region protected by the spin lock, but must do it before queuing the packet. Once the packet is on the queue and the executing thread releases the spin lock, the driver must assume that other threads can dequeue the packet immediately.

Avoiding Deadlock Problems

A problem to be avoided when using spin locks is deadlock. Windows NT does not restrict a network driver from simultaneously holding more than one spin lock. However, if one section of the driver attempts to acquire Spin Lock A while holding Spin Lock B, and another section attempts to acquire Spin Lock B while holding Spin Lock A, deadlock results. If it acquires more than one spin locks, a driver should avoid deadlock by enforcing an order of acquisition. That is, if a driver enforces acquiring Spin Lock A before Spin Lock B, the situation described above will not occur.

Using spin locks impacts performance and, in general, a driver should not use many spin locks. Occasionally, functions that are usually distinct (for example, send and receive functions) have minor overlaps for which two spin locks can be used. Use of more than one spin lock might be a worthwhile tradeoff in order to allow the two functions to operate independently on separate processors.

Timers

Timers are used for polling or timing out operations. A driver creates a timer and associates a function with the timer. The associated function is called when the period specified in the timer expires. Timers can be one-shot or periodic. Once a periodic timer is set, it will continue to fire at the expiration of every period until explicitly cleared. A one-shot timer must be reset each time it fires.

Timers are created and initialized by calling NdisMInitializeTimer, and set by calling NdisMSetTimer or for a periodic timer, by calling NdisMSetPeriodicTimer. If a nonperiodic timer is used, it must reset by calling NdisMSetTimer. A timer is cleared by calling NdisMCancelTimer.

Events

Events are used to synchronize operations between two threads of execution. An event is allocated by a driver and initialized by calling NdisInitializeEvent. A thread running at IRQL PASSIVE_LEVEL calls NdisWaitEvent to put itself into a wait state. When a driver thread waits on an event, it specifies a maximum time to wait as well as the event to be waited on. The thread's wait is satisfied when NdisSetEvent is called causing the event to be signaled, or when the specified maximum wait-time interval expires, whichever occurs first.

Typically, the event is set by a cooperating thread that calls NdisSetEvent. Events are unsignaled when they are created and must be set in order to signal waiting threads. Events remain signaled until NdisResetEvent is called.