美文网首页
关于 select、poll、epoll 的区别

关于 select、poll、epoll 的区别

作者: _给我一支烟_ | 来源:发表于2019-07-13 19:27 被阅读0次

    1. 函数说明

    1.1 select

    /* According to POSIX.1-2001 */
    #include <sys/select.h>
    /* According to earlier standards */
    #include <sys/time.h>
    #include <sys/types.h>
    #include <unistd.h>
    
    int select(int nfds, fd_set *readfds, fd_set *writefds, 
               fd_set *exceptfds, struct timeval *timeout);
    
    void FD_ZERO(fd_set *set);          //清空集合
    void FD_CLR(int fd, fd_set *set);   //将一个给定的文件描述符从集合中删除
    void FD_SET(int fd, fd_set *set);   //将一个给定的文件描述符加入集合之中
    int  FD_ISSET(int fd, fd_set *set); //检查集合中指定的文件描述符是否可以读写 
    
    struct timeval {
       long    tv_sec;         /* seconds 秒*/
       long    tv_usec;        /* microseconds 微妙*/
    };
    

    该函数准许进程指示内核等待多个事件中的任何一个发生,并在有一个或多个事件发生或经历一段指定的时间后才被唤醒。

    • 第一个参数 nfds 指定待测试的描述符个数,其值为最大的文件描述符加1,描述符0、1、2...nfds-1均将被测试(因为文件描述符是从0开始的)。
    • 中间的三个参数 readset、writeset、exceptset 指定我们要让内核测试读、写、异常条件的描述符。如果对某一个的条件不感兴趣,就可以把它设为 NULL。struct fd_set 可以理解为一个集合(实际上是一long类型的数组),这个集合中存放的是文件描述符,可通过上面那四个函数进行设置。
    • timeout 告知内核等待所指定描述符中的任何一个就绪最多等待时长。没有等到就绪就会超时。该参数控制三种可能:
      ① 传 NULL 时,一直阻塞直到有感兴趣的描述符上有IO事件就绪。
      ② timeval结构中指定的秒数和微秒数都是0,检查描述符后立即返回,不阻塞。
      ③ timeval结构中指定的秒数和微秒数指定了时长,在指定时长超时之前有 IO事件就绪返回,或者超时返回。

    1.2 poll

    #include <poll.h>
    
    int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    
    struct pollfd {
        int   fd;         /* file descriptor 文件描述符 */
        short events;     /* requested events 等待的事件 */
        short revents;    /* returned events 实际发生了的事件 */
    };
    

    每一个 struct pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,让 poll() 监视多个文件描述符。每个结构体的 events 域是监视该文件描述符的事件掩码,由用户来设置这个域。revents 域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回。

    合法的事件如下:

    常量 说明
    POLLIN 普通或优先级带数据可读
    POLLRDNORM 普通数据可读
    POLLRDBAND 优先级带数据可读
    POLLPRI 高优先级数据可读
    POLLOUT 普通数据可写
    POLLWRNORM 普通数据可写
    POLLWRBAND 优先级带数据可写
    POLLERR 发生错误
    POLLHUP 发生挂起
    POLLNVAL 描述字不是一个打开的文件

    POLLIN | POLLPRI 等价于 select() 的读事件
    POLLOUT 等价于 select() 的写事件

    ① timeout 参数指定等待的毫秒数,在超时之前有 IO 事件就绪或者超时,poll 都会返回
    ② timeout 为负数值,表示一直阻塞直到一个指定事件发生,poll 才返回。
    ③ timeout 为0,poll 调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。

    返回值和错误代码
    成功时:poll() 返回结构体中 revents 域不为0的文件描述符个数。
    超时后:如果在超时前没有任何事件发生,poll()返回0。
    失败时:poll() 返回-1,并设置errno为下列值之一

    EFAULT    指针指向的地址超出进程的地址空间。
    EINTR    请求的事件之前产生一个信号,调用可以重新发起。
    EINVAL    参数超出 PLIMIT_NOFILE 值。
    ENOMEM   可用内存不足,无法完成请求。

    1.3 epoll

    #include <sys/epoll.h>
    
    int epoll_create(int size);
    int epoll_create1(int flags);
    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);
    
    typedef union epoll_data {
       void        *ptr;
       int          fd;
       __uint32_t   u32;
       __uint64_t   u64;
    } epoll_data_t;
    
    struct epoll_event {
       __uint32_t   events;      /* Epoll events */
       epoll_data_t data;        /* User data variable */
    };
    

    (1)创建 epoll 实例
    现在一般使用 epoll_create1(EPOLL_CLOEXEC),原因:

    • 在linux 内核版本大于2.6.8 后,epoll_create(int size) 这个 size 参数就被弃用了,但是传入的值必须大于0。最初实现版本时, size参数的作用告诉内核需要使用多少个文件描述符。内核会使用 size 的大小去申请对应的内存。现在这个size参数不再使用了,内核会动态的申请需要的内存。
    • 使用 epoll_create1() 的优点是它允许你指定标志,指定 EPOLL_CLOEXEC 标记在执行另一个进程时文件描述符会自动关闭。

    (2)epoll 的事件注册
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    先注册要监听的事件类型。
    第一个参数是 epoll_create1 的返回值
    第二个参数表示动作,用三个宏来表示:

    EPOLL_CTL_ADD:注册新的 fd 到 epfd 中
    EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件
    EPOLL_CTL_DEL :从 epfd 中删除一个 fd

    第三个参数是需要监听的 fd
    第四个参数是告诉内核需要监听什么事,struct epoll_event 结构见上面代码
    events 可以是以下几个宏的集合:

    常量 说明
    EPOLLIN 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
    EPOLLOUT 表示对应的文件描述符可以写
    EPOLLPRI 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
    EPOLLERR 表示对应的文件描述符发生错误
    EPOLLHUP 表示对应的文件描述符被挂断
    EPOLLET 将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是相对于水平触发 (Level Triggered) 来说的,LT是缺省的工作方式
    EPOLLONESHOT 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个 socket 加入到 EPOLL 队列里

    LT (Level Triggered) 水平触发:默认的模式。内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你。

    ET (Edge Triggered) 边缘触发:“高速”模式。内核只会提示一次,直到下次再有数据流入之前都不会再提示了,无论 fd 中是否还有数据可读。在ET模式下,read一个fd的时候一定要把数据读完。

    (3)等待事件的产生
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    等待事件的产生,类似于 select() 调用。
    参数 events 用来从内核得到事件的集合
    参数 maxevents 告之内核这个 events 有多大
    参数 timeout 是超时时间(毫秒,0会立即返回,-1一直阻塞直到有IO事件就绪)
    该函数返回需要处理的事件数目,如返回0表示已超时。

    2. 三者之间的关联和区别

    关联

    select,poll,epoll 都是 I/O 多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
    select,poll,epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。

    区别

    关于最大连接数先理清楚2个概念:
    ⑴ 一个进程能打开的最大文件描述符,使用 ulimit -n 或者 cat /proc/进程号/limits |grep "Max open file"
    ❶ 使用 ulimit -n xxx 修改(只针对当前session有效),
    ❷ 通过 setrlimit 系统调用修改(只对当前进程有效),
    ❸ 修改 /etc/security/limits.conf 在该文件中添加以下两行:

        *      soft    nofile     100000
        *      hard    nofile     100000
    

    这个值是可以修改的,但是最大不要超过系统所能打开的最大数,超过了也没有什么意义。

    ⑵ 一个系统所能打开的最大数也是有限的,跟内存大小有关,可以通过cat /proc/sys/fs/file-max 查看。

    • 注意:对于服务器程序来说并不是“理论”最大只能打开65535个 socket 连接。65535 只是linux系统的最大可用socket端口,比如在一台服务器(这里指主机而非服务程序)上“理论”上最多只能发起65535个客户端连接(实际要小于)这是对的,因为每发起一个连接都需要一个端口。服务器程序只需监听几个端口就够了,它的上限是系统所能打开的最大文件描述符的数量。不要搞混了这两个概念。
    > 支持一个进程所能打开的最大连接数
    • select 最大连接数有一定限制的,由 FD_SETSIZE 值决定的,默认值是2048。可以对进行修改,需要重新编译内核,但是性能可能会受到影响。
    • poll 最大连接数上限是系统能最大可以打开文件的数目,一般来说这个数目受限于系统内存,1GB内存的机器上大约是10万左右,具体数目可以 cat /proc/sys/fs/file-max。
    • epoll 与 poll 一样,最大连接数上限是系统能最大可以打开文件的数目。
    > 连接数剧增后带来的 IO 效率问题
    • select 每次调用都对所有的连接进行线性遍历,所以随着连接数的增加会造成遍历速度的线性下降的性能问题。
    • poll 与 select 有相同的问题
    • epoll 是事件驱动的,内核中的实现是根据每个连接 fd 上的 callback 函数来实现的,只有活跃的 socket 才会主动调用 callback,所以在活跃 socket 较少的情况下,使用 epoll 没有前面两者的线性下降的性能问题,但是所有 socket 都很活跃的情况下,可能也会有性能问题。

    epoll 如何实现只处理活跃连接
    epoll实现了eventpoll数据结构
    数据结构中rdlist将活跃连接存储在链表中,当网卡发送报文时,增加节点,当读取一个事件后,链表删除节点,需要得到活跃连接就只需要遍历链表
    数据结构中rdr使用红黑树(自平衡二叉树)将事件存储,例如:当有读事件时,就新增节点,事件复杂度为logN

    一般来说编写高并发服务器程序都会首先 epoll 因为这种环境下其性能最好,但是在连接数少并且连接都十分活跃的情况下,select 和 poll 的性能可能比 epoll 好,毕竟 epoll 的通知机制需要很多函数回调。

    相关文章

      网友评论

          本文标题:关于 select、poll、epoll 的区别

          本文链接:https://www.haomeiwen.com/subject/kniykctx.html