驱动开发学习笔记---阻塞和非阻塞IO
一、阻塞和非阻塞简介
当应用程序对设备驱动进行操作的时候,如果不能获取到设备资源,那么阻塞式 IO 就会将应用程序对应的线程挂起,直到设备资源可以获取为止。对于非阻塞 IO,应用程序对应的线程不会挂起,它要么一直轮询等待,直到设备资源可以使用,要么就直接放弃。
二、阻塞访问(等待队列)
阻塞访问最大的好处就是当设备文件不可操作的时候进程可以进入休眠态,这样可以将CPU 资源让出来。但是,当设备文件可以操作的时候就必须唤醒进程,一般在中断函数里面完成唤醒工作。 Linux 内核提供了等待队列(wait queue)来实现阻塞进程的唤醒工作, wait queue 很早就作为一个基本的功能单位出现在 Linux 内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制。等待队列可以用来同步对系统资源的访问,信号量在内核中也依赖等待队列来实现。
定义等待队列头:wait_queue_head_t my_queue; 初始化等待队列头:init_waitqueue_head(&my_queue); 定义并初始化等待队列项:DECLARE_WAITQUEUE(name, tsk) 将队列项添加/移除等待队列头 :void add_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)
三、非阻塞访问(轮询)
poll、 epoll 和 select 可以用于处理轮询,应用程序通过 select、 epoll 或 poll 函数来查询设备是否可以操作,如果可以操作的话就从设备读取或者向设备写入数据。当应用程序调用 select、 epoll 或 poll 函数的时候设备驱动程序中的 poll 函数就会执行 。
1、select
函数原型:
int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
其中 readfds、writefds、exceptfds 分别是被 select()监视的读、写和异常处理的文件描述符集合,numfds 的值是需要检查的号码最高的文件描述符加 1。timeout 参数是一个指向 struct timeval 类型的指针,它可以使 select()在等待 timeout 时间后若没有文件描述符准备好则返回。struct timeval 数据结构的定义如下代码:
struct timeval { long tv_sec; /* 秒 */ long tv_usec; /* 微妙 */ };
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 = 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; } }
2、poll
在单个线程中, select 函数能够监视的文件描述符数量有最大的限制,一般为 1024,可以修改内核将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用 poll 函数,poll 函数本质上和 select 没有太大的差别,但是 poll 函数没有最大文件描述符限制。
函数原型:
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
fds: 要监视的文件描述符集合以及要监视的事件,为一个数组,数组元素都是结构体 pollfd类型的, pollfd 结构体如下所示:
struct pollfd { int fd; /* 文件描述符 */ short events; /* 请求的事件 */ short revents; /* 返回的事件 */ };
fd 是要监视的文件描述符,如果 fd 无效的话那么 events 监视事件也就无效,并且 revents返回 0。
nfds: poll 函数要监视的文件描述符数量。 timeout: 超时时间,单位为 ms。
poll 函数示例:
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) { /* 错误 */ ...... } }
3、epoll
传统的 selcet 和 poll 函数都会随着所监听的 fd 数量的增加,出现效率低下的问题,而且poll 函数每次必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此, epoll应运而生, epoll 就是为处理大并发而准备的,一般常常在网络编程中使用 epoll 函数。
创建epoll句柄
int epoll_create(int size)
使用 epoll_ctl 函数向其中添加要监视的文件描述符以及监视的事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd: 要操作的 epoll 句柄,也就是使用 epoll_create 函数创建的 epoll 句柄。
op: 表示要对 epfd(epoll 句柄)进行的操作
fd:要监视的文件描述符。
event: 要监视的事件类型,为 epoll_event 结构体类型指针
通过 epoll_wait 函数来等待事件的发生,类似 select 函数 。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
epfd: 要等待的 epoll。
events: 指向 epoll_event 结构体的数组,当有事件发生的时候 Linux 内核会填写 events,调用者可以根据 events 判断发生了哪些事件。
maxevents: events 数组大小,必须大于 0。
timeout: 超时时间,单位为 ms。