【网络编程】更强的IO复用模型:epoll

在前面学习了select、poll和非阻塞IO之后,再继续学习一下性能更加优越的epoll。下面放置了一张图,这张图来自 The Linux Programming Interface(No Starch Press)。这张图直观地为我们展示了 select、poll、epoll 几种不同的 I/O 复用技术在面对不同文件描述符大小时的表现差异。

img

从图中可以明显地看到,epoll 的性能是最好的,即使在多达 10000 个文件描述的情况下,其性能的下降和有 10 个文件描述符的情况相比,差别也不是很大。而随着文件描述符的增大,常规的 select 和 poll 方法性能逐渐变得很差。

那么,epoll是如何做到的呢?

一、相关函数介绍

epoll 可以说是和 poll 非常相似的一种 I/O 多路复用技术,epoll 通过监控注册的多个描述字,来进行 I/O 事件的分发处理。不同于 poll 的是,epoll 不仅提供了默认的 level-triggered(条件触发)机制,还提供了性能更为强劲的 edge-triggered(边缘触发)机制,在后面小结会展开分析。

使用 epoll 进行网络程序的编写,需要三个步骤,分别是

  • epoll_create
  • epoll_ctl
  • epoll_wait

下面将逐一分析一下。

1.1 epoll_create

函数原型如下:

int epoll_create(int size);//创建一颗监听红黑树

参数如下:

  • size:创建的红黑树的监听节点个数(仅供内核参考),不过现在被自动忽略了,内核可以动安胎分配了,但是该值仍需要一个大于0的整数。
  • 返回值:成功时,指向新创建的红黑树的根节点的fd;失败时,返回-1 errno。
1.2 epoll_ctl

函数原型如下:

 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//操作监听红黑树

参数如下:

  • epfd:epoll_create函数的返回值。

  • op:对该监听红黑树所做的操作。

    • EPOLL_CTL_ADD:添加fd到监听红黑树。
    • EPOLL_CTL_DEL:将一个fd从监听红黑树上摘下(取消监听)。
    • EPOLL_CTL_MOD:修改fd在红黑树上的监听事件。
  • fd:待监听的套接字。

  • event:本质上是struct epoll_event结构体的地址

    
    typedef union epoll_data {
         void        *ptr;
         int          fd; //对应监听事件的fd
         uint32_t     u32;
         uint64_t     u64;
     } epoll_data_t;
    
     struct epoll_event {
         uint32_t     events;
         epoll_data_t data;
     };
    
    • 成员event:EPOLLIN、EPOLLOUT、EPOLLEDHUP(套接字的一端关闭,或者半关闭)、EPOLLHUP(对应的文件描述符被挂起)和EPOLLET(ET或者LT)
  • 返回值:成功返回0;失败 -1 errno;

1.3 epoll_wait

函数原型如下:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll_wait() 函数类似之前的 poll 和 select 函数,调用者进程被挂起,在等待内核 I/O 事件的分发。

参数如下:

  • epfd:epoll_create函数的返回值。
  • events:传出参数,是一个结构体数组,存储了满足监听条件的那些fd结构体,事件类型取值和 epoll_ctl 可设置的值一样,这个 epoll_event 结构体里的 data 值就是在 epoll_ctl 那里设置的 data,也就是用户空间和内核空间调用时需要的数据。
  • maxevents:大于0的整数,表示epoll_wait 可以返回的最大事件值。
  • timeout:epoll_wait 阻塞调用的超时值,如果这个值设置为 -1,表示不超时;如果设置为 0 则立即返回,即使没有任何 I/O 事件发生。
  • 返回值:成功时,返回大于0,表事件个数;返回0表示超时时间到了;若出错,则返回-1。
二、示例代码
#define MAXEVENTS 128

char rot13_char(char c) {
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

int main(int argc, char **argv) {
    int listen_fd, socket_fd;
    int n, i;
    int efd;
    struct epoll_event event;
    struct epoll_event *events;

    listen_fd = tcp_nonblocking_server_listen(SERV_PORT);

    efd = epoll_create(1); 	//创建监听红黑树套接字的树根
    if (efd == -1) {
        error(1, errno, "epoll create failed");
    }
	//初始化监听套接字的事件
    event.data.fd = listen_fd;
    event.events = EPOLLIN | EPOLLET;
    if (epoll_ctl(efd, EPOLL_CTL_ADD, listen_fd, &event) == -1) { //将listen_fd添加到监听红黑树上
        error(1, errno, "epoll_ctl add listen fd failed");
    }

    /* Buffer where events are returned */
    events = calloc(MAXEVENTS, sizeof(event));

    while (1) {
        n = epoll_wait(efd, events, MAXEVENTS, -1); //实时监听
        printf("epoll_wait wakeup\n");
        for (i = 0; i < n; i++) {
            if ((events[i].events & EPOLLERR) ||
                (events[i].events & EPOLLHUP) ||
                (!(events[i].events & EPOLLIN))) {
                fprintf(stderr, "epoll error\n");
                close(events[i].data.fd);
                continue;
            } else if (listen_fd == events[i].data.fd) { //listen_fd满足读事件,有新的客户端发起连接请求
                struct sockaddr_storage ss;
                socklen_t slen = sizeof(ss);
                int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);
                if (fd < 0) {
                    error(1, errno, "accept failed");
                } else {
                    make_nonblocking(fd);
                    event.data.fd = fd; //初始化cfd的监听事件
                    event.events = EPOLLIN | EPOLLET; //edge-triggered
                    if (epoll_ctl(efd, EPOLL_CTL_ADD, fd, &event) == -1) {
                        error(1, errno, "epoll_ctl add connection fd failed");
                    }
                }
                continue;
            } else {
                socket_fd = events[i].data.fd;
                printf("get event on socket fd == %d \n", socket_fd);
                while (1) {
                    char buf[512];
                    if ((n = read(socket_fd, buf, sizeof(buf))) < 0) {
                        if (errno != EAGAIN) {
                            error(1, errno, "read error");
                            close(socket_fd);
                        }
                        break;
                    } else if (n == 0) {
                        close(socket_fd);
                        break;
                    } else {
                        for (i = 0; i < n; ++i) {
                            buf[i] = rot13_char(buf[i]);
                        }
                        if (write(socket_fd, buf, n) < 0) {
                            error(1, errno, "write error");
                        }
                    }
                }
            }
        }
    }

    free(events);
    close(listen_fd);
}
三、ET模式和LT模式
3.1 ET模式------>边沿触发:
  • 缓冲区剩余未读尽的数据不会导致epoll_wait返回(因为是想同的事件),有新的事件满足,才会触发

  • 用法:

struct epoll_event event;
event.events = EPOLLIN | EPOLLET
3.2 LT模式-------->水平触发
  • 默认采用的模式
  • 缓冲区剩余未读尽的数据会导致epoll_wait返回
四、总结

本文主要是对epoll的相关函数进行了介绍,也给出了示例代码。通过代码或者函数的介绍,想必你也大概知道了,为什么epoll比poll模型更加高效,epoll返回的是有事件发生的数组,而poll返回的是准备好的个数,每次poll函数返回都要注册的描述符结合数组,尤其是当数量越大遍历次数越多。

随后分析了ET和LT两种方式,不知道你发现没有,epoll的ET模式是一种高效的模式(也就是其他请求不会卡住),但是只支持非阻塞模式(这个应该很好理解)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值