阻塞和非阻塞 IO 是 Linux 驱动中常见的两种设备访问模式,这里的IO 并不是指GPIO引脚,而是指 Input/Output,也就是应用程序对驱动设备的输入/输出操作。一个完整的 IO 过程需要包含以下三个步骤:用户空间的应用程序向内核发起 IO 调用请求(系统调用);内核操作系统准备数据,把 IO 设备的数据加载到内核缓冲区;操作系统拷贝数据,把内核缓冲区的数据拷贝到用户进程缓冲区。IO模型分类可以参考:《Linux驱动开发之ioctl控制定时器并实现任意整数级秒计时器》
阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足条件后再进行操作。被挂起的进程进入休眠状态,从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时,并不挂起,它要么放弃,要么轮询等待,直至可以进行操作为止。阻塞式 IO 的优点在于能够提升 CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出 CPU资源;而非阻塞式IO一般会为了抓紧利用 CPU 资源不断地去轮询,这样就会导致该程序占有非常高的 CPU 使用率!
等待队列
在 Linux 驱动程序中,阻塞进程可以使用等待队列来实现。等待队列是内核实现阻塞和唤醒的机制,以双循环链表为基础结构,由链表头和链表项两部分组成,分别表示等待队列头和等待队列元素。等待队列头使用结构体 wait_queue_head_t 来表示,等待队列头是一个等待队列的头部,每个访问设备的进程都是一个队列项, 当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面,等待队列项使用结构体 wait_queue_t 来表示。只有添加到等待队列头中以后进程才能进入休眠态。当设备可以访问以后再将进程对应的等待队列项从等待队列中移除即可。当设备可以使用的时候就要唤醒进入休眠态的进程,除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒等待队列中的进程。
IO多路复用与poll轮询
如果用户应用程序以非阻塞的方式访问设备,设备驱动程序就要提供非阻塞的处理方式。I/O 多路复用一般用于并发式的非阻塞 IO。IO 多路复用可以实现一个进程监视多个文件描述符。一旦某个文件描述符准备就绪,就通知应用程序进行相应的读写操作。没有文件描述符就绪时就会阻塞应用程序,从而释放出 CPU 资源。Linux应用程序通过 select、epoll 或 poll 函数来轮询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据,使用这种方式可以避免非阻塞IO由于一直读取/返回而导致的高CPU使用率。关于应用层select、epoll 和poll函数本篇不作介绍。当应用程序调用 select或 poll 函数的时候设备驱动程序file_operations 操作集中的 poll 函数就会执行。需要在驱动程序的 poll 函数中调用 poll_wait 函数,poll_wait 函数不会引起阻塞,只是将应用程序添加到 poll_table 中。
阻塞IO实验
对于设备驱动文件的默认读取方式就是阻塞式的,在之前字符设备驱动程序基础上添加等待队列相关代码实现设备驱动文件的阻塞访问。并编写应用程序,使用open函数打开设备驱动文件,并使用read和write函数从文件读取和写入测试数据。字符设备驱动程序可参考《没有开发板在Ubuntu下体验Linux驱动开发之字符设备框架实验》。增添部分主要在读写函数实现上,代码如下:
......
#include <linux/wait.h>
struct device_test{
dev_t dev_num;
int major ;
int minor ;
struct cdev cdev_test; // cdev
struct class *class;
struct device *device;
char kbuf[32];
int flag; //标志位
};
struct device_test dev1;
DECLARE_WAIT_QUEUE_HEAD(read_wq); //定义并初始化等待队列头/*写入函数*/
static ssize_t cdev_test_write(struct file *file, const char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev=(struct device_test *)file->private_data;
if (copy_from_user(test_dev->kbuf, buf, size) != 0)
{
printk("copy_from_user error\r\n");
return -1;
}
test_dev->flag=1;//将条件置 1
wake_up_interruptible(&read_wq); //使用 wake_up_interruptible 唤醒等待队列中的休眠进程
return 0;
}
/**读取函数*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev=(struct device_test *)file->private_data;
wait_event_interruptible(read_wq,test_dev->flag); //可中断的阻塞等待,使进程进入休眠态
if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0)
{
printk("copy_to_user error\r\n");
return -1;
}
return 0;
}
......
分别编译驱动程序与应用程序,由于只不涉及相关硬件操作,故直接在Ubuntu虚拟机上进行测试即可。编译结果如下:
加载测试驱动,并运行读取程序,可以看出,驱动加载成功,程序运行成功并阻塞,进入休眠状态,等待有数据可读。
另开一个终端,运行写入程序,向驱动文件中写入测试数据,观察读取窗口的打印。
可以看出,write程序成功写入数据并且read程序成功唤醒并读取到测试数据。
非阻塞IO(IO多路复用)实验
若以非阻塞式方式访问设备文件,无论有没有数据可读程序都会直接返回。若程序中循环进行IO操作,那么程序就会一直占用CPU,导致CPU使用率过高。因此需要结合poll函数进行轮询来监视文件描述符,当文件没有数据可读时进行阻塞监视,当到有数据可读时唤醒程序去读取数据。对上述驱动程序中相关接口函数进行修改并向文件操作集中加入poll操作。
......
#include <linux/poll.h>
......
/**读取函数*/
static ssize_t cdev_test_read(struct file *file, char __user *buf, size_t size, loff_t *off)
{
struct device_test *test_dev=(struct device_test *)file->private_data;
//判断非阻塞标志
if(file->f_flags & O_NONBLOCK ){
if (test_dev->flag !=1)
return -EAGAIN;
}
wait_event_interruptible(read_wq,test_dev->flag);
if (copy_to_user(buf, test_dev->kbuf, strlen( test_dev->kbuf)) != 0)
{
printk("copy_to_user error\r\n");
return -1;
}
printk("This is cdev_test_read\r\n");
return 0;
}
static __poll_t cdev_test_poll(struct file *file, struct poll_table_struct *p){
struct device_test *test_dev=(struct device_test *)file->private_data;
__poll_t mask=0;
poll_wait(file,&read_wq,p); //应用阻塞
if (test_dev->flag == 1)
{
mask |= POLLIN;
}
return mask;
}struct file_operations cdev_test_fops = {
.owner = THIS_MODULE,
.open = cdev_test_open,
.read = cdev_test_read,
.write = cdev_test_write,
.release = cdev_test_release,
.poll = cdev_test_poll,
};......
编写测试程序,以非阻塞式方式打开设备文件,并通过poll轮询实现对文件描述符的监听,此处只监听了一个测试文件。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <poll.h>
int main(int argc, char *argv[])
{
int fd;
char buf1[32] = {0};
char buf2[32] = {0};
struct pollfd fds[1];
int ret;
fd = open("/dev/test", O_RDWR | O_NONBLOCK);
if (fd < 0)
{
perror("open error \n");
return fd;
}
fds[0] .fd =fd;
fds[0].events = POLLIN;
printf("start read \n");
while (1)
{
ret = poll(fds,1,3000); //轮询文件是否可操作,超时时间 3000ms
if(!ret){
printf("time out !!\n");
}else if(fds[0].revents == POLLIN) //如果返回事件是有数据可读取
{
read(fd,buf1,sizeof(buf1));
printf("buf is %s \n,",buf1);
sleep(1);
}
}
printf("read successed \n");
close(fd);
return 0;
}
同样分别编译驱动程序和测试程序,加载驱动并运行read程序。
可以看出,驱动成功加载并且read程序成功运行实现poll轮询,3秒超时时间过后强制返回并打印了time out,接着另开一个终端运行write程序,观察read打印。
在write程序成功写入测试数据后,read程序成功打印了该数据。当不使用poll轮询时,使用非阻塞方式访问会直接返回,测试结果如下:
总结:本篇分别介绍了阻塞IO和非阻塞IO的定义以及各自的驱动处理方式,并使用等待队列和poll轮询的方式分别实现了对字符设备驱动文件的阻塞式、非阻塞式访问。