高性能框架的基石-epoll

一、背景

1.程序员的三高

高并发,高性能,高可用

2.高性能框架

redis:为什么那么快?

netty:广泛使用的Java网络编程框架

dubbo:高性能的Java rpc框架

kafka,nginx

这一切基石,epoll。

概念:epoll是linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。---百度百科

 

二、前言

在了解epoll之前,先做几个知识的总结:

1.linux一切皆文件

linux的一切皆文件是指,在linux世界中,所有、任意、一切东西都可以通过文件的方式访问、管理。

串口是文件,内存是文件,usb是文件,进程信息是文件,网卡是文件,建立的每个网络通讯都是文件,蓝牙设备也是文件,等等等等。

所有外设都是文件,本质上就是说他们都支持用来访问文件的那些接口,可以被当做文件来访问。这个原理与子类都能当做基类访问是一样的,就是操作系统层面的oop思想。

2.OSI七层模型

高性能框架的基石-epoll_第1张图片

第一层:物理层,二进制传输,bit(比特流)第二层:数据链路层,介质访问,frame(帧)

第三层:网络层,确定地址和最佳路径,packet(包)

第四层:传输层,端到端连接,segment(段)

第五层:会话层,互连主机通信

第六层:表示层,数据表示

第七层:应用层,为应用程序提供网络服务

五至七层为节点传输,发送和接收消息。

3.中断

网卡会把接收到的数据写入内存,当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。

4.一个简单的C/S模式

高性能框架的基石-epoll_第2张图片

//创建

socket int s = socket(AF_INET, SOCK_STREAM, 0);    

//绑定 

bind(s, ...) 

//监听 listen(s, ...) 

//接受客户端连接 

int c = accept(s, ...) 

//接收客户端数据 

recv(c, ...)

//将数据打印出来 

printf(...)

5.同步/异步,阻塞非阻塞

同步与异步关注的是消息通信机制。

所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。就是由调用者主动等待这个调用的结果。

而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

 

三、select

在传统socket中,服务端要管理多个客户端连接,而accept、recv只能监视单个socket。

为了能同时监视多个socket的,首先出现了select这种方式。

select设计思想是,预先传入一个socket列表,如果列表中都没有数据,挂起进程,直到有一个socket

收到数据,唤起进程。

 

select流程:

1.如上图,程序同时监视sock1、sock2、sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中。

高性能框架的基石-epoll_第3张图片

2.当socket2收到数据后,中断程序将唤起进城A,进城A从所有的等待队列中移除,加入到工作队列。

3.当进城A被唤醒后,它知道知道有一个socket接收了数据。程序只需遍历一遍socket列表,就可以得到有序的socket。

缺点:

1.每次select调用都要将进程加入到所有监视socket的等待队列,每次唤醒都要从队列中移除。

这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。

2.进城被唤醒后,程序并不知道哪些socket收到数据,还需要再遍历一次

 

四、epoll

优化点:减少遍历,保存就绪socket

epoll概述

1.创建一个epoll对象,通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据

2.内核维护一个“就绪列表”,引用收到数据的socket,避免遍历

 

epoll三个函数

高性能框架的基石-epoll_第4张图片

1.epoll_create函数

epoll_create(int size)

该 函数生成一个epoll专用的文件描述符。它其实是在内核申请一空间,用来存放你想关注的socket fd上是否发生以及发生了什么事件。size就是你在这个epoll fd上能关注的最大socket fd数。

2.epoll_ctl函数

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) 

该函数用于控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。 参数: 

epfd:由 epoll_create 生成的epoll专用的文件描述符; 

op:要进行的操作例如注册事件,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除 

3.epoll_wati函数

int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout) 

该函数用于轮询I/O事件的发生; 参数: 

epfd:由epoll_create 生成的epoll专用的文件描述符; 

epoll_event:用于回传代处理事件的数组; 

maxevents:每次能处理的事件数; 

timeout:等待I/O事件发生的超时值(单位我也不太清楚);-1相当于阻塞,0相当于非阻塞。一般用-1即可 

返回发生事件数。

 

epoll工作流程:

1.如上图所示,当某个进程调用epoll_create方法时,内核会创建一个eventpoll对象。eventpoll对象也是文件系统中的一员,和socket一样,它也会有等待队列。

2.创建epoll对象后,可以用epoll_ctl添加或删除所要监听的socket。以添加socket为例,如上图,如果通过epoll_ctl添加sock1、sock2和sock3的监视,内核会将eventpoll添加到这三个socket的等待队列中。

3.当socket收到数据后,中断程序会操作eventpoll对象,而不是直接操作进程。

4.当socket收到数据后,中断程序会给eventpoll的“就绪列表”添加socket引用。如上图展示的是sock2和sock3收到数据后,中断程序让rdlist引用这两个socket。

同时,唤醒eventpoll等待队列中的进程,进城A再次进入运行状态,由于rdlist存在,进程A可以知道哪些socket发生了变化

高性能框架的基石-epoll_第5张图片
epoll中的数据结构:

高性能框架的基石-epoll_第6张图片

就绪列表的数据结构

就绪列表引用着就绪的socket,所以它应能够快速的插入数据。

程序可能随时调用epoll_ctl添加监视socket,也可能随时删除。当删除时,若该socket已经存放在就绪列表中,它也应该被移除。

所以就绪列表应是一种能够快速插入和删除的数据结构。双向链表就是这样一种数据结构,epoll使用双向链表来实现就绪队列(对应上图的rdllist)。

索引结构

epoll使用红黑树保存监视的socket。至少要方便的添加和移除,还要便于搜索,以避免重复添加。红黑树是一种自平衡二叉查找树,搜索、插入和删除时间复杂度都是O(log(N)),效率较好。

 

 

对比:

 

select

poll

epoll

操作方式

遍历

遍历

回调

底层实现

数组

链表

红黑树

IO效率

每次调用都进行线性遍历,时间复杂度为O(n)

每次调用都进行线性遍历,时间复杂度为O(n)

事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)

最大连接数

1024(x86)或2048(x64)

无上限

无上限

fd拷贝

每次调用select,都需要把fd集合从用户态拷贝到内核态

每次调用poll,都需要把fd集合从用户态拷贝到内核态

调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝

你可能感兴趣的:(应用服务器技术,epoll,IO)