Linux NIO 系列(04-3) epoll

Linux NIO 系列(04-3) epoll

Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

一、why epoll

1.1 select 模型的缺点

  1. 句柄限制:单个进程能够监视的文件描述符的数量存在最大限制,通常是 1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在 linux 内核头文件中,有这样的定义: #define __FD_SETSIZE 1024)

  2. 数据拷贝:内核 / 用户空间内存拷贝问题,select 需要复制大量的句柄数据结构,产生巨大的开销;

  3. 轮询机制:select 返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;

select 的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行 IO 操作,那么之后每次 select 调用还是会将这些文件描述符通知进程。

设想一下如下场景:有 100 万个客户端同时与一个服务器进程保持着 TCP 连接。而每一时刻,通常只有几百上千个 TCP 连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

粗略计算一下,一个进程最多有 1024 个文件描述符,那么我们需要开 1000 个进程来处理 100 万个客户连接。如果我们使用 select 模型,这 1000 个进程里某一段时间内只有数个客户连接需要数据的接收,那么我们就不得不轮询 1024 个文件描述符以确定究竟是哪个客户有数据可读,想想如果 1000 个进程都有类似的行为,那系统资源消耗可有多大啊!

针对 select 模型的缺点,epoll 模型被提出来了!

1.2 epoll 模型优点

  1. 支持一个进程打开大数目的 socket 描述符
  2. IO 效率不随 FD 数目增加而线性下降
  3. 使用 mmap 加速内核与用户空间的消息传递
  4. epoll 支持水平触发和边沿触发两种工作模式

二、epoll API

epoll 在 Linux2.6 内核正式提出,是基于事件驱动的 I/O 方式,相对于 select 来说,epoll 没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

Linux 中提供的 epoll 相关函数如下:

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

2.1 epoll_create

函数创建一个 epoll 句柄,参数 size 表明内核要监听的描述符数量。调用成功时返回一个 epoll 句柄描述符,失败时返回 -1。

2.2 epoll_ctl

函数注册要监听的事件类型。四个参数解释如下:

  • epfd 表示 epoll 句柄

  • op 表示 fd 操作类型,有如下 3 种

    EPOLL_CTL_ADD   注册新的fd到epfd中
    EPOLL_CTL_MOD   修改已注册的fd的监听事件
    EPOLL_CTL_DEL   从epfd中删除一个fd
    
  • fd 是要监听的描述符

  • event 表示要监听的事件。epoll_event 结构体定义如下:

    struct epoll_event {
        __uint32_t events;  /* Epoll events */
        epoll_data_t data;  /* User data variable */
    };
    
    typedef union epoll_data {
        void *ptr;
        int fd;
        __uint32_t u32;
        __uint64_t u64;
    } epoll_data_t;
    

2.3 epoll_wait

函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0。

  • epfd 是 epoll 句柄
  • events 表示从内核得到的就绪事件集合
  • maxevents 告诉内核 events 的大小
  • timeout 表示等待的超时事件

epoll 是 Linux 内核为处理大批量文件描述符而作了改进的 poll,是 Linux 下多路复用 IO 接口 select/poll 的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入 Ready 队列的描述符集合就行了。

三、epoll 工作模式

epoll 除了提供 select/poll 那种 IO 事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存 IO 状态,减少 epoll_wait/epoll_pwait 的调用,提高应用程序效率。

  • 水平触发(LT):默认工作模式,即当 epoll_wait 检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用 epoll_wait 时,会再次通知此事件。

    水平触发同时支持 block 和 non-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。比如内核通知你其中一个fd可以读数据了,你赶紧去读。你还是懒懒散散,不去读这个数据,下一次循环的时候内核发现你还没读刚才的数据,就又通知你赶紧把刚才的数据读了。这种机制可以比较好的保证每个数据用户都处理掉了。

  • 边缘触发(ET): 当 epoll_wait 检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait 时,不会再次通知此事件。(直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。

    边缘触发是高速工作方式,只支持 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,等到下次有新的数据进来的时候才会再次出发就绪事件。简而言之,就是内核通知过的事情不会再说第二遍,数据错过没读,你自己负责。这种机制确实速度提高了,但是风险相伴而行。

LT 和 ET 原本应该是用于脉冲信号的,可能用它来解释更加形象。Level 和 Edge 指的就是触发点,Level 为只要处于水平,那么就一直触发,而 Edge 则为上升沿和下降沿的时候触发。比如:0->1 就是 Edge,1->1 就是 Level。
ET 模式很大程度上减少了 epoll 事件的触发次数,因此效率比 LT 模式下高。

附1:epoll 网络编程

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define SERVER_PORT 8888
#define OPEN_MAX    3000
#define BACKLOG     10
#define BUF_SIZE    1024

int main() {
    int ret, i;
    int listenfd, connfd, epollfd;
    int nready;
    int recvbytes, sendbytes;

    char* recv_buf;
    struct epoll_event ev;
    struct epoll_event* ep;
    ep = (struct epoll_event*) malloc(sizeof(struct epoll_event) * OPEN_MAX);
    recv_buf = (char*) malloc(sizeof(char) * BUF_SIZE);
    
    struct sockaddr_in seraddr;
    struct sockaddr_in cliaddr;
    int addr_len;

    memset(&seraddr, 0, sizeof(seraddr));
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = htons(SERVER_PORT);
    seraddr.sin_addr.s_addr = htonl(INADDR_ANY);

    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == -1) {
        perror("create socket failed.\n");
        return 1;
    }
    ret = bind(listenfd, (struct sockaddr*)&seraddr, sizeof(seraddr));
    if(ret == -1) {
        perror("bind failed.\n");
        return 1;
    }
    ret = listen(listenfd, BACKLOG);
    if(ret == -1) {
        perror("listen failed.\n");
        return 1;
    }

    epollfd = epoll_create(1);
    if(epollfd == -1) {
        perror("epoll_create failed.\n");
        return 1;
    }
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;
    ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
    if(ret == -1) {
        perror("epoll_ctl failed.\n");
        return 1;
    }

    while(1) {
        nready = epoll_wait(epollfd, ep, OPEN_MAX, -1);
        if(nready == -1) {
            perror("epoll_wait failed.\n");
            return 1;
        }
        for(i = 0; i < nready; i++) {
            if(ep[i].data.fd == listenfd) {
                addr_len = sizeof(cliaddr);
                connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &addr_len);
                printf("client IP: %s\t PORT : %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
                if(connfd == -1) {
                    perror("accept failed.\n");
                    return 1;
                }
                ev.events = EPOLLIN;
                ev.data.fd = connfd;
                ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev);
                if(ret == -1) {
                    perror("epoll_ctl failed.\n");
                    return 1;
                }
            }
            else {
                recvbytes = recv(ep[i].data.fd, recv_buf, BUF_SIZE, 0);
                if(recvbytes <= 0) {
                    close(ep[i].data.fd);
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, ep[i].data.fd, &ev);
                    continue;
                }
                printf("receive %s\n", recv_buf);
                sendbytes = send(ep[i].data.fd, recv_buf, (size_t)recvbytes, 0);
                if(sendbytes == -1) {
                    perror("send failed.\n");
                }
            }
        } // for each ev
    } // while(1)

    close(epollfd);
    close(listenfd);
    free(ep);
    free(recv_buf);

    return 0;
}

参考:


每天用心记录一点点。内容也许不重要,但习惯很重要!

posted on 2019-07-03 07:31  binarylei  阅读(558)  评论(0编辑  收藏  举报

导航