一、基础简介
在应用程序中读/写(read/write)设备节点时,比如dev/xxx,需要用到阻塞和非阻塞IO两种设备访问模式,在编写驱动时一定要考虑到阻塞和非阻塞。默认情况下,应用程序对设备驱动的读取方式时阻塞式的。
IO是指Input/Output,即应用程序对驱动设备的输入/输出操作。当应用程序对设备驱动进行操作的时候,如果不能立即获取到设备资源,阻塞式IO会将应用程序对应的线程挂起,直至设备资源可以获取为止;非阻塞式IO对应的线程不会挂起,而是要么一直轮询等待,直至设备资源可以使用,要么直接放弃返回。
1、阻塞式访问
int data = 0;
int fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */
int ret = read(fd, &data, sizeof(data)); /* 读取数据 */
2、非阻塞式访问
int data = 0;
int fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
int ret = read(fd, &data, sizeof(data)); /* 读取数据 */
二、阻塞IO:等待队列
1、等待队列简介
阻塞式访问可以在设备文件不可操作时,让进程进入休眠从而让出CPU资源,在设备文件可以操作时唤醒进程,一般在中断函数中完成唤醒工作。Linux内核提供等待队列(wait queue)来实现阻塞进程的唤醒工作。等待队列就是一个进程链表,其中包含了等待某个特定事件的所有进程;等待队列可以将进程以一种安全的方式进入休眠,并且在完成唤醒后找到该进程。
等待队列通过一个等待队列头(wait queue head)来管理,并提供简单休眠和高级休眠两种方式,以及多种唤醒接口。简单休眠即wait_event相关宏,高级休眠需要手动设置等待队列项(wait_queue_t)以及手动进程调度等。
2、等待队列头
(1)、等待队列头结构体
在驱动中使用等待队列时,必须首先创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t表示,该结构体定义在include/linux/wait.h,结构体内容如下:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
(2)、等待队列头的初始化
a.使用宏DECLARE_WAIT_QUEUE_HEAD来一次性完成等待队列头的定义和初始化,完整形式如下:
DECLARE_WAIT_QUEUE_HEAD(name);
b.使用init_waitqueue_head函数初始化等待队列头,函数原型如下:
void init_waitqueue_head(wait_queue_head_t *q);
3、简单休眠:等待事件
当进程休眠时,他将期待某个条件会在未来成真,当一个休眠进程被唤醒时,它必须再次检查所等待的条件的确为真。Linux内核中最简单的休眠方式是称为wait_event的宏以及它的几个变种,在实现休眠的同时,它也会检查进程等待的条件。wait_event的形式如下:
wait_event(wq, condition);
等待以 wq 为等待队列头的等待队列被唤醒,前提是 condition 条件必须满足(为真),否则一直阻塞 。 此函数会将进程设置为TASK_UNINTERRUPTIBLE 状态。
wait_event_timeout(wq, condition, timeout);
功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位。此函数有返回值,如果返回 0 的话表示超时时间到,而且 condition为假。为 1 的话表示 condition 为真,也就是条件满足。
wait_event_interruptible(wq, condition);
与 wait_event 函数类似,但是此函数将进程设置为 TASK_INTERRUPTIBLE,就是可以被信号打断。
wait_event_interruptible_timeout(wq, condition, timeout);
与 wait_event_timeout 函数类似,此函数也将进程设置为 TASK_INTERRUPTIBLE,可以被信号打断。
4、高级休眠:等待队列项
(1)、等待队列项结构体
等待队列头就是一个等待队列的头部,每个访问设备的进程都是一个队列项,当设备不可用的时候就要将这些进程对应的等待队列项添加到等待队列里面。结构体wait_queue_t表示等待队列项,结构体内容如下:
struct __wait_queue {
unsigned int flags;
void *private;
wait_queue_func_t func;
struct list_head task_list;
};
typedef struct __wait_queue wait_queue_t;
(2)、等待队列项接口
a. 队列项初始化
使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
DECLARE_WAITQUEUE(name, tsk);
参数含义:
name就是等待队列项的名字,tsk表示这个等待队列项属于哪个任务(进程),一般设置为current,在Linux内核中current相当于一个全局变量,表示当前进程。因此宏DECLARE_WAITQUEUE就是给当前正在运行的进程创建并初始化了一个等待队列项。
b. 队列项添加
当设备不可访问的时候就需要将进程对应的等待队列项添加到前面创建的等待队列头中,只有添加到等待队列头中以后进程才能进入休眠态。等待队列项添加API函数如下:
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
函数参数含义:q:等待队列项要加入的等待队列头。wait:要加入的等待队列项。
c. 队列项移除
当设备可以访问以后再将进程对应的等待队列项从等待队列头中移除即可,等待队列项移除API函数如下:
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
函数参数含义:q: 要删除的等待队列项所处的等待队列头。wait:要删除的等待队列项。
5、等待唤醒
当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒可以使用如下两个函数:
void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
参数q就是要唤醒的等待队列头,这两个函数会将这个等待队列头中的所有进程都唤醒。wake_up函数可以唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程,而wake_up_interruptible函数只能唤醒处于TASK_INTERRUPTIBLE状态的进程。
三、非阻塞IO:轮询
1、轮询简介
当应用程序以非阻塞方式访问设备时,设备驱动程序就要提供非阻塞的处理方式,也就是轮询。应用程序中通过select、epoll或poll函数来查询设备是否可以操作,如果可以操作,就从设备读取或向设备写入数据。当应用程序调用select、epoll或poll函数的时候,设备驱动程序中的poll函数就会执行,因此需要在设备驱动程序中编写poll函数。
2、用户空间:select
(1)、select函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
函数参数含义:
nfds:所要监视的这三类文件描述集合中,最大文件描述符加1。
readfds、writefds和exceptfds:这三个指针指向描述符集合,这三个参数指明了关心哪些描述符、需要满足哪些条件等等;这三个参数都是 fd_set 类型,fd_set类型变量的每一个位都代表一个文件描述符。
其中readfds用于监视指定描述符集的读变化,只要这些集合里面有一个文件可以读取那么seclect就返回一个大于0的值表示文件可以读取。如果没有文件可以读取,那么就会根据timeout参数来判断是否超时。可以将readfs设置为NULL,表示不关心任何文件的读变化。writefds和readfs类似,只是writefs用于监视这些文件是否可以进行写操作。exceptfds用于监视这些文件的异常。
timeout:超时时间。
返回值含义:0,表示的话就表示超时发生,但是没有任何文件描述符可以进行操作; -1,发生错误;其他值,可以进行操作的文件描述符个数。
(2)、fd_set变量相关宏
void FD_ZERO(fd_set *set); //将fd_set变量的所有位清零
void FD_SET(int fd, fd_set *set); //将fd_set变量某个位置1,即向fd_set添加一个文件描述符fd
void FD_CLR(int fd, fd_set *set); //将fd_set变量中某个位清零,即从fd_set中删除文件描述符fd
int FD_ISSET(int fd, fd_set *set); //测试文件描述符为fd的文件是否属于某个集合
(3)、timeval结构体
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
};
调用select函数等待某些文件描述符可以设置超时时间,当参数timeout为NULL时表示无限期的等待。
(4)、select使用示例
void main(void)
{
int ret, fd; /* 要监视的文件描述符 */
fd_set readfds; /* 读操作文件描述符集 */
struct timeval timeout; /* 超时结构体 */
fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
/* 文件描述集合 */
FD_ZERO(&readfds); /* 清除readfds */
FD_SET(fd, &readfds); /* 将fd添加到readfds里面 */
/* 构造超时时间 */
timeout.tv_sec = 0;
timeout.tv_usec = 80000; /* 80ms */
ret = select(fd + 1, &readfds, NULL, NULL, &timeout);
switch (ret) {
case 0: /* 超时 */
printf("timeout!\r\n");
break;
case -1: /* 错误 */
printf("error!\r\n");
break;
default: /* 可以读取数据 */
if(FD_ISSET(fd, &readfds)) { /* 判断是否为fd文件描述符 */
/* 使用read函数读取数据 */
}
break;
}
}
3、设备驱动:poll
当应用程序调用select或poll函数来对设备驱动进行非阻塞访问的时候,驱动程序file_operations操作集中的poll函数就会执行,所以驱动程序中需要提供相应的poll函数,poll函数原型如下:
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait);
函数参数含义:
filp: 要打开的设备文件(文件描述符)。
wait: 结构体 poll_table_struct 类型指针, 由应用程序传递进来的。一般将此参数传递给poll_wait 函数。
返回值含义:向应用程序返回设备或者资源状态,可返回的资源状态如下:
POLLIN //有数据可以读取。
POLLPRI //有紧急的数据需要读取。
POLLOUT //可以写数据。
POLLERR //指定的文件描述符发生错误。
POLLHUP //指定的文件描述符挂起。
POLLNVAL //无效的请求。
POLLRDNORM //等同于 POLLIN,普通数据可读。
注意:需要在poll函数中调用poll_wait函数,poll_wait函数不会引起阻塞,只是将应用程序添加到poll_table中,poll_wait函数原型为:
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);
参数filp对应poll函数的filp;参数wait_address是要添加到poll_table中的等待队列头;参数p就是poll_table,对应poll函数的wait参数。