Linux Device Drivers 2nd Edition (779877), страница 21
Текст из файла (страница 21)
Two processes, A and B, both have the same scull device openfor writing. Both attempt simultaneously to append data to the device. A newquantum is required for this operation to succeed, so each process allocates therequired memory and stores a pointer to it in the quantum set.The result is trouble. Because both processes see the same scull device, each willstore its new memory in the same place in the quantum set. If A stores its pointerfirst, B will overwrite that pointer when it does its store.
Thus the memory allocated by A, and the data written therein, will be lost.This situation is a classic race condition; the results vary depending on who getsthere first, and usually something undesirable happens in any case. On uniprocessor Linux systems, the scull code would not have this sort of problem, becauseprocesses running kernel code are not preempted. On SMP systems, however, lifeis more complicated.
Processes A and B could easily be running on different processors and could interfere with each other in this manner.The Linux kernel provides several mechanisms for avoiding and managing raceconditions. A full description of these mechanisms will have to wait until Chapter9, but a beginning discussion is appropriate here.A semaphor e is a general mechanism for controlling access to resources.
In its simplest form, a semaphore may be used for mutual exclusion; processes usingsemaphores in the mutual exclusion mode are prevented from simultaneously running the same code or accessing the same data. This sort of semaphore is oftencalled a mutex, from “mutual exclusion.”Semaphores in Linux are defined in <asm/semaphore.h>. They have a type ofstruct semaphore, and a driver should only act on them using the providedinterface. In scull, one semaphore is allocated for each device, in the Scull_Devstructure.
Since the devices are entirely independent of each other, there is noneed to enforce mutual exclusion across multiple devices.Semaphores must be initialized prior to use by passing a numeric argument tosema_init. For mutual exclusion applications (i.e., keeping multiple threads fromaccessing the same data simultaneously), the semaphore should be initialized to avalue of 1, which means that the semaphore is available. The following code inscull ’s module initialization function (scull_init) shows how the semaphores areinitialized as part of setting up the devices.for (i=0; i < scull_nr_devs; i++) {scull_devices[i].quantum = scull_quantum;scull_devices[i].qset = scull_qset;sema_init(&scull_devices[i].sem, 1);}7622 June 2001 16:35http://openlib.org.uaA Brief Introduction to Race ConditionsA process wishing to enter a section of code protected by a semaphore must firstensure that no other process is already there.
Whereas in classical computer science the function to obtain a semaphore is often called P, in Linux you’ll need tocall down or down_interruptible. These functions test the value of the semaphoreto see if it is greater than 0; if so, they decrement the semaphore and return. If thesemaphore is 0, the functions will sleep and try again after some other process,which has presumably freed the semaphore, wakes them up.The down_interruptible function can be interrupted by a signal, whereas downwill not allow signals to be delivered to the process. You almost always want toallow signals; otherwise, you risk creating unkillable processes and other undesirable behavior. A complication of allowing signals, however, is that you alwayshave to check if the function (here down_interruptible) was interrupted.
As usual,the function returns 0 for success and nonzero in case of failure. If the process isinterrupted, it will not have acquired the semaphores; thus, you won’t need to callup. A typical call to invoke a semaphore therefore normally looks something likethis:if (down_interruptible (&sem))return -ERESTARTSYS;The -ERESTARTSYS return value tells the system that the operation was interrupted by a signal. The kernel function that called the device method will eitherretry it or return -EINTR to the application, according to how signal handling hasbeen configured by the application. Of course, your code may have to performcleanup work before returning if interrupted in this mode.A process that obtains a semaphore must always release it afterward.
Whereascomputer science calls the release function V, Linux uses up instead. A simple calllikeup (&sem);will increment the value of the semaphore and wake up any processes that arewaiting for the semaphore to become available.Care must be taken with semaphores. The data protected by the semaphore mustbe clearly defined, and all code that accesses that data must obtain the semaphorefirst. Code that uses down_interruptible to obtain a semaphore must not callanother function that also attempts to obtain that semaphore, or deadlock willresult. If a routine in your driver fails to release a semaphore it holds (perhaps as aresult of an error return), any further attempts to obtain that semaphore will stall.Mutual exclusion in general can be tricky, and benefits from a well-defined andmethodical approach.In scull, the per-device semaphore is used to protect access to the stored data.
Anycode that accesses the data field of the Scull_Dev structure must first have7722 June 2001 16:35http://openlib.org.uaChapter 3: Char Driversobtained the semaphore. To avoid deadlocks, only functions that implementdevice methods will try to obtain the semaphore. Internal routines, such asscull_trim shown earlier, assume that the semaphore has already been obtained.As long as these invariants hold, access to the Scull_Dev data structure is safefrom race conditions.read and writeThe read and write methods perform a similar task, that is, copying data from andto application code. Therefore, their prototypes are pretty similar and it’s worthintroducing them at the same time:ssize_t read(struct file *filp, char *buff,size_t count, loff_t *offp);ssize_t write(struct file *filp, const char *buff,size_t count, loff_t *offp);For both methods, filp is the file pointer and count is the size of the requesteddata transfer.
The buff argument points to the user buffer holding the data to bewritten or the empty buffer where the newly read data should be placed. Finally,offp is a pointer to a “long offset type” object that indicates the file position theuser is accessing. The return value is a “signed size type;” its use is discussed later.As far as data transfer is concerned, the main issue associated with the two devicemethods is the need to transfer data between the kernel address space and theuser address space. The operation cannot be carried out through pointers in theusual way, or through memcpy.
User-space addresses cannot be used directly inkernel space, for a number of reasons.One big difference between kernel-space addresses and user-space addresses isthat memory in user-space can be swapped out. When the kernel accesses a userspace pointer, the associated page may not be present in memory, and a pagefault is generated. The functions we introduce in this section and in “Using theioctl Argument” in Chapter 5 use some hidden magic to deal with page faults inthe proper way even when the CPU is executing in kernel space.Also, it’s interesting to note that the x86 port of Linux 2.0 used a completely different memory map for user space and kernel space.
Thus, user-space pointerscouldn’t be dereferenced at all from kernel space.If the target device is an expansion board instead of RAM, the same problemarises, because the driver must nonetheless copy data between user buffers andkernel space (and possibly between kernel space and I/O memory).Cross-space copies are performed in Linux by special functions, defined in<asm/uaccess.h>. Such a copy is either performed by a generic (memcpy -like)function or by functions optimized for a specific data size (char, short, int,long); most of them are introduced in “Using the ioctl Argument” in Chapter 5.7822 June 2001 16:35http://openlib.org.uaread and writeThe code for read and write in scull needs to copy a whole segment of data to orfrom the user address space.
This capability is offered by the following kernelfunctions, which copy an arbitrary array of bytes and sit at the heart of every readand write implementation:unsigned long copy_to_user(void *to, const void *from,unsigned long count);unsigned long copy_from_user(void *to, const void *from,unsigned long count);Although these functions behave like normal memcpy functions, a little extra caremust be used when accessing user space from kernel code.
The user pages beingaddressed might not be currently present in memory, and the page-fault handlercan put the process to sleep while the page is being transferred into place. Thishappens, for example, when the page must be retrieved from swap space. The netresult for the driver writer is that any function that accesses user space must bereentrant and must be able to execute concurrently with other driver functions(see also “Writing Reentrant Code” in Chapter 5). That’s why we use semaphoresto control concurrent access.The role of the two functions is not limited to copying data to and from userspace: they also check whether the user space pointer is valid. If the pointer isinvalid, no copy is performed; if an invalid address is encountered during thecopy, on the other hand, only part of the data is copied.