int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select()函数把可读描述符、可写描述符、错误描述符分在了三个集合里,这三个集合可以看做是用bit位来标记一个描述符,一旦有若干个描述符状态发生变化,那么它将被置位,而其他没有发生变化的描述符的bit位将被clear(实际上fd_set是一个long型数组)。
调用select函数后程序会阻塞,直到有描述副就绪,或者超时,函数返回。select各个参数含义如下:
int n:最大描述符值 + 1
fd_set *readfds:对可读感兴趣的描述符集
fd_set *writefds:对可写感兴趣的描述符集
fd_set *errorfds:对出错感兴趣的描述符集
struct timeval *timeout:超时时间(注意:对于linux系统,此参数没有const限制,每次select调用完毕timeout的值都被修改为剩余时间,而unix系统则不会改变timeout值)
fd_set结构体内部实际上是一long类型的数组,操作系统定义的该数组大小为1024,每一个数组元素都能与一打开的文件句柄(不管是socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一socket或文件可读。
select函数会在发生以下情况时返回:
readfds集合中有描述符可读
writefds集合中有描述符可写
errorfds集合中有描述符遇到错误条件
指定的超时时间timeout到了
select()的返回值如下:
-1:有错误产生
0:超时时间到,而且没有描述符有状态变化
>0:有状态变化的描述符个数
当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。也可以用FD_ISSET宏对描述符进行测试以找到状态变化的描述符。
设置描述符集合通常用如下几个宏定义:
fd_set set;
FD_ZERO(&set); /*将set清零使集合中不含任何fd*/
FD_SET(fd, &set); /*将fd加入set集合*/
FD_CLR(fd, &set); /*将fd从set集合中清除*/
FD_ISSET(fd, &set); /*测试fd是否在set集合中*/
主要有以下两方面的原因:
其中进程的文件描述符上限是可以手动修改的,但是fd_set的大小是改不了的,要改只能重新编译内核。
uint32 SocketWait(TSocket *s,bool rd,bool wr,uint32 timems)
{
fd_set rfds,wfds;
#ifdef _WIN32
TIMEVAL tv;
#else
struct timeval tv;
#endif /* _WIN32 */
FD_ZERO(&rfds);
FD_ZERO(&wfds);
if (rd) //TRUE
FD_SET(*s,&rfds); //添加要测试的描述字
if (wr) //FALSE
FD_SET(*s,&wfds);
tv.tv_sec=timems/1000; //second
tv.tv_usec=timems%1000; //ms
for (;;) //如果errno==EINTR,反复测试缓冲区的可读性
switch(select((*s)+1,&rfds,&wfds,NULL,(timems==TIME_INFINITE?NULL:&tv))) //测试在规定的时间内套接口接收缓冲区中是否有数据可读
{ //0--超时,-1--出错
case 0: /* time out */
return 0;
case (-1): /* socket error */
if (SocketError()==EINTR)
break;
return 0; //有错但不是EINTR
default:
if (FD_ISSET(*s,&rfds)) //如果s是rfds或wfds中的一员返回非0,否则返回0
return 1;
if (FD_ISSET(*s,&wfds))
return 2;
return 0;
};
}
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时。
poll还有一个特点是水平触发,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
各参数含义如下:
struct pollfd *fds:一个结构体数组,用来保存各个描述符的相关状态。
unsigned long nfds:fdarray数组的大小,即里面包含有效成员的数量。
int timeout:设定的超时时间。(以毫秒为单位)
pollfd结构体第一项fd代表描述符,第二项代表要监听的事件,也就是感兴趣的事件,而第三项代表poll()返回时描述符的状态。如下所示:
struct pollfd {
int fd; /* 描述符 */
short events; /* 要监听的事件 */
short revents; /* 返回时描述符的状态 */
};
pollfd的合法状态如下:
POLLIN: 有普通数据或者优先数据可读
POLLRDNORM: 有普通数据可读
POLLRDBAND: 有优先数据可读
POLLPRI: 有紧急数据可读
POLLOUT: 有普通数据可写
POLLWRNORM: 有普通数据可写
POLLWRBAND: 有紧急数据可写
POLLERR: 有错误发生
POLLHUP: 有描述符挂起事件发生
POLLNVAL: 描述符非法
对于timeout的设置如下:
INFTIM: wait forever
0: return immediately, do not block
>0: wait specified number of milliseconds
poll函数返回值及含义如下:
-1:有错误产生
0:超时时间到,而且没有描述符有状态变化
>0:有状态变化的描述符个数
poll()与select()中的socket就绪条件一致。
优点:
缺点:
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_BUFFER_SIZE 1024
#define IN_FILES 3
#define TIME_DELAY 60*5
#define MAX(a,b) ((a>b)?(a):(b))
int main(int argc ,char **argv)
{
struct pollfd fds[IN_FILES];
char buf[MAX_BUFFER_SIZE];
int i,res,real_read, maxfd;
fds[0].fd = 0;
if((fds[1].fd=open("data1",O_RDONLY|O_NONBLOCK)) < 0)
{
fprintf(stderr,"open data1 error:%s",strerror(errno));
return 1;
}
if((fds[2].fd=open("data2",O_RDONLY|O_NONBLOCK)) < 0)
{
fprintf(stderr,"open data2 error:%s",strerror(errno));
return 1;
}
for (i = 0; i < IN_FILES; i++)
{
fds[i].events = POLLIN;
}
while(fds[0].events || fds[1].events || fds[2].events)
{
if (poll(fds, IN_FILES, TIME_DELAY) <= 0)
{
printf("Poll error\n");
return 1;
}
for (i = 0; i< IN_FILES; i++)
{
if (fds[i].revents)
{
memset(buf, 0, MAX_BUFFER_SIZE);
real_read = read(fds[i].fd, buf, MAX_BUFFER_SIZE);
if (real_read < 0)
{
if (errno != EAGAIN)
{
return 1;
}
}
else if (!real_read)
{
close(fds[i].fd);
fds[i].events = 0;
}
else
{
if (i == 0)
{
if ((buf[0] == 'q') || (buf[0] == 'Q'))
{
return 1;
}
}
else
{
buf[real_read] = '\0';
printf("%s", buf);
}
}
}
}
}
exit(0);
}
int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核监听的文件描述符数目的大小。这个参数不同于select()中的第一个参数(select第一个参数()给出最大监听的fd号+1的值)。
当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型,参数如下:
第一个参数是epoll_create()的返回值 ,
第二个参数表示动作,用三个宏来表示 :
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数 是需要监听的fd ;
第四个参数 是告诉内核需要监听什么事件。
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可以是以下几个宏的集合:
EPOLLIN:触发该事件,表示对应的文件描述符上有可读数据。(包括对端SOCKET正常关闭);
EPOLLOUT:触发该事件,表示对应的文件描述符上可以写数据;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
举例
ev.data.fd的用法:
struct epoll_event ev;
//设置与要处理的事件相关的文件描述符
ev.data.fd=listenfd;
//设置要处理的事件类型
ev.events=EPOLLIN|EPOLLET;
//注册epoll事件
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
event.data.ptr的用法:
//自定义一个结构体或者类
struct myevent_s {
int fd;
int events;
void *arg;
void (*call_back)(int fd, int events, void *arg);
int status;
char buf[BUFLEN];
int len;
long last_active;
};
struct myevent_s g_event;
struct epoll_event ev;
//设置要处理的事件类型
ev.events=EPOLLIN|EPOLLET;
//令ev.data.ptr指向myevent_s类型的对象g_event
ev.data.ptr=&g_event;
//然后在epoll_wait()的第二个参数中的data.ptr会原封不动的把上边的g_event返回给我们,所以我们可以利用这个ptr传参。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
epoll_wait()等侍注册在epfd上的socket fd的事件的发生,如果发生则将发生的sokct fd和事件类型放入到events数组中。并且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。epoll_wait()参数如下:
epfd:由epoll_create 生成的epoll专用的文件描述符;
events:用于存储待处理事件的数组;
maxevents:events数组中成员的个数;
timeout:等待I/O事件发生的超时值(单位我也不太清楚);-1相当于阻塞,0相当于非阻塞。一般用-1即可
该函数返回需要处理的事件数目,如返回0表示已超时。
返回的事件集合存储在events数组中,即数组中实际存放的成员个数是函数的返回值。
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式
LT模式:同时支持block和no-block socket,当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:只支持no-block socket,当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
LT的处理过程:
ET的处理过程:
从ET的处理过程中可以看到,ET的要求是需要一直用while循环读写,直到返回EAGAIN,否则就会遗漏事件。而LT的处理过程中,直到返回EAGAIN不是硬性要求,LT比ET多了一个开关EPOLLOUT事件的步骤。
EAGAIN:例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读,此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。
LT模式的优点如下:
LT模式的缺点如下:
ET模式的优点如下:
ET模式的缺点如下:
一般认为ET更好,毕竟可以从内核中少拷贝就绪文件描述符。但是,ET伴随着使用非阻塞socket,要一次性读完、写完数据,至于是否真的更好,目前没有定论,需要在更多的环境、场景下去测试。
如下图所示,当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象(也就是程序中epfd所代表的对象)。eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列。创建一个代表该epoll的eventpoll对象是必须的,因为内核要维护 就绪列表(rdlist)等数据 。
创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。以添加socket为例,如下图,如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。
当socket收到数据后,中断程序会给eventpoll的rdlist添加socket引用。如下图展示的是sock2和sock3收到数据后,中断程序让rdlist引用这两个socket。eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。当程序执行到epoll_wait时,如果rdlist已经引用了socket,那么epoll_wait直接返回,如果rdlist为空,阻塞进程。
假设计算机中正在运行进程A和进程B,在某时刻进程A运行到了epoll_wait语句。如下图所示,内核会将进程A放入eventpoll的等待队列中,阻塞进程。
当socket接收到数据,中断程序一方面修改rdlist,另一方面唤醒eventpoll等待队列中的进程,进程A再次进入运行状态(如下图)。也因为rdlist的存在,进程A可以知道哪些socket发生了变化。
如下图所示,eventpoll包含了lock、mtx、wq(等待队列)、rdlist(就绪队列)、rbr(索引结构)成员。rdlist和rbr是比较重要的两个成员。
select用FD_SET/FD_CLR/FD_ZERO/FD_ISSET几个宏来需要关注的文件描述符集合,注意,这是宏,不是系统调用,他们只是简单的操作一个bit集合而已,只有select是内核提供出来的。与之相反,epoll则不同,有三个由内核提供出来的接口,他们都可以操作epoll内核对象(即有epoll_create创建的)。因此,在epoll_wait睡眠时,另一个线程可以操作这个epoll对象,例如给他添加要检测的文件描述符,这个添加操作会打断epoll_wait的睡眠,从而让他及时处理,而select则不行。
这种需要打断epoll_wait/select的需求之一在:多个线程,一个用于像缓冲区写数据,另一个负责管理文件描述符(如把缓冲区的数据写到文件描述符里),如果一个文件描述符fd1可写,但是它的缓冲区没有数据,fd1不被加入到被监测文件描述符集合中,这时,如果fd1的缓冲区里有数据了,而epoll/select还阻塞在其他文件描述符集合(不包括fd1),就需要打断阻塞。如何做到这一点呢?select可以建立一个本地文件描述符用于传递命令,而对于epoll则不必,直接调用epoll_ctl就可以了。
当socket就绪时,中断程序会操作eventPoll,在eventPoll中的就绪列表(rdlist),添加scoket引用。这样的话,进程A只需要不断循环遍历rdlist,从而获取就绪的socket,而不会遍历所有的socket。
所以,poll和select的时间复杂度为O(n):每个监听的文件描述符都遍历一遍。epoll的时间复杂度为O(1):只遍历就绪的socket,内核中断程序会添加就绪的socket到rdlist。