T12 阻塞与非阻塞IO
1 阻塞与非阻塞
阻塞I/O与非阻塞I/O是设备访问的两种不同模式,驱动程序可以灵活的支持用户空间对设备的这两种访问方式。
- 阻塞操作:指的是执行设备操作的时候如果不能获得资源,那么就挂起进程直到满足操作条件之后再进行操作。被挂起的进程进而进入休眠状态,被调度器移走,直到能够获取资源后才继续执行
- 非阻塞操作:进程在不能进行设备操作的时候并不会挂起,它可能直接返回或者放弃,或者不断地查询,直到可以进行操作。非阻塞应用程序通常使用
select
系统调用查询是否可以对设备进行无阻塞的访问最终会引发设备驱动中poll
函数执行 - 注意:阻塞与非阻塞是设备访问的两种方式,在写阻塞与非阻塞驱动的时候,经常用到等待队列
2 等待队列
- 在linux设备驱动中阻塞进程可以使用等待队列来实现。
- 在内核中,等待队列是有很多用处的,尤其是在中断处理、进程同步、定时等场合,可以使用等待队列实现阻塞进程的唤醒。它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问
2.1 等待队列API
- 在linux中等待队列的结构如下:
struct __wait_queue_head {
spinlock_t lock; //自旋锁,用来对task_list链表起保护作用,实现了等待队列的互斥访问
struct list_head task_list; //用来存放等待的进程,
};
typedef struct __wait_queue_head wait_queue_head_t;
- API
/* 定义等待队列 */
wait_queue_head_t wait;
/* 初始化等待队列 */
init_waitqueue_head(&wait);
/* 定义并初始化等待队列 */
#define DECLARE_WAIT_QUEUE_HEAD(name) wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
/* 添加等待队列,将等待队列元素wait添加到等待队列队头q所指向的等待队列链表中 */
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
/* 移除等待队列 */
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
/* 等待事件,在等待队列中睡眠直到condition为真 */
/* 注意:
@queue:作为等待队列头的等待队列被唤醒
@condition:必须为真,否则就阻塞
@timeout:相较于condition,timeout有更高的优先级
*/
wait_event(wq, condition);
wait_evevt_timeout(wq, condition, timeout);
wait_event_interruptible(wq, condition);
wait_event_interruptible_timeout(wq, condition, timeout);
/* 睡眠,其中sleep_on作用是将目前进程的状态设置为TASK_UNINTERRUPTIBLE,直到资源可用,q引导的等待队列被唤醒;
interruptible_sleep_on是将进程状态设置为TASK_INTERRUPTIBLE
*/
sleep_on(wait_queue_head_t *q);
interruptible_sleep_on(wait_queue_head_t *q);
/* 唤醒等待队列,可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程 */
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
/* 只能唤醒处于TASK_INTERRUPTIBLE状态的进程 */
#define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
2.2 睡眠、阻塞、挂起解释
对线程的控制好比一个老板招募了一个雇工干活。
- 挂起:好比老板对雇工说:“你睡觉去吧,等用得着你的时候我会去主动叫你,然后你接着干”
- 睡眠:好比老板对雇工说:“你睡觉去吧,某时某刻之后来报道,然后接着干”
- 阻塞:老板突然发现雇工在睡觉,原因是因为雇工的笤帚被偷了(无资源),然后你也没让雇工干其他的事情,他就只好睡觉。等雇工找到笤帚之后,他将继续干活
2.3 等待队列示例
- 驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <asm/uaccess.h>
#include <linux/slab.h>
#include <linux/miscdevice.h>
#include <linux/wait.h>
#include "cdev_test_v4.h"
#define BUF_SIZR 1024
#define MAJOR_NUM 168
#define MINOR_NUM 0
struct demo_cdev {
char *buffer; //my private memory
int value;
struct miscdevice *mdev;
wait_queue_head_t queue_head;
bool is_empty;
};
/* step1: malloc memory for char device */
struct demo_cdev *demo_dev;
static ssize_t demo_read(struct file *file, char __user *buf, size_t size, loff_t *pos)
{
int ret = -1;
int read_bytes;
struct demo_cdev *demo = file->private_data;
char *kbuf = demo->buffer + *pos;
printk(KERN_INFO "Enter: %s\n", __func__);
/* determine whether user has finished reading data */
if (*pos >= BUF_SIZR) {
return 0;
}
if (size > (BUF_SIZR - *pos)) {
read_bytes = BUF_SIZR - *pos;
} else {
read_bytes = size;
}
if (demo->is_empty) {
/* make this process sleep */
wait_event_interruptible(demo->queue_head, !(demo->is_empty));
}
ret = copy_to_user(buf, kbuf, read_bytes);
if (ret != 0) {
return -EFAULT;
}
*pos += read_bytes;
return read_bytes;
}
static ssize_t demo_write(struct file *file, const char __user *buf, size_t size, loff_t *pos)
{
int ret;
int write_bytes;
struct demo_cdev *demo = file->private_data;
char *kbuf = demo->buffer + *pos;
printk(KERN_INFO "Enter: %s\n", __func__);
if (*pos >= BUF_SIZR) {
return 0;
}
if (size > (BUF_SIZR - *pos)) {
write_bytes = BUF_SIZR - *pos;
} else {
write_bytes = size;
}
ret = copy_from_user(kbuf, buf, write_bytes);
if (ret != 0) {
return -EFAULT;
}
demo->is_empty = false;
/* wake up this process */
wake_up(&demo->queue_head);
*pos += write_bytes;
return write_bytes;
}
static int demo_open(struct inode *node, struct file *file)
{
printk(KERN_INFO "Enter: %s\n", __func__);
file->private_data = (void *)demo_dev;
return 0;
}
static int demo_release(struct inode *node, struct file *file)
{
printk(KERN_INFO "Enter: %s\n", __func__);
return 0;
}
static long demo_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
int ret = 0;
struct demo_cdev *demo = file->private_data;
switch(cmd) {
case CDEV_TEST_V4_CLEAN:
memset(demo->buffer, 0x00, BUF_SIZR);
printk(KERN_INFO "cmd: clean\n");
break;
case CDEV_TEST_V4_GETVAL:
put_user(demo->value, (int *)arg);
printk(KERN_INFO "cmd: getval\n");
break;
case CDEV_TEST_V4_SETVAL:
demo->value = (int)arg;
printk(KERN_INFO "cmd: setval\n");
break;
default:
break;
}
return (long)ret;
}
static struct file_operations demo_operation= {
.open = demo_open,
.release = demo_release,
.read = demo_read,
.write = demo_write,
.unlocked_ioctl = demo_ioctl,
};
static struct miscdevice misc_struct = {
.minor = MISC_DYNAMIC_MINOR,
.name = "misc_dev",
.fops = &demo_operation,
};
static int __init demo_init(void)
{
int ret = -1;
printk(KERN_INFO "Enter: %s\n", __func__);
demo_dev = (struct demo_cdev *)kmalloc(sizeof(struct demo_cdev), GFP_KERNEL);
if (!demo_dev) {
printk(KERN_ERR "failed to malloc demo_dev\n");
ret = -ENOMEM;
goto ERROR_MALLOC_DEMODEV;
}
demo_dev->value = 1;
demo_dev->buffer = (char *)kmalloc(BUF_SIZR, GFP_KERNEL);
if (!demo_dev->buffer) {
printk(KERN_ERR "malloc %d bytes failed\n", BUF_SIZR);
ret = -ENOMEM;
goto ERROR_MALLOC_BUFFER;
}
memset(demo_dev->buffer, 0x00, BUF_SIZR);
demo_dev->is_empty = true;
init_waitqueue_head(&demo_dev->queue_head);
demo_dev->mdev = &misc_struct;
ret = misc_register(demo_dev->mdev);
if (ret < 0) {
printk(KERN_ERR "failed to register misc\n");
goto ERROR_MISC;
}
printk(KERN_INFO "demo char device init done\n");
return 0;
ERROR_MISC:
kfree(demo_dev->buffer);
demo_dev->buffer = NULL;
ERROR_MALLOC_BUFFER:
kfree(demo_dev);
demo_dev = NULL;
ERROR_MALLOC_DEMODEV:
return ret;
}
static void __exit demo_exit(void)
{
printk(KERN_INFO "Enter: %s\n", __func__);
misc_deregister(demo_dev->mdev);
kfree(demo_dev->buffer);
demo_dev->buffer = NULL;
kfree(demo_dev);
demo_dev = NULL;
printk(KERN_INFO "demo char device exit done\n");
}
module_init(demo_init);
module_exit(demo_exit);
MODULE_AUTHOR("Qi Han");
MODULE_LICENSE("GPL");
- 调试过程
hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/cdev_test$ sudo insmod wait_queue.ko
[sudo] hq 的密码:
hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/cdev_test$ sudo chmod 666 /dev/misc_dev # 此处会阻塞直到有数据
hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/cdev_test$ cat /dev/misc_dev
Peng fei tan -
# 写入数据唤醒阻塞的进程
hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/cdev_test$ echo "Peng fei tan" -> /dev/misc_dev
hq@hq-virtual-machine:~/桌面/linux-5.8/drivers/study_driver_hq/cdev_test$
3 环形队列FIFO
3.1 老式设备驱动不足与改进
- 不足:原来的驱动设备在驱动的类里面维护了一段缓存空间,大小为1024个字节,其中读取代码的数据如下,如果单次读写不超过
BUF_SIZE
大小个字节的数据,那么可以正常使用下述读写方法。假如单次读写数据量超过了BUF_SIZE
大小个数据量的话将会导致数据无法正常地写入与读出,主要是因为偏移量的问题,即*pos += read_bytes;
,当读取超过1024个字节数据的时候,偏移量就会大于1024,那么会导致数组越界,因此我们可以使用环形队列FIFO来储存数据,当偏移量pos大于1024的时候会重新从0开始偏移
#define BUF_SIZR 1024
struct demo_cdev {
char *buffer; //my private memory
...
};
static ssize_t demo_read(struct file *file, char __user *buf, size_t size, loff_t *pos)
{
int ret = -1;
int read_bytes;
struct demo_cdev *demo = file->private_data;
char *kbuf = demo->buffer + *pos;
/* determine whether user has finished reading data */
/* 判断当前读取的数据量是否大于buffer所容纳的数据量 */
if (*pos >= BUF_SIZR) {
return 0;
}
/* 判断当前读取的数据量是否大于buffer中剩余的数据量 */
if (size > (BUF_SIZR - *pos)) {
read_bytes = BUF_SIZR - *pos;
} else {
read_bytes = size;
}
ret = copy_to_user(buf, kbuf, read_bytes);
if (ret != 0) {
return -EFAULT;
}
*pos += read_bytes;
return read_bytes;
}
static ssize_t demo_write(struct file *file, const char __user *buf, size_t size, loff_t *pos)
{
int ret;
int write_bytes;
struct demo_cdev *demo = file->private_data;
char *kbuf = demo->buffer + *pos;
/* 判断当前写入的数据量是否大于buffer所容纳的数据量 */
if (*pos >= BUF_SIZR) {
return 0;
}
/* 判断当前写入的数据量是否大于buffer所剩余的数据量 */
if (size > (BUF_SIZR - *pos)) {
write_bytes = BUF_SIZR - *pos;
} else {
write_bytes = size;
}
ret = copy_from_user(kbuf, buf, write_bytes);
if (ret != 0) {
return -EFAULT;
}
*pos += write_bytes;
return write_bytes;
}
- 改进:我们可以添加2个指针和等待队列,一个读指针和一个写指针,刚开始读写指针均指向环形队列的队头,当读进程读取数据的时候如果发现队列中没有数据,那么读进程将阻塞;同理写进程向队列中写入数据的时候如果发现队列满了,那么写进程需阻塞
- 假设我们先写了4字节数据,那么写指针将会向前偏移4个单位,进而当读进程读取数据的时候可以用写指针减去读指针(此时写指针较读指针向前偏移了4个单位,即表示队列中有4字节数据可读),当读进程读取完4字节数据之后,读指针也需要偏移4个单位,那么此时读写指针将会重合,即表述队列中无数据可读
3.2 环形队列实现
- 驱动代码