本文目录
一、IO多路复用第二版(epoll)
从主动轮询转变为被动通知,一定程度上提升了性能,但是select和poll每次调用都需要拷贝管理的全量的fd到内核态
,(每次调用和管理的时候都需要拷贝全量的fd到内核态,然后就绪之后又得拷贝到用户态,最后上层应用判断的时候还要挨个进行判断哪个客户端就绪了),导致影响性能。
所以可以针对拷贝
还有模糊通知
改进成不拷贝
与明确通知
,这就是两个思路,也就是调用和返回的时候做一个改进。
二、epoll三大核心接口
1、epoll_create()
#include<sys/epoll.h>
int epoll_create(int size)
epoll_create()
创建返回的epollfd指向内核中的一个epoll实例
,同时该epollfd
用来调用所有和epoll相关的接口(epoll_ctl()、epoll_wait()
)。成功返回时,返回大于0的epollfd,失败时返回-1。需要根据errnum查看错误。
从linux2.6.8之后,size参数已经被忽略了,但必须要大于0.
当epollfd不再使用需要调用close()
关闭,当所有指向epoll的文件描述符关闭之后,内核会摧毁该epoll实例并且释放和其关联的资源。
2、epoll_ctl()
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:通过 epoll_create() 创建的 epoll 文件描述符(epollfd)。
op:操作类型,可以是以下三种之一:
EPOLL_CTL_ADD:添加一个文件描述符到 epoll 实例中。
EPOLL_CTL_MOD:修改已经添加到 epoll 实例中的文件描述符的监听事件。
EPOLL_CTL_DEL:从 epoll 实例中删除一个文件描述符。
fd:待监听的文件描述符。
event:指向 epoll_event 结构的指针,指定要监听的事件(如读、写、接受连接等)。
一言以蔽之,就是将哪个客户端(fd)的哪些事件(event)交给哪个 epoll(epfd)来管理(op:增删改)。
当一个客户端与server连接之后,就需要把fd添加到epoll中,这个时候对应的就是添加
的操作。处理好请求之后,发送数据给客户端,就需要把读事件更新为写事件,这就是更新
。
3、epoll_wait()
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:通过 epoll_create() 创建的 epoll 文件描述符。
events:返回就绪的事件列表,列表中的事件数量通过 epoll_wait() 的返回值传递。
maxevents:最多返回的事件数量,该值用来告诉内核创建的 events 数组有多大。
timeout:超时时间,-1 表示无限期等待,0 表示立即返回,timeout > 0 表示正常超时时间(单位为毫秒)。
返回值cnt:返回就绪的事件数量。0 表示在超时时间内没有就绪的事件列表;大于 0 表示返回就绪列表的个数(后续通过循环遍历 events[0] 到 events[cnt-1]);-1 表示错误,通过 errno 来识别具体错误信息。
4、epoll简单实例
5、epoll的ET模式和LT模式
6、epoll内核实现
首先,epoll_create()
函数在内核中分配一段空间,并初始化管理监听描述符的数据结构,包括红黑树和就绪事件链表。红黑树用于存储用户空间中感兴趣的文件描述符(fd)及其关联的事件(events),而就绪事件链表则用于存储内核空间中已经准备好进行I/O操作的事件。用户空间通过epoll_create()获得一个文件描述符,该描述符引用了内核空间中的epoll实例。
接下来,epoll_ctl()函数
提供了对红黑树的增删改接口,允许上层用户对感兴趣的文件描述符进行操作。具体来说,EPOLL_CTL_ADD用于添加新的文件描述符到红黑树中,EPOLL_CTL_MOD用于更新已经添加的文件描述符的监听事件,而EPOLL_CTL_DEL则用于从红黑树中删除文件描述符。这些操作会修改红黑树中的数据,从而影响后续的事件监听。
然后,epoll_wait()函数
用于从就绪事件链表中获取已经准备好进行I/O操作的事件,并将这些事件的文件描述符和关联的事件信息填充到用户空间提供的events数组中,然后返回给上层用户。这个过程是阻塞的,直到有就绪事件可用或者超时。
最后,就绪事件迁移是epoll机制的核心部分。当内核监听到某个文件描述符上有I/O事件发生时,会将该事件从红黑树中迁移一份到就绪事件链表中。这样,epoll_wait()函数就可以从就绪事件链表中获取这些事件,而不需要每次都遍历整个红黑树,从而大大提高了事件处理的效率。
三、异步IO
异步I/O是一种I/O操作模式,在这种模式下,应用程序发起I/O请求后可以继续执行,而不需要等待I/O操作完成。当I/O操作完成时,内核会通过某种机制(如信号)通知应用程序。
Linux中的异步IO是io_uring(linux5.1),windows中的异步IO是IOCP。
那么select/poll/epoll是同步还是异步IO?
本质上都是同步IO,因为在第二阶段(数据的拷贝), 从内核态到用户态数据的一个拷贝的过程,因为这个过程是由用户线程完成的,必须等待数据从内核缓冲区拷贝到用户空间,就算作是阻塞,(如果是由内核线程完成的,就是异步IO)。
也就是说在数据从内核缓冲区拷贝到用户空间的过程中,用户线程是被阻塞的。
尽管select、poll和epoll可以在不阻塞的情况下监视多个文件描述符,但当某个文件描述符准备好进行I/O操作时,用户线程仍然需要执行阻塞的系统调用来实际读取数据。在数据从内核缓冲区拷贝到用户空间的过程中,用户线程是被阻塞的,因此这些模型本质上是同步I/O。
Linux中的aio系列函数(如aio_read)是真正的异步I/O模型。在这些模型中,用户线程发起I/O请求后,立即可以继续执行其他任务,而不需要等待I/O操作完成。数据从内核缓冲区拷贝到用户空间的过程由内核线程异步完成,用户线程不会被阻塞。
四、Linux惊群效应与c10K问题
Linux惊群效应(Thundering Herd Problem)是指当多个进程或线程同时等待同一个事件时,如果该事件发生,所有等待的进程或线程都会被唤醒,但最终只有一个能够获得资源或锁,对事件进行处理,其余的进程或线程则需要重新进入等待状态。这种现象会导致大量的上下文切换和CPU资源的浪费,从而影响系统性能。惊群效应在服务器的监听等待调用中尤为常见,例如在多个进程中调用accept或epoll_wait等待客户端连接时。
C10K问题关注的是服务器处理大量并发连接的能力。"C"代表客户端(Clients),"10K"指的是大约10000个并发客户端连接。这个问题主要涉及到硬件资源、操作系统限制、应用性能和网络延迟等因素。随着互联网的发展,类似挑战已经扩展至C1M或者C10M。C10K问题不仅涉及到技术层面的挑战,还涉及到如何优化应用性能和调整操作系统参数以适应大规模并发连接的需求。尽管现代硬件和软件的发展已经使得处理大量并发连接成为可能,但随着互联网流量的不断增长,类似的挑战仍然存在。
五、主流网络模型介绍
1、基于Thread-based的架构模型
这个模型也叫做基于线程架构的模型。
2、Reator模型
Reactor模型是一种事件驱动的设计模式,主要用于处理服务端的多个并发请求。它通过事件分离和事件多路复用机制,实现了高效的事件处理。Reactor模型中定义的三种角色:Reactor负责监听和分配事件,将I/O事件分发给相应的处理器进行处理。其核心思想是将I/O事件的监听和实际的I/O操作分离开来,由事件循环(Event Loop)负责监听I/O事件,当事件发生时,将事件分发给相应的事件处理器(Event Handler)进行处理。
Reactor模型的工作原理可以分为以下几个步骤:事件注册、事件等待、事件分发和事件处理。在事件注册阶段,应用程序将感兴趣的I/O事件(如读、写事件)注册到事件多路分离器中。事件等待阶段,事件循环调用事件多路分离器的等待函数,阻塞等待I/O事件的发生。事件分发阶段,一旦有I/O事件发生,事件循环将事件分发给相应的事件处理器。最后,在事件处理阶段,事件处理器处理具体的I/O事件,如读取数据、处理业务逻辑、发送响应等。
Reactor模型的实现方式有多种,包括单线程Reactor模型、多线程Reactor模型和主从Reactor模型(Master-Slave Reactor)。单线程模型中,所有的I/O事件和业务逻辑都在一个线程中处理,适用于I/O密集型任务,业务处理时间较短的场景。多线程模型中,I/O事件和业务逻辑由不同的线程处理,事件循环线程负责监听I/O事件,并将事件分发给工作线程处理业务逻辑,适用于I/O和业务处理均较密集的场景,能够充分利用多核CPU的优势。主从Reactor模型中,主线程(Master)负责监听I/O事件,并将事件分发给从线程(Slave)处理,从线程池(Slave Pool)中的多个线程负责处理具体的I/O事件和业务逻辑,适用于高并发、大量连接的场景,进一步提升性能和可扩展性。
Single-Reactor单线程模型
一个epoll对象可以称为一个reactor。
单线程主要针对IO操作而言,也就是IO中的accept、read、write都是在一个线程完成的。
但是这个模型存在一定的问题,目前该模型中,除了IO操作在Reactor线程之外,业务逻辑处理操作也在Reactor线程上,当业务处理逻辑比较耗时,会大大降低IO请求的处理效率。
典型实现就是Redis(4.0之前)。
随后就演变而来一个Single-Reactor线程池模型。
IO线程处理IO的操作,工作线程处理工作的业务逻辑。
也就是引入线程池,专门处理业务逻辑操作,提升IO响应的速度。但是管理百万连接、高并发大数据的时候,单个Reactor线程还是吃不消,效率会比较低下。
Multi-Reactor多线程模型
引入多个Reactor线程,也就是主从结构。Main主要负责接受客户端连接,Main按照一定负载均衡的策略分发给Sub进行处理,Sub负责处理。
后面又延伸了多种拓展模式:单进程(多线程)模式、多进程模式。
多线程典型实现是Netty、Memcached。
多进程模式典型实现就是Nginx。