非阻塞IO
非阻塞式IO则会轮询等待知道设备资源可以使用或者直接放弃
如果用户以非阻塞方式访问设备
提供轮询的处理方式
可以通过select、epoll、poll函数来查询设备是否可以操作
到应用程序调用以上三个函数 驱动中的poll函数就会执行,我们在驱动中需要编写poll函数
三种IO多路复用的机制比较
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
select
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout
)
- nfds要操作的文件描述符
- readfds、writefds、exceptfds三个指针指向描述符集合,都是fd_set类型的,fd_set每一位代表了一个文件描述符,readfds用于监视指定描述符的读变化,只要可以读取,select返回一个大于0的值,如果不可以根据timeout来判断超时,可以设置为0表示不关心任何文件的读变化。
writefds、exceptfds类似
我们可以定义一个fd_set变量,这个变量要传递给readfds
可以用一些宏来进行操作
void FD_ZERO(fd_set *set) //清零
void FD_SET(int fd, fd_set *set) //某位置1,添加一个文件描述符
void FD_CLR(int fd, fd_set *set) //清除一个文件描述符
int FD_ISSET(int fd, fd_set *set)//判断某位是否为1
- timeout超时值
struct timeval { long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
};
select的实现过程如下图
select的几大缺点:
-
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
-
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
-
select支持的文件描述符数量太小了,默认是1024
poll
poll函数没有最大文件描述符限制
int poll(struct pollfd *fds,
nfds_t nfds,
int timeout
)
- fds要监视的文件描述符集合以及要监视的时间,为一个数组,数组元素是结构体pollfd类型
struct pollfd { int fd; /* 文件描述符 */
short events; /* 请求的事件 */
short revents; /* 返回的事件 */
};
fd为要监视的文件描述符,events是要监视的事件,可监视的事件类型如下:
revents为返回的事件
- nfd是poll函数要监视的文件描述符数量
- timeout为超时时间,单位为ms
- 返回值:返回 revents域中不为 0的 pollfd结构体个数,也就是发生事件或错误的文件描述符数量; 0,超时 ;-1,发生错误,并且设置 errno为错误类型。
epoll
传统的select和poll函数都会随着监听的fd的数量的增加出现效率低下的问题
poll函数每次必须遍历所有的文件描述符来检查就绪的描述符,浪费时间
epoll就是为了处理大并发而准备的,一般常用于网络编程中
应用程序需要先使用epoll_creat函数创建一个epoll句柄
int epoll_create(int size)
size从2.6.8开始就没有意义了 随便写一个大于0的值就可以
返回值为epoll句柄返回-1表示创建失败
句柄创建成功以后使用 epoll_ctl函数向其中添加要监视的文件描述符以及监视的事件
int epoll_ctl(int epfd,
int op,
int fd,
struct epoll_event *event
)
- epfd 要操作的epoll句柄,也就是使用epoll函数创建的句柄
- op 表示要对epfd进行的操作,可以为
- fd 要监视的文件描述符
- event 要监视的事件类型,为epoll_event结构体类型指针
struct epoll_event { uint32_t events; /* epoll事件 */
epoll_data_t data; /* 用户数据 */
};
events为要监视的事件,可选
EPOLLIN 有数据可以读取。
EPOLLOUT 可以写数据。
EPOLLPRI 有紧急的数据需要读取。
EPOLLERR 指定的文件描述符发生错误。
EPOLLHUP 指定的文件描述符挂起。
EPOLLET 设置 epoll为边沿触发,默认触发模式为水平触发。
EPOLLONESHOT 一次性的监视,当监视完成以后还需要再次监视某个 fd,那么就需要将fd重新添加到 epoll里面。
一切设置好后可以通过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结构体数组大小
- timeout 超时时间,单位ms
三者总结
一般来说当涉及的fd数量较少的时候,使用select是合适的;如果涉及的fd很多,如在大规模并发服务器中侦听许多socket的时候,则不太适合选用select,而时候选用epoll
驱动中的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中
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);
参数 wait_address是要添加到 poll_table中的等待队列头
参数 p就是poll_table,就是file_operations中 poll函数的 wait参数。
实验代码与分析
实验代码
驱动代码
unsigned int key_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct irqkey_dev *dev = (struct irqkey_dev *)filp->private_data;
poll_wait(filp, &dev->r_wait, wait);
if(atomic_read(&dev->releasekey))//按键按下
{
mask = POLLIN | POLLRDNORM; //返回PLLIN
}
return mask;
}
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.write = key_write,
.release = key_release,
.poll = key_poll,
};
应用程序代码
while (1)
{
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
/* 构造超时时间 */
timeout.tv_sec = 0;
timeout.tv_usec = 500000; /* 500ms */
retvalue = select(fd + 1, &readfds, NULL, NULL, &timeout);
switch (retvalue) {
case 0: /* 超时 */
/* 用户自定义超时处理 */
break;
case -1: /* 错误 */
/* 用户自定义错误处理 */
break;
default: /* 可以读取数据 */
if(FD_ISSET(fd, &readfds)) {
retvalue = read(fd, &keyvalue, sizeof(keyvalue));
if (retvalue < 0) {
/* 读取错误 */
} else {
if (keyvalue == KEYVALUE)
printf("key value=%d\r\n", keyvalue);
}
}
break;
}
}
代码分析
驱动部分
驱动中的工作很简单,在file_operations的poll函数中调用poll_wait函数,把当前进程添加到wait参数指定的等待列表(poll_table)中,实际作用是让唤醒参数queue对应的等待队列可以唤醒因select而睡眠的进程
同时poll函数还要返回设备资源的可获取状态,这里当按键被按下时会返回数据可读的标志
应用程序部分
在应用程序的while(1)循环中调用select函数来监控按键的状态,然后就会阻塞等待文件描述符集合超时或者可访问
当返回的值表示可以读取数据时,我们开始读取键值,首先使用FD_ISSET宏来判断是否确实可读,接着调用read函数从驱动中读取键值
通过以上的操作我们就实现了使用轮询操作来读取键值
总结
阻塞与非阻塞访问是IO操作的两种不同的模式,前者在暂时不可以进行IO操作时会让进程睡眠,后者则不然
在设备驱动中阻塞IO一般基于等待队列或者基于等待队列的其他Linux内核API来实现,等待队列可用于同步驱动中事件发生的先后顺序。使用非阻塞IO的应用程序可以借助轮询函数来查询设备是否能立即被访问,用户空间调用select()、poll()或者epoll()接口,设备驱动提供poll()函数,设备驱动的poll()函数不会阻塞,但是与select()、poll()、epoll()相关的系统调用则会阻塞的等待至少一个文件描述符集合可访问或超时。