当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
int fd;
int data = 0;
fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。 Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作,如果我们要在驱动中使用等待队列,必须创建并初始化一个等待队列头,等待队列头使用结构体wait_queue_head_t 表示, 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
定义好等待队列头以后需要初始化, 使用 init_waitqueue_head 函数初始化等待队列头:
void init_waitqueue_head(wait_queue_head_t *q)
也可以使用宏 DECLARE_WAIT_QUEUE_HEAD 来一次性完成等待队列头的定义的初始化。
等待队列头就是等待队列的头部,每个访问设备的进程都是队列项,当设备不可用时,将队列项加到队列。对列项结构体 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;
使用宏 DECLARE_WAITQUEUE 定义并初始化一个等待队列项,宏的内容如下:
···
DECLARE_WAITQUEUE(name, tsk)
···
name 就是等待队列项的名字, tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current , 在Linux内核中 current相当于一个全局变量 ,表示当前进程 。因此宏DECLARE_WAITQUEUE 就是给当前正在运行的进程创建并初始化了一个等待队列项。
当设备不可访问时,需要将进程对应队列项添加到队列头,只有添加后才可以进入休眠。当设备可以访问后,再将队列项从队列头移除。
队列项添加函数:
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)
当设备可以使用的时候就要唤醒进入休眠态的进程,唤醒使用如下函数:
void wake_up(wait_queue_head_t *q)
void wake_up_interruptible(wait_queue_head_t *q)
前者可以唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态的进程,后者只能唤醒处于TASK_INTERRUPTIBLE状态的进程。
除了主动唤醒外,也可以设置等待队列等待某个事件,事件满足后自动唤醒队列中的进程。相关API函数:
函数 | 描述 |
---|---|
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(wq,condition,timeout) | 与wait_event_timeout函数类似,但是此函数将进程设置为TASK_INTERRUPTIBLE,可以被信号打断 |
##实现过程
非阻塞式处理方式为轮询。
用户以poll,epoll,select查询设备是否可以操作。应用程序通过这些函数查询设备是否可以操作,驱动程序中的poll函数会响应。
int select(int nfds, //所监视三类文件描述集合中,最大文件描述符+1
fd_set *readfds, //用于监视描述符集的读变化,可设置为NULL表示不监视此状态
fd_set *writefds, //用于监视描述符集的写变化,可设置为NULL表示不监视此状态
fd_set *exceptfds, //用于监视描述符集的异常变化,可设置为NULL表示不监视此状态
struct timeval *timeout) //超时判断
返回值:
0 : 超时发生
-1 :发生错误
其他值 : 可以进行操作的文件描述符个数
void FD_ZERO(fd_set *set) //所有位清零
void FD_SET(int fd, fd_set *set) //指定位置1,fd为文件描述符
void FD_CLR(int fd, fd_set *set) //指定位清零,fd为文件描述符
int FD_ISSET(int fd, fd_set *set) //判断文件是否属于某个集合,fd为文件描述符
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
};
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 = 500000; /* 500ms */
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;
}
}
单个线程中,select函数能够监视的文件描述符数量限制最大为1024,此时可以使用poll函数,没有最大数量限制。
int poll(struct pollfd *fds, //要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd类型
nfds_t nfds, //poll 函数要监视的文件描述符数量
int timeout) //超时时间,单位为 ms。
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 要监视的事件 */
short revents; /* 返回的事件 */
};
POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN
-demo
void main(void)
{
int ret;
int fd; /* 要监视的文件描述符 */
struct pollfd fds;
fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式访问 */
/* 构造结构体 */
fds.fd = fd;
fds.events = POLLIN; /* 监视数据是否可以读取 */
ret = poll(&fds, 1, 500); /* 轮询文件是否可操作,超时 500ms */
if (ret) { /* 数据有效 */
......
/* 读取数据 */
......
} else if (ret == 0) { /* 超时 */
......
} else if (ret < 0) { /* 错误 */
......
}
}
传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此, epoll应运而生, epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。应用程序需要先使用 epoll_create 函数创建一个 epoll 句柄, epoll_create 函数原型如下:
int epoll_create(int size)
size: 从 Linux2.6.8 开始此参数已经没有意义了,随便填写一个大于 0 的值就可以。