• Select Poll Epoll


    上一篇文章我们已经介绍过了集中常用的 IO 模型了。IO 多路复用模型是我们用的最多的一种 IO 模型。select,poll,epoll 都是 IO 多路复用的机制。I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符。一旦某个描述符就绪(读或者写就绪),就通知程序进行相应的读写操作。

    select,poll,epoll 本质上都是同步 I/O,因为它们都需要在读写事件就绪后自己负责读写,也就是说,这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 实现会负责把数据从内核拷贝到用户空间。

    select

    int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    

    select 函数监视的文件描述符分为三类,分别是 writefds,readfds 和 exceptfds。调用后 select 函数会阻塞,直到有文件描述符就绪(有数据可读,可写或者有 except),或者超时(timeout 指定等待时间,如果立即返回设置为 null 即可),函数返回。当 select 函数返回后,可以通过遍历 fdset,来找到就绪的描述符。

    select 目前几乎在所有的平台上支持,其良好的跨平台也成为它的有点。select 的一个缺点在于单个进程能够监视的文件描述符最大为 1024。

    select 本质上是通过设置或者检查存放 fd 标志位的数据结构来进行下一步处理。这样所带来的缺点是:

    1. 单个进程可监视的 fd 数量被限制,即能监听端口的大小有限。32 位是 1024,64 位是 2048。
    2. 对 socket 进行扫描时都是线性扫描,即采用轮询的方法,效率比较低。不管是不是活跃的,都选哟遍历一遍。
    3. 需要维护一个用来存放大量 fd 的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

    poll

    int poll (struct pollfd *fds, unsigned int nfds, int timeout);
    

    不同于 select 使用三个位图来表示 fdset 的方式,poll 使用也给 pollfd 的指针实现。

    struct pollfd {
        int fd; /* file descriptor */
        short events; /* requested events to watch */
        short revents; /* returned events witnessed */
    };
    

    pollfd 结构包含了要监视的 event 和发生的 event,不再使用 select “参数-值”传递的方式。同时,pollfd 并没有最大数量限制(但是数量过大后性能也是会下降)。和 select 函数一样,poll 返回后,需要轮询 pollfd 来获取就绪的描述符。

    select 和 poll 都需要在返回后,通过遍历文件描述符来获取已经就绪的 socket。事实上,同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。

    poll 本质上和 select 并没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态,如果设备就绪则在设备等待队列加入一项并继续遍历,如果遍历完所有 fd 后没有发现就绪设备,则挂起当前线程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历 fd。

    1. poll 没有连接数限制,原因它是基于链表来存储的。
    2. 也是需要轮询来查询 fd 的状态,fd 越多,效率越差。

    epoll

    epoll 是在 2.6 内核中提出的,是之前的 select 和 poll 的增强版本。相对于 select 和 poll 来说,epoll 更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

    epoll 操作过程

    一个 epoll 操作过程需要三个接口,分别如下:

    int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    

    int epoll_create(int size)

    创建一个 epoll 的句柄,size 用来告诉内核这个监听的数目一共多大,这个参数不同于 select() 中的第一个参数,给出最大监听的 fd+1 的值,参数 size 并不是限制了 epoll 所能监听描述符的最大个数,只是对内核初始分配内部数据结构的一个建议。

    创建好 epoll 句柄后,它就会占用一个 fd 值,使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)

    函数是对指定描述符 fd 执行 op 操作。

    • epfd: 是 epoll_create() 返回值。
    • op: 表示 op 操作,用三个宏来表示:添加 EPOLL_CTL_ADD, 删除 EPOLL_CTL_DEL, 修改 EPOLL_CTL_MOD。分别添加,删除和修改对 fd 的监听事件。
    • fd: 需要监听的 fd(文件描述符)。
    • epoll_event: 是告诉内核需要监听什么事,struct epoll_event 的结构如下:
    struct epoll_event {
      __uint32_t events;  /* Epoll events */
      epoll_data_t data;  /* User data variable */
    };
    
    //events可以是以下几个宏的集合:
    EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    EPOLLOUT:表示对应的文件描述符可以写;
    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR:表示对应的文件描述符发生错误;
    EPOLLHUP:表示对应的文件描述符被挂断;
    EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
    

    int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)

    等待 epfd 上的 io 事件,最多返回 maxevents 个事件。

    参数 events 用来从内核得到事件的集合,maxevents 告之内核这个 events 有多大。该函数返回需要处理的事件数目,如返回 0 表示已超时。

    工作模式

    epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger)。

    LT 模式(默认):当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。通过这种模式,系统不会充斥大量你不关心的就绪文件描述符。

    ET模式(告诉):当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。在 ET 模式下,read 一个 fd 的时候一定要把它的 buffer 都读光。

    1. epoll 没有最大并发连接的限制,能打开的 FD 的上线远大于 1024。
    2. 效率提升,不是轮询的方式,不会随着 FD 数目的增加效率下降,只有活跃可用的 FD 才会调用 callback 函数。
    3. epoll 最大的有点在于它只管你活跃的连接,而跟连接总数无关。因此在实际的网络环境中,epoll 的效率会远高于 select 和 poll。
    4. 内存拷贝,利用 mmap() 文件映射内存加速与内核空间的消息传递,即 epoll 使用 mmap 减少复制开销。

    select,poll 和 epoll 的区别总结

    1. 支持一个进程能打开的最大连接数。

      select 是 1024,poll 没有最大限制,底层是基于链表的。epoll 有上限,但hi很大,1G 内存的及其可以打开 10 万左右的连接,2G 内存可以打开 20 万左右的连接。

    2. FD 剧增后带来的 IO 效率问题。

      select 和 poll 每次调用都会对 FD 进行线性遍历,所以随着 FD 增加遍历的速度会越慢。

      因为 epoll 内核中的实现是根据每个 fd 上的 callback 函数来上西安的,只有活跃的 socket 才会主动 callback,所以性能和总的监视的文件描述符无关。

    3. 消息传递方式。

      select 和 poll 内核需要将消息传递到用户空间,都需要内核拷贝动作。epoll 通过内核和用户共享一块内存来实现。

    select poll epoll
    操作方式 遍历 遍历 回调
    底层实现 数组 链表 哈希表
    IO效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)
    最大连接数 1024(x86)或2048(x64) 无上限 无上限
    fd拷贝 每次调用select,都需要把fd集合从用户态拷贝到内核态 每次调用poll,都需要把fd集合从用户态拷贝到内核态 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

    总结

    表面上看 epoll 的性能最好,但是在连接数少并且都十分活跃的情况下,select 和 poll 的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多函数回调。

    select 低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可以通过良好的设计改善。

  • 相关阅读:
    我喜欢的女孩有了男友 :(
    两个月后,我又回来了。
    准备辞职,想看看老板知道我要辞职之后的表情。
    已经交了辞职报告,今天下午跟老板谈一谈。
    上班第十天
    一年了,回来看看。
    上班第十一天
    领到了离职通知单
    对上班失去了兴趣
    还没有拿到回家的火车票,惨了啊。
  • 原文地址:https://www.cnblogs.com/paulwang92115/p/12188114.html
Copyright © 2020-2023  润新知