I/O复用(I/O multiplexing): select, pselect, poll, ppoll, epoll

来源:互联网 发布:在线算法 scc 编辑:程序博客网 时间:2024/05/01 23:12

  • 日期:2016.05.30
  • 作者:i.sshe
  • https://github.com/isshe

I/O复用:select, pselect, poll, epoll.

  • 注意:本文主要介绍的是epoll相关知识,无法确保正确

1. 相关问题:

  • 1.1 什么是I/O复用?
  • 1.2 四个I/O复用方法相关知识点?
  • 1.3 四个I/O复用方法的比较?
  • 1.4 epoll有哪些触发模式?有何区别?
  • 1.5 select 什么情况下返回?
  • 1.6 如果select返回可读,结果只读到0字节,什么情况?
  • 1.7 两个epoll等待同一个文件描述符会发生什么?[事件发生时会同时返回给两个epoll实例]
  • 1.8 如果epoll 把自己epoll_create()返回的描述符放入自己文件描述符集里面,会有发生什么情况?
  • 1.9 如何设计大规模的并发模型?

[参见man epoll手册后面的9个问题]

2.拓展问题:

  • 2.1 什么是线程安全?

3. 解答

3.1 什么是I/O复用?

  • I/O复用(I/O multiplexing): 单个线程通过记录跟踪每一个I/O流的状态来同时管理多个I/O流.

3.2 四个I/O复用方法相关知识点?

  • poll 和 select的工作机制是:内核遍历所有监听中的文件描述符,返回”准备好”的文件描述符的个数.

3.2.1 select:

 1). 原型:

       int select(int nfds, fd_set *readfds, fd_set *writefds,                  fd_set *exceptfds, struct timeval *timeout);       void FD_CLR(int fd, fd_set *set);       int  FD_ISSET(int fd, fd_set *set);       void FD_SET(int fd, fd_set *set);       void FD_ZERO(fd_set *set);

 2). 说明:

  • 永远等待: timeout == NULL;
  • 不等待: timeout->tv_sec == 0 && timeout->tv_usec == 0;
  • 等待指定时间: timeout->tv_sec != 0 || timeout->tv_usec != 0;
  • 声明描述符集以后,必须用FD_ZERO将描述符集置0!
  • 当第2,3,4个参数都为NULL时,select 只作为定时器.
  • 返回:-1: 出错;0: 没有描述符准备好;>0: 准备好的描述符的个数.
  • 描述符阻塞与否不影响select是否阻塞.
  • select关注的最大描述符数是:FD_SETSIZE, 一般为1024.(对于一般程序来说太大了)

3.2.2 pselect:

 1). 原型:

       int pselect(int nfds, fd_set *readfds, fd_set *writefds,                   fd_set *exceptfds, const struct timespec *timeout,                   const sigset_t *sigmask);

 和pselect 的区别在于:
 timespec 结构是s+ns(秒+纳秒)级别,且为const修饰的.(select 是s+ms)
 pselect 可使用可选信号屏蔽字.

3.2.3 poll和ppoll:

 1). 原型:

       #include <poll.h>       int poll(struct pollfd *fds, nfds_t nfds, int timeout);       #define _GNU_SOURCE         /* See feature_test_macros(7) */       #include <poll.h>       int ppoll(struct pollfd *fds, nfds_t nfds,               const struct timespec *timeout_ts, const sigset_t *sigmask);

 2). 说明:

  • struct pollfd结构:

    struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events,interest */
    short revents; /* returned events ,occurred */
    };
  • timeout == -1:永远等待
  • timeout == 0: 不等待
  • timeout > 0: 等待timeout**毫秒**!
  • 结构中的events是感兴趣的事件;revents是发生(返回)的事件.
  • poll关注的描述符数为nfds(第二个参数),一般为unsigned long 型.

3.2.4 epoll:

  • 由于select和poll的局限性,linux 2.6 内核引入了event poll(epoll)机制.
  • epoll的工作原理是:创建一个epoll上下文->添加/删除文件描述符到epoll上下文(描述符集)->事件等待,记录发生事件的文件描述符.

1). 可以通过epoll_create()[不赞成使用]和epoll_create1()创建一个epoll上下文[打开一个epoll文件描述符].

  • 原型:
       #include <sys/epoll.h>       int epoll_create(int size);       int epoll_create1(int flags);
  • 参数说明:

    • 从linux 2.6.8后,size就被忽略了(但必须大于0)
    • 当flags==0时,epoll_create1()功能和epoll_create()功能一样;
      flags==EPOLL_CLOEXEC时,新文件描述符中会设置close-on-exec (FD_CLOEXEC)标志.(见man 2 open)
  • 返回:

    • 返回值是一个文件描述符,但是此文件描述符和真实文件没有关系.当不用的时候,应当close().
    • 当返回-1时,代表出错,errno被设置:
    EINVAL: 无效的flags;[size不是正数]    EMFILE: 达到用户能打开最大文件数;    ENFILE: 达到系统能打开的最大文件数;    ENOMEM: 内存不足.

2). 可以通过epoll_ctl()添加或删除文件描述符到epoll上下文.

  • 原型:
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 说明:

    • 参数op的值:

          EPOLL_CTL_ADD: 指定fd添加到epfd关联的epoll上下文中,event定义事件;    EPOLL_CTL_DEL: 删除;    EPOLL_CTL_MOD: 修改指定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 是一个位集(bit set), 可用的值有:

         EPOLLIN: 可读.   EPOLLOUT: 可写.   EPOLLPRI: 高优先级数据可读.   EPOLLERR: 错误条件发生在关联的文件描述符中.(epoll_wait总是等待这个事件,不需要把它设置在events中.)   EPOLLHUP: 挂断(hangup)发生.(epoll_wait总是等待这个事件,不需要把它设置在events中.)   EPOLLET: 指定文件描述符设置为边缘触发(默认动作是水平触发).            需要用EPOLL_CTL_MOD调用epoll_ctl()重新设置事件才能再监听.   EPOLLRDHUP (since Linux 2.6.17): (Stream socket)关闭连接或半关闭写连接.            (This flag is  especially  useful  for  writing simple code to detect peer shutdown when using Edge Triggered monitoring.)
    • 返回值: 成功0,失败-1,errno呗设置:

         EBADF: epfd或fd不是有效的文件描述符.   EEXIST: op是 EPOLL_CTL_ADD,但fd已经注册过了.   EINVAL: epfd不是一个epoll文件描述符或fd和epfd相同或op所请求的操作不被支持.   ENOENT: op是EPOLL_CTL_MOD或EPOLL_CTL_DEL,但fd还没有注册.   ENOMEM: 内存不足.   ENOSPC: 达到最大监听数目.[?,百度一下]   EPERM: 目标fd不支持epoll.

3).等待一个I/O事件发生.

  • 原型:

       #include <sys/epoll.h>   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_wait()等待/收集监听事件中已经发生的事件.
    • events: 分配好内存的结构数组.
    • maxevents: 用户指定的events结构数组的大小(events的最大数目).[这个参数不是很理解.]
    • timeout: -1未定义; 0立即返回,>0指定毫秒.
      返回: 0超时,>0发生事件数目,-1错误,errno被设置:
         EBADF: epfd不是有效文件描述符   EFAULT: 进程对events指向的内存没有写权限.   EINTR: 调用在事件发生或超时前被信号中断.   EINVAL: epfd不是一个epoll文件描述符,或maxevents<=0.
  • epoll_wait()和epoll_pwait()的关系:

           ready = epoll_pwait(epfd, &events, maxevents, timeout, &sigmask);

    等于

           sigset_t origmask;       sigprocmask(SIG_SETMASK, &sigmask, &origmask);       ready = epoll_wait(epfd, &events, maxevents, timeout);       sigprocmask(SIG_SETMASK, &origmask, NULL);

    当sigmask==NULL的时候,两个函数相等.

4). 边缘触发和水平触发[摘自man epoll]
这里写图片描述

  • 用于读管道的文件描述符rfd在epoll实例中注册了.
  • writer在写端写2kB数据到管道.
  • 此时调用epoll_wait()会返回rfd,作为”准备好”读的文件描述符.
  • reader通过rfd从管道读取1KB数据.
  • 然后再调用一次epoll_wait(). //边缘触发和水平触发的区别在这里体现.
    • 当在步骤1中注册使用水平触发(EPOLLLT)时,步骤5会和步骤3一样返回rfd,因为此时管道中还有数据.
    • 当使用边缘触发(EPOLLET)时,步骤5将可能挂起,尽管有效的数据还在输入缓冲区中,同时,数据发送端(写端)可能还在等待一个反馈.发生这种情况的原因是:边缘触发只在文件描述符状态发生改变的时候才递交事件.所以,在步骤5中调用者可能会不再等待已经在输入缓冲区中的数据.
    • 在上面的例子中,rfd上的事件发生后,步骤2写数据,事件在步骤3销毁(但输入缓冲还有数据).因此如果步骤4读数据但没有全部读完,那么步骤5调用epoll_wait()可能未定义地阻塞.
    • 一个程序如果用了EPOLLET标志的话,应该使用非阻塞文件描述符来避免读/写阻塞把处理多个文件描述符的任务饿死.
    • 使用边缘触发的epoll时,建议:
      • 使用非阻塞文件描述符
      • 只在read()或write()返回EAGAIN后才等待一个事件(epoll_wait()).

3.3 四个I/O复用方法的比较?

3.3.1 select的问题:

  • select 会修改传入的参数数组,对于一个需要调用很多次的函数,是非常不友好的。
  • select 遍历数组,看哪个准备好,数组越大,所需时间越长.
  • 描述符上(I/O stream)出现了数据(准备好可读可写异常),select 仅仅返回准备好的描述符个数,并不会告诉你是哪个描述符.
  • select 只能监视1024个描述符.
  • select 不是线程安全的
  • 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销

3.3.2 poll:

  • poll的个数限制为unsigned long.
  • poll 不修改参数数组.
  • poll 仍然不是线程安全的.

3.3.3 epoll:

  • epoll把以上问题都解决了并且加入了一些新特性.(特性不知..)

3.4 epoll有哪些触发模式?有何区别?

  • 见3.2.4节关于epoll的知识点说明.

3.5 select 什么情况下返回?

  • 侦听到文件描述符可读/可写/异常时.

3.6 如果select返回可读,结果只读到0字节, 为什么?

  • 读到了文件尾.[EOF]
  • 如果在一个文件描述符上碰到文件尾端,则select会认为该描述符可读.然后调用read()返回0,这是UNIX系统指示到文件尾端的方法.[摘自<<unix环境高级编程(第3版)>>p407]

3.7 两个epoll等待同一个文件描述符会发生什么?

  • 事件发生时会同时返回给两个epoll实例

3.8 如果epoll 把自己epoll_create()返回的描述符放入自己文件描述符集里面,会有发生什么情况?

  • epoll_ctl会失败(EINVAL),但可以把自己的文件描述符放到别的epoll描述符集里面.

3. 9 如何设计大规模的并发模型?

  • ...

4. 拓展问题

4.1 什么是线程安全?

  • 多线程访问同一段代码,不会产生不确定的结果,就是线程安全的。

5. 参考资料

5.1 知乎答案
5.2 <<Unix 环境高级编程(第3版)>>
5.3 <<Linux系统编程>>

0 0