select/poll被监视的文件描述符数目非常大时要O(n)效率很低;epoll与旧的 select 和 poll 系统调用完成操作所需 O(n) 不同, epoll能在O(1)时间内完成操作,所以性能相当高。
epoll不用每次把注册的fd在用户态和内核态反复拷贝。
epoll不同与之前的轮询方式,用了类似事件触发的方式,能够精确得获得实际需要操作的fd.
今天看到一个说法是 epoll_wait 里面 maxevents 这个参数,不能大于epoll_create的size参数。而之前我的程序,epoll_wait用的都是1024,而epoll_create用的都是5. 看来以后epoll_create的参数要谢大一点了。
但是实际上,epoll_create的参数不使用了。
Since Linux 2.6.8, the size argument is unused. (The kernel dynamically sizes
the required data structures without needing this initial hint.)
然后epoll_ctl很重要,我一般都是单独写一个wrapper函数,如下:
void addfd(int epollfd, int fd, bool enable_et) {
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if (enable_et) {
event.events |= EPOLLET;
}
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
上面epoll_ctl的第二个参数,可以有如下选择:
EPOLL_CTL_ADD //注册新的fd到epfd中;
EPOLL_CTL_MOD //修改已经注册的fd的监听事件;
EPOLL_CTL_DEL //从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,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 */
};
上面,我一般都会把epoll_event.data里面的fd也写成正确的fd.
epoll_event里面的events可以是下面的宏的集合:
EPOLLIN //表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT //表示对应的文件描述符可以写;
EPOLLPRI //表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR //表示对应的文件描述符发生错误;
EPOLLHUP //表示对应的文件描述符被挂断;
EPOLLET //将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT//只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
注意上面的EPOLLONESHOT,在读取完一整个事件之后,要重置EPOLLONESHOT让其他的线程能够接收到事件,通过如下方式来重置:
void reset_oneshot(int epollfd, int fd) {
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_MOD, fd, &event);
}
注意以上的方式,是确知原来events内容的情况下;如果稳妥起见,最好把原来的events信息拿过来(更正:下面有讲到,其实对于EPOLLONESHOT,不是恢复事件,而是重新注册事件,所以也不一定要拿原来的events信息了)。
当对方关闭连接(FIN), EPOLLERR,都可以认为是一种EPOLLIN事件,在read的时候分别有0,-1两个返回值。
另注意:
read返回0,不管阻塞还是非阻塞,一概是对方关闭连接;阻塞的话读不到数据不会返回,返回0说明对方关闭;非阻塞的话读不到数据会返回-1同时errno是EAGIN,返回0也说明对方关闭。
read返回-1,对于阻塞,是有错误返回,需检查错误码处理;对于非阻塞,有可能是需要重试,也需要检查错误码,如果是EAGAIN,那么正常重试获取数据就可以了。
ERRNO及线程安全性
上面提到了errno,那么如果errno不是线程安全的,多个线程同时读取的时候,岂不是会出现大问题?还好!errno是线程安全的!
从字面上看,errno是全局变量,但是实际上,errno其实是线程局部变量!这是GCC中保证的。他保证了线程之间的错误原因不会互相串改,当你在一个线程中串行执行一系列过程,那么得到的errno仍然是正确的。
看下,bits/errno.h的定义:
# ifndef __ASSEMBLER__
/* Function to get address of global `errno' variable. */
extern int *__errno_location (void) __THROW __attribute__ ((__const__));
# if !defined _LIBC || defined _LIBC_REENTRANT
/* When using threads, errno is a per-thread value. */
# define errno (*__errno_location ())
# endif
# endif /* !__ASSEMBLER__ */
注意其中,飘红的那一句。是一个线程局部变量。
另外还有个errno.h中是这样定义的:
/* Declare the `errno' variable, unless it's defined as a macro by
bits/errno.h. This is the case in GNU, where it is a per-thread
variable. This redeclaration using the macro still works, but it
will be a function declaration without a prototype and may trigger
a -Wstrict-prototypes warning. */
#ifndef errno
extern int errno;
#endif
从上面可以看出,errno首先是在bits/errno.h中定义的,没定义的话,才会在errno.h中定义。而且errno实际上是一个整型指针(见bits/errno.h),并不是我们通常认为的是个整型数值,而是通过整型指针来获取值的。这个整型就是线程安全的。
如果想看下编译选项里面有没有加上_LIBC_REENTRANT,可以用下面的代码:
#include <stdio.h>
#include <errno.h> int main() { #ifndef __ASSEMBLER__
printf( "Undefine __ASSEMBLER__\n" );
#else
printf( "define __ASSEMBLER__\n" );
#endif #ifndef __LIBC
printf( "Undefine __LIBC\n" );
#else
printf( "define __LIBC\n" );
#endif #ifndef _LIBC_REENTRANT
printf( "Undefine _LIBC_REENTRANT\n" );
#else
printf( "define _LIBC_REENTRANT\n" );
#endif return ;
}
编译运行:
$ g++ -o errno_demo errno_demo.cpp $ ./errno_demo
Undefine __ASSEMBLER__
Undefine __LIBC
Undefine _LIBC_REENTRANT
注意,__ASSEMBLER__没有定义,所以进入了bits/errno.h的代码块,然后__LIBC没有定义,errno就会用线程安全的定义,不需要再看_LIBC_REENTRANT是不是定义。也就是说默认的编译选项,errno就已经是线程安全的!!!安全的!!!
errno的实现可以参考如下:
static pthread_key_t key;
static pthread_once_t key_once = PTHREAD_ONCE_INIT;
static void make_key()
{
(void) pthread_key_create(&key, NULL);
}
int *_errno()
{
int *ptr ;
(void) pthread_once(&key_once, make_key);
if ((ptr = pthread_getspecific(key)) == NULL)
{
ptr = malloc(sizeof(int));
(void) pthread_setspecific(key, ptr);
}
return ptr ;
}
其中有pthread_key_t 和 pthread_once_t。在另外的文章里面详细说吧。
epoll_wait的原型是这样的:
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
第四个参数timeout为0的时候表示不阻塞立即返回,为-1表示一直阻塞。
返回值是等待处理的事件数量,如果是0可能是因为超时或者非阻塞。
LT vs. ET
EPOLL事件有两种模型 Level Triggered (LT) 和 Edge Triggered (ET):
LT(level triggered,水平触发模式)是缺省的工作方式,并且同时支持 block 和 non-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。
ET(edge-triggered,边缘触发模式)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件。
要注意的是,如果设置了EPOLL_ONESHOT模式,那么在每次获取一个fd上的事件之后,这个fd上的这个事件会被清除(主要是为了避免多个线程读数据时候相互干扰),直到读完数据需要手动地使用epoll_ctl的EPOLL_CTL_MOD再对这个fd加上这个event事件才行。
EPOLL_ONESHOT的更多内容,可以参考我的另一篇文章:http://www.cnblogs.com/charlesblc/p/5538363.html
从man手册中,得到ET和LT的具体描述如下
EPOLL事件有两种模型:
Edge Triggered(ET) //高速工作方式,错误率比较大,只支持no_block socket (非阻塞socket)
LevelTriggered(LT) //缺省工作方式,即默认的工作方式,支持blocksocket和no_blocksocket,错误率比较小。
注意,ET这种方式对于accept也是一样的,如果是listen的句柄,那么ET模式下收到事件,必须循环确保都处理完,因为多个accept同时发生也只会触发一次事件。
EPOLLOUT
另外,EPOLLOUT这种监听方式,平时不太用的到。在网上搜到如下的解释和用法,觉得很好:
对于LT 模式,如果注册了EPOLLOUT,只要该socket可写(发送缓冲区)未满,那么就会触发EPOLLOUT。
对于ET模式,如果注册了EPOLLOUT,只有当socket从不可写变为可写的时候,会触发EPOLLOUT。
如果需要,一种用法:自己在应用层加个发送缓冲区,需要发送数据的时候,如果应用层的发送缓冲区为空,则直接写到socket中。否则就写到应用层的发送缓冲区,并注册OUT时间(LT模式)。
反正我是没用过EPOLLOUT,直接写就行了,哈哈哈。
负责listen的socket上同时注册EPOLLIN | EPOLLOUT,收到connet请求时,只看到EPOLLIN事件。 在accectp后的socket上同时注册EPOLLIN | EPOLLOUT,这时候客户端还没有操作,这时只发生了EPOLLOUT事件。 客户端send后,服务端收到了EPOLLIN事件,然后改为关注EPOLLOUT事件,立即就又收到了EPOLLOUT事件。 跟上面的分析一致。另外从实验中发现貌似listen的fd只有EPOLLIN会生效。
EAGAIN
最后,还是要再说一下EAGAIN,仔细领悟下面这句话:
/* If errno == EAGAIN, that means we have read all
data. So go back to the main loop. */
也就是说,对于ET模式循环读取数据的情况,如果read函数返回-1并且errno等于EAGAIN,是要跳出循环的,但是不需要close socket,因为不是真的有错误;其他的errno才是有错误,才需要关闭socket(为了兼容其他系统,有时候会把EWOULDBLOCK和EAGAIN放在一起处理,其实是等价的);只有read返回>0的时候,才需要继续在循环里面读取;read返回0表示对方关闭了,直接跳出循环,并且关闭socket.
以上基本就是ET模式对于read函数返回几种情况的处理方式。对于LT模式,基本也是相同的处理,只不过不需要放在循环里读取,也就是说read函数返回>0的时候,不回到循环继续读取也是可以的,因为对于这种还有数据没有读完的情况,LT模式会再次触发EPOLLIN事件的。