IO多路复用机制——select、poll、epoll的原理和区别

本文探讨了select,poll,和epoll三种I/O多路复用机制,强调了它们的同步与异步特性,以及epoll的回调机制和红黑树优化。特别指出epoll在处理大规模并发连接时的性能优势。
摘要由CSDN通过智能技术生成

     select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。  

问题:如果我们先前创建的几个进程承载不了目前快速发展的业务的话,是不是还得增加进程数?我们都知道系统创建进程是需要消耗大量资源的,所以这样就会导致系统资源不足的情况。

那么有没有一种方式可以让一个进程同时为多个客户端端提供服务?

对于IO复用,我们可以通过一个例子来很好的理解它。

某教室有10名学生和1名老师,这些学生上课会不停的提问,所以一个老师处理不了这么多的问题。那么学校为每个学生都配一名老师,也就是这个教室目前有10名老师。此后,只要有新的转校生,那么就会为这个学生专门分配一个老师,因为转校生也喜欢提问题。如果把以上例子中的学生比作客户端,那么老师就是负责进行数据交换的服务端。则该例子可以比作是多进程的方式。

后来有一天,来了一位具有超能力的老师,这位老师回答问题非常迅速,并且可以应对所有的问题。而这位老师采用的方式是学生提问前必须先举手,确认举手学生后在回答问题。则现在的情况就是IO复用。

select:

accept创建了五个客户端连接,并把相应的文件描述符fd放到数组中

文件描述符是随机产生的,并不是连续的

求出最大的文件描述符

if(fd[i]>max){
    max=fd[i];
}

 对需要关注的文件描述符初始化为0

FD_ZERO(&read_fds);

FD_SET设置哪个文件描述符被关注的置1

for(i=0;i<5;i++){
    FD_SET(fd[i],&read_fds);
}

过程:

select函数参数:

1、max+1,限定了循环遍历bitmap的范围,bitmap最多有1024位,限定了遍历的最大长度,可以减少无谓的扫描

2、读文件描述符的集合:读文件描述符实际上是bitmap,来表示具体哪个文件描述符被监听

3、写文件描述符的集合

4、异常文件描述符集合

5、超时时间,在指定的时间内如果还没有检测到某个文件描述符已就绪,这个时候也会阻塞,不再等待,立即返回。设置成NULL永不超时,一直阻塞下去,一直等到有数据到达,时间已就绪才返回。设置为0,表示不阻塞

ret = select(max + 1,&read_fds,NULL,NULL,NULL);

  进程A调用select会将bitmap拷贝到内核态中,由内核判断哪一个socket文件描述符也就是fd,有对应的数据到达,需要关注的事件已就绪。内核的效率比用户态要高,用户态判断的时候需要询问内核哪个文件描述符已就绪,这样就会产生用户态和内核态的上下文切换,如果每一次对每一个fd都判断一次的话,那么就会存在多次的内核态和用户态的上下文切换,会非常浪费资源,因此select函数就选择将bitmap一次性的拷贝内核态,由内核去遍历哪个fd有数据到达。

哪个socket文件描述符对应的数据接收队列有数据到达,那么就标志这个文件描述符已就绪,这个时候select函数就会返回,返回的时候不仅仅是拷贝已就绪的fd,他是把所有的文件描述符信息全部拷贝到用户态,同时告知用户态现在有几个文件描述符已就绪,只是告诉个数,并没有告诉具体哪个就绪,所以需要for循环去判断具体哪个文件描述符就绪了

for(int i=0;i<5;i++){
    if(FD_ISSET(fd[i],&read_fds)){
        ret = recv(fd[i],buff,sizeof(buff);
    }
}

以下函数的头文件都是#include<sys/select.h>

fd_set set ; //创建监听集合
int sockfd ; //套接字文件描述符
int max_fd ; //套接字文件描述符表中的描述符个数
struct timeval *timeout ; //时间结构体,在这里表示工作模式——1.阻塞、2.非阻塞、3.定时阻塞(非阻塞与定时阻塞需要设置该结构体)

timeout中有两个成员,一个表示秒(timeout.tv_sec),一个表示微秒(timeout.tv_usec)

struct timeval
{
__time_t  tv_sec;        /* Seconds. */
__suseconds_t  tv_usec;  /* Microseconds. */
};
  • timeout = NULL 就表示阻塞监听
  • timeout.tv_sec = 0 、timeout.tv_sec = 0 就表示非阻塞监听
  • timeout.tv_sec = 4、timeout.tv_usec = 30 就表示阻塞4秒30微秒,之后不阻塞
函数功能返回值
FD_ZERO(&set);初始化监听集合,将所有位的位码都初始化为0
FD_SET(sockfd , &set);将set集合中与sockfd对应位的位码设置为1
FD_CLR(sockfd , &set);将set集合中与sockfd对应位的位码设置为0
FD_ISSET(sockfd , &set);获取set集合中与sockfd对应位的位码0或1
int select(max_fd, 是否监听读事件 , 是否监听写事件 , 是否监听错误事件 , timeout);监听我们要求的文件描述符的状态变化情况,并通过返回值告知(PS:想监听对应时间就传入&set,不想就传NULL)返回处于就绪状态的套接字数量

poll

poll相比于select的亮点是定义了pollfd结构体,events代表用户关注的事件,比如读事件,写事件,revents指返回的事件,由系统的内核填充并返回,如果当前的文件描述符,有状态的变化,比如说已就绪的话,revents就会做出相应的变化,poll的结构体数组模式就不会有1024个文件描述符的限制,相对select来说,能够承受更高的并发。

执行原理:首先也是accept创建4个文件描述符,建立了四个客户端连接,同时注册了需要关注的读事件

参数的含义

1、传入文件描述符的结构体数组

2、结构体数组的最大长度

3、阻塞等待时间

ret = poll(fds,4,4000);

跟select一样,一次性将一批文件描述符发送到内核态,在内核中文件描述符遍历,看哪个文件描述符的数据接收队列有数据,就将revents置1,poll函数返回,将文件描述符的结构体数组拷贝回用户态,遍历具体哪个已就绪,与select不同的是,判断完成之后,直接将revents恢复成0. 

for(int i=0;i<4;i++){
    if(fds[i].revents & POLLIN){
        fd[i].revents = 0;
        ret = recv(fds[i].fd,buff,sizeof(buff)-1,0);
}

epoll 

不涉及原理的Epoll使用笔记(一)-CSDN博客

首先定义了epoll_event的结构体,events表示关注的事件(读写事件),epoll_data是一个自定义的联合体,fd保存需要监听的文件描述符信息

int epoll_fd = epoll_reate

进程调用epoll_create方法时,内核会创建一个eventpoll结构体:

rdyList(就绪链表)已就绪的文件描述双链表,当某个读事件或者写事件就绪的时候,就会把相应的文件描述符信息放到已就绪的双链表中

rbr是一颗红黑树,用红黑树去管理用户进程放进来的所有socket连接

wq是一个等待队列,当他需要关注的某个事件未就绪的时候,就会把当前进程的描述符以及回调函数放到进程等待队列里,当软中断数据到达的时候就会通过检查阻塞队列,找到相应的阻塞进程去唤醒他执行后续的动作

epoll_ctl函数用于增加,删除,修改epoll事件,epoll事件会存储于内核epoll结构体红黑树中。每当我们创建一个客户端和服务端的socket连接的时候,就会把链接添加到一个红黑树里面。

#include <sys/epoll.h>
 
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
 
参数:
epfd:epoll文件描述符
op:操作码
EPOLL_CTL_ADD:插入事件
EPOLL_CTL_DEL:删除事件
EPOLL_CTL_MOD:修改事件
fd:事件绑定的套接字文件描述符
events:事件结构体
 
返回值:
成功:返回0
失败:返回-1

epoll_wait检查双链表中是否有就绪的事件,如果有就绪的事件的话,wait函数会立即返回不阻塞,但是这个里面为空的时候,它就会进行阻塞,等待有就绪事件到达。

#include <sys/epoll.h>
 
int epoll_wait(int epfd, struct epoll_event *events,              
int maxevents, int timeout);
 
参数:
epfd:epoll文件描述符
events:epoll事件数组
maxevents:epoll事件数组长度
timeout:超时时间
小于0:一直等待
等于0:立即返回
大于0:等待超时时间返回,单位毫秒
 
返回值:
小于0:出错
等于0:超时
大于0:返回就绪事件个数

epoll使用红黑树作为监听集合,红黑树上的节点类型是epoll_event的结构体,结构体里面的成员有fd需要监听的sock,设置监听的事件events,epoll底层有自定义等待队列,队列里面有ep_item就绪项,可以简单理解为每个就绪项对应了红黑树的一个节点 ,ep_item会与网络设备绑定,网络设备收到数据,会给等待队列发送通知,有一个socket数据到达后,向等待队列发送就绪通知,等待队列里面就知道谁就绪了,当ep_item就绪后会调用回调函数将自身传出到rd_list就绪链表中,用户层中有就绪数组,epoll将就绪链表中的节点拷贝到用户层就绪数组,方便用户处理。

腾讯二面:epoll性能那么高,为什么? (qq.com)

优点:

1、通过异步通知的方式解决了轮询问题,可以最大程度的监听sock

2、epoll不仅返回就绪的数量,还会返回就绪的sock,便于用户处理,不像前两个只返回了就绪的数量

3、监听树直接创建在内核层,可以保证每个节点拷贝一次并挂载一次,没有无意义的开销

EPOLL的两种监听模式

1.EPOLLLT,水平触发模式,也被称之为负责模式,如果不进行特殊设置,默认情况下对套接字都是以此模式监听。

在此模式下,当epoll监听到某一套接字就绪时,就会先发送处理通知给上层。之后每隔一段时间就来查看该套接字缓冲区中的数据有没有被处理完。

如果没有处理完,就会继续发送处理通知给上层,一天不处理完就发一天,一周处理不完就发一周,直到缓冲区中的数据被全部处理完毕。这就是所说的负责模式

PS:在此模式下,如果缓冲区中的数据没有被处理完,epoll_wait()无法开启下一轮监听。意思就是如果数据没处理完,就去调用epoll_wait()函数的话,会立即返回,返回值为未处理的就绪事件数量

优点:能够保证套接字缓冲区中的所有数据被处理完毕

缺点:开销比较大

适用场景:该模式适合处理有效期较短、或需要被紧急处理的数据

2.EPOLLET,边缘触发模式,也被称之为不负责模式,使用该模式需要进行手动设置

在此模式下,当epoll监听到某一套接字就绪时,指挥发送一次处理通知给上层。之后无论该套接字缓冲区中的数据有没有被处理完,都与epoll无关,epoll可以立即进入新一轮的监听

优点:开销比较小

缺点:存在隐患,不能够保证套接字缓冲区中的所有数据被处理(完毕),用户要自行保证数据读取完毕

PS:边缘触发模式一般结合非阻塞读取套接字缓冲区的数据

int sockfd;//套接字文件描述符
 
struct epoll_event node;
node.data.fd = sockfd;
node.events = EPOLLIN|EPOLLET;//监听读事件,监听模式切换为边缘触发模式

区别

select,poll,epoll都是I/O多路复用机制,即能监视多个fd,一旦某fd就绪(读或写就绪),能够通知程序进行相应读写操作。 但select,poll,epoll本质都是同步I/O,因为他们都需在读写事件就绪后,自己负责进行读写,即该读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O实现会负责把数据从内核拷贝到用户空间。

select,poll需自己主动不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但它是设备就绪时,调用回调函数,把就绪fd放入就绪链表,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但select和poll在“醒着”时要遍历整个fd集合,而epoll在“醒着”的时候只需判断就绪链表是否为空,节省大量CPU时间,这就是回调机制带来的性能提升。

select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,且把current往等待队列上挂也只挂一次(在epoll_wait开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少开销。

select、poll和epoll都是用于实现 I/O 多路复用的方式,可以在同一时间内监听多个文件描述符的就绪状态。

  • select 是一种比较老的方式,它使用位图来表示文件描述符的状态。调用 select 时,内核需要遍历整个位图,检查每个文件描述符是否就绪。这种轮询的方式在连接数量很少时还是很有效的,但当连接数量增多时,性能会下降。由于使用位图来保存描述符,所以 select 还有描述符个数的限制,一般只能支持 2048 个,不过 select 的跨平台性比较好,几乎所有的平台都可以支持。
  • poll 使用链表结构来表示文件描述符的状态,没有最大连接数的限制。和 select 函数一样,poll 返回后,需要轮询来获取就绪的描述符,因此随着监视的描述符数量的增长,其效率也会线性下降。
  • epoll 是 Linux 特有的一种方式,它使用了事件驱动的模型,没有最大连接数的限制。它将文件描述符添加到 epoll 的事件集合中,等待事件的发生。与 select 和 poll 不同的是,epoll 不需要轮询,它使用回调的方式,只关注真正发生事件的文件描述符。这使得epoll在大规模高并发连接下具有卓越的性能。

应用场景上,select和poll适用于连接数量较少的场景,而epoll则适用于需要处理大规模并发连接且性能要求较高的场景。


参考:腾讯面试:请描述 select、poll、epoll 这三种IO多路复用技术的执行原理_哔哩哔哩_bilibili

  • 41
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值