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 ...
随机推荐
- md5加密报错解决方法(TypeError: Unicode-objects must be encoded before hashing)
update()必须指定要加密的字符串的字符编码
- Cocos2d-x之Action
| 版权声明:本文为博主原创文章,未经博主允许不得转载. 在Cocos2d-x中的Node对象可以有动作,特效和动画等动态特性.因此在Node类中定义了这些动态特性,因此精灵,标签,菜单,地图和粒 ...
- Vue 在手机上键盘把底部菜单顶上去的解决方案
Vue 在手机上键盘把底部菜单顶上去的解决方案 ios和安卓的键盘的区别 ios和安卓的键盘的区别弹起方式不同, ios直接弹出键盘, 不影响页面, 而安卓键盘弹起时会把页面顶起来, 这样就会把底部菜 ...
- 转 linux 服务器内存占用统计
linux 服务器内存占用统计 原文: https://www.cnblogs.com/eaglediao/p/6641811.html 当前内存占用率的计算,是根据top命令显示的Mem.used ...
- Harbor私有镜像仓库(下)
Harbor私有镜像仓库(下) 链接:https://pan.baidu.com/s/1MAb0dllUwmoOk7TeVCZOVQ 提取码:ldt5 复制这段内容后打开百度网盘手机App,操作更方便 ...
- 转:动态库路径配置- /etc/ld.so.conf文件
Linux 共享库 Linux 系统上有两类根本不同的 Linux 可执行程序.第一类是静态链接的可执行程序.静态可执行程序包含执行所需的所有函数 — 换句话说,它们是“完整的”.因为这一原因,静态可 ...
- 利用Python语言Appium启动ios app
首先配置好电脑环境,主要是appium太难配了,不多说 然后,分两步 第一步:启动appium服务器 有三种方法,1.下载appium-desk-top(桌面客户端),启动 2.终端启动:appium ...
- InnoDB索引存储结构
原创转载请注明出处:https://www.cnblogs.com/agilestyle/p/11429438.html InnoDB默认创建的主键索引是聚簇索引(Clustered Index),其 ...
- 【杂】聊聊我的男神:Jordan Peterson
这篇文章我打算聊聊我的男神Jordan Peterson(简称JP).如果还不太了解JP,那么下面两个链接是JP的背景介绍: [文字]Jordan Peterson From Wikipedia, t ...
- padding 填充
CSS padding(填充)是一个简写属性,定义元素边框与元素内容之间的空间,即上下左右的内边距. padding(填充) 当元素的 padding(填充)内边距被清除时,所释放的区域将会受到元素背 ...