浅析I/O多路转接之epll技术原理
我们终于走到了最终章epoll服务器,前面我们介绍过select和poll我在他们的博客里面曾经提到过epoll,我可真的是
把epoll吹上天了
但是呢人家
epoll
确实有这个实力值得吹,因为人家就是当今多路转接技术中性能最优的技术
没有之
一
!而且用到的方法也是巧妙地
不
得了,如果poll的设计很
大师,
那么epoll就是大神.如果你不明白select和poll建议
你
先去看看这两个博客,因为没有对比就没有伤
害,有对比你才知道epoll有多厉害.
I/O多路转接之select服务器
以及 I/O多路转接之poll服务器
epoll是当今效率最高的多路转接技术,没有之一,可以这样说其实epoll的出现就是为了改进select和poll,它拥有处
理大批量句柄
的
能力,它是在
2.5.44内核中被引进,它具有之前select和poll的所有优点,被公认linux2.6下性能最
优的多路I/O就绪通知方法.
首先
带着2个问题来学习epoll.
1.epoll为什么性能这么高,它的底层是什么
2.epoll是如何解决掉poll缺点的
epoll相关系统函数
epoll相关的系统函数只有3个: epoll_create,poll_wait,epoll_ctl
epoll_create函数
这样讲看不懂英语的看这里~,
创建一个epoll句柄.自从Linux2.6.8之后,size参数是被忽略的.需要注意的是,当创建好epoll句
柄后,他就是
会占用一个fd值,在Linux下如果查看、proc/进程id,是能够看到这个fd的,所以在使用完epoll后,必
须调用一个
close关闭,否则可能导致fd被
耗尽.
epoll_ctl函数
epoll的时间注册函数,他不同于select()是在监听事件时告诉内核想要监听什么类的事件,而是在这里先注册要监听的事件类
型.
第一个参数是epoll_create()的返回值.
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中:
EPOLL_CTL_MOD;修改已经注册的fd的监听事件:
EPOLL_CTL_DEL:从epfd中删除一个fd;第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
这里的结构体大家很好认吧,只需要关注两个fd和events就够了,这个很poll很相似,也就是提前注册关心事件.
events可以是一下标志的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入
到EPOLL队列里
epoll_wait函数
该函数用于轮询I/O事件的发生
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值(单位微秒);-1相当于阻塞,0相当于非阻塞。一般用-1。
返回值为发生事件的个数
epoll_wait运行的原理是等侍注册在epfd上的socket fd的事件的发生,如果发生则将发生的sokct fd和事件类型放入到events数组中。
并且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要用epoll_ctl函数
来重新设置socket fd的事件类型。
epoll在内核当中底层原理
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储被监控socket。当你调用epoll_create时,
epoll_create
创建
一个属于
该
文件系统的文件,并返回其文件描述符。struct eventpoll保存了epoll文件节点的扩展
信息,该结
构
保存在
private_data域中,每个由
epoll_create
得到的文件描述符都分配了一个该结构体,让我们来看一
下struct eventpll的内
部结构:
struct eventpoll {
/* 用于维护自身的状态,可用于中断上下文 */
spinlock_t lock;
/*
* 用户进程上下文中
*/
struct mutex mtx;
/* 进程等待队列,由 sys_epoll_wait()使用,调用epoll_wait时,休眠在这里 */
wait_queue_head_t wq;
/* 进程等待队列,由 file->poll()使用 ,epollfd本身被poll时,休眠在这里*/
wait_queue_head_t poll_wait;
/* 就绪文件描述符链表 */
struct list_head rdllist;
/* 红黑树头节点,该红黑树用于存储要监控的文件描述符 */
struct rb_root rbr;
/*
* ready事件的临时存放链表
*/
struct epitem *ovflist;
/* 创建eventpoll descriptor的用户 */
struct user_struct *user;
};
这里我们要挑重点瞧瞧,你看到什么
wait_queue_head_t poll_wait
就绪队列和
struct list_head rdllist
红黑树头结
点...what?
这里
这就是epoll
的优
化,因为select和poll存储文件描述符或者关心事件都是使用数组存储,无论是添加
还是删除查找某个节点都
要从头
开始遍历事件复杂度为
O(N),而我
们的epoll的底层使用红黑树的来存储文件描述符以
及关心的事件类型,也就是我们上面提
到
struct
poll_event结构体,因为红黑树的
性质,他是升级
版AVL树所以呢,
这时候查找的时间复杂度为O(NlgN),这样极大的节约
了效
率.这是epoll的第一个优化.
epoll第二个优化回调机制,何为回调机制? 举个例子你找一个朋友去吃饭,而他还在家收拾(如果我们使用select
和poll的主
动
轮询
方法)也就是
我们
站在楼下隔一段时间打电话给他,他不下来我们也走不了,就一直站在楼下隔一段
时间打电话傻傻的等着.
而所谓的回
调
机制就是我直接给他打
一个电
话,我先去小区门口的电玩城打会游戏,你好了
你过来找我,然后我们去吃饭.这个就是
回调机制通俗
易懂的理解.
操作系统以前需要主动轮询看某个文件描述符中的某个事件类型是否就绪,但是回调机制后,当某个节点事件就绪
后
,操作系统就
激
活该节点. 何
为激
活? 所谓的激活就是将发生的事件文件描述符和事件类型依次拷贝到生产进
就绪队
列当中.
epoll的第三个优化就
绪队列. 所谓的就绪队列还是
在内核中的一个链表,它存储的是在红黑树当中被激活的节点(关
心的事件就绪)
它就是表示文件描
述
符中的那个时间就绪,注意这里用户提取数据
的时间复杂度为O(1)!!为什么是
O(1)?因为你被激活的事件都
是从0开始依次存入就
绪队列当中,这
时候你比如说一次读取50个就绪事件那么你直
接依
次拿走就好了.但是这不是重点!!
我们还记得epoll_wait函数的原型吧:
int epoll_wait(int epfd,struct epoll events *events,int maxevents,int timeout)
其实我给讲epoll_wait函数它具体过程就是把events数组从0下标开始读取最多读取maxevents个就绪事件.对就这样就
完了,好多人
就
不明白了,
events是我传进去的参数我又没有定义它凭什么会有数据?到这里就是第三个优化的核心
了
内存映射.
所谓内存映射原
理参
照共享内存的实现,也就
是
让内核和events结构体数组通过页表映射看到同一份物
理内存
,如果实在不明白可以去看这个博客
:
共享内存的原理
发生了内存映射就节省
了从
内核拷贝就绪队列给用
户区,这
一步提高的性能很有效果的.其实调用
epoll_wait函
数后,events数组就和内核当中的就绪队列就会发生内
存映
射这时
候
你就可以骚起来拿数据了.这里注意events不能穿个
空指针进来
要不然就会出错,还有如果就绪队列当
中数据读取完了之
后epoll_wait直接
的返回.
如果你读取数据读取到maxevent还是
没有读完
那么你就再读一遍,反正它一直有序存在于就绪队列当中.
我再画一张图来帮助大家理解这三
个
优化.
(ps:红黑树时间复杂度为O(nlogN)).
epoll的工作方式
epoll对文件描述符的操作有两种模式:LT和ET. LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用
epoll_wait
时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理
,下次调用
epoll_wait时,不会再次响应应用程序并通知此事件。
1.LT模式LT是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了
,然后你可以
对这个就绪的fd
进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
2.ET模式ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。
然后它会假
设你知道文件描
述符
已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作
导致那个文件描述符不再
为就绪状态了(比如,你在发送,
接收或
者接收请求,或者发送接收的数据少于一定量时导
致了一个EWOULDBLOCK 错误)。但是
请注
意,如果一直不对这个fd作IO操作(从而导致
它再次变
成未就绪),内核
不会发送更多的通知
ET模式在很大程度上减少了epoll事
件被重复触发的次数,因此效率要比LT模式高。epoll工作在
ET模
式的时候,
必须使用非阻塞套
接口,以避免由于一个文件句柄的
阻塞读/阻塞写操作把处理多个文件描述符的任务饿死.
epoll服务器与select和poll之间的比较
《linux高性能服务器编程》摘自
简易的epoll服务器
最后的最后我编写一个最简单的epoll服务器,并且使用局域网的网页访问服务器,让服务器在网页上面对我们回显一句话.
epoll_server.c
/*************************************************************************
> File Name: epoll_server.c
> Author: ma6174
> Mail: ma6174@163.com
> Created Time: Sun 30 Jul 2017 12:36:55 AM PDT
************************************************************************/
#include <fcntl.h>
#include<stdlib.h>
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<unistd.h>
#define SIZE 60
void usage(const char* argv)
{
printf("%s:[ip] [port]\n", argv);
}
int startup(char* ip, int port) //创建监听套接字不解释
{
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0)
{
perror("sock");
exit(2);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
//2
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if (bind(sock, (struct sockaddr*)&local, sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
if (listen(sock, 10) < 0)
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
usage(argv[0]);
exit(1);
}
int listen_sock = startup(argv[1], atoi(argv[2]));
int epoll = epoll_create(500); //创建epoll
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
epoll_ctl(epoll, EPOLL_CTL_ADD, listen_sock, &ev); //这里首先把监听套接字添加进红黑树
struct epoll_event revs[SIZE];
int nums = 0;
int i = 0;
while (1)
{
switch (nums = epoll_wait(epoll, revs, SIZE, -1))
{
case -1:
{
perror("wait");
break;
}
case 0:
{
perror("time out");
break;
}
default:
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
for (i = 0; i < nums; i++)
{
int rsock = revs[i].data.fd;
if (rsock == listen_sock)
{
int new_sock = accept(listen_sock, (struct sockaddr*)&client, &len); //创建链接套接字
if (new_sock > 0)
{
printf("get a new client:%s:%d\n", inet_ntoa(client.sin_addr), ntohs(client.sin_port));
}
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = new_sock;
epoll_ctl(epoll, EPOLL_CTL_ADD, new_sock, &ev);//将链接套接字存储在红黑当中,开始监视.
}
else
{
if (revs[i].events & EPOLLIN) // 读时间就绪
{
char buf[1024];
ssize_t s = read(rsock, buf, sizeof(buf)-1);
if (s > 0)
{
buf[s] = '\0';
printf("client ->#%s\n", buf);
ev.events = EPOLLOUT;
ev.data.fd = rsock;
epoll_ctl(epoll, EPOLL_CTL_MOD, rsock, &ev); //读完结束后 切换监视写状态.
}
else if (s == 0)
{
printf(" client :%d is quit\n", rsock);
epoll_ctl(epoll, EPOLL_CTL_DEL, rsock, NULL);
close(rsock);
}
else
{
perror("read");
}
}
else if (revs[i].events & EPOLLOUT)//这里用了一点html的小语句
{
const char* msg = "HTTP/1.0.200 OK\r\n\r\n<html><h2>shuowoailimengting</h2></html>\r\n";
write(rsock, msg, strlen(msg));
epoll_ctl(epoll, EPOLL_CTL_DEL, rsock, NULL);
close(rsock);
}
else
{
}
}
}
break;
}
}
}
}
运行结果:
总结:epoll可以说是已经无懈可击了,突然出现大批量数据时以前的select何poll是扛不住的,但是epoll不需要
遍历文件描述符
他拥有回调机制,以
及就
绪队列,也就是说数据越多epoll越厉害,越战俞勇.这已经是一个成熟
优秀的一个多路转接技术了.