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

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

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
return0;
}
编译运⾏:
$ 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的更多内容,可以参考我的另⼀篇⽂章:
从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事
件的。

相关文档
最新文档