IO多路复用:select、poll、epoll

文章目录

  • 前言
  • IO multiplexing - IO复用
  • IO多路复用的优势
  • select
    • select函数
    • select的使用
    • select的缺点
    • select的优势
  • poll
  • epoll
    • epoll函数
      • epoll_create
      • epoll_ctl
      • epoll_wait
    • 工作模式
  • 总结
  • Reference

前言

I/O多路复用(multiplexing)的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作。

select,poll,epoll本质上都是同步I/O,因为它们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间

IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者,本文重点讲解网络IO。

对于一个network IO,它会涉及到两个系统对象:

  • application:调用这个IO的进程
  • kernel:系统内核

它们经历的两个交互过程是:

  • 阶段1: wait for data,等待数据准备
  • 阶段2: copy data from kernel to user,将数据从内核拷贝到用户进程中

IO multiplexing - IO复用

I/O多路复用(multiplexing)是网络编程中最常用的模型,最常用的selectpollepoll都属于这种模型。以select为例:

IO多路复用:select、poll、epoll_第1张图片

看起来它与blocking I/O很相似,两个阶段都阻塞。但它与blocking I/O的一个重要区别就是它可以等待多个数据报就绪(datagram ready),即可以处理多个连接。这里的select相当于一个"代理",调用select以后进程会被select阻塞,这时候在内核空间内select会监听指定的多个datagram (如socket连接),如果其中任意一个数据就绪了就返回。此时程序再进行数据读取操作,将数据拷贝至当前进程内。由于select可以监听多个socket,可以用它来处理多个连接。

select模型中每个socket一般都设置成non-blocking,虽然等待数据阶段仍然是阻塞状态,但是它是被select调用阻塞的,而不是直接被I/O阻塞的。select底层通过轮询机制来判断每个socket读写是否就绪。

select也有一些缺点,比如底层轮询机制会增加开销、支持的文件描述符数量过少等。为此,Linux引入了epoll作为select的改进版本。

IO多路复用的优势

  • 与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销

select

select函数

函数原型

#include 
#include 

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)

函数参数

  • maxfdp1:指定待测试的描述符个数

  • readset、writeset、exceptset:指定要让内核监控的可写、可读、异常的描述符,如果对某一个的条件不感兴趣,可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:

    • void FD_ZERO(fd_set *fdset);:清空集合
    • void FD_SET(int fd, fd_set *fdset);:将一个给定的文件描述符加入集合之中
    • void FD_CLR(int fd, fd_set *fdset);:将一个给定的文件描述符从集合中删除
    • int FD_ISSET(int fd, fd_set *fdset); :检查集合中指定的文件描述符是否可以读写
  • timeout:告知内核等待所指定描述字中的任何一个就绪可花多少时间

    struct timeval{
        long tv_sec;   //seconds
        long tv_usec;  //microseconds
    };
    
    • 这个参数有三种可能:
      • 永远等待下去:仅在有一个描述符准备好时才返回。为此,把该参数设置为空指针NULL
      • 等待一段固定时间:在有一个描述符准备好时返回,否则超时返回
      • 根本不等待:检查描述符后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。

返回值

  • 若有就绪描述符返回其数目,若超时则为0,若出错则为-1

select函数监控3类文件描述符,调用select函数后会阻塞,直到描述符fd准备就绪(有数据可读、可写、异常)或者超时,函数便返回。 当select函数返回后,可通过遍历描述符集合,找到就绪的描述符。

select的使用

IO多路复用:select、poll、epoll_第2张图片

SOCKADDR_IN addrSrv;
int reuse = 1;
SOCKET sockSrv,connsock;
SOCKADDR_IN addrClient;
pool pool;
int len=sizeof(SOCKADDR);
/*创建TCP*/
sockSrv=socket(AF_INET,SOCK_STREAM,0);

/*地址、端口的绑定*/
addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(port);

if(bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR))<0)
{
    fprintf(stderr,"Failed to bind");
    return ;
}

if(listen(sockSrv,5)<0)
{
    fprintf(stderr,"Failed to listen socket");
    return ;
}
setsockopt(sockSrv,SOL_SOCKET,SO_REUSEADDR,(const char*)&reuse,sizeof(reuse));
init_pool(sockSrv,&pool);
while(1)
{
    /*通过selete设置为异步模式*/
    pool.ready_set=pool.read_set;
    pool.nready=select(pool.maxfd+1,&pool.ready_set,NULL,NULL,NULL);
    if(FD_ISSET(sockSrv,&pool.ready_set))
    {
        connsock=accept(sockSrv,(SOCKADDR *)&addrClient,&len);
        //loadDeal()/*连接处理*/
        add_client(connsock,&pool);//添加到连接池
    }
    /*检查是否有事件发生*/
    check_client(&pool);
}

上面是一个服务器代码的关键部分,设置为异步的模式,然后接受到连接将其添加到连接池中。监听描述符上使用select,接受客户端的连接请求,在check_client函数中,遍历连接池中的描述符,检查是否有事件发生。

select的缺点

  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大

  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大

  • 为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)

select的优势

  • 从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的

poll

#include 
/**
 * fds:要监视的描述符
 * nfds:fds中描述符的总数量
 * 返回值:返回fds集合中就绪的读、写,或出错的描述符数量,返回0表示超时,返回-1表示出错;
**/
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
// 表示监视的描述符
struct pollfd {
    int fd;        /* file descriptor */
    short events;  /* requested events to watch 监视的请求事件 */
    short revents; /* returned events witnessed 已发生的事件 */
};

poll的使用和select基本类似,相比selectpoll解决了单个进程能够打开的文件描述符数量有限制这个问题,和select函数一样,当poll函数返回后,可以通过遍历描述符集合,找到就绪的描述符。

selectpoll共同具有的一个很大的缺点就是包含大量fd的数组被整体复制于用户态和内核态地址空间之间,开销会随着fd(文件描述符)数量增多而线性增大。

此外,selectpoll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。同时连接的大量客户端在同一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其性能会线性下降。

epoll

目前,epoll是Linux2.6下最高效的IO复用方式,也是Nginx、Node的IO实现方式。epoll的出现,解决了selectpoll的缺点:

  • 基于事件驱动的方式,避免了每次都要把所有fd(文件描述符)都扫描一遍
  • epoll_wait只返回就绪的fd(文件描述符)
  • epoll使用nmap内存映射技术避免了内存复制的开销
  • epoll的fd(文件描述符)数量上限是操作系统的最大文件句柄数目,这个数目一般和内存有关,通常远大于1024

epoll函数

#include 
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_create

  • 创建一个epoll句柄,参数size表明内核要监听的描述符数量。调用成功时返回一个epoll句柄描述符,失败时返回-1

epoll_ctl

注册要监听的事件类型,参数解释如下:

  • epfd 表示epoll句柄

  • op 表示fd操作类型,有如下3种

    • EPOLL_CTL_ADD 注册新的fd到epfd中
    • EPOLL_CTL_MOD 修改已注册的fd的监听事件
    • EPOLL_CTL_DEL 从epfd中删除一个fd
  • 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;
      

epoll_wait

等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。

  • epfd 是epoll句柄
  • events 表示从内核得到的就绪事件集合
  • maxevents 告诉内核events的大小
  • timeout 表示等待的超时事件

工作模式

  • 水平触发(Level Triggered,LT):当就绪的fd(文件描述符)未被用户进程处理,下一次查询依旧会返回,这也是select和poll的触发方式

  • 边缘触发(Edge Triggered,ET):无论就绪的fd(文件描述符)是否被处理,下一次不再返回。理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发,通过相应选项可以使用边缘触发

总结

select poll epoll
操作方式 遍历 遍历 回调
底层实现 数组 链表 红黑树
IO效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)
最大连接数 1024(x86)或2048(x64) 无上限 无上限
fd拷贝 每次调用select,都需要把fd集合从用户态拷贝到内核态 每次调用poll,都需要把fd集合从用户态拷贝到内核态 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超selectpoll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。

Reference

  • 一文读懂阻塞、非阻塞、同步、异步

  • IO多路复用的三种机制

  • select/poll/epoll对比分析

  • select、poll、epoll之间的区别总结

  • IO多路复用之select总结

  • IO多路复用之poll总结

  • IO多路复用之epoll总结

你可能感兴趣的:(操作系统,网络,服务器,epoll)