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 ...
随机推荐
- log4j/slf4j
log4j的使用 引入log4j.jar包 <dependency> <groupId>log4j</groupId> <artifactId>log4 ...
- 在php中定义常量时,const和define的区别?
使用const使得代码简单易读,const本身就是一个语言结构,而define是一个函数.另外const在编译时要比define快很多. 1.const用于类成员变量的定义,一经定义,不可修改.Def ...
- BZOJ 4765(分块+树状数组)
题面 传送门 "奋战三星期,造台计算机".小G响应号召,花了三小时造了台普通计算姬.普通计算姬比普通计算机要厉害一些 .普通计算机能计算数列区间和,而普通计算姬能计算树中子树和.更 ...
- mysql数值字符串类型的按照数值进行排序
今天遇到一个问题,就是对mysql数值字符串类型进行排序,在默认情况下使用order by 字段名称 desc/asc 进行排序的时候,mysql进行的排序规则是按照ASCII码进行排序的,并不会自动 ...
- cnblogs博客主题原来可以弄得这么美观
参考了网友 https://www.cnblogs.com/maybreath/p/5253824.html的做法,没想到真的可以耶. 总想弄个方便的.简洁的.可以被搜索引擎搜到的博客.以前用过wor ...
- Asp.net core 使用log4net作为日志组件,记录日志到本地。
原文:Asp.net core 使用log4net作为日志组件,记录日志到本地. GitHub demo :https://github.com/zhanglilong23/Asp.NetCore.D ...
- SQL数据库—<6-001> 常用系统存储过程大全 --摘录网
-- 来源于网络 -- 更详细的介结参考联机帮助文档 xp_cmdshell --*执行DOS各种命令,结果以文本行返回. xp_fixeddrives --*查询各磁盘/分区可用空间 xp_logi ...
- jackson的readTree有坑
{"}] readTree认为上面的字符是json,坑啊 alibaba的fastjson 无论JSONObject.parseObject还是JSONObject.parseObject ...
- 【记录】Mybatis Generator生成数据对象Date/TimeStamp 查询时间格式化
Mybatis Generator是很好的工具帮助我们生成表映射关联代码,最近博主遇到一个问题,找了很久才解决, 就是用Mybatis Generator生成实体类的时候,Date 时间无法格式化输出 ...
- 【串线篇】SpringMvc视图解析
一. 请求处理方法执行完成后,最终返回一个 ModelAndView 对象.对于那些返回 String,View 或 ModeMap 等类型的处理方法,Spring MVC 也会在内部将它们装配成一个 ...