什么是I/O 多路复用:select、poll、epoll讲解

前言

在BIO方式实现的Socket 模型中,可以通过多线程实现客户端请求并发处理,每个线程通过BIO的方式处理一个客户端长链接。但是这种方式在高并发的场景下并不适用,假如有一万个连接,系统就要对应维护一万个线程,空闲的连接也要独占一个线程,频繁的线程上下文切换会对系统造成很大压力。

既然为每个请求分配一个线程的方式不合适,那有没有可能只使用一个线程来维护多个 Socket 呢?答案是有的,那就是 I/O 多路复用技术。线程通过read()、write()这些系统调用只能阻塞在一个文件描述符(fd)上,而IO多路复用是指通过一个结构绑定监听多个文件描述符,在线程中通过一次IO调用可阻塞在多个文件描述符上,内核发现这些文件有新的IO事件时就通知线程并返回结果让线程去处理这些IO事件。这样就可以更加高效地同时完成对多个文件的操作。select/poll/epoll就是内核提供给用户态的多路复用系统调用。而在一切皆文件的Linux中,socket也是一个文件,创建 socket 的系统调用返回的就是一个socket文件描述符,因此我们就可以通过IO多路复用在一个线程中监听多个socket,以实现更高的并发度。

在这里插入图片描述

一、select

select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。

所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。

select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。

二、poll

poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。

但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。

三、epoll

1. 使用方法

先复习下 epoll 的用法。如下的代码中,先用e poll_create 创建一个 epol l对象 epfd,再通过 epoll_ctl 将需要监视的 socket 添加到epfd中,最后调用 epoll_wait 阻塞等待数据。

int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...)

int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中

while(1) {
    int n = epoll_wait(...);
    for(接收到数据的socket){
        //处理
    }
}

2. 原理

epoll 通过两个方面,很好解决了 select/poll 的问题。

  • 第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述符,红黑树是一种高效率的二叉查找树,在内核中使用红黑树来维护待检测的fd集,增删改查的效率都很高。把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn)。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。

  • 第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

从下图你可以看到 epoll 相关的接口作用:

在这里插入图片描述
epoll 的方式即使监听的 Socket 数量越多的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常的多了,上限就为系统定义的进程打开的最大文件描述符个数。因而,epoll 被称为解决 C10K 问题的利器。

参考:https://www.bilibili.com/video/BV1qJ411w7du

3. 触发模式——水平触发和边缘触发

epoll 支持两种事件触发模式,分别是水平触发和边缘触发。select/poll 只有水平触发模式。

  • 水平触发模式:当线程收到IO事件从epoll_wait苏醒时,若IO事件没有处理完比如说没有读完缓冲区的数据,线程就又进入了epoll_wait等待,那么内核检测到fd仍处于可读可写状态,会继续通知,线程就会从epoll_wait中重复苏醒,直到所有数据都处理完毕。
  • 边缘触发模式:对于IO事件只通知一次,若没有处理完就进入epoll_wait阻塞了不会再次通知,因此需要我们程序保证一次性将内核缓冲区的数据读取完;

一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数。

如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误。

使用 I/O 多路复用时,最好就搭配非阻塞 I/O 一起使用

转载于:https://xiaolincoding.com/os/8_network_system/selete_poll_epoll.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值