In addition to protecting shared data, drivers often need to synchronize execution among multiple threads.
Condition variables are a standard form of thread synchronization. They are designed to be used with mutexes. The associated mutex is used to ensure that a condition can be checked atomically, and that the thread can block on the associated condition variable without missing either a change to the condition or a signal that the condition has changed.
Table 3-4 lists the condvar(9F) interfaces.
Table 3-4 Condition Variable Routines
Name |
Description |
---|---|
cv_init(9F) |
Initializes a condition variable |
cv_destroy(9F) |
Destroys a condition variable |
cv_wait(9F) |
Waits for condition |
cv_timedwait(9F) |
Waits for condition or timeout |
cv_wait_sig(9F) |
Waits for condition or return zero on receipt of a signal |
cv_timedwait_sig(9F) |
Waits for condition or timeout or signal |
cv_signal(9F) |
Signals one thread waiting on the condition variable |
cv_broadcast(9F) |
Signals all threads waiting on the condition variable |
Declare a condition variable (type kcondvar_t) for each condition. Usually, this is done in the driver's soft-state structure. Use cv_init(9F) to initialize each one. Similar to mutexes, condition variables are usually initialized at attach(9E) time. For example:
cv_init(&xsp->cv, NULL, CV_DRIVER, NULL);
For a more complete example of condition variable initialization see Chapter 5, Autoconfiguration.
To use condition variables, follow these steps in the code path waiting for the condition:
Acquire the mutex guarding the condition.
Test the condition.
If the test results do not allow the thread to continue, use cv_wait(9F) to block the current thread on the condition. cv_wait(9F) releases the mutex before blocking. Upon return from cv_wait(9F) (which will reacquire the mutex before returning), repeat the test.
Once the test allows the thread to continue, set the condition to its new value. For example, set a device flag to busy.
Release the mutex.
Follow these steps in the code path signaling the condition:
Acquire the mutex guarding the condition.
Set the condition.
Signal the blocked thread with cv_signal(9F).
Release the mutex.
Example 3-1 uses a busy flag, and mutex and condition variables to force the read(9E) routine to wait until the device is no longer busy before starting a transfer.
static int xxread(dev_t dev, struct uio *uiop, cred_t *credp) { struct xxstate *xsp; ... mutex_enter(&xsp->mu); while (xsp->busy) cv_wait(&xsp->cv, &xsp->mu); xsp->busy = 1; mutex_exit(&xsp->mu); /* perform the data access */ } static uint_t xxintr(caddr_t arg) { struct xxstate *xsp = (struct xxstate *)arg; mutex_enter(&xsp->mu); xsp->busy = 0; cv_broadcast(&xsp->cv); mutex_exit(&xsp->mu); }
If a thread blocks on a condition with cv_wait(9F), and that condition does not occur, it can wait forever. One way to prevent this is to establish a callback with timeout(9F). This callback sets a flag indicating that the condition did not occur normally, and then unblocks the thread. The notified thread then notices that the condition did not occur and can return an error.
A better solution is to use cv_timedwait(9F). An absolute wait time is passed to cv_timedwait(9F), which returns -1 if the time is reached and the event has not occurred. It returns a positive value if the condition is met.
cv_timedwait(9F) requires an absolute wait time expressed in clock ticks since the system was last rebooted. This can be determined by retrieving the current value with ddi_get_lbolt(9F). The driver usually has a maximum number of seconds or microseconds to wait, so this value is converted to clock ticks with drv_usectohz(9F) and added to the value from ddi_get_lbolt(9F).
Example 3-2 shows how to use cv_timedwait(9F) to wait up to five seconds to access the device before returning EIO to the caller.
clock_t cur_ticks, to; mutex_enter(&xsp->mu); while (xsp->busy) { cur_ticks = ddi_get_lbolt(); to = cur_ticks + drv_usectohz(5000000); /* 5 seconds from now */ if (cv_timedwait(&xsp->cv, &xsp->mu, to) == -1) { /* * The timeout time 'to' was reached without the * condition being signalled. */ /* tidy up and exit */ mutex_exit(&xsp->mu); return (EIO); } } xsp->busy = 1; mutex_exit(&xsp->mu);
There is always the possibility that either the driver accidentally waits for a condition that will never occur or that the condition will not happen for a long time. In either case, the user can abort the thread by sending it a signal. Whether the signal causes the driver to wake up depends upon the driver.
cv_wait_sig(9F) allows a signal to unblock the thread. This allows the user to break out of potentially long waits by sending a signal to the thread with kill(1) or by typing the interrupt character. cv_wait_sig(9F) returns zero if it is returning because of a signal, or nonzero if the condition occurred.
Example 3-3 shows how to use cv_wait_sig(9F) to allow a signal to unblock the thread.
mutex_enter(&xsp->mu); while (xsp->busy) { if (cv_wait_sig(&xsp->cv, &xsp->mu) == 0) { /* Signalled while waiting for the condition */ /* tidy up and exit */ mutex_exit(&xsp->mu); return (EINTR); } } xsp->busy = 1; mutex_exit(&xsp->mu);
cv_timedwait_sig(9F) is similar to cv_timedwait(9F) and cv_wait_sig(9F), except that it returns -1 without the condition being signaled after a timeout has been reached, or 0 if a signal (for example, kill(2)) is sent to the thread.
For both cv_timedwait(9F) and cv_timedwait_sig(9F), time is measured in absolute clock ticks since the last system reboot.