前言

后端开发的应该都知道Nginx服务器,Nginx是一个高性能的 HTTP 和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。后端部署中一般使用的就是Nginx反向代理技术。

Nginx 相较于 Apache 具有占有内存少,稳定性高等优势,并发能力强的优点。它所使用的网络通信模型就是epoll。

*注:epoll模型编程实例需要先了解红黑树、tcp/ip、socket、文件描述符fd、阻塞、回调等概念。

epoll介绍

一、epoll模型概念

传统的并发服务器Apache,使用的是多进程/线程模型,每一个客户端请求都要开启一个进程去处理,占用的资源大。

epoll是一个I/O多路复用模型,可以用一个进程去处理处理多个客户端。

epoll是在2.6内核中提出的,是之前的select和poll的增强版本。关于select和poll是更早的多路复用IO模型,这里不做介绍。

相对于select和poll来说,epoll更加灵活,没有描述符限制。

epoll使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

epoll是基于事件驱动模型。展开应该叫event poll,事件轮询(猜测),所以程序围绕着event运行。

二、epoll模型详细执行过程

在Linux中,epoll模型相关的有3个系统API,通过man 2查看手册。

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);

*此外还有一个close(),create返回的是一个文件描述符int epoll_fd,结束时和普通fd一样要关闭,保证逻辑的完整性。

1.第一步,创建eventepoll结构体

当某一进程调用epoll_create函数时(参数size是事件最大数量,实际上这只是给内核的一个参考值,Linux2.6.8以后这个参数被忽略,但是api文档仍然建议填写),

Linux内核会创建一个eventpoll结构体,并返回一个int epoll_fd,这就是epoll通过一个文件描述符操作多个文件描述符的方法。

这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

struct eventpoll{
  struct rb_root rbr;
  struct list_head rdlist;
};

其中rbr是一个红黑树,它的每个结点用来存储用户关心的事件(用户关心的事件,比如服务端server_fd的accept连接请求就是一个事件)。

rblist是一个双向链表用来存储已发生的事件。

事件的结构体

typedef union epoll_data {
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
}epoll_data_t; struct epoll_event {
  uint32_t events; /* Epoll events */
  epoll_data_t data; /* User data variable */
};

一个事件应该对应至少着一个文件描述符加I/O操作,代表这个事件对应的文件描述符和它是读事件还是写事件。

I/O是同一个变量events,通过"按位或"操作可以同时添加关心读和写事件,"按位与"操作把它读取。

而它对应的文件描述符在变量data中,epoll_event.data.fd。

2. 第二步,epoll_ctl操作红黑树

当用户调用epoll_ctl向结构体加入event时,会把事件挂在到红黑树rbr中。

而所有添加到epoll中的事件都会与低层接口(设备、网卡驱动程序)建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。

这个回调方法在内核中叫ep_poll_callback,这个过程中,因为用户关心的事件挂载在红黑树上,所以查找效率高只有O(ln(n))的事件复杂度。

然后它会将发生的事件添加到rdlist双链表中。红黑树加上函数回调的机制造就了它的高效。

3. 第三步,epoll_wait检查双向链表

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。

如果rdlist不为空,则把发生的事件复制到用户态(把内核的双向链表拷贝成一个struct epoll_event数组),同时返回文件描述符的数量。

用户只需要用这个数组去接收就可以。

为什么这里要用双向链表而不是单链表?

就绪列表引用着就绪的Socket,所以它应能够快速的插入数据。程序可能随时调用 epoll_ctl 添加监视Socket,也可能随时删除。

当删除时,若该Socket 已经存放在就绪列表中,它也应该被移除。所以就绪列表应是一种能够快速插入和删除的数据结构。

双向链表就是这样一种数据结构,Epoll 使用双向链表来实现就绪队列。

epoll_event示意图

三、编程实例

具体实现可以用一个进程去处理多客户端请求,而不用

1. 使用的头文件

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <errno.h>
#include <sys/time.h>
#include <sys/epoll.h>  /* epoll模型api */

2. 函数声明

#define SERV_PORT 5001
#define SERV_IP_ADDR "192.168.1.7"
#define QUIT "quit"  /*用户退出指令*/
#define BACKLOG 5  /*监听的最大等待连接队列*/
#define EPOSIZE 100  /*接收已发生事件的最大数量*/

/* 套接字初始化的封装 */
int sock_init(int fd, struct sockaddr_in *sin); /* epoll_wait获得已发生的事件集合之后,具体的业务逻辑 */
void handle_events(int epoll_fd, struct epoll_event *events, int num, int accept_fd); /* 具体操作1,接收客户端连接 */
void do_accpet(int epoll_fd, int accept_fd); /* 具体操作2,读操作 */
void do_read(int epoll_fd, int fd, char *buff); /* 具体操作3,写操作 */
void do_write(int epoll_fd, int fd, char *buff); /*把epoll_ctl函数的操作再封装*/
void event_ctl(int epoll_fd, int fd, int flag, int state);
/*
argument: flag
EPOLL_CTL_ADD 添加事件
EPOLL_CTL_DEL 删除事件
EPOLL_CTL_MOD 修改事件 argument: state
EPOLLIN input事件
EPOLLOUT output事件
*/

3. demo实现

#include "server.h"
int main()
{
int ret = -1;
int accept_fd = socket(AF_INET, SOCK_STREAM, 0);
if(accept_fd < 0)
{
perror("socket");
return 1;
} struct sockaddr_in sin;
ret = sock_init(accept_fd, &sin);
if(ret < 0)
{
perror("sock_init");
return 2;
} int epoll_fd = epoll_create(EPOSIZE);
struct epoll_event events[EPOSIZE];/*用户空间数组去接收内核的双向链表*/
event_ctl(epoll_fd, accept_fd, EPOLL_CTL_ADD, EPOLLIN);//先把server_fd accept input事件加入红黑树 while(1)
{
ret = epoll_wait(epoll_fd, events, EPOSIZE, -1);/*参数4,超时时间,特别的-1为阻塞等待,详见linux api: man 2 epoll_wait*/
handle_events(epoll_fd, events, ret, accept_fd);
} close(epoll_fd);
close(accept_fd);
}

int sock_init(int fd, struct sockaddr_in *sin)
{
bzero(sin,sizeof(*sin));
sin->sin_family = AF_INET;
sin->sin_port = htons(SERV_PORT);
sin->sin_addr.s_addr = INADDR_ANY;
if(bind(fd, (struct sockaddr*)sin, sizeof(*sin)) < 0)
{
perror("bind");
return -1;
} if(listen(fd, BACKLOG) < 0)
{
perror("listen");
return -2;
}
return 0;
}

void handle_events(int epoll_fd, struct epoll_event *events, int num, int accept_fd)
{
int i,fd;
char buff[BUFSIZ];
for(i=0; i<num; i++)
{
fd = events[i].data.fd;
if((fd == accept_fd) && events[i].events & EPOLLIN)/* 对events和EPOLLIN“与”操作,判断这个文件描述符是否有input事件*/
do_accpet(epoll_fd, fd);
else if(events[i].events & EPOLLIN)
do_read(epoll_fd, fd, buff);
else if(events[i].events & EPOLLOUT)
do_write(epoll_fd, fd, buff);
} }

void do_accpet(int epoll_fd, int accept_fd)
{
int new_fd;
struct sockaddr_in cin;
socklen_t len;
new_fd = accept(accept_fd, (struct sockaddr*)&cin, &len);
if(new_fd < 0)
{
perror("accpet");
return;
}
printf("a new client connected!\n");

  /*add_event input,
  client连接之后,把它的文件描述符的input事件加入到红黑树中*/
  */

event_ctl(epoll_fd, new_fd, EPOLL_CTL_ADD, EPOLLIN);
}

void do_read(int epoll_fd, int fd, char *buff)
{
int ret = -1;
bzero(buff, BUFSIZ);
ret = read(fd, buff, BUFSIZ-1);
if(ret == 0 || !strncmp(buff, QUIT, strlen(QUIT)))
{
printf("a client quit.\n");
close(fd);
event_ctl(epoll_fd, fd, EPOLL_CTL_DEL, EPOLLIN);//delete_event in
return;
} if(ret < 0)
{
perror("read");
return;
}
printf("%s\n", buff);
event_ctl(epoll_fd, fd, EPOLL_CTL_MOD, EPOLLOUT);//modif_event out
} void do_write(int epoll_fd, int fd, char *buff)
{
int ret = -1;
ret = write(fd, buff, strlen(buff));
if(ret <= 0)
perror("write");
event_ctl(epoll_fd, fd, EPOLL_CTL_MOD, EPOLLIN);//modif_event in
}

/*
参考的资料中把他分成几个操作,更直观
这里为了方便多加一个参数,增加事件/删除事件/修改事件/,把增删改集成到一个函数。
*/
void event_ctl(int epoll_fd, int fd, int flag, int state)
{
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epoll_fd, flag, fd, &ev);
}

四、其它

触发模式

关于epoll的水平触发LT和边缘触发ET还没研究清楚,

应该是类似驱动程序中检测硬件信号中,高/低电平触发,上升沿/下降沿触发。

* !!FIXME

参考资料:

https://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html

https://blog.csdn.net/u011063112/article/details/81771440

https://blog.csdn.net/armlinuxww/article/details/92803381

网络编程学习——Linux epoll多路复用模型的更多相关文章

  1. Python网络编程:Linux epoll

    原文地址:http://scotdoyle.com/python-epoll-howto.html 介绍 Python已于2.6版本添加访问Linux epoll库的API.这篇教程使用Python ...

  2. UNIX网络编程学习指南--epoll函数

    epoll是select/poll的强化版,都是多路复用的函数,epoll有了很大的改进. epoll的功能 1.支持监听大数目的socket描述符 一个进程内,select能打开的fd是有限制的,有 ...

  3. Linux 网络编程的5种IO模型:多路复用(select/poll/epoll)

    Linux 网络编程的5种IO模型:多路复用(select/poll/epoll) 背景 我们在上一讲 Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO中,对于其中的 阻塞/非阻塞IO 进行了 ...

  4. Linux 网络编程的5种IO模型:信号驱动IO模型

    Linux 网络编程的5种IO模型:信号驱动IO模型 背景 上一讲 Linux 网络编程的5种IO模型:多路复用(select/poll/epoll) 我们讲解了多路复用等方面的知识,以及有关例程. ...

  5. Linux 网络编程的5种IO模型:异步IO模型

    Linux 网络编程的5种IO模型:异步IO模型 资料已经整理好,但是还有未竟之业:复习多路复用epoll 阅读例程, 异步IO 函数实现 背景 上一讲< Linux 网络编程的5种IO模型:信 ...

  6. Linux C++ 网络编程学习系列(1)——端口复用实现

    Linux C++ 网络编程学习系列(1)--端口复用实现 源码地址:https://github.com/whuwzp/linuxc/tree/master/portreuse 源码说明: serv ...

  7. 转 网络编程学习笔记一:Socket编程

    题外话 前几天和朋友聊天,朋友问我怎么最近不写博客了,一个是因为最近在忙着公司使用的一些控件的开发,浏览器兼容性搞死人:但主要是因为这段时间一直在看html5的东西,看到web socket时觉得很有 ...

  8. C,C++网络编程学习简明指南

    C,C++网络编程学习简明指南 1. 扎实的C,C++基础知识 参考资料<C程序设计>,<C++ primer>. 2. TCP/IP协议 经典书是:W.Richard Ste ...

  9. [转]Windows网络编程学习-面向连接的编程方式

    直接附上原文链接:windows 网络编程学习-面向连接的编程方式

随机推荐

  1. mysql使用group by分组时出现错误ERROR 1055 (42000): Expression #1 of SELECT list is not in GROUP BY clause and

    问题: 1055 - Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column ...

  2. vue+echarts可视化大屏,全国地图下钻,页面自适应

    之前写过一篇关于数据大屏及地图下钻的文章 https://www.cnblogs.com/weijiutao/p/13977011.html ,但是存在诸多问题,如地图边界线及行政区划老旧,无法自适应 ...

  3. centos7安装mysql(完整)

    安装包下载并上传到Linux系统中 官网5.7版本:https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-5.7.29-1.el7.x86_64.rpm-b ...

  4. Flask Web开发读书笔记

    开篇:目前想自学Flask Web开发--基于Python,找了几本书准备啃啃,同时也会分享读书笔记.希望和大家一起进步. Flask是小型框架,可以算是微框架,但是他的功能还是比较多 Flask有三 ...

  5. MySQL基础_索引

    MySQL 索引(入门): 一.介绍 1.什么是索引? 一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,在生产环境中,我们遇到最多的,也是最容易出问题的,还是一些 ...

  6. 如何使用Google Analytics Universal Analytics增强型电子商务

    Google Analytics: Universal Analytics增强型电子商务,可以让运营人员轻松地跟踪用户在其购物历程中与产品的互动,包括产品展示.产品点击.查看产品详情.将产品添加到购物 ...

  7. 盘点|2021年最受欢迎Linux桌面操作系统前十名

    镜像下载.域名解析.时间同步请点击 阿里云开源镜像站 阿里云开源镜像站利用云服务上的优势,提供快速.稳定的镜像分发服务.和免费的CDN加速服务.更新频率高,基本上一天一更新,对于Centos/Ubun ...

  8. J20航模遥控器开源项目系列教程(四)PCB打印 | 嘉立创PCB下单教程,5元顺丰包邮解君愁!

    我们的开源宗旨:自由 协调 开放 合作 共享 拥抱开源,丰富国内开源生态,开展多人运动,欢迎加入我们哈~ 和一群志同道合的人,做自己所热爱的事! 项目开源地址:https://github.com/J ...

  9. Vscode的使用小技巧

    命令行启动code 如果你的系统是Linux系统(我使用的是Ubuntu 16.04)这样就可以直接使用 code + filename来编辑文件(就像vi + filename) 如果你的系统是Ma ...

  10. lucence 内部结构是什么?

    面试官:想了解你的知识面的广度和深度. 解答: Lucene 是有索引和搜索的两个过程,包含索引创建,索引,搜索三个要点.可以 基于这个脉络展开一些. 最近面试一些公司,被问到的关于 Elastics ...