epoll函数与参数总结学习 & errno的线程安全

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 0;
}

编译运行:

$ 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事件的。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值