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


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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值