IO多路复用——select,poll,epoll
IO多路复用是一种操作系统技术,旨在提高系统处理多个输入输出操作的性能和资源利用率。与传统的多线程或多进程模型相比,IO多路复用避免了因阻塞IO而导致的资源浪费和低效率问题。它通过将多个IO操作合并到一个系统调用中,允许程序同时等待多个文件描述符(如sockets、文件句柄等)变为可读或可写状态,然后再执行实际的IO操作。
在IO多路复用的实现中,常用的系统调用包括select()
、poll()
和epoll()
。这些机制允许程序监视多个描述符,一旦某个描述符就绪(通常是读就绪或写就绪),程序就会被通知进行相应的读写操作。这个过程通常涉及两个阶段:
-
等待数据到达:程序等待数据从IO设备传输到内核空间。在这个阶段,IO多路复用的系统调用会阻塞,直到至少有一个描述符准备好进行IO操作。
-
数据复制:当一个或多个描述符就绪时,程序负责将数据从内核空间复制到用户空间(进程或线程的缓冲区)。这第二个阶段是实际的读写操作,它在IO多路复用的上下文中是同步的,因为程序需要自己执行数据的读写。
尽管select()
、poll()
和epoll()
都是同步IO操作,但它们提供了一种有效的方式来处理并发IO,降低了系统开销,并提高了并发处理能力。与此不同,异步IO(AIO)模型进一步简化了IO操作,因为它允许操作系统自动处理数据从内核到用户空间的复制过程,无需程序显式调用读写操作。这意味着在异步IO模型中,读写操作由操作系统在后台完成,从而进一步提高了应用程序的效率和响应性。
select
概述
-
系统提供了select函数来实现多路复用输入/输出模型
-
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的
-
程序会停在select函数等待,直到被监视的文件描述符有一个或者多个发生了状态改变。
函数
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
-
函数参数:
参数 | 说明 |
---|---|
nfds | 是需要监视的最大的文件描述符值+1 |
readfds | 需要检测的可读文件描述符的集合 |
writefds | 需要检测的可写文件描述符的集合 |
exceptfds | 需要检测的异常文件描述符的集合 |
timeout | 当timeout等于NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件; 当timeout为0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。 当timeout为特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。 |
返回 | —— |
> 0 | 返回文件描述词状态已改变的个数 |
== 0 | 代表在描述词状态改变前已超过timeout时间,没有返回 |
< 0 | 错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的值变成不可预测,错误值可能为: EBADF:文件描述词为无效的或该文件已关闭 EINTR:此调用被信号所中断 EINVAL:参数n 为负值 ENOMEM:核心内存不足 |
-
其中:可读,可写,异常文件描述符的集合是一个fd_set类型,fd_set是系统提供的位图类型,位图的位置是否是1,表示是否关系该事件。例如:
输入时:假如我们要关心 0 1 2 3 文件描述符
0000 0000->0000 1111 比特位的位置,表示文件描述符的编号
比特位的内容 0 or 1 表示是否需要内核关心
输出时:
0000 0100->此时表示文件描述符的编号
比特位的内容 0 or 1哪些用户关心的fd 上面的读事件已经就绪了,这里表示2描述符就绪了
-
系统提供了关于fd_set的接口,便于我们使用位图:
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
执行流程:
-
执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
-
若fd=5,执行FD_SET(fd,&set);后set变为0001,0000(第5位置为1) 。
-
若再加入fd=2,fd=1,则set变为0001,0011 。
-
执行select(6,&set,0,0,0)阻塞等待,表示最大文件描述符+1是6,监控可读事件,立即返回。
-
若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
优缺点
-
优点:
-
可监控的文件描述符个数取决与sizeof(fd_set)的值。一般大小是1024,但是fd_set的大小可以调整。
-
将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd。①是用于再select 返回后,array作为源数据和fd_set进行FD_ISSET判断。②是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
缺点:
-
每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便。
-
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
-
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
-
select支持的文件描述符数量太小。
实例
#include <stdio.h>
#include <string.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/select.h>
const static int MAXLINE = 1024;
const static int SERV_PORT = 10001;
int main()
{
int i , maxi , maxfd, listenfd , connfd , sockfd ;
/*nready 描述字的数量*/
int nready ,client[FD_SETSIZE];
int n ;
/*创建描述字集合,由于select函数会把未有事件发生的描述字清零,所以我们设置两个集合*/
fd_set rset , allset;
char buf[MAXLINE];
socklen_t clilen;
struct sockaddr_in cliaddr , servaddr;
/*创建socket*/
listenfd = socket(AF_INET , SOCK_STREAM , 0);
/*定义sockaddr_in*/
memset(&servaddr , 0 ,sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(SERV_PORT);
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(listenfd, (struct sockaddr *) & servaddr , sizeof(servaddr));
listen(listenfd , 100);
/*listenfd 是第一个描述字*/
/*最大的描述字,用于select函数的第一个参数*/
maxfd = listenfd;
/*client的数量,用于轮询*/
maxi = -1;
/*init*/
for(i=0 ;i<FD_SETSIZE ; i++)
client[i] = -1;
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
for (;;)
{
rset = allset;
/*只select出用于读的描述字,阻塞无timeout*/
nre