Back

linux的epoll机制

  • IO多路复用
  • epoll池原理

IO多路复用

  • 实现形式:后台程序只需要1个就可以负责管理多个fd句柄,负责应对所有业务放的IO请求。这种一对多的IO模式叫做IO多路复用
    • 多路是指:多个业务方(句柄)并发下来的IO
    • 复用是指:复用这个一个后台处理程序

1、最朴实的实现方式 - for循环

  • 使用for循环,每次都尝试IO一下,读/写到了就处理,否则sleep

    • 1
      2
      3
      4
      5
      
      while True:
          for each 句柄数组 {
              read/write(fd, /* 参数 */)
          }
          sleep(1s)
      
    • 存在问题:

      • 默认情况下,create出的句柄是阻塞类型的。读写数据的时候,如果数据没有准备好,会需要等待。所以上面代码第三行可能直接被卡死,导致整个线程都得不到运行。
    • 解决方案

      • 只需要把fd都设置成非阻塞的模式。这样read/write的时候,如果数据没有准备好,返回EAGIN的错误即可,不会卡住线程,从而整个系统就运转起来了。
    • 缺点:

      • for循环每次都要定期sleep,会导致吞吐能力极差。
      • 不加sleep会导致CPU一直告诉运转,浪费资源
    • 解决思路:

      • 求助内核提供机制协助。因为内核才能及时的管理这些事件的通知和调度。
      • 要把所有的时间都用在处理句柄的IO上,不能有任何空转、sleep的时间浪费。

2、Linux内核机制

  • 内核提供了3中工具selectpollepoll
  • 这三种都方式都能够管理fd的可读可写事件,在所有fd不可读不可写的时候,可以阻塞线程,切走cpufd有情况的时候,线程能够被唤醒。
  • 这三种方式epoll池效率最高。selectpoll都需要遍历知道fd,所以效率低。

epoll 池原理

1、epoll 涉及的系统调用

epoll涉及下面三个系统调用:

  • 1
    2
    3
    
    epoll_create
    epoll_ctl
    epoll_wait
    
  • epoll_create:负责创建一个池子,一个监控和管理句柄fd的池子

  • epoll_ctl:负责管理这个池子里fd的增、删、改

  • epoll_wait:负责等待,让出CPU调度,但是只有有事,立马会从这里唤醒

2、epoll 高效的原理

  • epoll的实现几乎没有做任何无效功

实现步骤:

  1. epoll创建一个池子。使用epoll_create

    1
    2
    3
    4
    5
    6
    7
    8
    
    // 原型
    int epoll_create(int size)
    // 示例
    epollfd = epoll_create(1024);
    if (epollfd == -1) {
        perror("epoll_create");
        exit(EXIT_FAILURE);
    }
    
    • 代码解释
      • epollfd:唯一代表这个epoll
      • 用户可以创建多个epoll
  2. 往这个epoll池放fd。使用epoll_ctl

    1
    2
    3
    4
    5
    6
    7
    
    // 原型
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    // 示例
    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, 11, &ev) == -1) {
        perror("epoll_ctl: listen_sock");
        exit(EXIT_FAILURE);
    }
    
    • 代码解释

      • 上述操作把句柄11放入池子
      • op(EPOLL_CTL_ADD)表明操作时增加、修改、删除
      • event结构体可以指定监听事件类型,可读、可写
    • 高效问题1:添加fd进了池子,如果是修改、删除,怎样做到高效?

      • 红黑树。时间复杂度O(log n)
    • 高效问题2:怎样保证数据准备好后,立马感知?

      • 通过设置回调
      • epoll_ctl的内部实现中,除了把句柄用红黑树管理,还会设置poll回调
    • poll回调是什么?

      • file_operations->poll:定制监听事件的机制实现。通过poll机制让上层能告诉底层,我这个fd一旦读写就绪了,请底层硬件(比如网卡)回调的时候自动把这个fd相关的结构体放到指定队列中,并且唤醒操作系统。
      • 这个poll事件回调机制是epoll池高效最核心原理
      • epoll池管理的句柄只能是支持了file_operations->poll的文件fd。如果一个“文件”所在的文件系统没有实现poll接口,那么就用不了epoll机制
    • poll怎么设置?

      • epoll_ctl的实现中,有一步是调用vfs_poll,这个里面会有个判断,如果fd所在的文件系统的file_operations实现了poll,那么就会直接调用,否则报告相应的错误码

        1
        2
        3
        4
        5
        6
        
        static inline __poll_t vfs_poll(struct file *file, struct poll_table_struct *pt)
        {
            if (unlikely(!file->f_op->poll))
                return DEFAULT_POLLMASK;
            return file->f_op->poll(file, pt);
        }
        
    • poll调用里面实现了什么?

      • 挂了个钩子,设置了唤醒的回调路径。epoll跟底层对接的回调函数是ep_poll_callback,这个函数做两件事:
        1. 把事件就绪的fd对应的结构体放到一个特定的队列(就绪队列,ready list
        2. 唤醒epoll
      • fd满足可读可写的时候,就会经过层层回调,最终调用到这个回调函数,把对应fd的结构体放入就绪队列中,从而把epollepoll_wait唤醒
        • 结构体叫做epitem,每个注册到epoll池的fd都会对应一个
        • 就绪队列,因为没有查找需求,所以就绪队列使用最简单的双指针链表
    • 小结。epoll之所以做到了高效,最关键的两点:

      • 内部管理fd使用了高效的红黑树
      • epoll池添加fd的时候,调用file_operations->poll,把这个fd就绪之后的回调路径安排好。通过事件通知的形式,做到最高效的运行
      • epoll池核心的两个数据结构:红黑树和就绪列表。

3、哪些fd可以用epoll来管理

  • 由于并不是所有的fd对应的文件系统都实现了poll接口,所以自然并不是所有的fd都可以放进epoll池,那么有哪些文件系统的file_operations实现了poll接口?

  • 类似ext2ext4xfs这种常规的文件系统是没有实现的。最常见的、真正是文件的文件系统反倒是用不了epoll机制的

  • 哪些支持

    • 最常见的就是网络套接字socket。网络也是epoll池最常见的应用地点。Linux下一些皆文件,socket实现了一套socket_file_operations的逻辑(net/socket.c):

      1
      2
      3
      4
      5
      6
      
      static const struct file_operations socket_file_ops = {
          .read_iter =    sock_read_iter,
          .write_iter =   sock_write_iter,
          .poll =     sock_poll,
          // ...
      };
      
      • socket实现了poll调用,所以socket fd是天然可以放到epoll池管理的
    • eventfdeventfd实现非常简单,故名思义就是专门用来做事件通知用的。使用系统调用 eventfd 创建,这种文件fd无法传输数据,只用来传输事件,常常用于生产消费者模式的事件实现;

    • timerfd:这是一种定时器fd,使用 timerfd_create 创建,到时间点触发可读事件;

  • 小结

    • ext2,ext4,xfs 等这种真正的文件系统的 fd ,无法使用 epoll 管理;
    • socket fd,eventfd,timerfd 这些实现了 poll 调用的可以放到 epoll 池进行管理;

总结

  • IO 多路复用的原始实现很简单,就是一个 1 对多的服务模式,一个 loop 对应处理多个 fd
  • IO 多路复用想要做到真正的高效,必须要内核机制提供。因为 IO 的处理和完成是在内核,如果内核不帮忙,用户态的程序根本无法精确的抓到处理时机;
  • fd 记得要设置成非阻塞的哦,切记;
  • epoll 池通过高效的内部管理结构,并且结合操作系统提供的 poll 事件注册机制,实现了高效的 fd 事件管理,为高并发的 IO 处理提供了前提条件;
  • epoll 全名 eventpoll,在 Linux 内核下以一个文件系统模块的形式实现,所以有人常说 epoll 其实本身就是文件系统也是对的;
  • socketfd,eventfd,timerfd 这三种“文件”fd 实现了 poll 接口,所以网络 fd,事件fd,定时器fd 都可以使用 epoll_ctl 注册到池子里。我们最常见的就是网络fd的多路复用;
  • **ext2,ext4,xfs 这种真正意义的文件系统反倒没有提供 poll 接口实现,所以不能用 epoll 池来管理其句柄。**那文件就无法使用 epoll 机制了吗?不是的,有一个库叫做 libaio ,通过这个库我们可以间接的让文件使用 epoll 通知事件
持续性混吃等死...
Built with Hugo
Theme Stack designed by Jimmy