epoll是linux下高性能的IO复用技术,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

  为什么会出现IO复用技术呢,比如在Web应用中,大量的请求连接事件,如果采用多进程方式处理,也就是一个连接对应一个fork来处理,这样开销太大了,毕竟创建进程还是很耗资源的;如果采用多线程方式处理,也就是一个连接对应一个线程来处理,当请求并发量上去的话,系统中就会充斥着很多处理线程,毕竟一个系统创建线程是有一定上限的。这时,就需要我们的IO复用技术了。常见的网络模型中,有多进程+IO复用编程模型,也有多线程+IO复用编程模型,比如大名鼎鼎的nginx默认采用的就是多进程+IO复用技术来处理网络请求的;开源网络库libevent也是基于IO复用技术来完成网络数据处理的。

epoll系列函数

  epoll是Linux特有的IO复用函数,它在实现和使用上与select和poll有很大差异,首先,epoll使用一组函数来完成操作,而不是单个函数。其次,epoll把用户关心的文件描述符上的事件放在内核上的一个事件表中,从而无须像select和poll那样每次调用都要重复传入文件描述符集合事件表。但epoll需要使用一个额外的文件描述符,来唯一标识内核中这个事件表,这个文件描述符使用如下epoll_create函数创建:

#include <sys/epoll.h>
int epoll_create(int size); // 返回:成功返回创建的内核事件表对应的描述符,出错-1

  size参数现在并不起作用,只是给内核一个提示,告诉它内核表需要多大,该函数返回的文件描述符将用作其他所有epoll函数的第一个参数,以指定要访问的内核事件表。用epoll_ctl函数操作内核事件表

#include <sys/epoll.h>
int epoll_ctl(int opfd, int op, int fd, struct epoll_event *event); // 返回:成功返回创建的内核事件表对应的描述符,出错-1

  fd参数是要操作的文件描述符,op指定操作类型,操作类型有3种

  • EPOLL_CTL_ADD:往事件表中注册fd上的事件
  • EPOLL_CTL_MOD:修改fd上的注册事件
  • EPOLL_CTL_DEL:删除fd上的注册事件

  event指定事件类型,它是epoll_event结构指针类型:

struct epoll_event
{
__uint32_t events; /* epoll事件 */
epoll_data_t data; /* 用户数据 */
};

  其中events描述事件类型,epoll支持的事件类型和poll基本相同,表示epoll事件类型的宏是在poll对应的宏加上”E”,比如epoll的数据可读事件是EPOLLIN,但epoll有两个额外的事件类型-EPOLLET和EPOLLONESHOT,它们对于高效运作非常关键,data用于存储用户数据,其类型epoll_data_t定义如下:

typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;

  epoll_data_t是一个联合体,其4个成员最多使用的是fd,它指定事件所从属的目标文件描述符,ptr成员可用来指定fd相关的用户数据,但由于opoll_data_t是一个联合体,我们不能同时使用fd和ptr,如果要将文件描述符嗯哼用户数据关联起来,以实现快速的数据访问,则只能使用其他手段,比如放弃使用fd成员,而在ptr指针指向的用户数据中包含fd。

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 返回:成功返回就绪的文件描述符个数,出错-1

  timeout参数的含义与poll接口的timeout参数相同,maxevents参数指定最多监听多少个事件,它必须大于0。

  epoll_wait如果检测到事件,就将所有就绪的事件从内核事件表(由epfd指定)中复制到events指定的数组中,这个数组只用来输epoll_wait检测到的就绪事件,而不像select和poll的参数数组既传递用于用户注册的事件,有用于输出内核检测到就绪事件,这样极大提高了应用程序索引就绪文件描述符的效率。

epoll原理与实现

  epoll是怎么实现的呢?其实很简单,从这3个方法就可以看出,它比select聪明的避免了每次频繁调用“哪些连接已经处在消息准备好阶段”的 epoll_wait时,是不需要把所有待监控连接传入的。这意味着,它在内核态维护了一个数据结构保存着所有待监控的连接。这个数据结构就是一棵红黑树,它的结点的增加、减少是通过epoll_ctrl来完成的。

图片来源于CSDN:高性能网络编程5--IO复用与并发编程

  图中左下方的红黑树由所有待监控的连接构成。左上方的链表,同是目前所有活跃的连接。于是,epoll_wait执行时只是检查左上方的链表,并返回左上方链表中的连接给用户。这样,epoll_wait的执行效率能不高吗?

  最后,再看看epoll提供的2种玩法ET和LT,即翻译过来的边缘触发和水平触发。其实这两个中文名字倒也有些贴切。这2种使用方式针对的仍然是效率问题,只不过变成了epoll_wait返回的连接如何能够更准确些。
例如,我们需要监控一个连接的写缓冲区是否空闲,满足“可写”时我们就可以从用户态将响应调用write发送给客户端 。但是,或者连接可写时,我们的“响应”内容还在磁盘上呢,此时若是磁盘读取还未完成呢?肯定不能使线程阻塞的,那么就不发送响应了。但是,下一次epoll_wait时可能又把这个连接返回给你了,你还得检查下是否要处理。可能,我们的程序有另一个模块专门处理磁盘IO,它会在磁盘IO完成时再发送响应。那么,每次epoll_wait都返回这个“可写”的、却无法立刻处理的连接,是否符合用户预期呢?
  于是,ET和LT模式就应运而生了。LT是每次满足期待状态的连接,都得在epoll_wait中返回,所以它一视同仁,都在一条水平线上。ET则不然,它倾向更精确的返回连接。在上面的例子中,连接第一次变为可写后,若是程序未向连接上写入任何数据,那么下一次epoll_wait是不会返回这个连接的。ET叫做 边缘触发,就是指,只有连接从一个状态转到另一个状态时,才会触发epoll_wait返回它。可见,ET的编程要复杂不少,至少应用程序要小心的防止epoll_wait的返回的连接出现:可写时未写数据后却期待下一次“可写”、可读时未读尽数据却期待下一次“可读”。 
  当然,从一般应用场景上它们性能是不会有什么大的差距的,ET可能的优点是,epoll_wait的调用次数会减少一些,某些场景下连接在不必要唤醒时不会被唤醒(此唤醒指epoll_wait返回)。但如果像我上面举例所说的,有时它不单纯是一个网络问题,跟应用场景相关。当然,大部分开源框架都是基于ET写的,框架嘛,它追求的是纯技术问题,当然力求尽善尽美。

基于epoll的"迷你"网络事件库

  网络事件库封装了底层IO复用函数,同时提供给外部使用的接口,提供的接口可以多种多样,但是一般有添加事件、删除事件、开始事件循环等接口。为了展示下网络事件库的是如何封装IO复用函数,同时学习epoll的使用,"迷你"网络事件库-tomevent今天诞生了 :) (ps:tomevent采用C++语言实现)。

  既然是网络事件库,那首先需要定义一个事件的结构,LZ这里就使用Event结构体了,事件结构体中包含监听的文件描述符、事件类型、回调函数、传递给回调函数的参数,当然,这只是一个简单的事件结构,如果还需要其他信息可另外添加。

/**
* event struct.
*/
struct Event {
int fd; /* the fd want to monitor */
short event; /* the event you want to monitor */
void *(*callback)(int fd, void *arg); /* the callback function */
void *arg; /* the parameter of callback function */
};

  定义一个事件处理接口IEvent,该接口定义了3个基本的事件操作函数,也就是添加事件、删除事件、开始事件循环。定义IEvent接口,与具体的底层IO技术解耦,使用具体的IO复用类来实现该接口,比如对应select的SelectEvent,或者是对应poll的PollEvent,当然,这里就用epoll对应的EpollEvent来实现IEvent接口(ps:c++中接口貌似应该称为抽象类,不过这里称为接口更合适一点)。

/**
* the interface of event.
*/
class IEvent {
public:
virtual int addEvent(const Event &event) = ;
virtual int delEvent(const Event &event) = ;
virtual int dispatcher() = ; virtual ~IEvent() { }
};

  IEvent的实现类EpollEvent,其中封装了epoll相关的函数。EpollEvent有3个成员,分别是pollCreateSize、epollFd、events,pollCreateSize表示调用epoll_create时传递的参数值,epollFd表示epoll_create的返回值,events是记录事件的map,events中记录了监听事件的信息,当事件来临时被用到。

class EpollEvent : public IEvent {
public:
EpollEvent() : EpollEvent() { }
EpollEvent(int createSize) {
if (createSize < ) {
createSize = ;
} epollCreateSize = createSize;
initEvent();
} virtual int addEvent(const Event &event);
virtual int delEvent(const Event &event);
virtual int dispatcher(); private:
int initEvent() {
int epollFd = epoll_create(this->epollCreateSize);
if (epollFd <= ) {
perror("create_create error:");
return epollFd; /* here epollFd is -1 */
} this->epollFd = epollFd;return ;
} int epollCreateSize;
int epollFd; //Event event;
map<int, Event> events;
};
int EpollEvent::addEvent(const Event &event) {
struct epoll_event epollEvent; epollEvent.data.fd = event.fd;
epollEvent.events = event.event;
int retCode = epoll_ctl(this->epollFd, EPOLL_CTL_ADD, event.fd, &epollEvent);
if (retCode < ) {
perror("epoll_ctl error:");
return retCode;
} /* add event to this->events */
this->events[event.fd] = event;return ;
} int EpollEvent::delEvent(const Event &event) {
struct epoll_event epollEvent; epollEvent.data.fd = event.fd;
epollEvent.events = event.event;
int retCode = epoll_ctl(this->epollFd, EPOLL_CTL_DEL, event.fd, &epollEvent);
if (retCode < ) {
perror("epoll_ctl error:");
return retCode;
} this->events.erase(event.fd);
return ;
} int EpollEvent::dispatcher() {
struct epoll_event epollEvents[]; //cout << "epoll_wait before" << endl;
int nEvents = epoll_wait(epollFd, epollEvents, , -);
if (nEvents <= ) {
perror("epoll_wait error:");
return -;
}
//cout << "epoll_wait after nEvent" << endl;
for (int i = ; i < nEvents; i++) {
int fd = epollEvents[i].data.fd;
Event event = this->events[fd];
if (event.callback) {
event.callback(fd, event.arg);
}
}
return ;
}

  到这里整个tomevent的框架代码就结束了,那么该如何使用呢,以下是一个测试用例。使用tomevent来同时监听2个文件描述符,一个是标准输入(fd为0),另一个是提供UDP服务的一个文件描述符。

void *test(int fd, void *arg) {
cout << "****************test(): fd=" << fd << endl; char buff[];
int len = recvfrom(fd, buff, sizeof(buff), , NULL, NULL);
if (len > ) {
buff[len] = '\0';
cout << buff << endl;
}
else {
perror("recvfrom error:");
}
cout << "****************test()**********" << endl;
} void *inTest(int fd, void *arg) {
cout << "****************inTest(): fd=" << fd << endl; char buff[];
int len = read(fd, buff, sizeof(buff));
if (len > ) {
buff[len] = '\0';
cout << buff << endl;
}
else {
perror("read stdin error:");
}
cout << "****************inTest()**********" << endl;
} int main(int argc, char **argv) {
int listenFd = -;
int connFd = -;
struct sockaddr_in servAddr; listenFd = socket(AF_INET, SOCK_DGRAM, ); memset(&servAddr, , sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons();
servAddr.sin_addr.s_addr = INADDR_ANY;
bind(listenFd, (struct sockaddr *)&servAddr, sizeof(servAddr)); listen(listenFd, ); Event event, inEvent;
EpollEvent eventBase; event.fd = listenFd;
event.event = EPOLLIN;
event.arg = NULL;
event.callback = test; inEvent.fd = ;
inEvent.event = EPOLLIN;
inEvent.arg = NULL;
inEvent.callback = inTest; eventBase.addEvent(event);
eventBase.addEvent(inEvent); for (; ;) {
eventBase.dispatcher();
} return ;
}

  以下是测试结果 ,同时提供UDP服务和响应键盘输入。

参考

  1、epoll-百度百科

  2、高性能网络编程5--IO复用与并发编程

  3、Libevent初探

利用epoll写一个"迷你"的网络事件库的更多相关文章

  1. 学了C语言,如何利用cURL写一个程序验证某个网址的有效性?

    在<C程序设计伴侣>以及这几篇关于cURL的文章中,我们介绍了如何利用cURL写一个下载程序,从网络下载文件.可是当我们在用这个程序下载文件时,又遇到了新问题:如果这个网址是无效的,那么我 ...

  2. 学了C语言,如何利用CURL写一个下载程序?—用nmake编译CURL并安装

    在这一系列的前一篇文章学了C语言,如何为下载狂人写一个磁盘剩余容量监控程序?中,我们为下载狂人写了一个程序来监视磁盘的剩余容量,防止下载的东西撑爆了硬盘.可是,这两天,他又抱怨他的下载程序不好用,让我 ...

  3. 写一个迷你版Smarty模板引擎,对认识模板引擎原理非常好(附代码)

    前些时间在看创智博客韩顺平的Smarty模板引擎教程,再结合自己跟李炎恢第二季开发中CMS系统写的tpl模板引擎.今天就写一个迷你版的Smarty引擎,虽然说我并没有深入分析过Smarty的源码,但是 ...

  4. 如何写一个跨浏览器的事件处理程序 js

    如何 写一个合格的事件处理程序,看如下代码: EventUtil可以直接拿去用 不谢 <!DOCTYPE html> <html> <head> <title ...

  5. 利用Python写一个抽奖程序,解密游戏内抽奖的秘密

    前言 本文的文字及图片来源于网络,仅供学习.交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理. 作者: 极客挖掘机 PS:如有需要Python学习资料的小伙伴可以加点击下 ...

  6. 利用 Python 写一个颜值测试小工具

    我们知道现在有一些利用照片来测试颜值的网站或软件,其实使用 Python 就可以实现这一功能,本文我们使用 Python 来写一个颜值测试小工具. 很多人学习python,不知道从何学起.很多人学习p ...

  7. 利用反射--调用一个按钮的Click事件

    最基本的调用方法 (1)button1.PerformClick();(2)button1_Click(null,null);(3)button_Click(null,new EventArgs()) ...

  8. python利用socket写一个文件上传

    1.先将一张图片拖入‘文件上传’的目录下,利用socket把这张图片写到叫‘yuan’的文件中 2.代码: #模拟服务端 import subprocess import os import sock ...

  9. 利用jmSlip写一个移动端顶部日历选择组件

    可滚动选日期,并限制哪些日期可选和不可选. 主要用来根据后台返回生成一个日期选择器. 具体实现可关注jmslip: https://github.com/jiamao/jmSlip 示例:http:/ ...

随机推荐

  1. 纯C#实现屏幕指定区域截屏

    以前在别的地方见过一个通过调用系统API实现屏幕截图的例子,从内心来说我不太喜欢在C#代码中出现这种情况,现在什么都讲“和谐”,我觉得这种做法就是破坏了我们的“和谐”代码,呵呵,开玩笑,有的时候,不通 ...

  2. gRPC C#学习

    前些天gRPC 发布1.0 版本,代表着gRPC 已经正式进入稳定阶段. 今天我们就来学习gRPC C# .而且目前也已经支持.NET Core 可以实现完美跨平台. 传统的.NET 可以通过Mono ...

  3. browserCaps与浏览器功能设置

    使用 Web.config 文件中的 browserCaps 元素来定义浏览器的行为在 .NET Framework 2.0 中被否决,但仍受支持.此元素中的数据与浏览器定义文件 (.browser) ...

  4. WPF模板

    WPF的中模板有三种:ControlTemplate.ItemsPanelTemplate.DataTemplate,他们继承抽象类FrameworkTemplate,下面是它们的继承关系: Wind ...

  5. ComponentOne 2016 V3 发布

    ComponentOne Studio Enterprise 2016 V3 新特性 我们很高兴的宣布ComponentOne 2016 V3发布了!2016 Connect开发者大会上微软发布了Vi ...

  6. Android Studio创建AVD

    Android Studio是专门为Android开发设计的IDE,比Eclipse开发Android更加方便.快捷. 安装Android Studio以后,想运行AVD,出现了下面的错误: 提示没有 ...

  7. BUTTONS V. 2.0.0——CSS按钮库

    BUTTONS-V2-CSS库样式职责 CSS库样式职责分离优点 模块样式命名更清晰化 易于维护.扩展性强 动画效果——修改样式后有过度效果,默认样式 源码如下 <!DOCTYPE html&g ...

  8. DOM节点属性

    节点属性 在文档对象模型 (DOM) 中,每个节点都是一个对象.DOM 节点有三个重要的属性 : 1. nodeName : 节点的名称 2. nodeValue :节点的值 3. nodeType ...

  9. MacDown语法教程

    MacDown Hello there! I'm MacDown, the open source Markdown editor for OS X. Let me introduce myself. ...

  10. android 传感器应用

    本章讲述Android开发中,传感器应用相关的知识点. 1.功能需求 做一个基于传感器的水平尺应用. 2.软件实现