Linux NIO 系列(04-3) epoll
Linux NIO 系列(04-3) epoll
Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)
一、why epoll
1.1 select 模型的缺点
句柄限制:单个进程能够监视的文件描述符的数量存在最大限制,通常是 1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在 linux 内核头文件中,有这样的定义:
#define __FD_SETSIZE 1024
)数据拷贝:内核 / 用户空间内存拷贝问题,select 需要复制大量的句柄数据结构,产生巨大的开销;
轮询机制:select 返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
select 的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行 IO 操作,那么之后每次 select 调用还是会将这些文件描述符通知进程。
设想一下如下场景:有 100 万个客户端同时与一个服务器进程保持着 TCP 连接。而每一时刻,通常只有几百上千个 TCP 连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
粗略计算一下,一个进程最多有 1024 个文件描述符,那么我们需要开 1000 个进程来处理 100 万个客户连接。如果我们使用 select 模型,这 1000 个进程里某一段时间内只有数个客户连接需要数据的接收,那么我们就不得不轮询 1024 个文件描述符以确定究竟是哪个客户有数据可读,想想如果 1000 个进程都有类似的行为,那系统资源消耗可有多大啊!
针对 select 模型的缺点,epoll 模型被提出来了!
1.2 epoll 模型优点
- 支持一个进程打开大数目的 socket 描述符
- IO 效率不随 FD 数目增加而线性下降
- 使用 mmap 加速内核与用户空间的消息传递
- 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的更多相关文章
- Linux NIO 系列(04-4) select、poll、epoll 对比
目录 一.API 对比 1.1 select API 1.2 poll API 1.3 epoll API 二.总结 2.1 支持一个进程打开的 socket 描述符(FD)不受限制(仅受限于操作系统 ...
- Linux NIO 系列(02) 阻塞式 IO
目录 一.环境准备 1.1 代码演示 二.Socket 是什么 2.1 socket 套接字 2.2 套接字描述符 2.3 文件描述符和文件指针的区别 三.基本的 SOCKET 接口函数 3.1 so ...
- Linux NIO 系列(04-1) select
目录 一.select 机制的优势 二.select API 介绍与使用 2.1 select 2.2 fd_set 集合操作 2.3 select 使用范例 三.深入理解 select 模型: 四. ...
- Linux NIO 系列(03) 非阻塞式 IO
目录 一.非阻塞式 IO 附:非阻塞式 IO 编程 Linux NIO 系列(03) 非阻塞式 IO Netty 系列目录(https://www.cnblogs.com/binarylei/p/10 ...
- Linux NIO 系列(04-2) poll
目录 一.select 和 poll 比较 二.poll API 附1:linux 每个进程IO限制 附2:poll 网络编程 Linux NIO 系列(04-2) poll Netty 系列目录(h ...
- Java NIO系列教程(七) selector原理 Epoll版的Selector
目录: Reactor(反应堆)和Proactor(前摄器) <I/O模型之三:两种高性能 I/O 设计模式 Reactor 和 Proactor> <[转]第8章 前摄器(Proa ...
- c/c++ linux epoll系列1 创建epoll
linux epoll系列1 创建epoll 据说select和poll的弱点是,随着连接(socket)的增加,性能会直线下降. epoll不会随着连接(socket)的增加,性能直线下降. 知识点 ...
- 深入理解NIO(四)—— epoll的实现原理
深入理解NIO(四)—— epoll的实现原理 本文链接:https://www.cnblogs.com/fatmanhappycode/p/12362423.html 终于来到最后了,万里长征只差最 ...
- Java NIO系列教程(八)JDK AIO编程
目录: Reactor(反应堆)和Proactor(前摄器) <I/O模型之三:两种高性能 I/O 设计模式 Reactor 和 Proactor> <[转]第8章 前摄器(Proa ...
随机推荐
- fiddler接口知识
1. 访问接口时经常会出现乱码的情况,需要fiddler解码操作 2.域名过滤
- easyUI学习笔记二
1. 拖拉大小 <!DOCTYPE html> <html> <head> <title>easyui学习</title> <scr ...
- [fW]中断处理函数数组interrupt[]初始化
中断处理函数数组interrupt[]初始化 2011-05-13 15:51:40 分类: LINUX 在系统初始化期间,trap_init()函数将对中断描述符表IDT进行第二次初始化(第一次只是 ...
- create-react-app创建项目后,运行npm run eject报错解决方法
运行npm run eject报错解决方法 主要问题是脚手架添加.gitgnore文件,但是却没有本地仓库,使用以下命令操作以下就可以了 git init git add . git commit - ...
- Java Exception异常介绍
一:介绍java异常 异常指不期而至的各种状况,如:文件找不到.网络连接失败.非法参数等.异常是一个事件,它发生在程序运行期间,干扰了正常的指令流程.Java通 过API中Throwab ...
- vs2010管理员运行
VS2010 Configuation->Linker->Manifest File->UAC Execution Level-> requireAdministrator
- Java高频经典面试题(第一季)一:自增的分析
package will01; public class testZiZeng { public static void main(String[] args) { int i = 1; i = i ...
- 从零开始做一个Android自动化
移动端自动化简单说就是,写好操作app的程序,运行起来,自动执行程序和测试用例,输出执行结果,结果正确,测试通过. 自动化可以方便地完成安装/卸载.启动/运行.UI适配等环节,节省时间: 同一测试脚本 ...
- java实现http协议发送和接收数据
public void sendMessage() throws Exception { System.out.println("调用servlet开始=================&q ...
- sql中的(case when then else end )的用法(相当于java中的if else)
Case具有两种格式:简单Case函数和Case搜索函数. 1.简单Case函数: CASE sex WHEN‘1’THEN‘男’ WHEN‘0’THEN‘女’ ELSE‘其他’END 2.Case搜 ...