Linux设备驱动中的阻塞和非阻塞I/O
阻塞和非阻塞I/O
阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时,并不挂起,它要么放弃,要么不停地查询,直至可以进行操作为止。
驱动程序通常需要提供这样的能力:当应用程序进行read()、write()等系统调用时,若设备的资源不能获取,而用户又希望以阻塞的方式访问设备,驱动程序应在设备驱动的xxx_read()、xxx_write()等操作中将进程阻塞直到资源可以获取,此后,应用程序的read()、write()等调用才返回,整个过程仍然进行了正确的设备访问,用户并没有感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的xxx_read()、xxx_write()等操作应立即返回,read()、write()等系统调用也随即被返回,应用程序收到-EAGAIN返回值。
下图中,在阻塞访问时,不能获取资源的进程将进入休眠,它将CPU资源“礼让”给其他进程。因为阻塞的进程会进入休眠状态,所以必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的“寿终就寝”了。唤醒进程的地方最大可能发生在中断里面,因为在硬件资源获得的同时往往伴随着一个中断。而非阻塞的进程则不断尝试,直到可以进行I/O。
以下代码分别演示了以阻塞和非阻塞方式读取串口一个字符的代码。前者在打开文件的时候没有O_NONBLOCK标记,后者使用O_NONBLOCK标记打开文件。
- char buf;
- fd = open("/dev/ttyS1", O_RDWR);
- /* code */
- res = read(fd, &buf, 1);
- if (res == 1)
- printf("%c\n", buf);
- char buf;
- fd = open("/dev/ttyS1", O_RDWR | O_NONBLOCK);
- /* code */
- while (read(fd, &buf, 1) != 1)
- continue;
- printf("%c\n", buf);
除了在打开文件时可以指定阻塞还是非阻塞方式以外,在文件打开后,也可以通过ioctl()和fcntl()改变读写的方式,如从阻塞变更为非阻塞或者从非阻塞变更为阻塞。例如,调用fcntl(fd, FSETFL, O_NONBLOCK)可以设置fd对应的I/O为非阻塞。
等待队列在Linux驱动程序中,可以使用等待队列(Wait Queue)来实现阻塞进程的唤醒。等待队列很早就作为一个基本的功能单位出现在Linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,可以用来同步对系统资源的访问,信号量在内核中也依赖等待队列来实现。
Linux内核提供了如下关于等待队列的操作。
1.定义“等待队列头部”
- wait_queue_head_t my_queue;
wait_queue_head_t是wait_queue_head结构体的一个typedef。
2.初始化“等待队列头部”
- init_waitqueue_head(&my_queue);
而下面的DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头部的“快捷方式”。
- DECLARE_WAIT_QUEUE_HEAD(name)
3.定义等待队列元素
- DECLARE_WAITQUEUE(name, tsk)
该宏用于定义并初始化一个名为name的等待队列元素。
4.添加/移除等待队列
- void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);
- void remove_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);
add_wait_queue()用于将等待队列元素wait添加到等待队列头部q指向的双向链表中,而remove_wait_queue()用于将等待队列元素wait从由q头部指向的链表中移除。
5.等待事件
- #define wait_event(wq_head, condition)
- #define wait_event_interruptible(wq_head, condition)
- #define wait_event_timeout(wq_head, condition, timeout)
- #define wait_event_interruptible_timeout(wq_head, condition, timeout)
等待第一个参数queue作为等待队列头部的队列被唤醒,而且第2个参数condition必须满足,否则继续阻塞。wait_event()和wait_event_interruptible()的区别在于后者可以被信号打断,而前者不行。加上_timeout后的宏意味着阻塞等待的超时时间,以jiffy为单位,在第3个参数的timeout到达时,不论condition是否满足,均返回。
6.唤醒队列
- wake_up(x)
- wake_up_interruptible(x)
上述操作会唤醒以queue作为等待队列头部的队列中所有的进程。
wake_up()应该与wait_event()或wait_event_timeout()成对使用,而wake_up_interruptible()则应与wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。wake_up()可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE的进程。
7.在等待队列上睡眠
在linux5版本中找不到sleep_on函数,暂留。
以下代码演示了一个在设备驱动中使用等待队列的模板,在进行写I/O操作的时候,判断设备是否可写,如果不可写且为阻塞I/O,则进程睡眠并挂起到等待队列。
- ssize_t xxx_write(struct file* filp, const char __user* buf, size_t count, loff_t* ppos)
- {
- /* code */
- DECLARE_WAITQUEUE(wait, current); /* 定义等待队列元素 */
- add_wait_queue(&xxx_wait, &wait); /* 添加元素到等到队列 */
- /* 等待设备缓冲区可写 */
- do
- {
- avail = device_writeable();
- if (avail < 0)
- {
- if (filp->f_flags & O_NONBLOCK) /* 非阻塞 */
- {
- ret = -EAGAIN;
- goto out;
- }
- __set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态 */
- schedule(); /* 调度其他进程执行 */
- if (signal_pending(current) ) /* 如果是因为信号唤醒 */
- {
- ret = -ERESTARTSYS;
- goto out;
- }
- }
- } while (avail < 0);
- /* 写设备缓冲区 */
- device_write();
- out:
- remove_wait_queue(&xxx_wait, &wait); /* 将元素移出xxx_wait指引的队列 */
- set_current_state(TASK_RUNNING); /* 设置进程状态TASK_RUNNING */
- return ret;
- }
读懂以上代码对理解Linux进程状态切换非常重要,所以提请反复阅读此段代码,直至完全领悟,几个要点如下。
1)如果是非阻塞访问(O_NONBLOCK被设置),设备忙时,直接返回-EAGAIN。
2)对于阻塞访问,会调用__set_current_state(TASK_INTERRUPTIBLE)进行进程状态切换并显式通过schedule()调度其他进程执行。
3)醒来的时候要注意,由于调度出去的时候,进程状态是TASK_INTERRUPTIBLE,即浅度睡眠,所以唤醒它的有可能是信号,因此,我们首先通过signal_pending(current)了解是不是信号唤醒的,如果是,立即返回-ERESTARTSYS。
DECLARE_WAITQUEUE、add_wait_queue这两个动作加起来完成的效果如下图所示。在wait_queue_head_t指向的链表上,新定义的wait_queue元素被插入,而这个新插入的元素绑定了一个task_struct(当前做xxx_write的current,这也是DECLARE_WAITQUEUE使用current作为参数的原因)。
支持阻塞操作的globalfifo设备驱动现在我们给globalmem增加这样的约束:把globalmem中的全局内存变成一个FIFO,只有当FIFO中有数据的时候(即有进程把数据写到这个FIFO而且没有被读进程读空),读进程才能把数据读出,而且读取后的数据会从globalmem的全局内存中被拿掉;只有当FIFO不是满的时(即还有一些空间未被写,或写满后被读进程从这个FIFO中读出了数据),写进程才能往这个FIFO中写入数据。
现在,将globalmem重命名为globalfifo,在globalfifo中,读FIFO将唤醒FIFO的进程(如果之前FIFO正好是满的),而写进程也将唤醒读FIFO的进程(如果之前FIFO正好是空的)。首先,需要修改设备结构体,在其中增加两个等待队列头部,分别对应于读和写,如下代码所示。
- struct globalfifo_dev
- {
- struct cdev cdev;
- unsigned int current_len;
- unsigned char mem[GLOBALFIFO_SIZE];
- struct mutex mutex;
- wait_queue_head_t r_wait;
- wait_queue_head_t w_wait;
- };
与globalfifo设备结构体的另一个不同是增加了current_len成员以用于表征目前FIFO中有效数据的长度。current_len等于0意味着FIFO空,current_len等于GLOBALFIFO_SIZE意味着FIFO满。
这两个等待队列头部需在设备模块加载函数中调用init_waitqueue_head()被初始化,新的设备驱动模块加载函数如下代码所示。
- static int __init globalfifo_init(void)
- {
- int ret;
- dev_t devno = MKDEV(globalfifo_major, 0);
- if (globalfifo_major)
- {
- ret = register_chrdev_region(devno, 1, "globalfifo");
- }
- else
- {
- ret = alloc_chrdev_region(&devno, 0, 1, "globalfifo");
- globalfifo_major = MAJOR(devno);
- }
- if (ret < 0)
- {
- return ret;
- }
- globalfifo_devp = kzalloc(sizeof(struct globalfifo_dev), GFP_KERNEL);
- if (!globalfifo_devp)
- {
- ret = -ENOMEM;
- goto fail_malloc;
- }
- globalfifo_setup_cdev(globalfifo_devp, 0);
- mutex_init(&globalfifo_devp->mutex);
- init_waitqueue_head(&globalfifo_devp->r_wait);
- init_waitqueue_head(&globalfifo_devp->w_wait);
- return 0;
- fail_malloc:
- unregister_chrdev_region(devno, 1);
- return ret;
- }
- module_init(globalfifo_init);
设备驱动读写操作需要被修改,在读函数中需要增加唤醒w_wait等待队列的语句,而在写操作中唤醒r_wait等待队列的语句,如下代码所示。
- static ssize_t globalfifo_read(struct file* filp, char __user* buf, size_t count, loff_t* ppos)
- {
- int ret;
- struct globalfifo_dev* dev = filp->private_data;
- DECLARE_WAITQUEUE(wait, current);
- mutex_lock(&dev->mutex);
- add_wait_queue(&dev->r_wait, &wait);
- while (dev->current_len == 0)
- {
- if (filp->f_flags & O_NONBLOCK)
- {
- ret = -EAGAIN;
- goto out;
- }
- __set_current_state(TASK_INTERRUPTIBLE);
- mutex_unlock(&dev->mutex);
- schedule();
- if (signal_pending(current) )
- {
- ret = -ERESTARTSYS;
- goto out2;
- }
- mutex_lock(&dev->mutex);
- }
- if (count > dev->current_len)
- {
- count = dev->current_len;
- }
- if (copy_to_user(buf, dev->mem, count) )
- {
- ret = -EFAULT;
- goto out;
- }
- else
- {
- memcpy(dev->mem, dev->mem + count, dev->current_len - count);
- dev->current_len -= count;
- printk(KERN_INFO "read %d bytes(s), current len = %d\n", count, dev->current_len);
- wake_up_interruptible(&dev->w_wait);
- ret = count;
- }
- out:
- mutex_unlock(&dev->mutex);
- out2:
- remove_wait_queue(&dev->w_wait, &wait);
- set_current_state(TASK_RUNNING);
- return ret;
- }
- ssize_t globalfifo_write(struct file* filp, const char __user* buf, size_t count, loff_t* ppos)
- {
- struct globalfifo_dev* dev = filp->private_data;
- int ret;
- DECLARE_WAITQUEUE(wait, current);
- mutex_lock(&dev->mutex);
- add_wait_queue(&dev->w_wait, &wait);
- while (dev->current_len == GLOBALFIFO_SIZE)
- {
- if (filp->f_flags & O_NONBLOCK)
- {
- ret = -EAGAIN;
- goto out;
- }
- __set_current_state(TASK_INTERRUPTIBLE);
- mutex_unlock(&dev->mutex);
- schedule();
- if (signal_pending(current))
- {
- ret = -ERESTARTSYS;
- goto out2;
- }
- mutex_lock(&dev->mutex);
- }
- if (count > GLOBALFIFO_SIZE - dev->current_len)
- {
- count = GLOBALFIFO_SIZE - dev->current_len;
- }
- if (copy_from_user(dev->mem + dev->current_len, buf, count) )
- {
- ret = -EFAULT;
- goto out;
- }
- else
- {
- dev->current_len += count;
- printk(KERN_INFO "written %d byte(s), current len = %d\n", count, dev->current_len);
- wake_up_interruptible(&dev->r_wait);
- ret = count;
- }
- out:
- mutex_unlock(&dev->mutex);
- out2:
- remove_wait_queue(&dev->w_wait, &wait);
- set_current_state(TASK_RUNNING);
- return ret;
- }
globalfifo_read()通过第5行和第8行将自己加到了r_wait这个队列里面,但此时读的进程并未睡眠,之后第18行调用__set_current_state(TASK_INTERRUPTIBLE)时,也只是标记了task_struct的一个浅度睡眠标记,并未真正睡眠,直到21行调用schedule(),读进程进入睡眠。进行完读操作后,第47行调用wake_up_interruptible(&dev->w_wait)唤醒可能阻塞的写进程。globalfifo_write()的过程与之类似。
关注代码的19行和78行,无论是读函数还是写函数,进入schedule()把自己切换出去之前,都主动释放了互斥体。原因是如果读进程阻塞,实际意味着FIFO空,必须依赖写的进程往FIFO里面写东西来唤醒它,但是写的进程为了写FIFO,它必须拿到这个互斥体来访问FIFO这个临界资源,如果读进程把自己调度出去之前不释放这个互斥体,那么写进程之间就死锁了。所谓死锁,就是多个进程循环等待他方占有的资源而无期限地僵持下去。如果没有外力的作用,那么死锁涉及的各个进程将永远处于封锁状态。因此驱动工程师一定要注意:当多个等待队列、信号量、互斥体等机制同时出现时,谨慎死锁!
现在回过来了看一下代码的第12行和71行,发现在设备驱动的read()、write()等功能函数中,可以通过filp->f_flags标志获得用户空间是否要求非阻塞访问。驱动中可以依据此标志判断用户究竟要求阻塞还是非阻塞访问,从而进行不同的处理。
代码中还有一个关键点,就是无论读函数还是写函数,在进行真正的读写之前,都要再次判断设备是否可以读写,见第10行和第69行。主要目的是为了让并发的读或者写都正确。设想如果两个读进程都阻塞在读上,写进程执行的wake_up_interruptible实际会同时唤醒它们,其中先执行的那个进程可能会率先将FIFO再次读空!
在用户空间验证globalfifo的读写与之前的操作一样:
- $ sudo insmod main.ko
- $ cat /proc/devices
- 231 globalmem
- $ sudo mknod -m 0666 /dev/globalmem c 231 0
然后启用两个控制台,都执行cat命令;再启用一个进程执行echo命令。每当echo进程写入一串数据,cat进程就立即将该串数据显示出来。
其他记录1. 记录完整代码如下。
- #include <linux/module.h>
- #include <linux/types.h>
- #include <linux/sched.h>
- #include <linux/init.h>
- #include <linux/cdev.h>
- #include <linux/slab.h>
- #include <linux/poll.h>
- #include <linux/interrupt.h>
- #include <linux/sched/signal.h>
- #define FIFO_CLEAR 0x01
- #define GLOBALFIFO_SIZE 0x1000
- #define GLOBALFIFO_MAJOR 231
- static int globalfifo_major = GLOBALFIFO_MAJOR;
- module_param(globalfifo_major, int, S_IRUGO);
- struct globalfifo_dev
- {
- struct cdev cdev;
- unsigned int current_len;
- unsigned char mem[GLOBALFIFO_SIZE];
- struct mutex mutex;
- wait_queue_head_t r_wait;
- wait_queue_head_t w_wait;
- };
- struct globalfifo_dev* globalfifo_devp;
- static int globalfifo_open(struct inode* inode, struct file* filp)
- {
- filp->private_data = globalfifo_devp;
- return 0;
- }
- static int globalfifo_release(struct inode* inode, struct file* filp)
- {
- return 0;
- }
- static long globalfifo_ioctl(struct file* filp, unsigned int cmd, unsigned long arg)
- {
- struct globalfifo_dev* dev = filp->private_data;
- switch (cmd)
- {
- case FIFO_CLEAR:
- mutex_lock(&dev->mutex);
- dev->current_len = 0;
- memset(dev->mem, 0, GLOBALFIFO_SIZE);
- mutex_unlock(&dev->mutex);
- printk(KERN_INFO "globalfifo is set to zero\n");
- break;
- default:
- return -EINVAL;
- }
- return 0;
- }
- static ssize_t globalfifo_read(struct file* filp, char __user* buf, size_t count, loff_t* ppos)
- {
- int ret;
- struct globalfifo_dev* dev = filp->private_data;
- DECLARE_WAITQUEUE(wait, current);
- mutex_lock(&dev->mutex);
- add_wait_queue(&dev->r_wait, &wait);
- while (dev->current_len == 0)
- {
- if (filp->f_flags & O_NONBLOCK)
- {
- ret = -EAGAIN;
- goto out;
- }
- __set_current_state(TASK_INTERRUPTIBLE);
- mutex_unlock(&dev->mutex);
- schedule();
- if (signal_pending(current) )
- {
- ret = -ERESTARTSYS;
- goto out2;
- }
- mutex_lock(&dev->mutex);
- }
- if (count > dev->current_len)
- {
- count = dev->current_len;
- }
- if (copy_to_user(buf, dev->mem, count) )
- {
- ret = -EFAULT;
- goto out;
- }
- else
- {
- memcpy(dev->mem, dev->mem + count, dev->current_len - count);
- dev->current_len -= count;
- printk(KERN_INFO "read %d bytes(s), current len = %d\n", count, dev->current_len);
- wake_up_interruptible(&dev->w_wait);
- ret = count;
- }
- out:
- mutex_unlock(&dev->mutex);
- out2:
- remove_wait_queue(&dev->w_wait, &wait);
- set_current_state(TASK_RUNNING);
- return ret;
- }
- ssize_t globalfifo_write(struct file* filp, const char __user* buf, size_t count, loff_t* ppos)
- {
- struct globalfifo_dev* dev = filp->private_data;
- int ret;
- DECLARE_WAITQUEUE(wait, current);
- mutex_lock(&dev->mutex);
- add_wait_queue(&dev->w_wait, &wait);
- while (dev->current_len == GLOBALFIFO_SIZE)
- {
- if (filp->f_flags & O_NONBLOCK)
- {
- ret = -EAGAIN;
- goto out;
- }
- __set_current_state(TASK_INTERRUPTIBLE);
- mutex_unlock(&dev->mutex);
- schedule();
- if (signal_pending(current))
- {
- ret = -ERESTARTSYS;
- goto out2;
- }
- mutex_lock(&dev->mutex);
- }
- if (count > GLOBALFIFO_SIZE - dev->current_len)
- {
- count = GLOBALFIFO_SIZE - dev->current_len;
- }
- if (copy_from_user(dev->mem + dev->current_len, buf, count) )
- {
- ret = -EFAULT;
- goto out;
- }
- else
- {
- dev->current_len += count;
- printk(KERN_INFO "written %d byte(s), current len = %d\n", count, dev->current_len);
- wake_up_interruptible(&dev->r_wait);
- ret = count;
- }
- out:
- mutex_unlock(&dev->mutex);
- out2:
- remove_wait_queue(&dev->w_wait, &wait);
- set_current_state(TASK_RUNNING);
- return ret;
- }
- static const struct file_operations globalfifo_fops =
- {
- .owner = THIS_MODULE,
- .read = globalfifo_read,
- .write = globalfifo_write,
- .unlocked_ioctl = globalfifo_ioctl,
- .open = globalfifo_open,
- .release = globalfifo_release,
- };
- static void globalfifo_setup_cdev(struct globalfifo_dev* dev, int index)
- {
- int err, devno = MKDEV(globalfifo_major, index);
- cdev_init(&dev->cdev, &globalfifo_fops);
- dev->cdev.owner = THIS_MODULE;
- err = cdev_add(&dev->cdev, devno, 1);
- if (err)
- {
- printk(KERN_NOTICE "Error %d : adding globalfifo%d", err, index);
- }
- }
- static int __init globalfifo_init(void)
- {
- int ret;
- dev_t devno = MKDEV(globalfifo_major, 0);
- if (globalfifo_major)
- {
- ret = register_chrdev_region(devno, 1, "globalfifo");
- }
- else
- {
- ret = alloc_chrdev_region(&devno, 0, 1, "globalfifo");
- globalfifo_major = MAJOR(devno);
- }
- if (ret < 0)
- {
- return ret;
- }
- globalfifo_devp = kzalloc(sizeof(struct globalfifo_dev), GFP_KERNEL);
- if (!globalfifo_devp)
- {
- ret = -ENOMEM;
- goto fail_malloc;
- }
- globalfifo_setup_cdev(globalfifo_devp, 0);
- mutex_init(&globalfifo_devp->mutex);
- init_waitqueue_head(&globalfifo_devp->r_wait);
- init_waitqueue_head(&globalfifo_devp->w_wait);
- return 0;
- fail_malloc:
- unregister_chrdev_region(devno, 1);
- return ret;
- }
- module_init(globalfifo_init);
- static void __exit globalfifo_exit(void)
- {
- cdev_del(&globalfifo_devp->cdev);
- kfree(globalfifo_devp);
- unregister_chrdev_region(MKDEV(globalfifo_major, 0), 1);
- }
- module_exit(globalfifo_exit);
2. rmmod ko文件可以移除指定模块。
3. Visual Studio Code中按Ctrl+Shift+P键可以选择编辑文件指定头文件。以下记录当前代码指定的头文件。
- "/usr/src/linux-headers-5.3.0-53/include/",
- "/usr/src/linux-headers-5.3.0-53/arch/x86/include/",
- "/usr/src/linux-headers-5.3.0-53/include/uapi/",
- "/usr/src/linux-headers-5.3.0-53/arch/ia64/include/",
- "/usr/src/linux-headers-5.3.0-53-generic/include/",
- "/usr/src/linux-headers-5.3.0-53-generic/arch/x86/include/generated/"
总结和思考
通过这个例子了解了如何设置进程状态,阻塞的实现是使进程处于睡眠状态。同时也了解了fifo的锁实现,之前了解到linux下有fifo管道文件,可能实现原理有相通之处。
同时,这章也留下一处疑惑。同时开两个进程cat,往设备文件里写内容时,总是第一个读进程获得内容,希望之后学习能了解为什么这样(感觉可能和等待队列有关)。