DMA 使得I/O设备与内存之间的数据传输大大加快,且无需占用CPU。每个I/O设备都有自己的DMA控制器
用户和内核可以共享缓冲区,从而避免了用户和内核缓冲区间的一次拷贝,现在只需要三次数据拷贝,两次 DMA 一次 CPU。这通过 mmap
实现
另外,我们可以用一个系统调用发送文件:sendfile
,现在只需要两次上下文切换
若网卡支持 SG-DMA(scatter-gather)技术,则无需CPU搬运数据,整个过程只需要DMA进行数据的搬运
至此,整个过程需要2次数据搬运,以及2次上下文切换。一些利用此技术的项目包括:Kafka、Nginx。要求内核版本大于2.1
前文所述的内核缓冲区指页缓存,如果需要传输的文件就在页缓存中,则可以省去从磁盘拷贝数据的过程,即一次拷贝两次上下文切换。
但对于大文件,使用 page cache 是纯纯在浪费缓存
朴素方式:阻塞
可以使用异步 I/O以及 DMA 避免此期间的CPU事件浪费:
我们不把数据拷贝到页缓存,而是拷贝到用户的缓冲区。也就是不适用缓冲I/O,而是使用直接 I/O
直接I/O的应用场景通常为:大文件传输、应用程序自己实现了磁盘缓存。它无法享受内核的优化:预读 & I/O合并
需要为自己的 socket 绑定端口和ip,即绑定到特定的网卡上
在全连接队列不为空时,服务端的 listen 会返回,接着调用 accept 从全连接队列中取出一个 socket 返回应用程序
最大TCP连接数 = 客户端 IP 数 × 每个客户端端口数,但这实际上受服务器资源限制:文件描述符数量、系统内存大小、内存网卡性能、I/O模型
父进程负责监听,有握手好的 socket 时 fork 一个子进程去处理和客户端的连接 socket
此模式下,一个进程对应一个客户端;进程资源需要被在 wait 中回收。
如果有过多的客户端,那么会消耗大量的系统资源;且需要大量的上下文切换
思路与多进程模型类似,而同进程的线程的上下文切换开销更小、且与其他线程共享资源
线程在结束运行后可以被销毁,但也可以被放入一个线程池,省去销毁线程的开销
为啥没有进程池?进程之间是隔离的,监听的进程本身无法触碰其他进程
然而此模型仍需过多的线程对应客户端连接,上下文开销仍然有,且系统仍不一定能负担得起这些线程占用的系统资源
如此,我们需要一个线程负责处理多个客户端的连接。
为此,线程需要注册自己处理的 socket,在产生事件时,内核返回有事件的 socket 与事件,然后在用户态进行处理
有点像多个进程分时复用一个CPU
select 的实现方式是,用户将注册的 socket 位图拷贝到内核,内核通过 遍历 检查fd集合是否有事件发生,若有则标记位可读/可写,然后再拷贝到用户空间,用户通过遍历检查发生事件的socket**
poll 使用动态数组/链表形式组织所关注的 fd,突破了位图大小的限制,但仍是线性结构组织,需要大量的遍历以及拷贝
epoll 使用 红黑树 跟踪所有的 fd,其增删改的复杂度为 logn;且使用 事件驱动 的机制,它维护了一个就绪事件链表,返回时只复制发生/就绪的事件到用户空间
epoll 有两种事件触发模式,分别是 边缘触发 和 水平触发。有事件触发时边沿触发会唤醒一次,即便之后未被读取也不会再次唤醒;而水平触发只要缓冲区中有事件就会不断唤醒
select、poll 只支持水平触发,epoll 默认使用水平触发,但可切换为边缘触发
Reactor 模式也叫 Dispatcher 模式,即分配发生的事件给某个线程。在这种模式下,
reactor 和 处理资源池均可以有一个或多个,一般不采用 多 Reactor 单进程 / 线程 方案。可选的方案有三个:
使用线程还是进程看编程语言以及平台
C语言一般是单
单进程,Java一般是单单线程
进程中一般有 Reactor、Acceptor、Handler 三个对象,分别负责监听和分发事件、获取连接、处理事件
这种模式无需考虑进程间通信、多线/进程竞争。而若业务处理极度耗时,则会造成其他请求的延迟响应。且此模式无法充分利用多核CPU的性能
因此,此模式不适用于计算密集型场景,只适用于业务处理非常快速的场景
为了充分利用多核CPU的性能,我们可以使用单 Reactor 多线程 / 多进程模式
对于多线程模式:
此模式中,Handler 只负责读取和发送数据,而将业务处理交给处理线程池中的线程进行,在处理完成后将结果发送回原 Handler 对象,由原 Handler 进行数据发送
此方案可以充分利用多核CPU。但:
对于多进程模式,它的通信问题比多线程模式复杂得多,所以一般不考虑。
主线程中,主 reactor 仅负责监听、建立新连接,由子线程的从 reactor 负责后续的读写事件监听、以及业务处理
前面提到的 Reactor 是非阻塞同步网络模式,而 Proactor 是异步网络模式。异步模式不需要等待数据准备、以及数据拷贝过程,Reactor 的读写数据需要等待。换句话说:
在此模式下,需要在内核中注册 Proactor、Handler。在有新事件时,内核进程自动进行 Proactor 操作,并在数据读写完毕后,回调用户进程的 Handler,进行业务处理。
Linux 的异步I/O 并不是OS级别支持的,而是一个用户态库。Windows 实现了一套完整的支持 socket 的异步编程接口,这套接口就是 IOCP,是由操作系统级别实现的异步 I/O,真正意义上异步 I/O,因此在 Windows 里实现高性能网络程序可以使用效率更高的 Proactor 方案。