首发于网络编程

select、poll和epoll模型

上一篇介绍了Unix网络编程的5种IO模型,这一篇介绍一下IO多路复用里面被广泛使用的select、poll和epoll模型,IO多路复用实现的主旨思想是,不再由应用程序自己监视客户端连接,取而代之由内核替应用程序监视文件

select

/* @Param:
   nfds:        监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
   readfds: 监控读数据文件描述符集合,传入传出参数
   writefds:    监控写数据文件描述符集合,传入传出参数
   exceptfds:   监控异常发生文件描述符集合,如带外数据到达异常,传入传出参数
   timeout: 定时阻塞监控时间,3种情况: 
                   1. NULL(永远等下去); 
                   2. 设置timeval,等待固定时间; 
                   3. 设置timeval里时间均为0,检查描述字后立即返回,轮询
 */
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  1. 使用select模型处理IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差,但select模型可以一个线程内同时处理多个socket的IO请求,即如果处理的连接数不是很高的话,使用select的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大,select的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接
  2. 每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
  3. select采用的是轮询模型,每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
  4. 为了减少数据拷贝以及轮询fd_set带来的性能损坏,内核对被监控的fd_set集合大小做了限制,这个是通过宏FD_SETSIZE控制的,一般32位平台为1024,64位平台为2048,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数

poll

/*  @Param
    struct pollfd {
        int fd;           /* 文件描述符 */
        short events;     /* 监控的事件 */
        short revents;    /* 监控事件中满足条件返回的事件 */
    };

*/

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  1. poll本质上和select没有区别,只是它没有最大连接数的限制,原因是它是基于链表来存储的,它将用户传入的需要监视的文件描述符拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd
  2. poll只解决了select监视文件描述符数量的限制,并没有改变每次调用都需要将文件描述符从程序空间(用户空间)拷贝到内核空间和内核底层轮询所有文件描述符带来的性能开销

epoll

/* 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关 */
int epoll_create(int size);

/* 控制某个epoll监控的文件描述符上的事件:注册、修改、删除 */
/*  @Param
    epfd:   epoll_creat的句柄
    op: 表示动作,用3个宏来表示:
            EPOLL_CTL_ADD (注册新的fd到epfd),
            EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
            EPOLL_CTL_DEL (从epfd删除一个fd);
    event:  告诉内核需要监听的事件
            struct epoll_event {
                __uint32_t events; // Epoll events 
                epoll_data_t data; // User data variable 
            };
            typedef union epoll_data {
                void *ptr;
                int fd;
                uint32_t u32;
                uint64_t u64;
            } epoll_data_t;
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

/* 等待所监控文件描述符上有事件的产生 */
/*  @Param
    epfd:       epoll_creat的句柄
    events: 用来存内核得到事件的集合
    maxevents:  告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size
    timeout:    是超时时间
                -1: 阻塞
                 0: 立即返回,非阻塞
                >0: 指定毫秒
    返回值:        成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
*/
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
  1. epoll是Linux下IO多路复用select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了(采用回调机制,不是轮询的方式,不会随着FD数目的增加效率下降,只有活跃可用的FD才会调用callback函数)
  2. 虽然表面看起来epoll非常好,但是对于连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,因为epoll是建立在大量的函数回调的基础之上
  3. epoll除了提供select/poll那种IO事件的水平触发(Level Triggered,水平触发只要有数据都会触发)外,还提供了边沿触发(Edge Triggered,边缘触发只有数据到来才触发,不管缓存区中是否还有数据),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率

参考文章:

[1]. Redis,Nginx,Netty为什么这么香?

[2]. IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)

[3]. Unix网络编程之同步/异步/阻塞/非阻塞

[4]. UNIX网络编程--socket中的同步/异步 阻塞/非阻塞

如有侵权,请联系删除,如有错误,欢迎大家指正,谢谢

发布于 2020-04-07 11:50