Waiting and Blocking in a Linux Driver

0
320

Block driver

In an era in which there is an ever increasing demand for speed and agility, waiting is the last thing anybody wants. Yet, waiting is an inevitable part of the system. Speed and waiting are two sides of the same coin. For example, though traffic signals make us wait, they are needed to ensure safety. So, why do we need to wait in the driver? What mechanisms does the kernel provide for waiting? Read on to find the crux of waiting in Linux drivers.

Many a time, while interacting with the hardware, a process needs to wait for some event. The event can be the specified period of time before which the device is available for use. To give an example, while interacting with an LCD, after sending one command, there is a need to wait for a specified length of time, before another command can be sent. The event can be the arrival of data from some device such as a disk. The event can very well be waiting for some shared resource to be available. All such scenarios require the process to wait, until the event occurs.

Process states in Linux
A process goes through various stages during its life cycle. At any point of time, a process can be in any one of the states mentioned below.

  • TASK_RUNNING: The process is in the run queue or is running.
  • TASK_INTERRUPTIBLE: The process is waiting for an event, but can be woken up by a signal.
  • TASK_UNINTERRUPTIBLE: This is similar to TASK_INTERRUPTIBLE, but the receipt of the signal has no effect.
  • TASK_ZOMBIE: The process is terminated, but not cleaned up yet.
  • TASK_STOPPED: The process was stopped by the debugger.

For the process to be scheduled to run, it needs to be in the run queue. This in turn means that the process state should be TASK_RUNNING in order for the scheduler to run it. TASK_INTERRUPTIBLE and TASK_UNINTERRUPTIBLE correspond to the waiting process states, wherein the process moves out of the run queue and wouldn’t be scheduled by the scheduler till it moves back to the run queue.

Linux kernel wait mechanisms
The basic wait mechanism which the kernel provides is the schedule() API, where the process voluntarily gives up the processor by invoking the scheduler. Let’s look at the following programming example to understand the usage of this API:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/delay.h>
#define FIRST_MINOR 0
#define MINOR_CNT 1

static dev_t dev;
static struct cdev c_dev;
static struct class *cl;

int open(struct inode *inode, struct file *filp)
{
printk(KERN_INFO “Inside open \n”);
return 0;
}

int release(struct inode *inode, struct file *filp)
{
printk (KERN_INFO “Inside close \n”);
return 0;
}

ssize_t read(struct file *filp, char *buff, size_t count, loff_t *offp)
{
printk(KERN_INFO “Inside read\n”);
printk(KERN_INFO “Scheduling out\n”);
schedule();
printk(KERN_INFO “Woken up\n”);
return 0;
}

ssize_t write(struct file *filp, const char *buff, size_t count, loff_t *offp)
{
printk(KERN_INFO “Inside Write\n”);
return 0;
}

struct file_operations fops = {
.read = read,
.write = write,
.open = open,
.release = release
};

int schd_init (void)
{
int ret;
struct device *dev_ret;
if ((ret = alloc_chrdev_region(&dev, FIRST_MINOR, MINOR_CNT, “wqd”)) < 0)
{
printk(“Major Nr: %d\n”, MAJOR(dev));

cdev_init(&c_dev, &fops);

if ((ret = cdev_add(&c_dev, dev, MINOR_CNT)) < 0)
{
unregister_chrdev_region(dev, MINOR_CNT);
return ret;
}
if (IS_ERR(cl = class_create(THIS_MODULE, “chardrv”)))
{
cdev_del(&c_dev);
unregister_chrdev_region(dev, MINOR_CNT);
return PTR_ERR(cl);
}
if (IS_ERR(dev_ret = device_create(cl, NULL, dev, NULL, “mychar%d”, 0)))
{
class_destroy(cl);
cdev_del(&c_dev);
unregister_chrdev_region(dev, MINOR_CNT);
return PTR_ERR(dev_ret);
}
return 0;
}

void schd_cleanup(void)
{
printk(KERN_INFO “ Inside cleanup_module\n”);
device_destroy(cl, dev);
class_destroy(cl);
cdev_del(&c_dev);
unregister_chrdev_region(dev, MINOR_CNT);
}

module_init(schd_init);
module_exit(schd_cleanup);

MODULE_LICENSE(“GPL”);
MODULE_AUTHOR(“Pradeep Tewani”);
MODULE_DESCRIPTION(“Waiting Process Demo”);

The example shown above is a simple character driver demonstrating the usage of schedule() API. schd_init() and schd_cleanup() are the initialisation and exit functions for the driver, respectively. Apart from these, there are open, close, read and write functions, which get invoked when the process performs the respective operations on the device file. One point worth noting is that in the read function, we invoke the schedule function. This in turn means that whenever the process performs the read operation on the device file, it would be voluntarily giving up the process. Let’s compile this program as a module and load it using the insmod command. Shown below is the sample run:

$ insmod wait.ko
Major Nr: 250
$ cat /dev/mychar0
Inside open
Inside read
Scheduling out
Woken up

Note: The above message won’t directly come on the shell. We will have to execute the dmesg command to view these messages.

Upon loading the driver, we will get the device file with a name mychar0 in /dev directory. Then, we use the cat command to perform the read operation. This, in turn, will invoke the schedule function. One thing that we notice from the sample run is that invoking the schedule() doesn’t really block the process, instead it immediately comes out of the wait. Why is it so? Is there something wrong with the schedule function? Hold on… if you recall the functionality of schedule, it gives up the processor, but doesn’t move the process out of the run queue. And as long as the process is in the run queue, it is scheduled to run during the next scheduling cycle. This means that in order to block the process, it needs to be moved out of the run queue. So, how do we move the process out of the run queue? This requires the process state to be changed from TASK_RUNNING to either TASK_INTERRUPTIBLE or TASK_UNINTERRUPTIBLE. For this, we have an API set_current_state(). Shown below is the modified version of the read function:

ssize_t read(struct file *filp, char *buff, size_t count, loff_t *offp)
{
printk(KERN_INFO “Inside read\n”);
printk(KERN_INFO “Scheduling out\n”);
set_current_state(TASK_INTERRUPTIBLE);
schedule();
printk(KERN_INFO “Woken up\n”);
return 0;
}

Recompile the program with this change and load the module. Given below is the sample run:

$ insmod wait.ko
Major Nr: 250
$ cat /dev/mychar0
Inside open
Inside read
Scheduling out

With this, we are able to get the process blocked. The next question is: how do we unblock the process? Since this process is blocked, there has to be some other process which can unblock this. So, does the kernel provide any mechanism to wake up the process? It definitely does! For this, we have an API wake_up_process() as shown below:

wake_up_process(task_struct *ts)
ts – pointer to task_struct of blocked process

Since we have not implemented the wake up functionality in the above example, the only way to unblock the process is to signal it. Pressing Ctrl + c will terminate the process. So now, let’s modify the above program to provide the wakeup functionality. Shown below is the modified code snippet:

staticstruct task_struct *sleeping_task;

ssize_t read(struct file *filp, char *buff, size_t count, loff_t *offp)
{
printk(KERN_INFO “Inside read\n”);
printk(KERN_INFO “Scheduling out\n”);
sleeping_task = current;
set_current_state(TASK_INTERRUPTIBLE);
schedule();
printk(KERN_INFO “Woken up\n”);
return 0;
}

ssize_t write(struct file *filp, constchar *buff, size_t count, loff_t *offp)
{
printk(KERN_INFO “Inside Write\n”);
wake_up_process(sleeping_task);
return count;
}

In the above example, we have modified the write function to wake up the blocked process. Since wake_up_process() API requires the pointer to the task_struct of the blocked process, we update the global variable sleeping_task with the task control block of the sleeping process.
Recompile the example with the above changes and load it using insmod. Shown below is what we get:

$ insmod wait.ko
Major Nr: 250
$ cat /dev/mychar0
Inside open
Inside read
Scheduling out

With the current process being blocked in one shell, open another shell and execute the command as shown below:

$ echo 1 > /dev/mychar0
Inside open
Inside write
Woken Up
Inside close
Inside close

So, here we have two processes – one (cat) blocked on the read call and another (echo) unblocking the first process by invoking the wake-up call. As soon as we execute the echo command, the first process comes out of the waiting mode.
What we have seen here is how to implement the basic wait mechanism in the driver. The process was being put to sleep unconditionally. But in real life scenarios, the process always waits on some valid event. Also, this is more like manual waiting and thereby prone to errors. So, how do we make our process wait on some event? How do we implement a robust wait mechanism in a driver? Does the kernel provide any mechanism for this? To find out the answers to these questions, stay tuned to my next article. Till then, good bye and happy waiting!

LEAVE A REPLY

Please enter your comment!
Please enter your name here