Linux NIO 系列(04-3) epoll

Linux NIO 系列(04-3) epolllinux

Netty 系列目录(http://www.javashuo.com/article/p-hskusway-em.html)编程

1、why epoll

1.1 select 模型的缺点

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

  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 支持水平触发和边沿触发两种工做模式

2、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 队列的描述符集合就好了。

3、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;
}

参考:


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

相关文章
相关标签/搜索