epoll哪些触发模式_网络编程:epoll

前言

前面讲了IO多路复用的API,select和poll的缺点是性能不够,客户端连接越多性能下降越明显,epoll的出现解决了这个问题,引用The Linux Programming Interface的一个统计对比如下:

fd数量      poll CPU时间(秒)    select CPU时间(秒)   epoll CPU时间(秒)
---------------------------------------------------------------------
10              0.61                0.73                0.41
100             2.9                 3.0                 0.42
1000            35                  35                  0.53
10000           990                 930                 0.66
---------------------------------------------------------------------

可以看出fd达到100个以后,select/poll就非常慢了,而epoll即使达到10000个也表现得非常好,因为:

  • 每次调用select/poll,内核必须检查所有传进来的描述符;而对于epoll,每次调用epoll_ctl,内核会把相关信息与底层的文件描述关联起来,当IO事件就绪时,内核把信息加到epoll的就绪列表里。随后调用epoll_wait,内核只需把就绪列表中的信息提取出来返回即可。
  • 每次调用select/poll,都要把待监控的所有文件描述符传给内核,函数返回时,内核要把描述符返回并标识哪些就绪,得到结果后还要逐个判断所有描述符,才能确定哪些有事件;epoll在调用epoll_ctl时就已经维护着监控的列表,epoll_wait不需要传入任何信息,并且返回的结果只包含就绪的描述符,这样就不用去判断所有描述符。

从概念上理解epoll是这样的,把要监控的fd的IO事件注册给epoll(调用epoll_ctl),然后调用epoll的API等待事件到达(调用epoll_wait),内核可能对每个fd维护着一个读和写的缓冲区,那么:

  • 如果我监控读事件,并且读缓冲区有数据了,epoll_wait就会返回,此时我可以调用read读数据。
  • 如果我监控写事件,并且写缓存区未满,epoll_wait也会返回,此时我可以调用write写数据。
  • 如果fd发生了一些错误,epoll_wait也会返回,此时我根据返回的标志位,就可以知道。
  • 如果我监控读事件 并且有客户端连接进来,epoll_wait就会返回,此时我可以调用accept接受客户端。

epoll的API介绍

  • int epoll_create(int size);
    创建一个epoll实例,返回代表实例的文件描述符(fd),size自Linux 2.6.8以后忽略,但必须大于0.
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    epoll控制接口,epfd就是epoll的文件描述符,fd是要操作的文件描述符,op有如下几种:
    • EPOLL_CTL_ADD 注册fd的事件,事件类型在event指定。
    • EPOLL_CTL_MOD 修改已注册的fd事件。
    • EPOLL_CTL_DEL 删除fd的事件。

epoll_event有一个events成员,指定要注册的事件类型,比较重要的几个:

    • EPOLLIN fd可读事件
    • EPOLLOUT fd可写事件
    • EPOLLERR fd发生错误,这个事件总是会被监控,不必手动增加
    • EPOLLHUP fd被挂起时,这个事件总是会被监控,不必手动增加,这通常发生在socket异常关闭时,此时read返回0,然后正常的清理socket资源。

epoll_event还有一个epoll_data_t成员,由外部设置自定义数据,以方便后续处理。

  • int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
    等待事件发生,如果没有事件发生,线程会被挂起,maxevents指定最大事件数,events外部传入的事件数组,长度应当等于maxevents,当事件发生时,epoll会把事件信息填到这里, timeout指定等待的最大时间, 0表示马上返回,-1表示无限等待。
    epoll_wait返回等待到的事件数,返回时,遍历events对fd进行处理。 当epoll不再使用时,应该调用close关闭epollfd。

水平触发和边缘触发

epoll触发事件有两种模式,默认的叫水平触发(LT),另一种叫边缘触发(ET):

  • LT模式:只要fd的读缓冲区不空,或写缓冲区不满,epoll_wait就会一直触发事件(也就是返回)。
  • ET模式:当被监控的fd状态变化时(从未就绪变成就绪状态),事件触发一次。此后内核不再通知,除非有新的事件到来。

// 多谢 @黄蔚 的指正,原来ET模式的描述有误,仔细阅读过man文档后已修正过来。

LT处理起来比ET要简单得多,读事件触发,只需要read一次,如果数据没读完,下次epoll_wait还会返回,写也是一样的;ET模式就要求事件触发时,一直读一直写直到明确知道已经读写完毕(返回EAGIN或EWOULDBLOCK的错误码)。

水平触发的服务器程序大概是这样的流程:

  • accept一个新连接,将这个新连接的fd加到epoll事件中,监听EPOLLIN事件。
  • EPOLLIN事件到达时,read该fd中的数据。
  • 如果要向该fd写事件,向epoll增加EPOLLOUT事件。
  • EPOLLOUT事件到达,向fd write数据,如果数据太大无法一次写出,那么先保留EPOLLOUT事件,下次事件到达继续写;如果写出完毕,从epoll删除EPOLLOUT事件。

一个实用的echo程序:

这一次我们要使用epoll和非阻塞socket来写一个真正实用的echo服务器,调用fcntl函数,设置O_NONBLOCK标志位,即可让socket的文件描述符变成非阻塞模式。非阻塞模式处理起来比阻塞模式要复杂一些:

  • read, write, accept这些函数不会阻塞,要么成功,要么返回-1失败,errno记录了失败的原因,有几个错误码要关注:
    • EAGAIN 或 EWOULDBLOCK 只有非阻塞的fd才会发生,表示没数据可读,或没空间可写,或没有客户端可接受,下次再来吧。这两个值可能相同也可能不同,最好一起判断。
    • EINTR 表示被信号中断,这种情况可以再一次尝试调用。
    • 其他错误表示真的出错了。
  • 向一个fd写数据比较麻烦,我们没法保证一次性把所有数据都写完,所以需要先保存在缓冲里,然后向epoll增加写事件,事件触发时再向fd写数据。等数据都写完了,再把事件从epoll移除。这个程序把写数据保存在链表中。

我们把监听fd保留为阻塞模式,因为epoll_wait返回,可以确定一定有客户端连接进来,所以accept一般可以成功,并不用担心会阻塞。客户端连接使用的是非阻塞模式,确保读写未完成时不会阻塞。

下面是这个程序的代码,关键地方加了一些注释,仔细看代码比看文字描述更有用:)

#include "socket_lib h"
#include <unistd.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/epoll.h>

#define MAX_CLIENT 10000
#define MIN_RSIZE 124
#define BACKLOG 128
#define EVENT_NUM 64

// 缓存结点
struct twbuffer {
    struct twbuffer *next;      // 下一个缓存
    void *buffer;        // 缓存
    char *ptr;           // 当前未发送的缓存,buffer != ptr表示只发送了一部分
    int size;            // 当前未发送的缓存大小
};

// 缓存列表
struct twblist {
    struct twbuffer *head;
    struct twbuffer *tail;
};

// 客户端连接信息
struct tclient {
    int fd;             // 客户端fd
    int rsize;          // 当前读的缓存区大小
    int wbsize;         // 还未写完的缓存大小
    struct twblist wblist;  // 写缓存链表
};

// 服务器信息
struct tserver {
    int listenfd;       // 监听fd
    int epollfd;        // epollfd
    struct tclient clients[MAX_CLIENT];     // 客户端结构数组
};

// epoll增加读事件
void epoll_add(int efd, int fd, void *ud) {
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.ptr = ud;
    epoll_ctl(efd, EPOLL_CTL_ADD, fd, &ev);
}

// epoll修改写事件
void epoll_write(int efd, int fd, void *ud, int enabled) {
    struct epoll_event ev;
    ev.events = EPOLLIN | (enabled ? EPOLLOUT : 0);
    ev.data.ptr = ud;
    epoll_ctl(efd, EPOLL_CTL_MOD, fd, &ev);
}

// epoll删除fd
void epoll_del(int efd, int fd) {
    epoll_ctl(efd, EPOLL_CTL_DEL, fd, NULL);
}

// 设置socket为非阻塞
void set_nonblocking(int fd) {
    int flag = fcntl(fd, F_GETFL, 0);
    if (flag >= 0) {
        fcntl(fd, F_SETFL, flag | O_NONBLOCK);
    }
}

// 增加写缓存
void add_wbuffer(struct twblist *list, void *buffer, int sz) {
    struct twbuffer *wb = malloc(sizeof(*wb));
    wb->buffer = buffer;
    wb->ptr = buffer;
    wb->size = sz;
    wb->next = NULL;
    if (!list->head) {
        list->head = list->tail = wb;
    } else {
        list->tail->next = wb;
        list->tail = wb;
    }
}

// 释放写缓存
void free_wblist(struct twblist *list) {
    struct twbuffer *wb = list->head;
    while (wb) {
        struct twbuffer *tmp = wb;
        wb = wb->next;
        free(tmp);
    }
    list->head = NULL;
    list->tail = NULL;
}

// 创建客户端信息
struct tclient* create_client(struct tserver *server, int fd) {
    int i;
    struct tclient *client = NULL;
    for (i = 0; i < MAX_CLIENT; ++i) {
        if (server->clients[i].fd < 0) {
            client = &server->clients[i];
            break;
        }
    }
    if (client) {
        client->fd = fd;
        client->rsize = MIN_RSIZE;
        set_nonblocking(fd);        // 设为非阻塞模式
        epoll_add(server->epollfd, fd, client);     // 增加读事件
        return client;
    } else {
        fprintf(stderr, "too many client: %dn", fd);
        close(fd);
        return NULL;
    }
}

// 关闭客户端
void close_client(struct tserver *server, struct tclient *client) {
    assert(client->fd >= 0);
    epoll_del(server->epollfd, client->fd);
    if (close(client->fd) < 0) perror("close: ");
    client->fd = -1;
    client->wbsize = 0;
    free_wblist(&client->wblist);
}

// 初始化服务信息
struct tserver* create_server(const char *host, const char *port) {
    struct tserver *server = malloc(sizeof(*server));
    memset(server, 0, sizeof(*server));
    for (int i = 0; i < MAX_CLIENT; ++i) {
        server->clients[i].fd = -1;
    }
    server->epollfd = epoll_create(MAX_CLIENT);
    server->listenfd = tcpListen(host, port, BACKLOG);
    epoll_add(server->epollfd, server->listenfd, NULL);
    return server;
}

// 释放服务器
void release_server(struct tserver *server) {
    for (int i = 0; i < MAX_CLIENT; ++i) {
        struct tclient *client = &server->clients[i];
        if (client->fd >= 0) {
            close_client(server, client);
        }
    }
    epoll_del(server->epollfd, server->listenfd);
    close(server->listenfd);
    close(server->epollfd);
    free(server);
}

// 处理接受
void handle_accept(struct tserver *server) {
    struct sockaddr_storage claddr;
    socklen_t addrlen = sizeof(struct sockaddr_storage);
    for (;;) {
        int cfd = accept(server->listenfd, (struct sockaddr*)&claddr, &addrlen);
        if (cfd < 0) {
            int no = errno;
            if (no == EINTR)
                continue;
            perror("accept: ");
            exit(1);        // 出错
        }
        char host[NI_MAXHOST];
        char service[NI_MAXSERV];
        if (getnameinfo((struct sockaddr *)&claddr, addrlen, host, NI_MAXHOST, service, NI_MAXSERV, 0) == 0)
            printf("client connect: fd=%d, (%s:%s)n", cfd, host, service);
        else
            printf("client connect: fd=%d, (?UNKNOWN?)n", cfd);

        create_client(server, cfd);
        break;
    }   
}

// 处理读
void handle_read(struct tserver *server, struct tclient *client) {
    int sz = client->rsize;
    char *buf = malloc(sz);
    ssize_t n = read(client->fd, buf, sz);
    if (n < 0) {        // error
        free(buf);
        int no = errno;
        if (no != EINTR && no != EAGAIN && no != EWOULDBLOCK) {
            perror("read: ");
            close_client(server, client);
        }
        return;
    }
    if (n == 0) {       // client close
        free(buf);
        printf("client close: %dn", client->fd);
        close_client(server, client);
        return;
    }
    // 确定下一次读的大小
    if (n == sz)
        client->rsize >>= 1;
    else if (sz > MIN_RSIZE && n *2 < sz)
        client->rsize <<= 1;
    // 加入写缓存
    add_wbuffer(&client->wblist, buf, n);
    // 增加写事件
    epoll_write(server->epollfd, client->fd, client, 1);
}

// 处理写
void handle_write(struct tserver *server, struct tclient *client) {
    struct twblist *list = &client->wblist;
    while (list->head) {
        struct twbuffer *wb = list->head;
        for (;;) {
            ssize_t sz = write(client->fd, wb->ptr, wb->size);
            if (sz < 0) {
                int no = errno;
                if (no == EINTR)        // 信号中断,继续
                    continue;
                else if (no == EAGAIN || no == EWOULDBLOCK)   // 内核缓冲满了,下次再来
                    return;
                else {      // 其他错误 
                    perror("write: ");
                    close_client(server, client);
                    return;
                }
            }
            client->wbsize -= sz;
            if (sz != wb->size) {       // 未完全发送出去,下次再来
                wb->ptr += sz;
                wb->size -= sz;
                return;
            }
            break;
        }
        list->head = wb->next;
        free(wb);
    }
    list->tail = NULL;
    // 到这里写全部完成,关闭写事件
    epoll_write(server->epollfd, client->fd, client, 0);
}

// 先处理错误
void handle_error(struct tserver *server, struct tclient *client) {
    perror("client error: ");
    close_client(server, client);
}

int main() {
    signal(SIGPIPE, SIG_IGN);

    struct tserver *server = create_server("127.0.0.1", "3459");

    struct epoll_event events[EVENT_NUM];
    for (;;) {
        int nevent = epoll_wait(server->epollfd, events, EVENT_NUM, -1);
        if (nevent <= 0) {
            if (nevent < 0 && errno != EINTR) {
                perror("epoll_wait: ");
                return 1;
            }
            continue;
        }
        int i = 0;
        for (i = 0; i < nevent; ++i) {
            struct epoll_event ev = events[i];
            if (ev.data.ptr == NULL) {  // accept
                handle_accept(server);
            } else {
                if (ev.events & (EPOLLIN | EPOLLHUP)) {  // read
                    handle_read(server, ev.data.ptr);
                }
                if (ev.events & EPOLLOUT) {     // write
                    handle_write(server, ev.data.ptr);
                }
                if (ev.events & EPOLLERR) {     // error
                    handle_error(server, ev.data.ptr);
                }
            }
        }
    }

    release_server(server);
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值