在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于:
1、epoll不会随着监听fd数目的增长而降低效率。因为在内核中的实现中,select是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。
2、select同时监听文件描述符数受限,并且修改select监听文件描述符数要困难些。
linux/posix_types.h头文件有这样的声明:
表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。
#define __FD_SETSIZE 1024
3、epoll使用mmap加速内核与用户空间的消息传递。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。
epoll和select是I/O多路复用,可以同时处理不同的文件描述符类型,包括客户端、服务端、TCP和UDP等。
epoll机制的简介
epoll的接口非常简单,一共就三个函数:
1.创建epoll句柄
int epfd = epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值(不是fd数目+1)。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2.将被监听的描述符添加到epoll句柄或从epoll句柄中删除或者对监听事件进行修改。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select(),select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
EPOLL_CTL_MOD: 修改已经注册的fd的监听事件;
EPOLL_CTL_DEL: 从epfd中删除一个fd;
第三个参数是需要监听的fd,
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正常关闭);
EPOLLIN事件只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。现在明白为什么说 epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会堵死在阻塞里。ET模式:
每次epoll_ctl做EPOLL_CTL_ADD或EPOLL_CTL_MOD时,如果加入前,就是可读状态,那么加入后会触发1次。
不管socket缓冲是否读完,只要对方有send或connect或close或强退,epoll_wait就会触发EPOLLIN。
LT模式:只要socket可读,每次epoll_wait就会一直触发EPOLLIN。
ET模式下,EPOLLOUT事件只有在连接时触发一次,表示可写(这句话的准确意思如下:
服务端:
在负责listen的socket上同时注册EPOLLIN | EPOLLOUT,收到connet请求时,只看到EPOLLIN事件。在accectp后的socket上同时注册EPOLLIN | EPOLLOUT,这时候客户端还没有操作,这时只发生了EPOLLOUT事件。
客户端的:
socket fd同时注册EPOLLIN | EPOLLOUT,会触发一次EPOLLOUT。)。
其他时候想要触发,那你要先准备好下面条件:
1、(本地)某次write,写满了发送缓冲区,返回错误码为EAGAIN。
2、对端读取了一些数据,本地又重新可写了,此时会触发EPOLLOUT。
简单地说:
ET模式下,EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!
其实,如果你真的想强制触发一次,也是有办法的:每次epoll_ctl做EPOLL_CTL_ADD或EPOLL_CTL_MOD时,如果加入前,就是可写状态,那么加入后会触发1次。
对于LT 模式,如果注册了EPOLLOUT,只要该socket可写(发送缓冲区)未满,那么epoll_wait就会触发EPOLLOUT。
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
3. 等待事件触发,当超过timeout还没有事件触发时,就超时。
等待事件的产生,类似于select()调用。
ET和LT工作模式
EPOLL事件有两种模型:
Edge Triggered (ET) //高速工作方式,只支持no_block socket (非阻塞socket)
Level Triggered (LT) //缺省工作方式,即默认的工作方式,支持block socket和no_block socket。
假如有这样一个例子:
1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
4. 然后我们读取了1KB的数据
5. 调用epoll_wait(2)......
上例中,第5步时,LT方式下,内核会继续通知可以读数据;ET方式,内核不会再通知可以读数据。
Edge Triggered 工作模式:
上例中,如果我们在第1步将RFD添加到epoll描述符的时候,使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于接收端的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。但是,接收端只有在监视的文件句柄上发生了某个事件的时候,ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待已经存在于文件输入缓冲区内的剩余数据。
在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。由于第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,会永远挂起。
epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作,把处理多个文件描述符的任务饿死。因为数据读写不需要用到CPU,而是使用DMA。所以,非阻塞读的方式,CPU可以处理其他连接,而阻塞读的方式就会把处理其他文件描述符的任务饿死。
最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。(LT方式可以解决这种缺陷)
i 基于非阻塞文件句柄。
ii 只有当read(2)或者write(2)返回EAGAIN时(认为读完)才需要挂起,等待。
ET方式,读必须判断是不是都读完了。如果read()返回的读到的数据长度等于要求的大小,即sizeof(buf),那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取:
while(rs) //ET模型
{
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0)
{
// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区
// 已无数据可读
// 在这里就当作是该次事件已处理完.
// 即当buflen<0且errno=EAGAIN时,表示没有数据了。(读/写都是这样)
if(errno == EAGAIN || errno == EINT)
break;
else
return; //真的失败了。
}
else if(buflen == 0)
{
// 这里表示对端的socket已正常关闭.
}
if(buflen == sizeof(buf)
rs = 1; // 需要再次读取(有可能是因为数据缓冲区buf太小,所以数据没有读完)
else
rs = 0; //不需要再次读取(当buflen<sizeof(buf)时,这是ET模式下,非阻塞文件描述符的特性),
}
ET方式,写必须要关注返回值,当返回值小于实际要发送的数量,由于ET方式只有在不可写到可写才触发,所以也要跟读一样,循环发送,直到出现EAGAIN(说明发送完成或者发送缓冲区已满)。然后在EPOLL_OUT中发送剩余消息。所以要把发送数据和其他信息保存起来。
ET方式发送示例:
struct unixsock_info_t
{
struct sockaddr_un *p_addrun_client;
char psz_sendbuf[8];
uint32_t ui_sendlen;
};
typedef struct unixsock_info_t unixsock_info_t;
static int unixsock_sendall(
int i_unixfd,
unixsock_info_t *p_unixsock_info )
{
if ( !p_unixsock_info )
return -1;
struct sockaddr_un *p_addrun_client = p_unixsock_info->p_addrun_client;
int i_addrlen = strlen( p_addrun_client->sun_path ) +
sizeof( p_addrun_client->sun_family );
int i_sendlen = 0;
char *psz_sendbuf = p_unixsock_info->psz_sendbuf;
if ( !psz_sendbuf )
{
log_Err( "psz_sendbuf is null" );
return -1;
}
while( true )
{
i_sendlen = sendto( i_unixfd, p_unixsock_info->psz_sendbuf,
p_unixsock_info->ui_sendlen, 0, ( struct sockaddr * )p_addrun_client,
i_addrlen );
if ( i_sendlen < 0 )
{
if ( errno == EINTR )
continue;
else if ( errno == EAGAIN )
{
log_Info( "----unixsock_sendall success or sendbuf full" );
break;
}
else
{
log_Err( "serve unix socket send failed: %s(%d)",
strerror( errno ), errno );
break;
}
}
else if ( i_sendlen == p_unixsock_info->ui_sendlen )
{
p_unixsock_info->ui_sendlen -= i_sendlen;
break;
}
else if( i_sendlen < p_unixsock_info->ui_sendlen )
{
memmove( p_unixsock_info->psz_sendbuf,
p_unixsock_info->psz_sendbuf + i_sendlen,
p_unixsock_info->ui_sendlen - i_sendlen );
p_unixsock_info->ui_sendlen -= i_sendlen;
}
}
// send success
if ( 0 == p_unixsock_info->ui_sendlen )
{
// something to handle
}
return 0;
}
LT方式,如果没有写完,可以设置监听这个端口可写。当触发可写的时候(即epoll_wait得到EPOLL_OUT),把没发送完的数据发送完。这样的话,就需要把发送数据和其它相关信息保存起来,在下次触发时全部发送。
Level Triggered 工作模式 (默认的工作方式)
以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2)。
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作或者操作未完成,内核还是会继续通知你的(在epoll_wait调用后通知),所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。
listen的第二个参数决定了你的服务端口能同时接受/保持多少个连接,最大值现在取决于 /proc/sys/net/core/somaxconn,说的默认128也是这个文件内容缺省为128,可以实时调整。
参考:
http://www.cnblogs.com/moodlxs/archive/2011/12/16/2290288.html
http://my.oschina.net/u/732357/blog/78122
http://blog.csdn.net/tianmohust/article/details/8691644#
http://www.ccvita.com/515.html
关于EPOLL读写完成的判断参考博主的另一篇博文:http://blog.csdn.net/bytxl/article/details/40855917