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的完成事件.
Reactor和Proactor事件处理模式
同步I/O模型常用于实现Reactor模式,异步I/O模型则用于实现Proactor模式.
Reactor模式
- 它要求主线程(I/O处理单元)只负责监听文件描述符上是否有事件发生,有的话就立即将事件通知工作线程(逻辑单元).
实现Reactor模式的工作流程是,以(epoll_wait为例) - 主线程往epoll内核事件表中注册socket上的读就绪事件.
- 主线程调用epoll_wait等待socket上有数据可读。
- 当socket上有数据可读时,epoll_wait通知主线程,主线程则将socket可读事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它从socket上读取数据,并处理客户端的请求,然后往epoll内核事件中注册该socket上的写事件.
- 主线程调用epoll_wait等待socket可写.
- 当socket可写时,epoll_wait通知主线程,主线程将socket可写事件放入请求队列。
- 睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果.
Proactor模式
- Proactor模式将所有的I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑.
使用异步I/O模型(以aio_read和aio_write为例)实现Proactor模式的工作流程是: - 主线程调用aio_read函数向内核注册socket上的读完成事件,并告诉内核用户缓冲区的位置,以及读操作完成时如何通知应用程序.
- 主线程继续处理其他逻辑.
- 当socket上的数据被读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据已经可用.
- 应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求,工作线程处理完客户请求之后,调用aio_write函数向内核注册socket上的写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成后如何通知应用程序。
- 主线程继续处理其它逻辑。
- 当用户缓冲区的数据被写入socket之后,内核将向应用程序发送一个信号,以通知应用程序数据已经发送完毕.
- 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket。
模拟Proactor模式
- 原理:主线程执行数据读写操作,读写完成后,主线程向工作线程通知这一”完成事件”.那么从工作线程的角度来看,它就直接获得了数据读写的结果.
使用同步(I/O模型)epoll_wait模拟出Proactor模式的工作流程如下: - 主线程往epoll内核事件表中注册socket上的读写就绪事件.
- 主线程调用epoll_wait等待socket上有数据可读.
- 当socket上有数据可读时,epoll_wait通知主线程,主线程从socket循环读取数据直到没有更多数据可读,然后将读取到的数据封装成一个请求对象插入请求队列.
- 睡眠在请求队列上的某个工作线程被唤醒,它获得请求对象并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件.
- 主线程调用epoll_wait等待socket可写.
- 当socket可写时,epoll_wait通知主线程,主线程往socket上写入服务器处理客户端请求的结果.
两种高效的并发模式
- 并发执行多个任务,适用于I/O密集型,不适用于计算密集型.
- 经常读写文件,访问数据库适用于并发,因为I/O操作速度远没有cpu的计算速度快,让程序阻塞于I/O操作将浪费大量的时间。
半同步和半异步
- 在I/O模型中,”同步”和”异步”是内核向应用程序通知的是何种I/O事件(就绪事件或完成事件),以及是应用程序还是内核来完成I/O读写.
- 在并发模式中,”同步”指的是程序完全按照代码序列的顺序执行,”异步”指的是程序的执行需要由系统事件来驱动,常见的系统事件包括中断、信号。
- 异步线程监听到客户请求后,就将其封装成请求对象并插入请求队列中,请求队列通知某个工作在同步模式的工作线程来读取并处理该请求对象.(具体选择哪个工作线程,取决于请求队列的设计,1.轮流选取工作线程算法,也可以通过条件变量或信号量)随机的选取一个工作线程.
半同步/半反应堆
* 主线程作为异步线程,如果监听socket上有可读事件,即有新的连接请求到来,主线程就接受以得到新的socket,然后往epoll内核缓冲区事件表中注册该socket上的读写事件,如果连接socket上有读写事件发生,,主线程就将该连接socket插入请求队列中,事件处理模式是Reactor模式.
半同步/半反应堆模式存在的缺点:
- 主线程和工作线程共享请求队列,主线程往请求队列中添加任务,或者工作线程从队列中取出任务,都需要对请求队列加锁保护.浪费了cpu。
- 每个工作线程在同一时间只能处理一个客户请求.
半同步/半异步模式
* 主线程只管理监听socket,连接socket由工作线程来管理,当有新的连接到来时,主线程就接受之并将新返回的连接派发给某个工作线程,此后该socket上的任何I/O操作都由被选中的工作线程来处理,直到客户关闭连接.
* 主线程向工作线程派发socket的最简单的方式,是往它和工作线程之间的管道里写数据,工作线程检测到管道上有数据可读时,就分析是否是一个新的客户连接请求,如果是,就将新的socket上的读写事件注册到自己的epoll内核事件中.
* 每个线程都是一个epoll.
领导者/追随者模式
指的是多个工作线程轮流获得事件源集合,轮流监听,分发并处理事件的一种模式,在任意时间,程序仅有一个领导线程,它负责监听I/O事件,其它追随者在线程池中休眠,如果检测到I/O事件,首先从线程池中推选出新的领导者线程.
领导者/追随者模式包含如下几个组件:句柄集、线程集、事件处理器,和具体的事件处理器.
1.句柄集:句柄用于表示I/O资源,也就是一个文件描述符,句柄集管理众多句柄,它使用wait_for_event方法来监听这些句柄上的I/O事件,并将其中的就绪事件通知给领导者线程,领导者则调用绑定Handle上的事件处理器来处理事件,领导者将Handle和事件处理器绑定是通过调用句柄集中的register_handle方法来实现.
2.线程集
这个组件是所有工作线程(包括领导者线程和追随者线程)的管理者.负责线程间的同步,以及新领导者线程的推选,以及新领带线程的推选,线程集中的线程任何时间都处于:
* Leader:线程当前处于领导者身份,负责等待句柄集上的I/O事件。
* Processing:线程正在处理事件.
* follower:线程当前处于追随者身份,等待成为新的领导.
事件处理器和具体的事件处理器
每个句柄上都绑定了一个回调函数,当这个句柄上有事件发生时,就调用这个回调函数处理,具体的事件处理器是事件处理器的派生类,它们重新实现基类的回调函数,处理特定的任务.
如何提高服务器性能
池
- 提高服务器性能一个很直接的方法就是以空间换时间,即“浪费”服务器的硬件资源,换取运行效率. 池是一组资源的集合,在服务器启动之初创建(静态资源),当开始处理客户请求时,直接从池中取,不需要动态分配.用完之后放回池中,不用释放资源.避免了服务器对内核的频繁访问.
- 内存池:用于socket的接收缓存和发送缓存.
- 进程池和线程池:用于服务器的并发.
- 连接池: 用于服务器或服务器机群的内部永久连接.比如服务器预先和数据库程序建立一组连接的集合,当某个逻辑单元需要访问数据库时,它可以直接从连接池是取一个连接的实体使用.完成数据库的访问之后,逻辑单元再将该连接返回给连接池.
数据复制
避免不必要的数据复制,尤其是当数据复制发生在用户代码和内核之间的时候,如果内核可以直接处理从socket或者文件读入的数据,则应用程序就没必要将这些数据从内核缓冲区复制到应用程序缓冲区.
上下文切换和锁
进程和线程的切换会导致系统开销,多线程服务器的一个优点是不同的线程可以运行在不同的CPU上.锁会导致服务器的效率低下.