1.一个进程能打开的最大文件描述符是有限的
2.FD_SETSIZE(fd_set)限制,select内部使用一个数据结构fd_set,它的容量最大不能超过FD_SETSIZE。
一个进程能打开的最大文件描述符是有限的
上面的进程能打开的最大文件描述符的个数可通过命令ulimit -n number更改,但也不是无限大,还受到系统所能打开的最大文件描述符个数的限制,这个大小和内存有关。
但是很肯定的说,这个只是个说明,你的内存不一定支持这么多。
内核要遍历所有我们所感兴趣的文件描述符,直到找到发生事件的文件描述符,这样的话,当并发数增长的时候,内核要遍历的文件描述符的个数也随之增大,导致效率下降。这也就是epoll引入的原因。
在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
epoll是Linux特有的I/O复用函数。它在实现和使用上与select、poll有很大差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select和poll那样每次调用都要重复传入文件描述符集或事件集。但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。这个文件描述符使用epoll_create函数来创建。
//通过拷贝的方式,每次循环都需要重复传入文件描述符集
nready=poll(client,maxi+1,-1);
//用户关心的文件描述符上的事件放在内核里的一个事件表中,通过epollfd标识
//这个事件表
nready=epoll_wait(epollfd,&*events.begin(),static_cast(events.size()),-1);
epoll_create函数
int epoll_create(int size);
作用:
该函数生成一个epoll专用的文件描述符,返回的文件描述符将用作其它所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
参数:
size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。(曾经表示在这个epoll fd上能关注的最大fd数)
epoll_ctl函数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
作用:
该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数:
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD修改、EPOLL_CTL_DEL 删除
fd:关联的文件描述符,加入epoll_ctl进行管理
event:事件,对文件描述符所感兴趣的事件,指向epoll_event的指针;
返回值
如果调用成功返回0,不成功返回-1
说明
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; //感兴趣的事件是可读还是可写
epoll_data_t data; /* User data variable */这是epoll高效的地方,数据类型是上面的那个共用体
};
events可以是以下几个宏的集合:
epoll_wait函数
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
说明:
epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制它的第二个参数events指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。
参数:
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传待处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值;-1相当于阻塞,0相当于非阻塞。一般用-1即可返回发生事件数。
返回值:
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目
https://blog.csdn.net/russell_tao/article/details/7160071
关键:
epoll是一种IO多路复用技术,可以非常高效的处理数以百万计的socket句柄,比起以前的select和poll效率高大发了。
首先要调用epoll_create建立一个epoll对象。参数size是内核保证能够正确处理的最大句柄数,多于这个最大数时内核可不保证效果。epoll_ctl可以操作上面建立的epoll,可以添加、移除socket句柄。epoll_wait在调用时,在给定的timeout时间内,当在监控的所有句柄中有事件发生时,就返回。
Epoll高效主要体现在以下三个方面:
①从上面的调用方式就可以看出epoll比select/poll的一个优势:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。
② 此外,内核使用了slab机制,为epoll提供了快速的数据结构:
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。
③ epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。
如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。
1.执行epoll_create时,创建了红黑树和就绪list链表。
2.执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。
3.执行epoll_wait时立刻返回准备就绪链表里的数据即可。
最后看看epoll独有的两种模式LT和ET。无论是LT和ET模式,都适用于以上所说的流程。区别是,LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时次次返回这个句柄,而ET模式仅在第一次返回。
这件事怎么做到的呢?当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait干了件事,就是检查这些socket,如果不是ET模式(就是LT模式的句柄了),并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表了。所以,非ET的句柄,只要它上面还有事件,epoll_wait每次都会返回。而ET模式的句柄,即使socket上的事件没有处理完,也是不会次次从epoll_wait返回的。
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
typedef std::vector EventList;
#define ERR_EXIT(m)\
do\
{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
void activate_nonblock(int fd)
{
int ret;
int flags=fcntl(fd,F_GETFL);
if(flags==-1)
ERR_EXIT("fcntl");
flags |= O_NONBLOCK;
ret=fcntl(fd,F_SETFL,flags);
if(ret==-1)
ERR_EXIT("fcntl");
}
ssize_t readn(int fd,void *buf,size_t count)
{
size_t nleft=count;
ssize_t nread;
char *bufp=(char*)buf;
while(nleft>0)
{
if((nread=read(fd,bufp,nleft))<0)
{
if(errno==EINTR)
continue;
return -1;
}
else if(nread==0)
return count-nleft;
bufp+=nread;
nleft-=nread;
}
return count;
}
ssize_t writen(int fd,void *buf,size_t count)
{
size_t nleft=count;
ssize_t nwritten;
char *bufp=(char*)buf;
while(nleft>0)
{
if((nwritten=write(fd,bufp,nleft))<0)
{
if(errno==EINTR)
continue;
return -1;
}
else if(nwritten==0)
continue;
bufp+=nwritten;
nleft-=nwritten;
}
return count;
}
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
while(1)
{
int ret=recv(sockfd,buf,len,MSG_PEEK);
if(ret==-1 && errno==EINTR)
continue;
return ret;
}
}
ssize_t readline(int sockfd,void *buf,size_t maxline)
{
int ret;
int nread;
char *bufp=(char *)buf;
int nleft=maxline;
while(1)
{
ret=recv_peek(sockfd,bufp,nleft);
if(ret<0)
return ret;
else if(ret==0)
return ret;
nread=ret;
int i;
for(i=0;inleft)
exit(EXIT_FAILURE);
nleft-=nread;
ret=readn(sockfd,bufp,nread);
if(ret!=nread)
exit(EXIT_FAILURE);
bufp+=nread;
}
return -1;
}
void handle_sigchld(int sig)
{
while(waitpid(-1,NULL,WNOHANG)>0);
}
void handle_sigpipe(int sig)
{
printf("recv a sig=%d\n",sig);
}
int main(void)
{
int count=0;
//signal(SIGPIPE,SIG_IGN);
signal(SIGPIPE,handle_sigpipe);//演示确实收到了一个信号handle_sigpipe
/* signal(SIGCHLD,SIG_IGN);*/
signal(SIGCHLD,handle_sigchld);
int listenfd;
if((listenfd=socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)
/*if((listenfd=socket(PF_INET,SOCK_STREAM,0))<0) */
ERR_EXIT("socket");
struct sockaddr_in servaddr;
memset(&servaddr,0,sizeof(servaddr));
servaddr.sin_family=AF_INET;
servaddr.sin_port=htons(5188);
servaddr.sin_addr.s_addr=htonl(INADDR_ANY);
/*servaddr.sin_addr.s_addr=inet_addr("127.0.0.1");*/
/*inet_aton("127.0.0.1,&servaddr.sin_addr");*/
int on=1;
if(setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on))<0)//设置一个选项地址重复利用
ERR_EXIT("setsockopt");
if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)//绑定
ERR_EXIT("bind");
if(listen(listenfd,SOMAXCONN)<0)//监听
ERR_EXIT("listen");
//以下就是epoll独有的,上面的都是copy poll而来的
//clients保存的是客户端的套接字
std::vector clients;
int epollfd;
//参数表示进程被替换的时候,文件描述符会被关闭
//epoll_create1内部开辟了一个共享内存,用来存放感兴趣的套接字的一些事件
epollfd=epoll_create1(EPOLL_CLOEXEC);//创建一个epoll实例
//通过man epoll 可了解下面各个步骤
struct epoll_event event;
//感兴趣的一个文件描述符
event.data.fd=listenfd;
//感兴趣的事件是EPOLLIN,以及以边沿的方式触发
event.events=EPOLLIN | EPOLLET;
//将感兴趣的加入epoll进行管理(将监听套接口以及感兴趣的事件加入)
epoll_ctlz(epollfd,EPOLL_CTL_ADD,listenfd,&event);
EventList events(16);
struct sockaddr_in peeraddr;
socklen_t peerlen;
int conn;
int i;
int nready;
while(1)
{
//检测所返回的事件,哪些I/O产生了事件
//返回的事件保存在events当中
//&*events.begin()是动态数组的首地址
nready=epoll_wait(epollfd,&*events.begin(),static_cast(events.size()),-1);
if(nready==-1)
{
if(errno==EINTR)
continue;
ERR_EXIT("epoll_wait");
}
if(nready==0)
continue;
//容量不够了,扩容
if((size_t)nready==events.size())
events.resize(events.size()*2);
for(int i=0;i
EPOLLIN事件(可读)
内核中的socket接收缓冲区为空 低电平
内核中的socket接收缓冲区不为空 高电平
EPOLLOUT事件(可写)
内核中的socke发送缓冲区不满 高电平
内核中的socke发送缓冲区满 低电平
LT是水平触发,也就是高电平就会触发
ET是边沿触发,低电平->高电平或者高电平->低电平触发
LT电平触发模式
完全靠kernel epoll驱动,应用程序只需要处理从epoll_wait返回的fds,这些fds我们认为它们处于就绪状态。当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到事件被处理。
开始的时候只能关注connfd套接字的EPOLLIN事件,不能马上关注EPOLLOUT事件,因为此时connfd的发送缓冲区为空,处于高电平状态。而此时没有数据可以发送,那么就会一直处于高电平状态,就会一直触发可写事件,导致出现busy-loop。
只能等到一次没有发送完的时候,才关注EPOLLOUT事件,如上图所示。而且要记得,应用层缓冲区中的数据发送完毕之后,要取消关注EPOLLOUT。
ET 边缘触发模式
开始的时候关注connfd套接字的EPOLLIN事件和EPOLLOUT事件,此时,发送缓冲区为空,处于高电平状态;而接收缓冲区也为空,处于低电平状态。当EPOLLIN事件到来的时候,接收缓冲区有数据了,由低电平->高电平。触发可读事件,一定要将数据全部接收,否则剩下的数据将读不出来,一直处于高电平状态。
如果发送的时候应用层缓冲区有100个字节,而发送缓冲区可容纳200个字节,这没问题。
如果发送的时候应用层缓冲区有1000个字节,而发送缓冲区可容纳200个字节,但是如果1000个字节只发送了100个字节,发了100个字节之后,由于发送缓冲区不满,仍然处于高电平状态,剩下的900个字节的数据就没办法发送了。除非将1000个字节中的200个字节发送到发送缓冲区中,让发送缓冲区满。就会处于低电平状态,一旦客户端接收走这200个字节之后,发送缓冲区不满,处于高电平状态,就会触发EPOLLOUT事件了。
此模式下,系统仅仅通知应用程序哪些fds变成了就绪状态,一旦fd变成就绪状态,epoll将不再关注这个fd的任何状态信息,(从epoll队列移除)直到应用程序通过读写操作触发EAGAIN状态,epoll认为这个fd又变为空闲状态,那么epoll又重新关注这个fd的状态变化(重新加入epoll队列)。随着epoll_wait的返回,队列中的fds是在减少的,所以在大并发的系统中,EPOLLET更具有优势。
一开始处理监听套接字的时候,EPOLLIN事件到来,返回一个已连接套接字。现在就关注了connfd的 EPOLLIN事件与EPOLLOUT事件,但是不会出现busy loop。现在缓冲区不满,处于的是高电平状态,从高电平到低电平或者从低电平到高电平才会触发EPOLLOUT,当缓冲区满的时候,处于的是低电平状态。
监听套接字一开始处于低电平状态,有一个连接过来之后就会处于高电平状态。然后要调用accept处理这个可读事件,但是如果accept返回失败,那么就会一直处于高电平状态。导致无法accept后面的连接。
epoll中et和lt的区别与实现原理
LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
如何使用et模式?
1.对于读操作,如果read没有一次读完buff数据,下一次将得不到就绪通知(ET特性),造成buff中数据无法读出,除非有新数据到达。
解决方法:将套接字设置为非阻塞,用while循环包住read,只要buff中有数据,就一直读。一直读到产生EAGIN错误。若ET模式下使用阻塞IO,则程序一定会阻塞在最后一次write或read操作,因此说ET模式下一定要使用非阻塞IO。
2.对于写操作主要因为ET模式下非阻塞需要我们考虑如何将用户要求写的数据写完。
解决方法:只要buff还有空间且用户请求写的数据还未写完,就一直写。
所以说LT同时支持阻塞I/O和非阻塞I/O,而ET模式只能支持非阻塞I/O。
epoll的ET模式下是必须使用非阻塞IO的?
ET模式指的是当数据从无到有时,才通知该fd。数据读不完,也不会再次通知,所以read时一定要采用循环的方式一直读到read函数返回-1为止。此时如果采用阻塞的read,那么就阻塞了整个线程。
eppll 即使使用ET模式,一个socket上的某个事件还是可能被触发多次,采用线程城池的方式来处理事件,可能一个socket同时被多个线程处理。造成一个很严重的问题,不同的线程或者进程在处理同一个SOCKET的事件,这会使程序的健壮性大降低而编程的复杂度大大增加!!
解决这种现象有两种方法,一种是在单独的线程或进程里解析数据,也就是说,接收数据的线程接收到数据后立刻将数据转移至另外的线程。
第二种方法就是本文要提到的EPOLLONESHOT这种方法,如果对描述符socket注册了EPOLLONESHOT事件,那么操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次。想要下次再触发则必须使用epoll_ctl重置该描述符上注册的事件,包括EPOLLONESHOT 事件。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里 。
相比于select与poll,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。
内核中的select与poll的实现是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。其算法时间复杂度是O(n)。
epoll的实现是基于回调的,如果fd有期望的事件发生就通过回调函数将其加入epoll就绪队列,也就是说它只关心“活跃”的fd,与fd数目无关。其算法复杂度是O(1)。
内核/用户空间内存拷贝问题,如何让内核把fd消息通知给用户空间呢?在这个问题上select/poll采取了内存拷贝方法。而epoll采用了共享内存的方式。
epoll不仅会告诉应用程序有I/O事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填写的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个集合。
当活动连接比较多的时候,epoll_wait的效率未必比select和poll高,因此此时回调函数被触发过于频繁。所以epoll_wait适用于连接数量多,但活动连接较少的情况。
epoll劣势(局限性):select可以跨平台,epoll只能在Linux上使用。
以前一直以为epoll模型比select和poll性能更好,毕竟select和poll都是轮询,即每次调用都扫描整个文件描述符集合,将其中就绪的文件描述符返回给用户程序,因此它们检测就绪事件算法复杂度是o(n)。epoll_wait采用回调的方式,内核检测到就绪的文件描述符时,触发回调,回调将该文件描述符对应的事件插入内核就绪队列,内核最后在适当的时间将该就绪队列中的内容拷贝到用户空间。因此epoll无须轮询整个文件描述符集合来检测哪些事件就绪,其算法复杂度是o(1)。但是,当活动链接比较多的时候,epoll的效率未必比select和poll高,因为此时回调函数被触发的过于频繁。所以,epoll适用于连接数量多,但活动连接比较少的情况。所以,用哪种模型,需要根据业务来综合考虑。
I/O复用使得程序能同时监听多个文件描述符,这对提高程序的性能至关重要。通常,网络程序在下列情况下需要使用I/O复用技术。
1.客户端程序要同时处理多个socket。比如非阻塞connect技术
2.客户端程序要同时处理用户输入和网络连接。
3.TCP服务器要同时处理监听socket和连接socket。
I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的,并且当多个文件描述符同时就绪时,如果不采取措施,程序就只能按顺序依次处理其中的每一个文件描述符。这使得服务器看起来就像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。
Linux下实现I/O复用的系统调用主要有select、poll和epoll。
非阻塞connect
对非阻塞的socket调用connect,而连接又没有立即建立时,我们可以调用select、poll等函数来监听这个连接失败的socket上的可写事件。当select、poll等函数返回后,再利用getsockopt来读取错误码并清除该socket上的错误。如果错误码是0,表示连接成功建立,否则连接失败。通过这种非阻塞connect方式,我们就能同时发起多个连接并一起等待。
聊天室程序
客户端使用poll同时监听用户输入和网络连接,服务端程序使用poll同时管理监听socket和连接socket。
同时处理TCP和UDP服务
对于一个端口,如果服务器要同时处理该端口上的TCP和UDP请求,那么需要创建两个不同的socket:一个流socket,另一个是数据报socket,并将它们都绑定到该端口上。
Q0:有什么方法可以用于区别哪些文件描述符已经注册到epoll集合里面了?
A0:关键就是结合 文件描述符 和 公开文件描述(the open file description ),或者又是被称为:公开文件句柄(open file handle),这是内核对一个文件的内在描述。
Q1:如果你注册一个相同的文件描述符给同一个epoll实例会发生什么情况?
A1:你也许会的EEXIST。然而,如果是通过复制的方式得到的文件描述符( (dup(2), dup2(2), fcntl(2) F_DUPFD) )那么,你很有可能能够将其注册到同一个epoll实例。对于过滤事件而言,这是一种非常有用的技术,这样可以使得复制得到文件描述符可以注册到不同的事件中。
Q2:两个不同epoll能够等待得到同一个文件描述符的事件么?如果可以的,那么事件是不是会同时报告给两个epoll实例呢?
A2:是的。事件会同时报告给两个epoll实例。然而,这样编程的话,需要更加小心。
Q3:这个epoll文件描述符自身能否被poll/epoll使用?
A3:可以,如果这个epoll这个文件描述符有事件等待被处理,那么其会报告为 可读事件。
Q4:如果把epoll这个文件描述符放进其自身的epoll的文件描述符集会发生什么事?
A4:那么调用epoll_ctl(2)将会得到失败((EINVAL))。然而,可以添加一个epoll文件描述符给另一个epoll文件描述符集。
Q5:我可以通过unix域套接字发送epoll的文件描述符给另一个进程吗?
A5:可以。但是这样做似乎有点不合理,因为接受这个epoll文件描述符的进程没有注册到这个epoll实例的文件描述符集。
Q6:关闭一个已经注册到一个epoll实例的文件描述符会自动被epoll实例移除吗?
A6:是的,但是请注意一下几点。一个文件描述符只是引用了一个文件描述(open(2)),当这个文件描述符通过:dup(2), dup2(2), fcntl(2) F_DUPFD, fork(2),这几种方式复制后,那么新的文件描述符实际上也是引用着同一个文件描述。这个文件描述直到所有引用其的文件描述符关闭之后才会关闭。只有当所有的引用着同一个文件描述的所有的文件描述符关闭之后,才会将注册到epoll实例的文件描述符移除。当然你也可以显式的使用带EPOLL_CTL_DEL参数的epoll_ctl(2)函数。这意味着:尽管注册到epoll的文件描述符的关闭了,但是其他引用同一个文件描述的文件描述符还存在的话,事件还是有可能会报告给epoll。
Q7:如果多于一个事件发生了,那么在调用epoll_wait(2)时,那么是一起报告还是分开报告。
A7:一起报告。
Q8:对文件描述符的操作会影响已经收集但是还没有报告的事件么?
A9:你可以对一个文件操作符做两个操作:移除的话在这样的情况下就没有讨论的意义了。如果是修改,那么会再次去从可用的I/0里面读取。
Q9:在边缘模式下,我需要不断地去读或者直到收到一个EAGAIN吗?
A9:当从epoll_wait(2)得到一个事件的时候,这只是建议你这个文件描述符已经准备好了所要求监控的I/0事件。你必须要意识到他现在是准备好了的,但是下一次read/write有可能产生EAGAIN。那么何时或者如何使用这个文件描述符完全由你自己决定。
对于报文或者token-oriented files(比如数据报socket, 以规范模式中止)唯一能够确定其缓冲区的数据是否被完全读完或者写完的方式就是使再去读或者写直到返回EAGAIN。
对流定向的文件(如:管道、FIFO、stream socket)。目前这个read/write缓冲区的状态(是否被读完、写完),你是可以通过检测针对这个文件描述符读的数量或者是写的数量来判断的。比如,如果你调用read去读一定量的数据,但是只是返回给你了很少的字节。那么可以肯定这个读的缓冲区内的数据已经被读完了。类似的事情发生你去写。(如果你不能保证被监控的文件描述符一定是一个流定向的文件,那么不要使用前面所述的方法。)