• Epoll 与 Select


    epoll: epoll的优势在于它不会随着监听fd数目的增长而降低效率。

    select: 在内核中select采用轮询方式来处理,轮询的fd数目越多,耗时就越多。

    在linux/posix_types.h头文件有这样的声明:

    #define __FD_SETSIZE 1024

    表示 select 最多同时监听1024个fd,可以通过修改头文件在重新编译内核来扩大这个数目,但这似乎并不治本。

    一 IO 多路复用的 select

    IO 多路复用相对于阻塞式和非阻塞式的优势在于它可以监听多个socket,并且不会消耗过多资源。当用户进程调用select时,它会监听其中所有socket知道有一个或多个socket数据已经准备好,否则就一直处于阻塞式状态。select的劣势在于单个进程能够监视的文件描述符的数量存在最大限制,select() 所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络相应时间的延迟使得大量TCP链接处于非常活跃状态,但调用select()会对所有的select进行一次线性扫描,所以这也浪费了一定的开销。优势在于它具有跨平台特性。

    二 Epoll

    epoll 的ET 是必须对非阻塞的socket 才能工作,LT对阻塞与非阻塞的socket 都可以。

    所有I/O 多路复用操作都是同步的,涵盖select/poll。

    阻塞/非阻塞是相对同步I/O来说的,与异步I/O无关。

    select/poll/epoll 本身是同步的,可以阻塞与可以不阻塞。(其中,阻塞与非阻塞 与 同步不同步不同;阻塞与否是自身,异步与否是与外部协作的关系)

    skater:无论是阻塞I/O、非阻塞I/O,还是基于非阻塞I/O的多路复用都是同步调用。因为他们在read调用时,内核将数据从内核空间拷贝至应用程序空间(epoll应该是从mmap),过程都是需要等待的,也就是说这个过程是同步的,如果内核实现的拷贝效率不高,read调用就会在这个同步过程中等待比较长的时间。

    1. epoll事件:
    2. EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    3. EPOLLOUT: 表示对应的文件描述符可以写;
    4. EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    5. EPOLLERR: 表示对应的文件描述符发生错误;
    6. EPOLLHUP: 表示对应的文件描述符被挂断;

    epoll高效的核心是:1 用户态和内核态共享内存mmap。2 数据到来采用事件通知机制(不需要轮询)。

    epoll 的api:
     

    1. epoll - I/O event notification facility
    2. #include
    3. int epoll_create(int size);
    4. int epoll_create1(int flags);
    5. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    6. int epoll_wait(int epfd, struct epoll_event *events,
    7.                       int maxevents, int timeout);
    8. int epoll_pwait(int epfd, struct epoll_event *events,
    9.                       int maxevents, int timeout,
    10.                       const sigset_t *sigmask);

    ① 

    int epoll_create(int size);

    int epoll_create1(int flags);

    epoll_create 返回一个句柄,之后epoll的使用都将依靠这个句柄来标识。参数size来说明epoll最大处理的事件个数。当不再使用epoll时,需要调用close函数来关闭这个句柄。

    (注意:size参数只是告诉内核这个epoll对象会处理的事件大概个数,而不是能够处理的事件的最大个数;在Linux最新的一些内核版本中,size参数没有任何意义)

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

    epoll_ctl 向 epoll 对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。epoll_wait方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。

    参数:

    epfd: epoll_create返回的句柄,

    op:的意义见下表:

    EPOLL_CTL_ADD:注册新的fd到epfd中;

    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

    EPOLL_CTL_DEL:从epfd中删除一个fd;

    fd:需要监听的socket句柄fd,

    event:告诉内核需要监听什么事的结构体,struct epoll_event结构如下:

    1. typedef union epoll_data {
    2. void *ptr;
    3. int fd;
    4. uint32_t u32;
    5. uint64_t u64;
    6. } epoll_data_t;
    7. struct epoll_event {
    8. uint32_t events; /* Epoll events */
    9. epoll_data_t data; /* User data variable */
    10. };
    1. __uint32_t events就要监听的事件(感兴趣的事件):
    2. EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    3. EPOLLOUT:表示对应的文件描述符可以写;
    4. EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    5. EPOLLERR:表示对应的文件描述符发生错误;
    6. EPOLLHUP:表示对应的文件描述符被挂断;
    7. EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
    8. EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

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

    int epoll_pwait(int epfd, struct epoll_event *events,
                          int maxevents, int timeout,
                          const sigset_t *sigmask);

    收集在 epoll监控的事件中已经发生的事件,如果 epoll中没有任何一个事件发生,则最多等待timeout毫秒后返回。epoll_wait的返回值表示当前发生的事件个数,如果返回0,则表示本次调用中没有事件发生,如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型。

    epfd:epoll的描述符。

    events:分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到 events数组中(events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。

    maxevents:表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。

    timeout:表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示 epoll_wait在 rdllist链表中为空,立刻返回,不会等待。

    epoll有两种工作模式:LT(水平触发)模式和ET(边缘触发)模式。

    默认情况下,epoll采用 LT模式工作,这时可以处理阻塞和非阻塞套接字,而上表中的 EPOLLET表示可以将一个事件改为 ET模式。ET模式的效率要比 LT模式高,它只支持非阻塞套接字。

    (水平触发LT:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上次没读写完的文件描述符上继续读写

    边缘触发ET:当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你

    此可见,水平触发时如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率,而边缘触发,则不会充斥大量你不关心的就绪文件描述符,从而性能差异,高下立见。)

  • 相关阅读:
    能碳双控| AIRIOT智慧能碳管理解决方案
    TinyKv Project2 PartA RaftKV
    ISO体系认证的流程 ISO认证需要的材料
    【Excel密码】四个方法,设置excel表格只读模式
    Java 变量之变量数据类型
    java入门,登录注册案例
    linux创建mysql新用户及授权
    区块链 | ERC721 标准
    Springcloud----Nacos配置中心
    AMD64(x86_64)架构abi文档:中
  • 原文地址:https://blog.csdn.net/hello_wordmy/article/details/133580198