Linux Device Drivers 2nd Edition (779877), страница 69
Текст из файла (страница 69)
It alsoassumes that the bit is either 0 when the lock is free or nonzero when the lock isbusy./* try to set lock */while (test_and_set_bit(nr, addr) != 0)wait_for_a_while();/* do your work *//* release lock, and check... */if (test_and_clear_bit(nr, addr) == 0)something_went_wrong(); /* already released: error */If you read through the kernel source, you will find code that works like thisexample. As mentioned before, however, it is better to use spinlocks in new code,unless you need to perform useful work while waiting for the lock to be released(e.g., in the wait_for_a_while() instruction of this listing).Atomic integer operationsKernel programmers often need to share an integer variable between an interrupthandler and other functions.
A separate set of functions has been provided to facilitate this sort of sharing; they are defined in <asm/atomic.h>.The facility offered by atomic.h is much stronger than the bit operations justdescribed. atomic.h defines a new data type, atomic_t, which can be accessedonly through atomic operations.
An atomic_t holds an int value on all supported architectures. Because of the way this type works on some processors,however, the full integer range may not be available; thus, you should not counton an atomic_t holding more than 24 bits. The following operations are definedfor the type and are guaranteed to be atomic with respect to all processors of anSMP computer.
The operations are very fast because they compile to a singlemachine instruction whenever possible.28522 June 2001 16:39http://openlib.org.uaChapter 9: Interrupt Handlingvoid atomic_set(atomic_t *v, int i);Set the atomic variable v to the integer value i.int atomic_read(atomic_t *v);Return the current value of v.void atomic_add(int i, atomic_t *v);Add i to the atomic variable pointed to by v. The return value is void,because most of the time there’s no need to know the new value.
This function is used by the networking code to update statistics about memory usagein sockets.void atomic_sub(int i, atomic_t *v);Subtract i from *v.void atomic_inc(atomic_t *v);void atomic_dec(atomic_t *v);Increment or decrement an atomic variable.intintintintatomic_inc_and_test(atomic_t *v);atomic_dec_and_test(atomic_t *v);atomic_add_and_test(int i, atomic_t *v);atomic_sub_and_test(int i, atomic_t *v);These functions behave like their counterparts listed earlier, but they alsoreturn the previous value of the atomic data type.As stated earlier, atomic_t data items must be accessed only through these functions. If you pass an atomic item to a function that expects an integer argument,you’ll get a compiler error.Going to Sleep Without RacesThe one race condition that has been omitted so far in this discussion is the problem of going to sleep.
Generally stated, things can happen in the time betweenwhen your driver decides to sleep and when the sleep_on call is actually performed. Occasionally, the condition you are sleeping for may come about beforeyou actually go to sleep, leading to a longer sleep than expected. It is a problemfar more general than interrupt-driven I/O, and an efficient solution requires a little knowledge of the internals of sleep_on.As an example, consider again the following code from the short driver:while (short_head == short_tail) {interruptible_sleep_on(&short_queue);/* ... */}In this case, the value of short_head could change between the test in thewhile statement and the call to interruptible_sleep_on. In that case, the driver will28622 June 2001 16:39http://openlib.org.uaRace Conditionssleep even though new data is available; this condition leads to delays in the bestcase, and a lockup of the device in the worst.The way to solve this problem is to go halfway to sleep before performing thetest.
The idea is that the process can add itself to the wait queue, declare itself tobe sleeping, and then perform its tests. This is the typical implementation:wait_queue_t wait;init_waitqueue_entry(&wait, current);add_wait_queue(&short_queue, &wait);while (1) {set_current_state(TASK_INTERRUPTIBLE);if (short_head != short_tail) /* whatever test your driver needs */break;schedule();}set_current_state(TASK_RUNNING);remove_wait_queue(&short_queue, &wait);This code is somewhat like an unrolling of the internals of sleep_on; we’ll stepthrough it here.The code starts by declaring a wait_queue_t variable, initializing it, and addingit to the driver’s wait queue (which, as you may remember, is of typewait_queue_head_t). Once these steps have been performed, a call towake_up on short_queue will wake this process.The process is not yet asleep, however.
It gets closer to that state with the call toset_curr ent_state, which sets the process’s state to TASK_INTERRUPTIBLE. Therest of the system now thinks that the process is asleep, and the scheduler will nottry to run it. This is an important step in the ‘‘going to sleep’’ process, but thingsstill are not done.What happens now is that the code tests for the condition for which it is waiting,namely, that there is data in the buffer. If no data is present, a call to schedule ismade, causing some other process to run and truly putting the current process tosleep. Once the process is woken up, it will test for the condition again, and possibly exit from the loop.Beyond the loop, there is just a bit of cleaning up to do.
The current state is set toTASK_RUNNING to reflect the fact that we are no longer asleep; this is necessarybecause if we exited the loop without ever sleeping, we may still be inTASK_INTERRUPTIBLE. Then remove_wait_queue is used to take the process offthe wait queue.So why is this code free of race conditions? When new data comes in, the interrupt handler will call wake_up on short_queue, which has the effect of setting28722 June 2001 16:39http://openlib.org.uaChapter 9: Interrupt Handlingthe state of every sleeping process on the queue to TASK_RUNNING.
If thewake_up call happens after the buffer has been tested, the state of the task will bechanged and schedule will cause the current process to continue running—after ashort delay, if not immediately.This sort of ‘‘test while half asleep’’ pattern is so common in the kernel source thata pair of macros was added during 2.1 development to make life easier:wait_event(wq, condition);wait_event_interruptible(wq, condition);Both of these macros implement the code just discussed, testing the condition (which, since this is a macro, is evaluated at each iteration of the loop)in the middle of the ‘‘going to sleep’’ process.Backward CompatibilityAs we stated at the beginning of this chapter, interrupt handling in Linux presentsrelatively few compatibility problems with older kernels.
There are a few, however, which we discuss here. Most of the changes occurred between versions 2.0and 2.2 of the kernel; interrupt handling has been remarkably stable since then.Differences in the 2.2 KernelThe biggest change since the 2.2 series has been the addition of tasklets in kernel2.3.43. Prior to this change, the BH bottom-half mechanism was the only way forinterrupt handlers to schedule deferred work.The set_curr ent_state function did not exist in Linux 2.2 (but sysdep.h implementsit).
To manipulate the current process state, it was necessary to manipulate thetask structure directly. For example:current->state = TASK_INTERRUPTIBLE;Further Differences in the 2.0 KernelIn Linux 2.0, there were many more differences between fast and slow handlers.Slow handlers were slower even before they began to execute, because of extrasetup costs in the kernel. Fast handlers saved time not only by keeping interruptsdisabled, but also by not checking for bottom halves before returning from theinterrupt. Thus, the delay before the execution of a bottom half marked in aninterrupt handler could be longer in the 2.0 kernel.
Finally, when an IRQ line wasbeing shared in the 2.0 kernel, all of the registered handlers had to be either fastor slow; the two modes could not be mixed.28822 June 2001 16:39http://openlib.org.uaQuick ReferenceMost of the SMP issues did not exist in 2.0, of course. Interrupt handlers couldonly execute on one CPU at a time, so there was no distinction between disablinginterrupts locally or globally.The disable_irq_nosync function did not exist in 2.0; in addition, calls to disable_irq and enable_irq did not nest.The atomic operations were different in 2.0.
The functions test_and_set_bit,test_and_clear_bit, and test_and_change_bit did not exist; instead, set_bit,clear_bit, and change_bit returned a value and functioned like the moderntest_and_ versions. For the integer operations, atomic_t was just a typedef forint, and variables of type atomic_t could be manipulated like ints. Theatomic_set and atomic_r ead functions did not exist.The wait_event and wait_event_interruptible macros did not exist in Linux 2.0.Quick ReferenceThese symbols related to interrupt management were introduced in this chapter.#include <linux/sched.h>int request_irq(unsigned int irq, void (*handler)(),unsigned long flags, const char *dev_name, void*dev_id);void free_irq(unsigned int irq, void *dev_id);These calls are used to register and unregister an interrupt handler.SA_INTERRUPTSA_SHIRQSA_SAMPLE_RANDOMFlags for request_irq.