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。
1.4.2.1 select
允许程序同时监视多个文件描述符,检测它们的状态变化(数据可读或可写),从而高效地管理多个I/O操作而不需为每个操作创建独立线程或进程。
1.头文件
#include
2.函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
3.参数
nfds:是监控的文件描述符集合中最大文件描述符的值加1。在使用select函数时,必须确保这个参数正确设置,以便函数能监视所有相关的文件描述符。
readfds, writefds, exceptfds:这三个参数分别代表读、写和异常监视的文件描述符集合。它们使用fd_set类型表示,这是一种通过位图来管理文件描述符的数据结构。以下是对fd_set操作的常用宏定义:
⚫FD_SET(fd, &set):将文件描述符fd添加到集合set中。
⚫FD_CLR(fd, &set):从集合set中移除文件描述符fd。
⚫FD_ISSET(fd, &set):检查文件描述符fd是否已被加入集合set。
⚫FD_ZERO(&set):清空集合set中的所有文件描述符。
timeout:这是一个指向timeval结构的指针,该结构用于设定select等待I/O事件的超时时间。有三种情况:
⚫当timeout为NULL时,select会无限等待,直到至少有一个文件描述符就绪。
⚫当timeout设置为0时(即tv_sec和tv_usec都为0),select会立即返回,用于轮询。
⚫设置具体的时间,select将等待直到该时间过去或者有文件描述符就绪。
4.返回值
大于0:表示就绪的文件描述符数量,即有多少文件描述符已经准备好进行I/O操作。
等于0:表示超时,没有文件描述符在指定时间内就绪。
小于0:发生错误并且会设置errno为下列值之一:
⚫EBADF:一个或多个结构体中指定的文件描述符无效。
⚫EFAULT:fds 指针指向的地址超出进程的地址空间。
⚫EINTR:请求的事件之前产生一个信号,调用可以重新发起。
⚫EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
⚫ENOMEM:可用内存不足,无法完成请求
5.示例:(同时读取鼠标和键盘输入)
#include
#include
#include
#include
#include
#include
#include
int main()
{
char buf[100];
int keyboard, mouse, ret, flag;
int num = 10;
fd_set rdfds;
keyboard = open("/dev/input/event1", O_RDONLY | O_NONBLOCK);
mouse = open("/dev/input/event2", O_RDONLY | O_NONBLOCK);
if (keyboard < 0) {
perror("event1 open error");
return -1;
} else if (mouse < 0) {
perror("event2 open error");
return -1;
}
while (num--) {
FD_ZERO(&rdfds);
FD_SET(keyboard,&rdfds); //添加键盘
FD_SET(mouse,&rdfds); //添加鼠标
ret = select(mouse + 1,&rdfds,NULL,NULL,NULL);
if (ret < 0) {
perror("select error");
goto out;
} else if (ret == 0) {
printf("selsct timeout\n");
continue;
}
if (FD_ISSET(keyboard,&rdfds)) { //检查键盘是否添加成功
if (read(keyboard,buf,sizeof(buf)) > 0) //读取键盘数据
printf("keyboard read %d num\n", ret);
}
if (FD_ISSET(mouse,&rdfds)) { //检查鼠标是否添加成功
if (read(mouse,buf,sizeof(buf)) > 0) //读取鼠标数据
printf("mouse read %d num\n", ret);
}
}
out:
close(keyboard);
close(mouse);
exit(ret);
}
6.编译运行并查看测试结果
keyboard read 1 num //回车按键抬起
keyboard read 1 num //按下按键1
1keyboard read 1 num //抬起按键1
mouse read 1 num //鼠标移动
mouse read 1 num
mouse read 1 num
mouse read 1 num
mouse read 1 num
mouse read 1 num
mouse read 1 num
将鼠标和键盘配置为非阻塞I/O,通过while进行10次循环读取。通过select判断键盘和鼠标是否有数据可读(除readfds都为NULL),发生输入事件(鼠标移动、键盘按键按下或松开)才会返回。
1.4.2.2 poll
相比于select,poll没有最大文件描述符数量的限制。
1.头文件
#include
2.函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
3.参数
fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定测试某个给定的fd的条件。
pollfd结构体:
struct pollfd{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
};
fd:每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。
events:指定监测fd的事件(输入、输出、错误),每一个事件有多个取值,如下:
常值 输入到events 从revents获得 说明
POLLIN √ √ 普通或优先带数据可读
POLLRDNORM √ √ 普通数据可读
POLLRDBAND √ √ 优先级带数据可读
POLLPRI √ √ 高优先级带数据可读
POLLRDHUP √ √ 对端套接字关闭
POLLOUT √ √ 普通或优先带数据可写
POLLWRNORM √ √ 普通数据可写
POLLWRBAND √ √ 优先级带数据可写
POLLERR √ 发生错误
POLLHUP √ 发生挂起
POLLNVAL √ 文件描述符未打开
revents:revents 域是文件描述符的操作结果事件,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回.
nfds:用来指定第一个参数数组元素个数。
timeout:如果timeout等于-1,则poll会一直阻塞,直到fds数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号时返回。如果timeout等于0,poll不会阻塞,只是执行一次检查看看哪个文件描述符处于就绪态。如果timeout大于0,则表示设置poll函数阻塞时间的上限值,意味着poll函数最多阻塞timeout毫秒,直到fds数组中列出的文件描述符有一个达到就绪态或者捕获到一个信号为止。
4.返回值
4.返回值
成功时,poll返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll返回0。
失败时,poll返回-1,并设置errno为下列值之一:
⚫EBADF:一个或多个结构体中指定的文件描述符无效。
⚫EFAULT:fds 指针指向的地址超出进程的地址空间。
⚫EINTR:请求的事件之前产生一个信号,调用可以重新发起。
⚫EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
⚫ENOMEM:可用内存不足,无法完成请求。
5.示例
可以参考select,功能上相同。