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

参考:


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

Linux NIO 系列(04-3) epoll的更多相关文章

  1. Linux NIO 系列(04-4) select、poll、epoll 对比

    目录 一.API 对比 1.1 select API 1.2 poll API 1.3 epoll API 二.总结 2.1 支持一个进程打开的 socket 描述符(FD)不受限制(仅受限于操作系统 ...

  2. Linux NIO 系列(02) 阻塞式 IO

    目录 一.环境准备 1.1 代码演示 二.Socket 是什么 2.1 socket 套接字 2.2 套接字描述符 2.3 文件描述符和文件指针的区别 三.基本的 SOCKET 接口函数 3.1 so ...

  3. Linux NIO 系列(04-1) select

    目录 一.select 机制的优势 二.select API 介绍与使用 2.1 select 2.2 fd_set 集合操作 2.3 select 使用范例 三.深入理解 select 模型: 四. ...

  4. Linux NIO 系列(03) 非阻塞式 IO

    目录 一.非阻塞式 IO 附:非阻塞式 IO 编程 Linux NIO 系列(03) 非阻塞式 IO Netty 系列目录(https://www.cnblogs.com/binarylei/p/10 ...

  5. Linux NIO 系列(04-2) poll

    目录 一.select 和 poll 比较 二.poll API 附1:linux 每个进程IO限制 附2:poll 网络编程 Linux NIO 系列(04-2) poll Netty 系列目录(h ...

  6. Java NIO系列教程(七) selector原理 Epoll版的Selector

    目录: Reactor(反应堆)和Proactor(前摄器) <I/O模型之三:两种高性能 I/O 设计模式 Reactor 和 Proactor> <[转]第8章 前摄器(Proa ...

  7. c/c++ linux epoll系列1 创建epoll

    linux epoll系列1 创建epoll 据说select和poll的弱点是,随着连接(socket)的增加,性能会直线下降. epoll不会随着连接(socket)的增加,性能直线下降. 知识点 ...

  8. 深入理解NIO(四)—— epoll的实现原理

    深入理解NIO(四)—— epoll的实现原理 本文链接:https://www.cnblogs.com/fatmanhappycode/p/12362423.html 终于来到最后了,万里长征只差最 ...

  9. Java NIO系列教程(八)JDK AIO编程

    目录: Reactor(反应堆)和Proactor(前摄器) <I/O模型之三:两种高性能 I/O 设计模式 Reactor 和 Proactor> <[转]第8章 前摄器(Proa ...

随机推荐

  1. PAT 2019-3 7-1 Sexy Primes

    Description: Sexy primes are pairs of primes of the form (p, p+6), so-named since "sex" is ...

  2. UVA1608_Non-boring sequences

    Non-boring sequences 大致题意: 给你一个字符串,问你他的任一子串是否都包含一个唯一的字符 思路: 看似简单,实际一丁点思路都没有 后面看汝佳的讲解都看了好长时间 大概思路就是,先 ...

  3. springCloud的使用05-----路由网关(zuul)

    zuul的主要功能是路由转发和过滤,比如让所有/api-a/*的请求都转发到服务器a上,所有/api-b/*的请求都转发到服务器b上,zuul默认和ribbon结合实现了负载均衡的功能. 1 zuul ...

  4. Yii 1.1 cookie删不掉

    我的cookie是这样设置的: $cookie = new CHttpCookie('username','Jack'); $cookie->expire = time()+60*60*24*3 ...

  5. 59.Target Sum(目标和)

    Level:   Medium 题目描述: You are given a list of non-negative integers, a1, a2, ..., an, and a target, ...

  6. css 图片有间隔多个Img标签之间的间隙

    今天写css时发现,图片加起来刚好是900px的三张图片,不能在一个900px宽容器放下,因为图片之间有间隔,我猜是浏览器把两个img标签之间的空格当成了空白节点. 在网上找到了几个不错的解决方法: ...

  7. automapper实体中的映射和聚合根中的使用

    一,如下例子: using AutoMapper; using System; using System.Collections.Generic; using System.Linq; using S ...

  8. Java 枚举(enum)详解

    概念: Java1.5发行版本中增加了新的引用类型--枚举类型(enum type).枚举类型是指由一组固定的常量组成合法值的类型.在Java虚拟机中,枚举类在进行编译时会转变成普通的Java类. 创 ...

  9. 【知识强化】第六章 查找 6.3 B树和B+树

    本节课我们来学习本章的第一个难点,就是B树.那么B树它其实是一种数据结构,我们设计出这种数据结构就是为了提高我们的查找效率的,提高我们在磁盘上的查找效率.那么什么是B树呢?了解B树之前,我们先来回忆一 ...

  10. 九、结构模式之装饰(Decorator)模式

    装饰模式又叫包装模式,装饰模式以客户端透明的方式扩展对象的功能,是继承关系的一个替代方案.装饰模式可以在不使用创造更多的子类的情况下,将对象的功能加以扩展. 装饰模式结构图如下: 其包含的角色就分为: ...