• IO多路复用与epoll机制浅析


    epoll是Linux中用于IO多路复用的机制,在nginx和redis等软件中都有应用,redis的性能好的原因之一也就是使用了epoll进行IO多路复用,同时epoll也是各大公司面试的热点问题。

    IO多路复用

    IO多路复用是一种同步IO模型,使得一个线程就可以对多个文件描述符进行监听。当有文件描述符准备就绪时,函数就会返回,从而通知应用进行相应的处理;当没有描述符就绪时,函数就会阻塞。

    IO多路复用对于网络应用来说是非常重要的,在没有IO多路复用时,应用一般通过同步阻塞(每个socket连接建立一个新线程,这将十分耗费系统性能)或者同步非阻塞(对所有socket进行反复遍历,当没有就绪描述符时就会做无用功)来实现,而这些方法的性能都不太好。

    在Linux中,IO多路复用主要有三种方法select、poll和epoll。

    select

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

    select是通过传递文件描述符数组fd_set*来实现的。当没有描述符准备就绪时,函数就会阻塞;当有一个或多个文件描述符准备就绪时就会返回,之后通过遍历数组找到准备就绪的描述符进行处理。select函数一般在所有操作系统中都会实现,因此具有良好的可移植性。

    fd_set的大小是固定的,在Linux中一般为1024,本质是一个bitmap,通过FD_SET将描述符加入fd_set,通过对所有文件描述符依次调用FD_ISSET来判断是否准备就绪。

    因此,select就有着以下的缺点:

    • select的文件描述符最大只能支持1024个
    • select需要通过遍历来判断是否准备就绪,因此时间复杂度为O(n)
    • 当监听文件描述符数量增加时,性能会明显下降
    • select内核态中通过轮询来判断文件描述符是否就绪
    • select每次调用都需要将fd_set从用户地址空间拷贝到内核地址空间中,函数返回时又要拷贝回来

    poll

    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    struct pollfd {
        int fd;               // 文件描述符
        short events;         // 等待的事件
        short revents;        // 发生的事件
    };
    

    pollselect的主要改进就是没有了描述符数组的大小限制,没有最大连接数的限制。但是poll仍然需要进行遍历才能知道哪些文件描述符准备就绪,因此,select的缺点poll也有。

    epoll

    int epoll_create(int 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);
    

    epoll使用了三个系统调用来实现,epoll_create创建一个句柄,epoll_ctl向句柄中添加、删除或修改文件描述符,epoll_wait对句柄进行监听,当有文件描述符准备就绪后,就会通过events参数返回。返回的参数中仅包含准备就绪的文件描述符,也就是说不再需要通过遍历来进行判断。epoll通过回调机制来快速将文件描述符加入就绪链表,避免轮询;同时epoll内部使用红黑树来保存所有监听的文件描述符。

    epoll有着以下的优点:

    • 没有最大文件描述符数量限制
    • 使用mmap,避免了每次wait都要将数组进行拷贝
    • 直接返回就绪的文件描述符,避免了遍历,时间复杂度为O(k),k为就绪文件描述符
    • 使用回调机制,当文件描述符就绪时会触发回调函数,将描述符加入到就绪链表,避免轮询
    • 监听的文件描述符数量对性能影响不大

    但是epoll也不是一定比selectpoll好,当就绪的文件描述符很多时,即O(k)中的k接近n时,两者性能就比较接近了;当文件描述符数量较少时,两者性能也差不多;epoll的回调函数注册也会带来一定的性能开销。

    触发方式

    epoll有两种触发方式,水平触发(LT, level-triggered)和边缘触发(ET, edge-triggered)。通过一个例子来理解两种方式:

    当描述符a中到达2kb数据,调用epoll_wait会返回a,之后从描述符中读取1kb数据,此时该描述符中仍有1kb数据,仍为就绪状态;第二次调用epoll_wait时,如果是LT,那么返回的描述符中仍包含a,如果为ET,那么就不包含a。

    即ET只会在状态发生改变时触发,只返回一次,类似于上升沿触发;而LT只要处于就绪状态就会一直返回,类似于电平触发。

    理论上ET的性能会比LT要好,但是ET要保证每次都要把数据全部处理完成,而LT使用起来就更加方便,不易出现bug。在实际当中两种的性能区别可以忽略,redis使用的就是LT方式。

  • 相关阅读:
    回溯 八皇后问题 与 0-1背包
    技术学习
    线程间通信
    计算机网络基础知识总结(各种协议)
    大数据Phoenix专题
    java中HashMap详解
    使用LocalBroadcastManager解决BroadcastReceiver安全问题
    Paint类常用方法
    链表结构对象池的一种实现
    android内存及内存溢出分析
  • 原文地址:https://www.cnblogs.com/liqiangwei/p/14354144.html
Copyright © 2020-2023  润新知