高并发,高性能,高可用
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七层模型
第一层:物理层,二进制传输,bit(比特流)第二层:数据链路层,介质访问,frame(帧)
第三层:网络层,确定地址和最佳路径,packet(包)
第四层:传输层,端到端连接,segment(段)
第五层:会话层,互连主机通信
第六层:表示层,数据表示
第七层:应用层,为应用程序提供网络服务
五至七层为节点传输,发送和接收消息。
3.中断
网卡会把接收到的数据写入内存,当网卡把数据写入到内存后,网卡向cpu发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
4.一个简单的C/S模式
//创建
socket int s = socket(AF_INET, SOCK_STREAM, 0);
//绑定
bind(s, ...)
//监听 listen(s, ...)
//接受客户端连接
int c = accept(s, ...)
//接收客户端数据
recv(c, ...)
//将数据打印出来
printf(...)
5.同步/异步,阻塞非阻塞
同步与异步关注的是消息通信机制。
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。就是由调用者主动等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
在传统socket中,服务端要管理多个客户端连接,而accept、recv只能监视单个socket。
为了能同时监视多个socket的,首先出现了select这种方式。
select设计思想是,预先传入一个socket列表,如果列表中都没有数据,挂起进程,直到有一个socket
收到数据,唤起进程。
select流程:
1.如上图,程序同时监视sock1、sock2、sock3三个socket,那么在调用select之后,操作系统把进程A分别加入这三个socket的等待队列中。
2.当socket2收到数据后,中断程序将唤起进城A,进城A从所有的等待队列中移除,加入到工作队列。
3.当进城A被唤醒后,它知道知道有一个socket接收了数据。程序只需遍历一遍socket列表,就可以得到有序的socket。
缺点:
1.每次select调用都要将进程加入到所有监视socket的等待队列,每次唤醒都要从队列中移除。
这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。
2.进城被唤醒后,程序并不知道哪些socket收到数据,还需要再遍历一次
优化点:减少遍历,保存就绪socket
epoll概述
1.创建一个epoll对象,通过epoll_ctl将需要监视的socket添加到epfd中,最后调用epoll_wait等待数据
2.内核维护一个“就绪列表”,引用收到数据的socket,避免遍历
epoll三个函数
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发生了变化
就绪列表的数据结构
就绪列表引用着就绪的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不拷贝 |