Continuing our journey with Linux kernel internals, the next few articles in this series will focus on wait mechanisms in kernel. Now, you might be wondering, why do we need to wait in Linux driver? Well, there can be quite a lot of reasons to wait in driver. Let’s say you are interfacing with hardware such as LCD, which requires you to wait for 5ms before sending a subsequent command. Another example is say you want to read the data from disk, and since disk is a slower device, it may require you to wait until valid data is available. In these scenarios, we have no option, but to wait. One of the simplest way to implement the wait is a busy loop, but it might not be efficient way of waiting. So, does kernel provide any efficient mechanisms to wait? Yes, of course, kernel does provide a variety of mechanisms for waiting. Read on to get the crux of waiting in Linux kernel.
Process States in Linux
Before moving on to the wait mechanisms, it would be worthwhile to understand the process states in Linux. At any point of time, a process can be in any of the below mentioned states:
- TASK_RUNNING :- Process is in run queue or is running
- TASK_STOPPED :- Process stopped by debugger
- TASK_INTERRUPTIBLE :- Process is waiting for some event, but can be woken up by signal
- TASK_UNINTERRUPTIBLE :- Similar to TASK_INTERRUPTIBLE, but can’t be woken up by signal
- TASK_ZOMBIE :- Process is terminated, but not cleaned up yet
For a process to be scheduled, it needs to be in TASK_RUNNING state, while TASK_INTERRUPTIBLE and TASK_INTERRUPTIBLE states correspond to a waiting process.
Wait Mechanism in Linux Kernel
API schedule() provides the basic wait mechanism in the linux kernel. Invoking this API yields the processor and invokes the scheduler to schedule any other process in run queue. Below is the programming example for the same:
#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) { return ret; } 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");
Example above is a simple character driver demonstrating the use of schedule() API. In read() function, we invoke schedule() to yield the processor. Below is the sample run, assuming that the above program is compiled as sched.ko:
$ insmod sched.ko Major Nr: 244 $ cat /dev/mychar0 Inside open Inside read Scheduling out Woken up
So, what do we get? Does the usage of schedule() API serves the purpose of waiting? Not really. Why is it so? Well, if you recall the definition of schedule(), it states that the process invoking this API voluntarily yields the processor, but only yielding the processor is not enough. Process is still in run queue and as long as process is in run queue, it would be scheduled again to run. This is exactly what happens in the above example, which makes our process to come out of wait quite immediately. So, the pre-condition for performing the wait with schedule() is to first move the process out of the run queue. How do we achieve this? For this, we have a API called set_current_state(). Below is the modified code snippet for 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; }
In the above example, before invoking the schedule() API, we are setting the state of the process to TASK_INTERRUPTIBLE. This will move the process out of run queue and hence it won’t be scheduled again to run. Below is the sample run for the modified example:
$ insmod sched.ko Major Nr: 250 $ cat /dev/mychar0 Inside open Inside read Scheduling out
Conclusion
So, finally we are able to get the process blocked. But do you see the problem with this code? This process is indefinitely blocked. When and who will wake this process up? Another thing worth noting is that in real life scenarios, process always waits on some event, but our process is put to an unconditional sleep. How do we make a process wait on an event? To find out the answer to these questions, stay tuned to my next article. Till then, Happy Waiting!
Pingback: Synchronization without Locking | Playing with Systems
Pingback: Waiting / Blocking in Linux Driver Part – 2 | Playing with Systems