- 日期: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系统编程>>