之前我们了解了非阻塞型I/O,就是遇到不可用资源的时候会立刻返回,那么对于阻塞型I/O的话就是遇到不可用的资源后,不会立刻返回,而是会在那里等待,等到资源可用的时候再去做操作。具体来说就是,如果进程发现资源不可用,主动会将自己的状态设置为TASK_UNINTERRUPTIBLE或者TASK_INTERRUPTIBLE,然后见自己加入一个驱动所维护的等待列队中,最后调用schedule主动放弃CPU,操作系统将从运行列队上移除,并调度其他的进程执行。对于这样一个比较谦逊的进程,内核是非常喜欢的,这也是将这种I/O方式设置成默认方式的原因。相比与非阻塞型I/O,其最大的有点就是资源不可用时,不占用CPU的时间,而阻塞型I/O必须要定期尝试,看看资源是否可以使用,这对于键盘、鼠标这类设备来说,其效率是非常低的。但是阻塞型I/O也有一个明显的缺点,那就是对于进程在休眠期间再也无法做其他事情了。
既然有休眠,那应该有对应的唤醒操作,否则进程将会一直休眠下去。驱动程序应该在资源可用是负责执行唤醒操作。比如读进程休眠了,那么对于虚拟串口(FIFO)而言,写进程就应该负责唤醒操作,在真实的串口设备中,通常应该是中断处理负责唤醒。例如当串口收到了数据,产生一个中断,为了让程序更友好,休眠的进程应该能够被信号唤醒,这在资源不可获得,但却想要撤销休眠时,是比较有用的,通常也是一种比较推荐的方法。另外,我们也可以指定进程休眠的最长时间,超时后程序自动苏醒。
从上面的描述中可以发现,要实现阻塞操作,最重要的数据结构是等待列队。等待列队头的数据类型是wait_queue_head_t,列队节点数据类型是wait_queue_t,围绕等待列队有很多宏和函数,如下
DECLARE_WAIT_QUEUE_HEAD(name)
init_waitqueue_head(q)
wait_event(wq, condition)
wait_event_timeout(wq, condition, timeout)
wake_up(x)
wait_event_interruptible(wq, condition)
wait_event_interruptible_timeout(wq, condition, timeout)
wake_up_interruptible(x)
wait_event_interruptible_locked(wq, condition)
wait_event_interruptible_locked_irq(wq, condition)
wait_event_interruptible_exclusive_locked(wq, condition)
wait_event_interruptible_exclusive_locked_irq(wq, condition)
wake_up_locked(x)
DECLARE_WAIT_QUEUE_HEAD静态定义了一个等待队列头,init_waitqueue_head用于初始化一个等待列对头。wait_event是在条件condition不成立的情况下将当前进程放入到等待队列并休眠的基本操作。他拥有非常多的变体,timeout表示超时限制,interruptiable表示进程在休眠时可以通过信号来唤醒;exclusive表示该进程具有排他性,默认情况下,唤醒操作将唤醒等待列队中所有进程,但是如果一个进程以排他的方式休眠,那么唤醒操作在唤醒这个进程后不会继续唤醒其他进程;locked要求在调用前先获得等待列队内部的锁,irq要求在上锁的同时禁止中断。这些函数如果不带timeout,那么返回0表示被成功唤醒,返回-ERESTARTSYS表示被信号唤醒,如果带timeout,返回0表示超时,返回大于0的值表示被成功唤醒,这个值是离超时还剩余的时间。他们唤醒有对应关系,简单说,带locked的用wake_up_locked唤醒,不带locked而带interruptiable的用wake_up_interruptible来唤醒,否则用wake_up唤醒。相关函数可以查看“include/linux/wait.h”。
/*************************************************************************
> File Name: blockio.c
> Author: longway.bai
> Mail: 953821672@qq.com
> Created Time: 2021年12月19日 星期日 14时12分50秒
************************************************************************/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kfifo.h>
#define CHRDEV_MAJOR 256
#define CHRDEV_MINOR 0
#define CHRDEV_CNT 1
#define CHRDEV_NAME "vser"
//static struct cdev vsdev;
struct vser_dev {
struct cdev cdev;
wait_queue_head_t rwqh;
wait_queue_head_t wwqh;
struct kfifo *fifo;
};
static struct vser_dev vsdev;
DEFINE_KFIFO(vsfifo, char, 32);
static int vser_open(struct inode *inode, struct file *filp)
{
filp->private_data = container_of(inode->i_cdev, struct vser_dev, cdev);
return 0;
}
static int vser_release(struct inode *inode, struct file *filp)
{
return 0;
}
static ssize_t vser_read(struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
int ret;
unsigned int copied;
struct vser_dev *dev = filp->private_data;
if(kfifo_is_empty(dev->fifo))
{
/*用户层已非阻塞的方式读,且kfifo已经empty的话则返回error*/
if(filp->f_flags & O_NONBLOCK)
return -EAGAIN;
/*fifo为空的情况下进程休眠,知道fifo不为空或者接收到信号才被唤醒,如果是被信号唤醒,则返回-ERESTARTSYS*/
if(wait_event_interruptible(dev->rwqh, !kfifo_is_empty(dev->fifo)))
return -ERESTARTSYS;
}
ret = kfifo_to_user(dev->fifo, buf, count, &copied);
/*当fifo不满的时候唤醒所有等待的写进程*/
if (!kfifo_is_full(dev->fifo))
wake_up_interruptible(&dev->wwqh);
return ret == 0 ? copied : ret;
}
static ssize_t vser_write(struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
int ret;
unsigned int copied;
struct vser_dev *dev = filp->private_data;
if(kfifo_is_full(dev->fifo))
{
/*用户层已非阻塞的方式写,且kfifo已经full的话则返回error*/
if (filp->f_flags & O_NONBLOCK)
return -EAGAIN;
/*fifo为满的情况下进程休眠,知道fifo不满或者接收到信号才被唤醒,如果是被信号唤醒,则返回-ERESTARTSYS*/
if(wait_event_interruptible(dev->wwqh, !kfifo_is_full(dev->fifo)))
return -ERESTARTSYS;
}
ret = kfifo_from_user(dev->fifo, buf, count, &copied);
/*当fifo不空的时候唤醒所有等待的读进程*/
if (!kfifo_is_empty(dev->fifo))
wake_up_interruptible(&dev->rwqh);
return ret == 0 ? copied : ret;
}
static struct file_operations vser_ops = {
.owner = THIS_MODULE,
.open = vser_open,
.release = vser_release,
.read = vser_read,
.write = vser_write,
};
static int __init vser_init(void)
{
int ret;
dev_t dev;
dev = MKDEV(CHRDEV_MAJOR, CHRDEV_MINOR);
ret = register_chrdev_region(dev, CHRDEV_CNT, CHRDEV_NAME);
if (ret)
goto reg_err;
cdev_init(&vsdev.cdev, &vser_ops);
vsdev.cdev.owner = THIS_MODULE;
ret = cdev_add(&vsdev.cdev, dev, CHRDEV_CNT);
if (ret)
goto add_err;
vsdev.fifo = (struct kfifo*)&vsfifo;
/*初始化一个等待列头*/
init_waitqueue_head(&vsdev.rwqh);
init_waitqueue_head(&vsdev.wwqh);
return 0;
add_err:
unregister_chrdev_region(dev, CHRDEV_CNT);
reg_err:
return ret;
}
static void __exit vser_exit(void)
{
dev_t dev;
dev = MKDEV(CHRDEV_MAJOR, CHRDEV_MINOR);
cdev_del(&vsdev.cdev);
unregister_chrdev_region(dev, CHRDEV_CNT);
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("longway<longway.bai@outlook.com>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");
测试结果
# cat /dev/vser
^C
# cat /dev/vser &
[2] 4853
# echo "sssssssssssssssssssss" > /dev/vser
# sssssssssssssssssssss
# echo "hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhssss" > /dev/vser
hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh
# hhhhhhssss
# fg
cat /dev/vser
^C
# echo "hhhhhhhhhhhhhhhhhhhhhhhhhhsssssssssssssssss" > /dev/vser
^C
# cat /dev/vser
hhhhhhhhhhhhhhhhhhhhhhhhhhssssss
以上我们发现,当fifo为空的时候,读的时候进程会卡住, 当我们写入数据后,会立刻打印出来。