三者都是UNIX下多路复用的内核接口,select是跨平台的接口,poll是systemV标准,epoll是linux专有的接口,基于poll改造而成。
select
函数原型:
int select (int n,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
select通过系统调用监视多个文件描述符的数组,select返回后,就绪的文件描述符会被修改标志位,使得进程可以获得准备就绪的文件描述符并继续I/O操作。
select目前的优点就是几乎所有的系统都支持select,缺点是最大只能监听1024个文件描述符,想监听超过1024个文件描述符时需要手动修改FD_SETSIZE大小,但这样活造成网络I/O性能下降;或者创建多进程,每个进程监听1024个文件描述符,这样会导致程序复杂度增加。
select的第一个参数n需要程序员自己处理,n为所有集合中最大的文件描述符值+1;select的第二个参数readfds列举的文件描述符被监视是否有可供读取的数据;select的第三个参数writefds列举的文件描述符被监视是否写入完成而不阻塞;select的第三个参数exceptfds列举的文件描述符被监视是否异常或者无法控制的数据是否可用(这些状态仅用于套接字);当文件描述符列表设置为NULL时该类文件描述符不被监视。select的第四个参数timeout用于设置超时时长,当select经过了timeout时长后仍然没有可用的文件描述符也将返回;select的返回值代表了当前可用的文件描述符的数量,范围为0-1024,当超时返回时返回值为0,其他时候是1-1024区间的值。
struct timeval {
long tv_sec ; /* seconds */};
如果select的timeout参数不为NULL,即便没有文件描述符准备好I/O,也会在等待ty_sec秒和ty_usec微秒后select返回,timeout在不同系统中的处理是不同的,最好每次调用select前设置一次。
向select监视的文件描述符集合中添加或者删除文件描述符通常采用系统定义的宏进行
FD_ZERO:将集合清空
FD_SET:向集合中添加一个文件描述符
FD_CLR:从集合中删除一个文件描述符
FD_ISSET :指定的文件描述符是否在集合中
能够放进fd_set中的最大的文件描述符值是1024,这个值是由FD_SETSIZE决定的。
select调用成功后非NULL的文件描述符集合将被修改,集合中将只剩下准备好的文件描述符,通过FD_ISSET轮询所有集合确定准备好I/O的文件描述符。
select的缺点:在于维护的存储文件描述符的数据结构需要在内核空间和用户空间来回复制,随着文件描述符数量的增加,复制所花费的开销也线性增长; 由于网络延迟,很多socket会处于非活跃状态,但select也会对其进行一次扫描。这两个缺点导致select的效率随文件描述符数量降低,在处理大量fd非活跃的集合时效率降低。
select仅支持水平触发(Level Triggered):即一个fd被返回可用后,如果没有对该fd进行I/O操作,那么下次select返回时仍然报告该fdI/O可用。
poll
poll和select本质上类似,但不存在最大文件描述符数量的限制,poll和select存在一样的缺点:维护的文件描述符数据结构在内核空间和用户空间来回拷贝,效率随监视的文件描述符数量而下降。
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中的每一个pollfd指明一个被监视的文件描述符,nfds指明最大监视的文件描述符数,timeout代表了超时时长,单位为毫秒。
pollfd结构体的fd是文件描述符;events是要监视的事件掩码;revents是被监视的文件描述符操作结果事件掩码,内核在返回时设置这个域。每一个events都可能在revents中被返回。
epoll
epoll是linux2.6以后出现的内核直接支持的方法,具有了select和poll的一切优点,被认为是linux2.6下最优秀的I/O就绪通知方式。
epoll可以支持水平触发(Level Triggered)和边缘触发(Edge Triggered,当文件描述符就绪时仅通知进程一次,即使进程没有对其进行I/O操作,以后也不会再通知),边缘触发方式适合高速I/O,但编程实现较为复杂。
epoll同样只告知那些准备就绪的文件描述符,当调用epoll_wait成功返回时,返回值代表准备就绪的文件描述符,此时epoll一个指定的数组中取出相应数量的文件描述符即可,epoll采用了文件映射技术(mmap),内核和用户态访问同一段内存,避免了文件描述符在内核和用户态之间的互相复制;epoll还采用了基于事件的就绪通知方式,epoll事先通过epoll_ctl注册一个文件描述符,一旦某个文件描述符就绪时,系统会采用类似于callback的机制迅速激活这个文件描述符,当进程调用epoll_wait便得到通知。
epoll的优点有可监视的文件描述符数量等于系统可打开的最大文件描述符;epoll的效率不随监视的文件描述符数量增加而线性下降;epoll通过文件映射减少了内核和用户态数据复制开销。
函数原型
int epoll_create(int size);
创建一个epoll句柄,指定要监听的文件描述符数量
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
对epoll句柄进行操作
epfd指定epoll句柄;
op为指定的操作EP_CTL_ADD(注册新的句柄到epoll);EP_CTL_MOD(修改已经注册的文件描述符的监听事件);EP_CTL_DEL(删除一个已经注册的fd)
fd为要注册的文件描述符
event为监听的事件
epoll_event结构如下:
struct epoll_event {
__uint32_t events; /*Epoll events*/
epoll_data_t data; /*User data variable*/
}
events可以用以下几个宏的集合
EPOLLIN:当前文件描述符可读
EPOLLOUT:当前文件描述符可以写
EPOLLPRI:当前文件描述符有紧急数据可以读(应该表示有带外数据到来)
EPOLLERR:当前文件描述符发生错误
EPOLLHUP:对应的文件描述符被挂断
EPOLLET:将EPOLL设置为边缘触发模式
EPOLLONESHOT:只监听一次事件,当完成一次监听以后还需要监听该描述符的话需要再次加入到EPOLL队列中。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
等待事件的发生,类似select返回。
后记
本次学习笔记基于网络搜索,非原创,也没有用到select、poll、epoll,可以说从零开始学,难免有些浅薄,待以后有了实际经验会进行补充。
补充
IPC和select、poll、epoll能否同时使用?通常不可以,这会带来不少麻烦,因为IPC的实现方式并不是文件描述符,如果需要同时使用IPC和多路复用(select、poll、epoll),可以采用fork子进程,然后父进程和子进程采用IPC或管道通信,其中一个进程使用多路复用技术监控文件描述符。
多路复用技能着重处理的文件描述符是socket、管道、伪终端(pty)、终端设备(tty)等系统文件描述符,对普通文件描述符不起作用。