一、前言
阻塞IO和非阻塞IO是Linux驱动开发中常见的两种访问设备的模式,这里的IO指的是Input/Output,也就是输入、输出。上一篇文章我们学习了阻塞访问,来回顾一下:
阻塞访问:当进程访问的驱动不可用或数据未准备好时,进程进入interruptible的休眠状态,也就是将当前进程添加到等待队列,等到按键按下进入中断,在中断中将线程唤醒(如果通过定时器消抖也可以在定时器中将线程唤醒),读取按键值。
二、非阻塞IO
1、非阻塞定义
当在应用程序以非阻塞形式访问设备文件时,如果设备不可用或数据未准备好,会立即向内核返回一个错误码,表示数据读取失败,应用程序会再次进行读取,如此重复循环,直至数据读取成功。
2、轮询
应用程序以非阻塞形式访问设备文件,驱动程序就需要相应的非阻塞处理方式,也就是轮询。应用程序中进行非阻塞访问的函数有三个:select、poll、epoll函数,这三个函数的主要作用就是:在应用程序中查询设备是否可以操作,如果可以操作就从设备读取或写入数据。
注意:以下三个函数都是在应用程序中使用。
0)三个非阻塞访问函数对比
详见,这里。
属性\函数 | select | poll | epoll |
事件集合 | 传入3个参数以区分可读、可写、异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪时间。 | 统一处理所有事件类型。通过pollfd.events传入感兴趣事件,内核通过pollfd.revemts反馈其中就绪的事件。 | 内核通过事件表直接管理用户感兴趣的所有事件。 |
应用程序索引就绪文件描述符的时间复杂度 | O(n) | O(n) | O(1) |
最大支持文件描述符数 | 1024 | 65535 | 65535 |
内核实现和工作效率 | 采用轮询方式检测就绪事件,复杂度O(n) | 采用轮询方式检测就绪事件,复杂度O(n) | 采用回调方式检测就绪事件,复杂度O(n) |
1) select 函数
int select( int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
※ nfds:三类文件描述符集合中最大文件描述符加1,可通过以下代码理解:
fd_set fds;
FD_ZERO(&fds);//清空集合
FD_SET(fd1, &fds);//设置描述符
FD_SET(fd2, &fds);//设置描述符
maxfdp = fd1 + 1;//描述符最大值加1,假设fd1 > fd2
※ readfds、writefds 和 exceptfds:需监视文件读、写、异常描述符集合。
比如writefds用于监视指定描述符集的写变化,也就是监视这些文件是否可以写入,只要文件集合中有一个文件可以写入那么select函数就会返回一个大于0的值,表示文件可以写入。如果没有文件可以写入,就会根据timeout参数来判断是否超时。当然,如果只需要监视文件读状态,其他两个可以设置为NULL。
※ timeout:超时时间,timeval结构体定义如下:
struct timeval {
long tv_sec; // 秒
long tv_usec; // 微秒
};
一般超时时间有三种情况如下:
- NULL:将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止。
- 0:select变成纯粹的非阻塞函数,立即返回继续执行,无文件满足要求返回0,有返回正值。
- 大于0: 在timeout内阻塞,timeout时间内有事件到来就返回大于0的值,否则就堵塞进程,超时后无论怎样一定返回0,并继续向下执行。
※ 返回值:
- 0: 超时返回,无文件描述符可以进行操作。
- -1 :发生错误。
- 大于0: 有返回数量的文件描述符可以进行操作。
2) poll 函数
int poll( struct pollfd *fds,
nfds_t nfds,
int timeout );
※ fds:pollfd结构体数组,pollfd结构体包含文件描述符,以及对对应文件的监视事件。pollfd结构体定义如下,其中events是要监视的fd文件描述符的事件,revents是内核设置的返回事件。
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 监视的事件 */
short revents; /* 返回的事件 */
};
events可监视的事件类型如下:
POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN
※ nfds:要监视的文件描述符的数量,也就是上面结构体数组的元素个数。
※ timeout:超时时间,单位是ms。
※ 返回值:
- 0:超时。
- -1:发生错误。
- 大于0:返回revents 域中不为0的pollfd 结构体个数。
3) epoll 函数
select和poll函数随着监听的fd数量的增加会出现效率低下的问题,epoll函数由此而生,epoll是为了处理大并发准备的,一般在网络编程中使用epoll函数。在应用程序中使用epoll函数,需要怎么操作呢?
🍉 首先,需要创建一个epoll句柄:
int epoll_create(int size);
※ size:从 Linux2.6.8以后该参数无意义,大于0即可。
※ 返回值:epoll句柄,返回-1表示创建失败。
🍉 然后,使用epoll_ctl函数向其中添加要监视的文件描述符以及监视的事件,epoll_ctl函数声明如下:
int epoll_ctl( int epfd,
int op,
int fd,
struct epoll_event *event );
※ epfd:epoll_create 函数创建的epoll 句柄。
※ op:要对epoll 句柄进行的操作,主要有以下几种:
EPOLL_CTL_ADD 向epfd 添加文件参数fd 表示的描述符。
EPOLL_CTL_MOD 修改参数fd 的event 事件。
EPOLL_CTL_DEL 从epfd 中删除fd 描述符。
※ fd:要监视的文件描述符。
※ event:要监视的事件类型,epoll_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里面。
※ 返回值:0,成功;-1,失败。
🍉 最后,在上面都设置好后,需要使用epoll_wait函数等待事件的发生,epoll_wait声明如下:
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。
※ 返回值:0,超时;-1,错误;大于0的整数,发生事件的文件描述符数量。
epoll主要用在大规模并发服务器上,因为这种场合select和poll函数效率低下。本章我们使用select、poll函数完成实验。
3、驱动程序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_table中。poll_awit函数原型如下:
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参数。
三、程序框图
上图为个人理解,如有错误,请在评论区指出。
2022/06/06于武汉