目录
- 非阻塞I/O
- 阻塞I/O 与非阻塞I/O 读文件
- 阻塞I/O 的优点与缺点
- 使用非阻塞I/O实现并发读取
- I/O 多路复用
- 何为I/O多路复用
- select()函数
- poll()函数
- 总结
非阻塞I/O
关于“阻塞”一词前面已经给大家多次提到,阻塞其实就是进入了休眠状态,交出了CPU 控制权。前面所学习过的函数,譬如wait()、pause()、sleep()等函数都会进入阻塞,本小节来聊一聊关于阻塞式I/O 与非阻塞式I/O。
对于某些文件类型(读管道文件、网络设备文件和字符设备文件),当对文件进行读操作时,如果数据未准备好、文件当前无数据可读,那么读操作可能会使调用者阻塞,直到有数据可读时才会被唤醒,这就是阻塞式I/O 。如果是非阻塞式I/O,即使没有数据可读,也不会被阻塞、而是会立马返回错误!
阻塞I/O 与非阻塞I/O 读文件
本小节我们将分别演示使用阻塞式I/O 和非阻塞式I/O 对文件进行读操作,在调用open()函数打开文件时,参数flags指定O_NONBLOCK 标志,open()调用成功后,后续的I/O 操作将以非阻塞式方式进行;这就是非阻塞I/O 的打开方式,如果参数flags未指定O_NONBLOCK 标志,则默认使用阻塞式I/O 进行操作。
对于普通文件来说,指定与未指定O_NONBLOCK 标志对其是没有影响,普通文件的读写操作是不会阻塞的,它总是以非阻塞的方式进行I/O 操作,这是普通文件本质上决定的。
本小节我们将以读取鼠标为例,使用两种I/O 方式进行读取,来进行对比,鼠标是一种输入设备,其对应的设备文件在/dev/input/目录下,如下所示:
通常情况下是mouseX(X 表示序号0、1、2),但也不一定,也有可能是eventX,如何确定到底是哪个设备文件,可以通过对设备文件进行读取来判断,譬如使用od 命令:
sudo od -x /dev/input/event3
Tips:需要添加sudo,在Ubuntu 系统下,普通用户是无法对设备文件进行读取或写入操作。
当执行命令之后,移动鼠标或按下鼠标、松开鼠标都会在终端打印出相应的数据,如下所示:
如果没有打印信息,那么这个设备文件就不是鼠标对应的设备文件。笔者使用的Ubuntu 系统,对应的鼠标设备文件是/dev/input/event3。接下来我们编写一个测试程序,使用阻塞式I/O 读取鼠标。
以阻塞方式读取鼠标,调用open()函数打开鼠标设备文件"/dev/input/event3",以只读方式打开,没有指定O_NONBLOCK 标志,说明使用的是阻塞式I/O;程序中只调用了一次read()读取鼠标。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{char buf[100];int fd, ret;/* 打开文件*/fd = open("/dev/input/event3", O_RDONLY);if (-1 == fd) {perror("open error");exit(-1);}/* 读文件*/memset(buf, 0, sizeof(buf));ret = read(fd, buf, sizeof(buf));if (0 > ret) {perror("read error");close(fd);exit(-1);}printf("成功读取<%d>个字节数据\n", ret);/* 关闭文件*/close(fd);exit(0);
}
编译上述示例代码进行测试:
执行程序之后,发现程序没有立即结束,而是一直占用了终端,没有输出信息,原因在于调用read()之后进入了阻塞状态,因为当前鼠标没有数据可读;
如果此时我们移动鼠标、或者按下鼠标上的任何一个按键,阻塞会结束,read()会成功读取到数据并返回,如下所示:
打印信息提示,此次read 成功读取了48 个字节,程序当中我们明明要求读取的是100 个字节,为什么这里只读取到了48 个字节?关于这个问题将会在第二篇内容当中进行介绍,这里暂时先不去理会这个问题。
接下来,我们将示例代码修改成非阻塞式I/O,如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{char buf[100];int fd, ret;/* 打开文件*/fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);if (-1 == fd) {perror("open error");exit(-1);}/* 读文件*/memset(buf, 0, sizeof(buf));ret = read(fd, buf, sizeof(buf));if (0 > ret) {perror("read error");close(fd);exit(-1);}printf("成功读取<%d>个字节数据\n", ret);/* 关闭文件*/close(fd);exit(0);
}
修改方法很简单,只需在调用open()函数时指定O_NONBLOCK 标志即可,对上述示例代码进行编译测试:
执行程序之后,程序立马就结束了,并且调用read()返回错误,提示信息为"Resource temporarilyunavailable",意思就是说资源暂时不可用;原因在于调用read()时,如果鼠标并没有移动或者被按下(没有发生输入事件),是没有数据可读,故而导致失败返回,这就是非阻塞I/O。
可以对示例代码进行修改,使用轮训方式不断地去读取,直到鼠标有数据可读,read()将会成功返回:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void)
{char buf[100];int fd, ret;/* 打开文件*/fd = open("/dev/input/event3", O_RDONLY | O_NONBLOCK);if (-1 == fd) {perror("open error");exit(-1);}/* 读文件*/memset(buf, 0, sizeof(buf));for ( ; ; ) {ret = read(fd, buf, sizeof(buf));if (0 < ret) {printf("成功读取<%d>个字节数据\n", ret);close(fd);exit(0);}}
}
具体的执行的效果便不再演示了,各位读者自己动手试试。
阻塞I/O 的优点与缺点
- 当对文件进行读取操作时,如果文件当前无数据可读,那么阻塞式I/O 会将调用者应用程序挂起、进入休眠阻塞状态,直到有数据可读时才会解除阻塞;
- 而对于非阻塞I/O,应用程序不会被挂起,而是会立即返回,它要么一直轮询等待,直到数据可读,要么直接放弃!
阻塞式I/O 的优点在于能够提升CPU 的处理效率,当自身条件不满足时,进入阻塞状态,交出CPU资源,将CPU 资源让给别人使用;而非阻塞式则是抓紧利用CPU 资源,譬如不断地去轮训,这样就会导致该程序占用了非常高的CPU 使用率!
执行示例代码13.1.3 对应的程序时,通过top 命令可以发现该程序的占用了非常高的CPU 使用率,如下所示:
其CPU 占用率几乎达到了100%,在一个系统当中,一个进程的CPU 占用率这么高是一件非常危险的事情。而示例代码13.1.1 这种阻塞式方式,其CPU 占用率几乎为0。
使用非阻塞I/O实现并发读取
上一小节给大家所举的例子当中,只读取了鼠标的数据,如果要在程序当中同时读取鼠标和键盘,那又该如何呢?本小节我们将分别演示使用阻塞式I/O 和非阻塞式I/O 同时读取鼠标和键盘。
键盘也是一种输入类设备,但是键盘是标准输入设备stdin,进程会自动从父进程中继承标准输入、标准输出以及标准错误,标准输入设备对应的文件描述符为0,所以在程序当中直接使用即可,不需要再调用open 打开。
首先我们使用阻塞式方式同时读取鼠标和键盘,示例代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define MOUSE "/dev/input/event3"int main(void)
{char buf[100];int fd, ret;/* 打开鼠标设备文件*/fd = open(MOUSE, O_RDONLY);if (-1 == fd) {perror("open error");exit(-1);}/* 读鼠标*/memset(buf, 0, sizeof(buf));ret = read(fd, buf, sizeof(buf));printf("鼠标: 成功读取<%d>个字节数据\n", ret);/* 读键盘*/memset(buf, 0, sizeof(buf));ret = read(0, buf, sizeof(buf));printf("键盘: 成功读取<%d>个字节数据\n", ret);/* 关闭文件*/close(fd);exit(0);
}
上述程序中先读了鼠标,接着读键盘,所以在实际测试当中,需要先动鼠标在按键盘(按下键盘上的按键、按完之后按下回车),这样才能既成功读取鼠标、又成功读取键盘,程序才能够顺利运行结束。因为read 此时是阻塞式读取,先读取了鼠标,没有数据可读将会一直被阻塞,后面的读取键盘将得不到执行。
这就是阻塞式I/O 的一个困境,无法实现并发读取(同时读取),那如何解决这个问题呢?当然大家可能会想到使用多线程,或者创建一个子进程,当然这些方法自然可以解决,但不是我们要学习的重点。
既然阻塞I/O 存在这样一个困境,那我们可以使用非阻塞式I/O 解决它,将示例代码13.1.4 修改为非阻塞式方式同时读取鼠标和键盘。
标准输入文件描述符(键盘)是从其父进程进程而来,并不是在我们的程序中调用open()打开得到的,将标准输入设置为非阻塞I/O,可以使用3.10.1 小节中给大家介绍的fcntl()函数,具体使用方法在该小节中已有详细介绍,这里不再重述!可通过如下代码将标准输入(键盘)设置为非阻塞方式:
int flag;
flag = fcntl(0, F_GETFL); //先获取原来的flag
flag |= O_NONBLOCK; //将O_NONBLOCK 标志添加到flag
fcntl(0, F_SETFL, flag); //重新设置flag
示例代码13.1.5 演示了以非阻塞方式同时读取鼠标和键盘。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define MOUSE "/dev/input/event3"int main(void)
{char buf[100];int fd, ret, flag;/* 打开鼠标设备文件*/fd = open(MOUSE, O_RDONLY | O_NONBLOCK);if (-1 == fd) {perror("open error");exit(-1);}/* 将键盘设置为非阻塞方式*/flag = fcntl(0, F_GETFL); //先获取原来的flagflag |= O_NONBLOCK; //将O_NONBLOCK 标准添加到flagfcntl(0, F_SETFL, flag); //重新设置flagfor ( ; ; ) {/* 读鼠标*/ret = read(fd, buf, sizeof(buf));if (0 < ret)printf("鼠标: 成功读取<%d>个字节数据\n", ret);/* 读键盘*/ret = read(0, buf, sizeof(buf));if (0 < ret)printf("键盘: 成功读取<%d>个字节数据\n", ret);}/* 关闭文件*/close(fd);exit(0);
}
将读取鼠标和读取键盘操作放入到一个循环中,通过轮训方式来实现并发读取鼠标和键盘,对上述代码进行编译,测试结果:
这样就解决了示例代码13.1.4 所出现的问题,不管是先动鼠标还是先按键盘都可以成功读取到相应数据。
虽然使用非阻塞I/O 方式解决了示例代码13.1.4 出现的问题,但由于程序当中使用轮训方式,故而会使得该程序的CPU 占用率特别高,终归还是不太安全。
I/O 多路复用
上一小节虽然使用非阻塞式I/O 解决了阻塞式I/O 情况下并发读取文件所出现的问题,但依然不够完美,使得程序的CPU 占用率特别高。解决这个问题,就要用到本小节将要介绍的I/O 多路复用方法。
何为I/O多路复用
I/O 多路复用(IO multiplexing)它通过一种机制,可以监视多个文件描述符,一旦某个文件描述符(也就是某个文件)可以执行I/O 操作时,能够通知应用程序进行相应的读写操作。I/O 多路复用技术是为了解决:在并发式I/O 场景中进程或线程阻塞到某个I/O 系统调用而出现的技术,使进程不阻塞于某个特定的I/O 系统调用。
由此可知,I/O 多路复用一般用于并发式的非阻塞I/O,也就是多路非阻塞I/O,譬如程序中既要读取鼠标、又要读取键盘,多路读取。
我们可以采用两个功能几乎相同的系统调用来执行I/O 多路复用操作,分别是系统调用select()和poll()。这两个函数基本是一样的,细节特征上存在些许差别!
I/O 多路复用存在一个非常明显的特征:外部阻塞式,内部监视多路I/O。
select()函数
系统调用select()可用于执行I/O 多路复用操作,调用select()会一直阻塞,直到某一个或多个文件描述符成为就绪态(可以读或写)。其函数原型如下所示:
#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
fd_set 数据类型是一个文件描述符的集合体。
- ⚫ readfds 是用来检测读是否就绪(是否可读)的文件描述符集合;
- ⚫ writefds 是用来检测写是否就绪(是否可写)的文件描述符集合;
- ⚫ exceptfds 是用来检测异常情况是否发生的文件描述符集合。
Tips:异常情况并不是在文件描述符上出现了一些错误。
fd_set: 数据类型是以位掩码的形式来实现的,我们并不需要关心这些细节,因为Linux 提供了四个宏用于对fd_set 类型对象进行操作:FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO(),后面介绍。
如果对readfds、writefds 以及exceptfds 中的某些事件不感兴趣,可将其设置为NULL,这表示对相应条件不关心。如果这三个参数都设置为NULL,则可以将select()当做为一个类似于sleep()休眠的函数来使用,通过select()函数的最后一个参数timeout 来设置休眠时间。
-
select()函数的第一个参数nfds 通常表示最大文件描述符编号值加1,考虑readfds、writefds 以及exceptfds这三个文件描述符集合,在3 个描述符集中找出最大描述符编号值,然后加1,这就是参数nfds。
-
select()函数的最后一个参数timeout 可用于设定select()阻塞的时间上限,控制select 的阻塞行为,可将timeout 参数设置为NULL,表示select()将会一直阻塞、直到某一个或多个文件描述符成为就绪态;也可将其指向一个struct timeval 结构体对象,该结构体在示例代码5.6.3 有详细介绍,这里不再重述!
如果参数timeout: 指向的struct timeval 结构体对象中的两个成员变量都为0,那么此时select()函数不会阻塞,它只是简单地轮训指定的文件描述符集合,看看其中是否有就绪的文件描述符并立刻返回。否则,参数timeout 将为select()指定一个等待(阻塞)时间的上限值,如果在阻塞期间内,文件描述符集合中的某一个或多个文件描述符成为就绪态,将会结束阻塞并返回;如果超过了阻塞时间的上限值,select()函数将会返回!
select()函数将阻塞直到有以下事情发生:
⚫ readfds、writefds 或exceptfds 指定的文件描述符中至少有一个称为就绪态;
⚫ 该调用被信号处理函数中断;
⚫ 参数timeout 中指定的时间上限已经超时。
宏:FD_CLR()、FD_ISSET()、FD_SET()、FD_ZERO()
文件描述符集合的所有操作都可以通过这四个宏来完成,这些宏定义如下所示:
#include <sys/select.h>void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
这些宏按照如下方式工作:
⚫ FD_ZERO()将参数set 所指向的集合初始化为空;
⚫ FD_SET()将文件描述符fd 添加到参数set 所指向的集合中;
⚫ FD_CLR()将文件描述符fd 从参数set 所指向的集合中移除;
⚫ 如果文件描述符fd 是参数set 所指向的集合中的成员,则FD_ISSET()返回true,否则返回false。
文件描述符集合有一个最大容量限制,由常量FD_SETSIZE 来决定,在Linux 系统下,该常量的值为1024。在定义一个文件描述符集合之后,必须用FD_ZERO()宏将其进行初始化操作,然后再向集合中添加我们关心的各个文件描述符,例如:
fd_set fset; //定义文件描述符集合FD_ZERO(&fset); //将集合初始化为空
FD_SET(3, &fset); //向集合中添加文件描述符3
FD_SET(4, &fset); //向集合中添加文件描述符4
FD_SET(5, &fset); //向集合中添加文件描述符5
在调用select()函数之后,select()函数内部会修改readfds、writefds、exceptfds 这些集合,当select()函数返回时,它们包含的就是已处于就绪态的文件描述符集合了。
譬如在调用select()函数之前,readfds 所指向的集合中包含了3、4、5 这三个文件描述符,当调用select()函数之后,假设select()返回时,只有文件描述符4 已经处于就绪态了,那么此时readfds 指向的集合中就只包含了文件描述符4。所以由此可知,如果要在循环中重复调用select(),我们必须保证每次都要重新初始化并设置readfds、writefds、exceptfds 这些集合。
select()函数的返回值
select()函数有三种可能的返回值,会返回如下三种情况中的一种:
⚫ 返回-1表示有错误发生,并且会设置errno。可能的错误码包括EBADF、EINTR、EINVAL、EINVAL以及ENOMEM,EBADF 表示readfds、writefds 或exceptfds 中有一个文件描述符是非法的;EINTR表示该函数被信号处理函数中断了,其它错误大家可以自己去看,在man 手册都有相信的记录。
⚫ 返回0表示在任何文件描述符成为就绪态之前select()调用已经超时,在这种情况下,readfds,writefds 以及exceptfds 所指向的文件描述符集合都会被清空。
⚫ 返回一个正整数表示有一个或多个文件描述符已达到就绪态。返回值表示处于就绪态的文件描述符的个数,在这种情况下,每个返回的文件描述符集合都需要检查,通过FD_ISSET()宏进行检查,以此找出发生的I/O 事件是什么。如果同一个文件描述符在readfds,writefds 以及exceptfds 中同时被指定,且它多于多个I/O 事件都处于就绪态的话,那么就会被统计多次,换句话说,select()返回三个集合中被标记为就绪态的文件描述符的总数。
使用示例
示例代码13.2.1 演示了使用select()函数来实现I/O 多路复用操作,同时读取键盘和鼠标。程序中将鼠标和键盘配置为非阻塞I/O 方式,本程序对数据进行了5 次读取,通过while 循环来实现。由于在while 循环中会重复调用select()函数,所以每次调用之前需要对rdfds 进行初始化以及添加鼠标和键盘对应的文件描述符。
该程序中,select()函数的参数timeout 被设置为NULL,并且我们只关心鼠标或键盘是否有数据可读,所以将参数writefds 和exceptfds 也设置为NULL。执行select()函数时,如果鼠标和键盘均无数据可读,则select()调用会陷入阻塞,直到发生输入事件(鼠标移动、键盘上的按键按下或松开)才会返回。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>
#define MOUSE "/dev/input/event3"
int main(void)
{char buf[100];int fd, ret = 0, flag;fd_set rdfds;int loops = 5;/* 打开鼠标设备文件*/fd = open(MOUSE, O_RDONLY | O_NONBLOCK);if (-1 == fd) {perror("open error");exit(-1);}/* 将键盘设置为非阻塞方式*/flag = fcntl(0, F_GETFL); //先获取原来的flagflag |= O_NONBLOCK; //将O_NONBLOCK 标准添加到flagfcntl(0, F_SETFL, flag); //重新设置flag/* 同时读取键盘和鼠标*/while (loops--) {FD_ZERO(&rdfds);FD_SET(0, &rdfds); //添加键盘FD_SET(fd, &rdfds); //添加鼠标ret = select(fd + 1, &rdfds, NULL, NULL, NULL);if (0 > ret) {perror("select error");goto out;}else if (0 == ret) {fprintf(stderr, "select timeout.\n");continue;}/* 检查键盘是否为就绪态*/if(FD_ISSET(0, &rdfds)) {ret = read(0, buf, sizeof(buf));if (0 < ret)printf("键盘: 成功读取<%d>个字节数据\n", ret);}/* 检查鼠标是否为就绪态*/if(FD_ISSET(fd, &rdfds)) {ret = read(fd, buf, sizeof(buf));if (0 < ret)printf("鼠标: 成功读取<%d>个字节数据\n", ret);}}
out:/* 关闭文件*/close(fd);exit(ret);
}
程序中分析select()函数的返回值ret,只有当ret 大于0 时才表示有文件描述符处于就绪态,并将这些处于就绪态的文件描述符通过rdfds 集合返回出来,
程序中使用FD_ISSET()宏检查返回的rdfds 集合中是否包含鼠标文件描述符以及键盘文件描述符,如果包含则表示可以读取数据了。
编译运行:
示例代码13.2.1 将鼠标和键盘都设置为了非阻塞I/O 方式,其实设置为阻塞I/O 方式也是可以的,因为select()返回时意味着此时数据是可读取的,所以以非阻塞和阻塞两种方式读取数据均不会发生阻塞。
poll()函数
系统调用poll()与select()函数很相似,但函数接口有所不同。在select()函数中,我们提供三个fd_set 集合,在每个集合中添加我们关心的文件描述符;而在poll()函数中,则需要构造一个struct pollfd 类型的数组,每个数组元素指定一个文件描述符以及我们对该文件描述符所关心的条件(数据可读、可写或异常情况)。poll()函数原型如下所示:
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
使用该函数需要包含头文件<poll.h>。
函数参数含义如下:
- fds:指向一个struct pollfd 类型的数组,数组中的每个元素都会指定一个文件描述符以及我们对该文件描述符所关心的条件,稍后介绍struct pollfd 结构体类型。
- nfds:参数nfds 指定了fds 数组中的元素个数,数据类型nfds_t 实际为无符号整形。
- timeout:该参数与select()函数的timeout 参数相似,用于决定poll()函数的阻塞行为,具体用法如下:
⚫ 如果timeout 等于-1,则poll()会一直阻塞(与select()函数的timeout 等于NULL 相同),直到fds
数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号时返回。
⚫ 如果timeout 等于0,poll()不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态。
⚫ 如果timeout 大于0,则表示设置poll()函数阻塞时间的上限值,意味着poll()函数最多阻塞timeout毫秒,直到fds 数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号为止。
struct pollfd 结构体
struct pollfd 结构体如下所示:
struct pollfd {int fd; /* file descriptor */short events; /* requested events */short revents; /* returned events */
};
fd 是一个文件描述符,struct pollfd 结构体中的events 和revents 都是位掩码,调用者初始化events 来指定需要为文件描述符fd 做检查的事件。当poll()函数返回时,revents 变量由poll()函数内部进行设置,用于说明文件描述符fd 发生了哪些事件(注意,poll()没有更改events 变量),我们可以对revents 进行检查,判断文件描述符fd 发生了什么事件。
应将每个数组元素的events 成员设置为表13.2.1 中所示的一个或几个标志,多个标志通过位或运算符( | )组合起来,通过这些值告诉内核我们关心的是该文件描述符的哪些事件。同样,返回时,revents 变量由内核设置为表13.2.1 中所示的一个或几个标志。
表13.2.1 中第一组标志(POLLIN、POLLRDNORM、POLLRDBAND、POLLPRI、POLLRDHUP)与数据可读相关;第二组标志(POLLOUT、POLLWRNORM、POLLWRBAND)与可写数据相关;而第三组标志(POLLERR、POLLHUP、POLLNVAL)是设定在revents 变量中用来返回有关文件描述符的附加信息,如果在events 变量中指定了这三个标志,则会被忽略。
如果我们对某个文件描述符上的事件不感兴趣,则可将events 变量设置为0;另外,将fd 变量设置为文件描述符的负值(取文件描述符fd 的相反数-fd),将导致对应的events 变量被poll()忽略,并且revents变量将总是返回0,这两种方法都可用来关闭对某个文件描述符的检查。
在实际应用编程中,一般用的最多的还是POLLIN 和POLLOUT。对于其它标志这里不再进行介绍了,后面章节内容中,如果需要使用时再给大家介绍!
poll()函数返回值
poll()函数返回值含义与select()函数的返回值是一样的,有如下几种情况:
⚫ 返回-1 表示有错误发生,并且会设置errno。
⚫ 返回0 表示该调用在任意一个文件描述符成为就绪态之前就超时了。
⚫ 返回一个正整数表示有一个或多个文件描述符处于就绪态了,返回值表示fds 数组中返回的revents变量不为0 的struct pollfd 对象的数量。
使用示例
示例代码13.2.3 演示了使用poll()函数来实现I/O 多路复用操作,同时读取键盘和鼠标。其实就是将示例代码13.2.1 进行了修改,使用poll 替换select。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#define MOUSE "/dev/input/event3"
int main(void)
{char buf[100];int fd, ret = 0, flag;int loops = 5;struct pollfd fds[2];/* 打开鼠标设备文件*/fd = open(MOUSE, O_RDONLY | O_NONBLOCK);if (-1 == fd) {perror("open error");exit(-1);}/* 将键盘设置为非阻塞方式*/flag = fcntl(0, F_GETFL); //先获取原来的flagflag |= O_NONBLOCK; //将O_NONBLOCK 标准添加到flagfcntl(0, F_SETFL, flag); //重新设置flag/* 同时读取键盘和鼠标*/fds[0].fd = 0;fds[0].events = POLLIN; //只关心数据可读fds[0].revents = 0;fds[1].fd = fd;fds[1].events = POLLIN; //只关心数据可读fds[1].revents = 0;while (loops--) {ret = poll(fds, 2, -1);if (0 > ret) {perror("poll error");goto out;}else if (0 == ret) {fprintf(stderr, "poll timeout.\n");continue;}/* 检查键盘是否为就绪态*/if(fds[0].revents & POLLIN) {ret = read(0, buf, sizeof(buf));if (0 < ret)printf("键盘: 成功读取<%d>个字节数据\n", ret);}/* 检查鼠标是否为就绪态*/if(fds[1].revents & POLLIN) {ret = read(fd, buf, sizeof(buf));if (0 < ret)printf("鼠标: 成功读取<%d>个字节数据\n", ret);}}
out:/* 关闭文件*/close(fd);exit(ret);
}
struct pollfd 结构体的events 变量和revents 变量都是位掩码,所以可以使用"revents & POLLIN"按位与的方式来检查是否发生了相应的POLLIN 事件,判断鼠标或键盘数据是否可读。测试结果:
总结
在使用select()或poll()时需要注意一个问题,当监测到某一个或多个文件描述符成为就绪态(可以读或写)时,需要执行相应的I/O 操作,以清除该状态,否则该状态将会一直存在;譬如示例代码13.2.1 中,调用select()函数监测鼠标和键盘这两个文件描述符,当select()返回时,通过FD_ISSET()宏判断文件描述符上是否可执行I/O 操作;如果可以执行I/O 操作时,应在应用程序中对该文件描述符执行I/O 操作,以清除文件描述符的就绪态,如果不清除就绪态,那么该状态将会一直存在,那么下一次调用select()时,文件描述符已经处于就绪态了,将直接返回。
同理对于poll()函数来说亦是如此,譬如示例代码13.2.3,当poll()成功返回时,检查文件描述符是否称为就绪态,如果文件描述符上可执行I/O 操作时,也需要对文件描述符执行I/O 操作,以清除就绪状态。