epoll模型的使用及其描述符耗尽问题的探讨

4707阅读 0评论2007-09-07 tysn
分类:LINUX

转自:
其中对epoll模型进行了较详细的讨论,这里记录一下主要内容

发表于: 2006-8-18 10:39 [引用] [投诉] [快速回复]
每次接受新连接的时候,我监视了这几个事件。

EPOLLIN | EPOLLET | EPOLLERR | EPOLLHUP | EPOLLPRI;

每次有一批事件返回,经过统计
返回的一批fd数量=出错关闭的fd数量+由EPOLLIN转为EPOLLOUT的fd数量+EPOLLOUT正常处理关闭的fd的数量。 也就是说,每批事件都完全处理,没有遗漏。

观察发现EPOLLET | EPOLLERR | EPOLLHUP 这3发事件的发生率为0。

但fd却成增大趋势。以前那写较小的fd在经历一段时间后渐渐丢失,不再可用。

请问fd都丢失到哪里去了?

=======================================================================================================

/*-------------------------------------------------------------------------------------------------
gcc -o httpd httpd.c -lpthread
author: wyezl
2006.4.28
---------------------------------------------------------------------------------------------------*/

#include
#include
#include
#include
#include
#include
#include
#include
#include

#define PORT 8888
#define MAXFDS 5000
#define EVENTSIZE 100

#define BUFFER "HTTP/1.1 200 OK\r\nContent-Length: 5\r\nConnection: close\r\nContent-Type: text/html\r\n\r\nHello"

int epfd;
void *serv_epoll(void *p);
void setnonblocking(int fd)
{
int opts;
opts=fcntl(fd, F_GETFL);
if (opts < 0)
{
fprintf(stderr, "fcntl failed\n");
return;
}
opts = opts | O_NONBLOCK;
if(fcntl(fd, F_SETFL, opts) < 0)
{
fprintf(stderr, "fcntl failed\n");
return;
}
return;
}

int main(int argc, char *argv[])
{
int fd, cfd,opt=1;
struct epoll_event ev;
struct sockaddr_in sin, cin;
socklen_t sin_len = sizeof(struct sockaddr_in);
pthread_t tid;
pthread_attr_t attr;

epfd = epoll_create(MAXFDS);
if ((fd = socket(AF_INET, SOCK_STREAM, 0)) <= 0)
{
fprintf(stderr, "socket failed\n");
return -1;
}
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (const void*)&opt, sizeof(opt));

memset(&sin, 0, sizeof(struct sockaddr_in));
sin.sin_family = AF_INET;
sin.sin_port = htons((short)(PORT));
sin.sin_addr.s_addr = INADDR_ANY;
if (bind(fd, (struct sockaddr *)&sin, sizeof(sin)) != 0)
{
fprintf(stderr, "bind failed\n");
return -1;
}
if (listen(fd, 32) != 0)
{
fprintf(stderr, "listen failed\n");
return -1;
}

pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, serv_epoll, NULL) != 0)
{
fprintf(stderr, "pthread_create failed\n");
return -1;
}

while ((cfd = accept(fd, (struct sockaddr *)&cin, &sin_len)) > 0)
{
setnonblocking(cfd);
ev.data.fd = cfd;
ev.events = EPOLLIN | EPOLLET | EPOLLERR | EPOLLHUP | EPOLLPRI;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
//printf("connect from %s\n",inet_ntoa(cin.sin_addr));
//printf("cfd=%d\n",cfd);
}

if (fd > 0)
close(fd);
return 0;
}

void *serv_epoll(void *p)
{
int i, ret, cfd, nfds;;
struct epoll_event ev,events[EVENTSIZE];
char buffer[512];

while (1)
{
nfds = epoll_wait(epfd, events, EVENTSIZE , -1);
//printf("nfds ........... %d\n",nfds);
for (i=0; i {
if(events[i].events & EPOLLIN)
{
cfd = events[i].data.fd;
ret = recv(cfd, buffer, sizeof(buffer),0);
//printf("read ret..........= %d\n",ret);

ev.data.fd = cfd;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, cfd, &ev);
}
else if(events[i].events & EPOLLOUT)
{
cfd = events[i].data.fd;
ret = send(cfd, BUFFER, strlen(BUFFER), 0);
//printf("send ret...........= %d\n", ret);

ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, &ev);

close(cfd);

}

else
{

cfd = events[i].data.fd;
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, cfd, &ev);
close(cfd);
}


}
}
return NULL;
}

===============================================================================================

可不可以不用分成
if(events[ i].events & EPOLLIN)

else if(events[ i].events & EPOLLOUT)
两种情况?

只要有事件来就进行读写,
当然要对读写cfd的函数进行出错判断,一旦出错就close(cfd)
这样就能避免EPOLLN之后EPOLLOUT永不再来的情况?

QUOTE:
原帖由 思一克 于 2006-8-22 14:07 发表
你的程序我看了。好象是有漏洞,而且是必须在大量连接,慢速的断线才可疑的。

一个fd有事件EPOLLIN后,如果断线,EPOLLOUT永不再来,你的fd不就永远不被关闭了吗?

请讨论。
前提是假定产生EPOLLN事件(可读)的socket一定也可写

================================================================================================

看了lighttpd代码。有了点思路。明天再继续修改。

CODE:
[Copy to clipboard]
#include

#include
#include
#include
#include
#include
#include
#include

#include "fdevent.h"
#include "settings.h"
#include "buffer.h"

#ifdef USE_LINUX_EPOLL
static void fdevent_linux_sysepoll_free(fdevents *ev) {
close(ev->epoll_fd);
free(ev->epoll_events);
}

static int fdevent_linux_sysepoll_event_del(fdevents *ev, int fde_ndx, int fd) {
struct epoll_event ep;

if (fde_ndx < 0) return -1;

memset(&ep, 0, sizeof(ep));

ep.data.fd = fd;
ep.data.ptr = NULL;

if (0 != epoll_ctl(ev->epoll_fd, EPOLL_CTL_DEL, fd, &ep)) {
fprintf(stderr, "%s.%d: epoll_ctl failed: %s, dying\n", __FILE__, __LINE__, strerror(errno));

SEGFAULT();

return 0;
}


return -1;
}

static int fdevent_linux_sysepoll_event_add(fdevents *ev, int fde_ndx, int fd, int events) {
struct epoll_event ep;
int add = 0;

if (fde_ndx == -1) add = 1;

memset(&ep, 0, sizeof(ep));

ep.events = 0;

if (events & FDEVENT_IN) ep.events |= EPOLLIN;
if (events & FDEVENT_OUT) ep.events |= EPOLLOUT;

/**
*
* with EPOLLET we don't get a FDEVENT_HUP
* if the close is delay after everything has
* sent.
*
*/

ep.events |= EPOLLERR | EPOLLHUP /* | EPOLLET */;

ep.data.ptr = NULL;
ep.data.fd = fd;

if (0 != epoll_ctl(ev->epoll_fd, add ? EPOLL_CTL_ADD : EPOLL_CTL_MOD, fd, &ep)) {
fprintf(stderr, "%s.%d: epoll_ctl failed: %s, dying\n", __FILE__, __LINE__, strerror(errno));

SEGFAULT();

return 0;
}

return fd;
}

static int fdevent_linux_sysepoll_poll(fdevents *ev, int timeout_ms) {
return epoll_wait(ev->epoll_fd, ev->epoll_events, ev->maxfds, timeout_ms);
}

static int fdevent_linux_sysepoll_event_get_revent(fdevents *ev, size_t ndx) {
int events = 0, e;

e = ev->epoll_events[ndx].events;
if (e & EPOLLIN) events |= FDEVENT_IN;
if (e & EPOLLOUT) events |= FDEVENT_OUT;
if (e & EPOLLERR) events |= FDEVENT_ERR;
if (e & EPOLLHUP) events |= FDEVENT_HUP;
if (e & EPOLLPRI) events |= FDEVENT_PRI;

return e;
}

static int fdevent_linux_sysepoll_event_get_fd(fdevents *ev, size_t ndx) {
# if 0
fprintf(stderr, "%s.%d: %d, %d\n", __FILE__, __LINE__, ndx, ev->epoll_events[ndx].data.fd);
# endif

return ev->epoll_events[ndx].data.fd;
}

static int fdevent_linux_sysepoll_event_next_fdndx(fdevents *ev, int ndx) {
size_t i;

UNUSED(ev);

i = (ndx < 0) ? 0 : ndx + 1;

return i;
}

int fdevent_linux_sysepoll_init(fdevents *ev) {
ev->type = FDEVENT_HANDLER_LINUX_SYSEPOLL;
#define SET(x) \
ev->x = fdevent_linux_sysepoll_##x;

SET(free);
SET(poll);

SET(event_del);
SET(event_add);

SET(event_next_fdndx);
SET(event_get_fd);
SET(event_get_revent);

if (-1 == (ev->epoll_fd = epoll_create(ev->maxfds))) {
fprintf(stderr, "%s.%d: epoll_create failed (%s), try to set server.event-handler = \"poll\" or \"select\"\n",
__FILE__, __LINE__, strerror(errno));

return -1;
}

if (-1 == fcntl(ev->epoll_fd, F_SETFD, FD_CLOEXEC)) {
fprintf(stderr, "%s.%d: epoll_create failed (%s), try to set server.event-handler = \"poll\" or \"select\"\n",
__FILE__, __LINE__, strerror(errno));

close(ev->epoll_fd);

return -1;
}

ev->epoll_events = malloc(ev->maxfds * sizeof(*ev->epoll_events));

return 0;
}

#else
int fdevent_linux_sysepoll_init(fdevents *ev) {
UNUSED(ev);

fprintf(stderr, "%s.%d: linux-sysepoll not supported, try to set server.event-handler = \"poll\" or \"select\"\n",
__FILE__, __LINE__);

return -1;
}
#endif

========================================================================================================

EPOLLET 是边沿触发。如果epoll_wait返回一个可读的文件描述符,必须要把它缓冲区中的数据都读出来。如果没有读完,就继续下一次epoll_wait(),epoll就不会在这个描述符上被唤醒。

请参考MAN手册的建议

The suggested way to use epoll as an Edge Triggered (EPOLLET) interface is below, and possible pitfalls to avoid follow.

i with non-blocking file descriptors
ii by going to wait for an event only after read(2) or write(2) return EAGAIN

-------------------------------------------------

突然发现一个问题:

你使用了 EPOLLET(边沿触发) 和 非阻塞IO。

但是在接收数据时,你只接收一次,没有等到recv返回EAGAIN,就设置EPOLLOUT继续等待了。

假如这个描述符的缓冲区内还有数据没有读完,它就可能“死”在epoll里,不再被返回了。

我看到过一个人非常形象的描述这个问题:
水平触发 --> 有事了,你不处理?不断骚扰你直到你处理。
边沿触发 --> 有事了,告诉你一次,你不处理?拉倒!

-------------------------------------------------------

我又按照下面步骤测试了一下

第一次:
1、epoll 在一个文件描述符上,等待一个EPOLLIN事件。
2、epoll 在这个文件描述符上被唤醒,然后只接收部分数据。(没有等到EAGAIN)
3、继续等待这个EPOLLIN事件。
4、epoll 没有再被唤醒。

正常现象。

第二次:
1、epoll 在一个文件描述符上,等待一个EPOLLIN事件。
2、epoll 在这个文件描述符上被唤醒,然后只接收部分数据。(没有等到EAGAIN)
3、继续等待这个EPOLLIN事件。
4、epoll 没有再被唤醒。
5、再次收到新数据后,epoll又在这个描述符上被唤醒了。

正常现象。

第三次:
1、epoll 在一个文件描述符上,等待一个EPOLLIN事件。
2、epoll 在这个文件描述符上被唤醒,然后只接收部分数据。(没有等到EAGAIN)
3、不再等待EPOLLIN,继续等待另一个EPOLLOUT事件。
4、epoll 在这个文件描述符上被唤醒了。

我怀疑EPOLLIN和EPOLLOUT是分别处理的。但是,也不能保证这种现象是正常行为。

第四次:
1、epoll 在一个文件描述符上,等待一个EPOLLOUT事件。
2、epoll 在这个文件描述符上被唤醒。
3、继续等待EPOLLOUT事件。
4、epoll 没有被唤醒。
5、收到新的数据,epoll_wait 在这个描述符上返回 EPOLLOUT 事件。

是否说明 IN 和 OUT 事件还是会相互影响呢?

=======================================================================================================

我认为原因是没有对每个FD进行超时的管理.
假设下列情况,一用户发起连接,服务器accept成功,返回了FD,但是在用户发起请求前,如果网络有问题,此FD当然无法返回POLLIN,当然就不会有POLLOUT等等了,这样FD就无法关闭.
KEEPALIVE可以解决此问题,这个不是指HTTP中的KEEPALIVE,是指socket选项.
个人观点,欢迎指教.

另,我个人认为,半连接,是不会占用FD的.如果只有半连接,我相信内核中的sock->ops->accept()函数是无法正确返回的.只有在这个函数正确返回的情况下,才会调用sock_map_fd(),把socket和fd关联起来.

-----------------------------------------------------------

我同意 solegoose 和 思一克 的说法。

其实,把这个问题的焦点集中在epoll上是不对的,我们有一点糊涂了。

当客户端的机器在发送“请求”前,就崩溃了(或者网络断掉了),则服务器一端是无从知晓的。

按照你现在的这个“请求响应方式”,无论是否使用epoll,都必须要做超时检查。

因此,这个问题与epoll无关。

===================================================================================================

在computer_xu的BLOG http://blog.sina.com.cn/u/544465b0010000bp
中看到了如下翻译。也贴在这。


在man epoll中的Notes说到:

EPOLL事件分发系统可以运转在两种模式下:
Edge Triggered (ET)
Level Triggered (LT)
接下来说明ET, LT这两种事件分发机制的不同。我们假定一个环境:
1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
4. 然后我们读取了1KB的数据
5. 调用epoll_wait(2)......

Edge Triggered 工作模式:
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
i 基于非阻塞文件句柄
ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待

Level Triggered 工作模式
相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

以上翻译自man epoll.

然后详细解释ET, LT:

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

在许多测试中我们会看到如果没有大量的idle-connection或者dead-connection,epoll的效率并不会比 select/poll高很多,但是当我们遇到大量的idle-connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。



上一篇:Gentoo 2007.0硬盘安装法——使用grub for dos
下一篇:吉安兔月月秀