服务器程序的基本框架如上图所示,上图既能表示一台服务器,也能表示一个服务器集群。其中各模块的含义和功能如下表所示。
模块 | 单个服务器程序 | 服务器集群 |
---|---|---|
I/O 处理单元 | 处理客户连接,读写网络数据 | 作为接入服务器,实现负载均衡 |
逻辑单元 | 业务进程或线程 | 逻辑服务器 |
请求队列 | 各单元之间的通信方式 | 各服务器之间的永久TCP连接 |
网络存储单元 | 本地数据库、文件或缓存 | 数据库服务器 |
I/O 模型指的是程序处理 I/O 的模式。例如,对于阻塞 I/O,程序只有等到上一个 I/O 事件完成后,才能继续往下处理其他任务。
几种 I/O 模型的对比如下表所示:
I/O模型 | 读写操作和阻塞阶段 |
---|---|
阻塞 I/O | 会在读写操作时发生阻塞 |
I/O 复用 | 对 I/O 本身的读写操作是非阻塞的;会在 I/O 复用系统调用时发生阻塞,但可以同时监听多个 I/O 事件 |
SIGIO 信号 | 程序没有阻塞阶段;信号触发读写就绪事件,应用程序来处理读写操作; |
异步 I/O | 程序没有阻塞阶段;内核执行读写操作并触发读写完成事件; |
阻塞 I/O 是指,执行可能会发生阻塞的系统调用后,系统调用不能立即完成并返回,因此操作系统会将其挂起,直到等待的事件(写完成、读完成等)发生为止。
非阻塞 I/O 执行的系统调用总是立即返回,不管事件是否已经发生。如果事件没有立即发生,这些系统调用返回 -1,然后设置 errno,应用程序需要根据返回的 errno 进行相应的处理。
I/O 复用是指,应用程序通过 I/O 复用函数(select、poll、epoll)向内核注册一组事件,内核通过 I/O 复用函数把其中就绪的事件通知(发送)给应用程序,应用程序接收并处理这些就绪事件。
I/O复用函数本身是阻塞的,它能提高程序效率的原因是 I/O 复用函数具有同时监听多个 I/O 事件的能力。
SIGIO 是指通过信号来捕获 I/O 事件,
阻塞I/O、I/O复用和信号驱动I/O都是同步I/O模型,在这三种 I/O 模型中,I/O 的读写操作都是由应用程序完成的。而异步 I/O 模型是指,I/O 的读写操作由内核完成。
对于异步 I/O,应用程序将用户读写缓冲区的位置以及 I/O 操作完成之后内核通知应用程序的方式告知内核。即,同步 I/O 模型需要用户程序自行将数据从内核缓冲区读入到用户缓冲区;异步 I/O 模型由内核将数据从内核缓冲区移动到用户缓冲区。从事件通知的角度来看,同步 I/O 向应用程序通知的是 I/O 就绪事件(你快来将数据取走吧),异步 I/O 向应用程序通知的是 I/O 完成事件(我已经将数据发送给你了,你可以直接使用了)。
注意:在 I/O模型中,“同步”和“异步”的概念是指,内核向应用程序通知的是何种 I/O 事件(就绪事件 还是 完成事件)。
服务器程序通常要处理三类事件:I/O 事件、信号及定时事件。
事件处理模式是指,各种事件之间的协同处理关系。例如,对于服务端监听 socket 的主进程/主线程,当监听到一个客户端发来的连接请求时,是在主线程处理连接请求,还是使用一个子进程/子线程处理连接请求。可以根据不同的处理连接请求的方式,划分不同的事件处理模式。
下面介绍两种事件处理模式,Reactor模式和Proactor模式。Reactor模式通常使用同步模型实现,Proactor模式通常使用异步模型实现。
Reactor模式是指,一个主线程(也即上面服务器框架图中的I/O处理单元)只负责监听文件描述符上的是否有事件发生,而不处理事件;若有事件发生,则将发生的事件通知(转交给)工作线程(也即上面服务器框架图中的逻辑单元)处理。
工作流程如下:
epoll
内核事件表中注册 socket
(监视 socket
和连接 socket
) 上的读就绪事件;epoll_wait
等待 socket
上有数据可读;socket
上有数据可读时,epoll_wait
通知主线程,于是主线程将 socket
可读事件放入请求队列;socket
上读取数据,然后进行相应的处理;然后该工作线程往 epoll
内核事件表中注册 socket
(连接 socket
,此时应该是已经完成了TCP连接的建立) 上的写就绪事件。例如,如果是监听 socket
上有数据可读,则处理客户端的连接请求;如果是连接 socket
上有数据可读,则根据数据内容处理客户端相应的请求。epoll_wait
等待 socket
可写;epoll_wait
调用都是由主线程完成,因为 Reactor 模式下事件的发生都是由主线程来监视的;socket
可写时,epoll_wait
通知主线程,主线程将 socket
可写事件放入请求队列;socket
;注意:
Proactor模式是指,所有的 I/O 操作都交给主线程和内核来处理,工作线程只负责业务逻辑处理。
工作流程如下:
注意:
内核通过信号向应用程序报告的是连接 socket 上的读写事件。
游双的《Linux高性能服务器编程》中给出的案例如下。
使用同步 I/O 模拟Proactor模式的原理是,主线程执行数据的读写操作,读写完成之后,主线程向工作线程通知“读写完成”事件。因此工作线程就可以直接获得完成读写后的数据,直接对数据进行相应的逻辑操作。
使用同步 I/O 模型(以epoll_wait 为例)模拟的Proactor模式的工作流程如下:
思考
使用同步 I/O 模拟Proactor模式时,首先考虑是使用阻塞的 I/O 还是非阻塞的 I/O;考虑并发,应该首选的是使用非阻塞的 I/O。
其次,对于使用同步 I/O 模拟Proactor模式,主线程要负责所有的 I/O 读写,让并发量上来,主线程的处理能力会受到很大的影响,思考当并发量变大,在该模式下,如何提高主线程的并发处理能力。
个人的一个思路:使用多个主线程来处理数据的读写操作(这里就要考虑线程间的竞争关系了),然后使用一个类似于负载均衡的线程来管理这些主线程,即选择使用当前工作负荷小的主线程来处理读写事件。
并发模式是指,I/O 处理单元和多个逻辑单元之间协调完成任务的方法,以使得CPU能够被充分利用。
并发模式中,“同步”是指程序完全按照代码的逻辑顺序进行执行,“异步”是指程序的执行需要由系统事件来驱动。常见的系统事件包括中断、信号等。在 I/O 模型中,“同步”和“异步”的区分是内核向应用程序通知的是何种 I/O 事件(就绪事件还是完成事件),以及由谁来负责完成 I/O 的读写(应用程序还是内核)。
按照同步方式运行的线程称为同步线程,按照异步方式运行的线程称为异步线程。
对于半同步、半异步模式,服务端程序同时使用同步线程和异步线程来实现。在该模式中,同步线程用于处理客户逻辑,即服务器基本框架中的逻辑处理单元;异步线程用于处理 I/O 事件,即服务器基本框架中的 I/O 处理单元。
半同步、半异步模式的基本逻辑如下:
考虑事件处理模式和 I/O 模型,一种半同步/半异步模式为 半同步/半反应堆(half-sync / half-reactive),如下图所示。
半同步 / 半反应堆模式的工作流程如下图所示:
对于上述的半同步/半反应堆模式,事件处理采用的是反应堆模式(Reactor),若事件处理模式采用的 Proactor 模式,区别在于,主线程负责完成 socket 上数据的读写,这种情况下,主线程一般将应用程序数据、任务类型等信息封装为一个任务对象,然后将其(或者指向该任务的一个指针)插入请求队列,工作线程从请求队列中取得任务对象之后,即可直接处理。
半同步/半反应堆模式存在的缺点主要如下:
一种相对高效的半同步/半异步模式是,工作线程可以同时处理多个客户连接,工作流程如下:
该模式相对高效的原因在于,每个工作线程负责独立管理各自的内核事件,因此每个工作线程可以独立的处理多个连接socket。需要注意的是,主线程和工作线程都是以异步模式进行工作的(事件驱动),因此并非严格意义上的半同步/半异步模式。
领导者/追随者模式的思想是,多个线程轮流监听、分发处理事件的一种模式。
更具体地,在线程池中,任意时刻都仅有一个线程作为领导者线程,负责监听 I/O 事件,而其他线程作为追随者线程;当领导者线程监听到 I/O 事件,领导者线程首先从其他线程中推选出一个线程作为新的领导者线程,然后新的领导者线程负责继续监听 I/O 事件,而原来的领导者变成追随者,并处理该 I/O 事件。
领导者线程监听到 I/O 事件后,也可以继续充当领导者,而指定某一个追随者线程来处理监听到的 I/O 事件,这样就有点退化为了上述的半同步半异步模式了。
《Linux高性能服务器编程》中给出的领导者/追随者模式中,主要包含三个组件:句柄集(HandleSet)、线程集(ThreadSet)和事件处理器(EventHandler、包括具体的事件处理器(ConcreteEventHandler))。
句柄集(HandleSet)
句柄集负责管理一组 I/O 资源,一个句柄(Handler)通常为 Linux 下的一个文件描述符。句柄集负责监听其所管理的文件描述符上的 I/O 事件(调用某个方法让领导者线程负责监听文件描述符上的 I/O 事件)和文件描述符上对应的事件处理器的绑定和解绑操作。
线程集
线程集负责管理所有工作线程(领导者线程和追随者线程),包括新的领导者线程的推选和线程之间的同步。
事件处理器(具体的事件处理器)
事件处理器通常包含一个或多个回调函数,用于处理对应事件的具体业务逻辑。事件处理器需要事先绑定在句柄上(某个文件描述符上),当某个文件描述符上的某个事件发生后,就执行与之绑定的事件处理器中的回调函数(由线程集决定是由原来的领导者执行还是指定一个追随者来执行)。
优缺点