网络编程-I/O复用服务器select/epoll/poll的使用基本概念

来源:互联网 发布:福禄克网络测试仪使用 编辑:程序博客网 时间:2024/05/21 08:55

,###序言

总结epoll/select/poll服务器编程API,之后有时间会补上具体的使用例子。


1. 同步I/O

  • [1] I/O复用服务器的优势: I/O复用服务器相比多进程/多线程服务器的最大优势是系统开销小,系统不必建立新的进程或线程,也不必维护这些进程/线程

    • 如果没有I/O复用,同时listen很多套接字并处理就需要很多线程,这导致不小的系统开销
  • [2] I/O复用:select(),epoll(),poll()都是I/O多路复用机制,可以监听多个描述符,一旦描述符就绪(读/写描述符),在文件描述符进行读写之前,通知程序进行相应的读写操作。

  • [3] 同步I/O:select(),epoll(),poll()本质上都是同步I/O,因为它们都需要在读写事件就绪后进行阻塞读写;而异步I/O则无需自己负责读写。


2. select()的使用

  • [1] I/O复用概念提出来后,select是第一个实现的。

    • 由于当时的硬件水平限制,上千的客户请求量已经很大了,select最大1024个链接的限制基本上能满足需求
  • [2] select()头文件

#include <sys/select.h>#include <sys/time.h>#include <sys/types.h>#include <unistd.h>
  • [3] 函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);返回值:    成功:返回就绪描述符的个数。超时返回0    失败:返回-1参数:    nfds:要监视的文件符的范围,一般取监视的最大描述符加1. Linux下最大为1024,即01023    readfds:监视的可读描述符集合,只要有文件描述符即将进行读操作,这个文件描述符就存储到这    writefds:监视的可写描述符集合    exceptfds:监视的错误异常描述符集合    fdset:是一个位数组,其大小限制为__FD_SETSIZE(1024),位数组的每一位代表其对应的描述符是否需要被检查    readfds/writefds/exceptfds:几个参数分别表示需要读、写、错误事件的文件描述符位数组,这些参数既是输入参数也是输出参数,可能会被内核修改用于标示哪些描述符上发生了关注的事件。所以每次调用select前都需要重新初始化fdset    timeout:参数为超时时间
  • [4] select()的功能

    • 调用select()后会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时(timeout),函数才返回。当select()返回后,通过遍历fd_set集合,来找到就绪的待处理的描述符。
  • [5] select()实现过程

    • select()对应于内核中sys_select调用,sys_select首先将readfds/writefds/exceptfds参数指向的fd_set拷贝到内核,然后对集合中的描述符调用进行poll,并记录在临时结果中,如果有事件发生,select()会将临时结果写到用户空间并返回;当轮询一遍后没有任何事件发生,如果指定了超时时间,则select()会睡眠到超时,睡眠结束后再进行一次轮询,并将临时结果写到用户空间,然后返回。
  • [6] 集合fd_set的操作函数

    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);   // 检查集合中指定的文件描述符是否可以读写 
  • [7] select()流程图

这里写图片描述


3. poll()的使用

  • [1] poll()的引入

    • 相比select(),poll()一个取消了1024个连接的限制,另一个不需要重新初始化传入数组

    • poll() 的实现和 select() 非常相似,只是描述 fd 集合的方式不同,poll() 使用 pollfd 结构而不是 select() 的 fd_set 结构,其他的都差不多

  • [2] 所需头文件

#include <poll.h>
  • [3] 函数原型
int poll(struct pollfd *fds, nfds_t nfds, int timeout);//返回值:    成功:返回pollfd结构体中 revents(实际发生了的事件) 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;    失败:返回-1.并设置errno为下列值        EBADF:一个或多个结构体中指定的文件描述符无效。        EFAULT:fds 指针指向的地址超出进程的地址空间。        EINTR:请求的事件之前产生一个信号,调用可以重新发起。        EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。        ENOMEM:可用内存不足,无法完成请求。//参数:1】fds:poll()使用了pollfd结构体来实现,结构体包含:文件描述符 + 等待的事件 + 实际发生了的事件struct pollfd{    int fd;         /* 文件描述符,针对每一个结构体 */    short events;   /* 等待的事件,由用户设置 */    short revents;  /* 实际发生了的事件,由内核设置 */}; fd:每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。    【2】events:每个结构体的 events 域是监视该文件描述符的事件掩码,由用户来设置这个域,可取的值如下    //处理输入:        POLLIN 普通或优先级带数据可读        POLLRDNORM 普通数据可读        POLLRDBAND 优先级带数据可读        POLLPRI 高优先级数据可读    //处理输出:        POLLOUT 普通或优先级带数据可写        POLLWRNORM 普通数据可写        POLLWRBAND 优先级带数据可写    //处理错误:        POLLERR发生错误        POLLHUP发生挂起        POLLVAL 描述字不是一个打开的文件例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events 为 POLLIN | POLLOUT。    【3】revents:    revents 域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。    events 域中请求的任何事件都可能在 revents 域中返回。    每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件。    【4】nfds:    用来指定结构体数组的元素个数    【5】timeout:    指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回。当等待时间为 0 时,poll() 函数立即返回,为 -1 则使 poll() 一直阻塞直到一个指定事件发生。
  • [4] 函数功能
    • 监视并等待多个文件描述符(结构体)


4. epoll()的使用

  • [1] epoll是I/O多路复用最新的一个实现

  • [2] 头文件

#include <sys/epoll.h> 
  • [3] epoll操作API
    • 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);  
  • [4] epoll功能

    • epoll通过epoll_create()创建一个用于epoll轮询的描述符

    • 通过epoll_ctl()添加/修改/删除事件

    • 通过epoll_wait检查事件,epoll_wait的第二个参数用于存放结果

  • [5] epoll操作函数

1】 描述符创建函数int epoll_create(int size);    (1) 功能:创建一个epoll句柄。该函数生成一个 epoll 专用的文件描述符    (2) 返回值:            成功:返回epoll专用的文件描述符            失败:返回-1    (3) 参数:        size:用来告诉内核这个监听的数目一共多大,并不限制epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议        自从 linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值    (4) 注意点:            需要注意的是,当创建好 epoll 句柄后,它就是会占用一个 fd 值,在 linux 下如果查看 /proc/ 进程 id/fd/,是能够看到这个 fd 的,所以在使用完 epoll 后,必须调用 close() 关闭,否则可能导致 fd 被耗尽。
2】 添加/修改/删除事件int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);    (1) 功能:    epoll 的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。    (2) 参数            epfd:epoll 专用的文件描述符,epoll_create()的返回值            op:表示动作,用三个宏来表示:                EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;                EPOLL_CTL_MOD:修改已经注册的fd的监听事件;                EPOLL_CTL_DEL:从 epfd 中删除一个 fd;            fd: 需要监听的文件描述符            event: 告诉内核要监听什么事件,struct epoll_event 结构如下: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 */  };      events 可以是以下几个宏的集合:            EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);            EPOLLOUT:表示对应的文件描述符可以写;            EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);            EPOLLERR:表示对应的文件描述符发生错误;            EPOLLHUP:表示对应的文件描述符被挂断;            EPOLLET :将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。            EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里
3】 检查事件int epoll_wait( int epfd, struct epoll_event * events, int maxevents, int timeout );    (1) 功能:            等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select() 调用。    (2)返回值:            成功:返回需要处理的事件数目,如返回 0 表示已超时。            失败:-1    (2) 参数:        epfd: epoll 专用的文件描述符,epoll_create()的返回值        events: 分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。        maxevents: maxevents 告之内核这个 events 有多大 。        timeout: 超时时间,单位为毫秒,为 -1 时,函数为阻塞
  • [6] epoll操作的两种模式

    epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger)。LT 模式是默认模式,LT 模式与 ET 模式的区别如下:

    • LT 模式:电平触发。当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait 时,会再次响应应用程序并通知此事件。

    • ET 模式:边沿触发。当 epoll_wait 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次响应应用程序并通知此事件。

    ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高

    epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。


5. select()/poll()/epoll()优缺点总结

  • [1] select()的优缺点

    • 优点:select()是最早实现的,目前几乎在所有的平台上支持,其良好跨平台支持是其优点

    • 缺点

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

      • (2) 每次调用 select() 不会告诉哪个fd上有数据,需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大

      • (3) 单个进程能够监视的文件描述符数量存在最大限制,在Linux上为1024个,即最多能支持1024个连接

        • 可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但也会造成效率的降低
      • (4) select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。

      • (5) select()不是线程安全的。如果把一个socket加入select中,然后另一个线程发现,这个socket不用要收回,这种行为是不支持的而且不可验证的

  • [2] poll()的优缺点

    • 优点

      • (1) poll()去掉了1024个连接的限制

      • (2) select()会修改传入数组,poll()不再修改传入数组

    • 缺点

      • 仍然是线程不安全的

      • 遍历拷贝的缺点仍没有避免

  • [3] epoll()的优缺点

    • 优点

      • (1) 避免了select:有1024连接限制 + 每次调用select都要把fd集合从用户态拷贝到内核态 + 每次调用都需要遍历fd集合,的缺点

        • 对于用户态到内核态拷贝的缺点:epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll()保证了每个fd在整个过程中只会拷贝一次

        • 对于遍历的缺点:epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,之后再判断。epoll()不仅告诉fd集合里有数据,而且还会告诉具体哪个fd有数据

        • 对于连接限制的缺点epoll()没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048。举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大

      • (2) epoll()是线程安全

      • (3) epoll()扩展性强:

        • select()和poll()每秒处理请求的数量随着请求数量的增加逐渐减小,而epoll()每秒处理的请求数量基本不会随着连接变多而下降
    • 缺点

      • 只有Linux支持,BSD上对应的实现是kqueue


参考文章

http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html(select使用实例)
http://www.cnblogs.com/xuxm2007/archive/2011/08/15/2139809.html
http://blog.csdn.net/tennysonsky/article/details/45745887(使用实例)
http://www.cnblogs.com/52php/p/5684594.html
http://www.cnblogs.com/Anker/p/3265058.html(推荐,区别)

2017.10.27

阅读全文
0 0