Sunday, October 26, 2014

SpinLock in Device Drivers

Locking is required in driver code to protect a shared variable or  a piece of code from concurrent access. This protective region is referred as critical section.
Concurrent access to a variable will lead to variable corruption if locking is not used.
Locking restricts the access to the shared variable, converts the concurrent access to a sequential one.
When writing a driver, its important to keep in mind that the driver code can be accessed by many independent processes especially if the driver interacts with the user space through sysfs, device file operations etc.

Spinlock is a type of lock mechanism, used to protect a critical section which can be accessed by interrupt service routine and other part of driver code such as read,write functions.
To access a critical section, hold a lock and then enter the section and release the lock while leaving. If another process tries to enter the section when the lock is held, the process just spins around the lock to check if the lock is released. Its a busy wait loop which checks if the lock is released or not. Hence the name spinlock. Obviously the process which is spinning is 'wasting CPU cycles'.
The reason why spinlock is often used in interrupts because the other locking mechanism such as mutex, semaphore puts the process to sleep if the process wants to acquire the lock which is held by another process. As we know interrupts are not designed to sleep. Hence spinlocks are used when we need to protect a shared variable between interrupts and other part of driver code.

A sample skeleton driver pseudo code to illustrate the importance of locking.

write () {
..
outb(COMMAND_0, COMMAND_REG);
inb(STATUS_REG);
..
}

ioctl () {
..
outb(COMMAND_1, COMMAND_REG);
inb(STATUS_REG);
..
}

The write function sends command to the device and reads the status of the command as executed by the device. Even the ioctl command sends command and reads the status of the command. But the command sent by the ioctl function is different from the write function. (COMMAND_1 and COMMAND_0)
Assume there are 2 user space clients for this driver. One of the user process P0 has issued the write system call and another user process P1 has issued the ioctl system call.
Lets consider P1 is running and the code inside ioctl() is executing and has issued the command 1. By the time it reads the status register of the result, the scheduler kicks in and hands over the control to process P0.  The write function issues command 0 and reads the status of that command. The scheduler hands back the control to process P1. It continues the execution from where it has left before i.e reading status register. But instead of reading status of command 1, it reads the status of command 0  !
This simple example illustrates the necessary of locking to maintain the right state machine.
Results are not predictable even if the processes are running on different CPU i.e P0 is running on CPU0 and P1 is running on CPU1.

Re-writing the code with the usage of spinlock.

write () {
..
spin_lock(&driver_lock);
outb(COMMAND_0, COMMAND_REG);
inb(STATUS_REG);
spin_unlock(&driver_lock);
..
}

ioctl () {
..
spin_lock(&driver_lock);
outb(COMMAND_1, COMMAND_REG);
inb(STATUS_REG);
spin_unlock(&driver_lock);
..
}


If a process acquires the spinlock then the other process or processes doesn't get the lock and only the process which has acquired the lock will be executing the critical section . Hence ioctl and write will always read the status of their respective commands.
We can use mutex instead of spinlock in the above example. But it might be overkill.

However in the above example. if the interrupt also uses the lock then we might need to use other version of spinlocks like spin_lock_bh(), spin_lock_irqsave() which I will discuss in the next section.
Till then Happy SpinLocking !!!

No comments: