目录
1 零拷贝技术
1.1 传统的文件传输方式
传统的网络文件传输流程如下:
-
将磁盘中的数据拷贝到内核缓冲区中(DMA拷贝)
-
将内核缓冲区中的数据拷贝到用户空间中(CPU拷贝)
-
将数据从用户空间再拷贝到socket缓冲区中(CPU拷贝)
-
将socket中的数据拷贝到网卡中(DMA拷贝)
前面两次过程涉及到一次read()系统调用,后面两次过程涉及到一次write()系统调用,整个过程共有两次系统调用,两次CPU拷贝和两次DMA拷贝,可见性能是非常差的,所以需要对传统的文件传输方式进行优化
1.2 mmap()
所谓的mmap技术就是将read()系统调用换成了mmap()系统调用,所谓的mmap()函数就是将用户空间与内核空间的缓冲区做了一个直接映射,这样我们的数据就可以省去一次CPU拷贝(read系统调用中的内核缓冲区->用户空间数据)这种网络传输模式还是涉及到两次系统调用,分别是mmap()和write()。具体流程如下:
-
将磁盘中的数据拷贝到内核缓冲区中(DMA拷贝)
-
将内核缓冲区中的数据拷贝到socket缓冲区(CPU拷贝)
-
将socket缓冲区的数据拷贝到网卡中(DMA拷贝)
可见性能相对于传统的网络IO有了减少一次CPU拷贝的提升
1.3 sendfile()
即使使用了mmap()技术我们还是涉及到两次系统调用,所以则是就有了sendfile()函数,调用一次该函数就能够完成一次网络传输过程,具体流程如下:
-
将磁盘中的数据拷贝到内核缓冲区中(DMA拷贝)
-
将内核缓冲区中的数据拷贝到socket缓冲区(CPU拷贝)
-
将socket缓冲区的数据拷贝到网卡中(DMA拷贝)
数据的拷贝过程和mmap()+write()是一致的,但是只有一次系统调用了,相应的,在这个基础上,支持SG-DMA技术的网卡能够再进一步提升IO性能,过程如下:这也是真正的零拷贝技术
-
将磁盘中的数据拷贝到内核缓冲区中(DMA拷贝)
-
将socket缓冲区的数据拷贝到网卡中(SG-DMA拷贝)
2 IO多路复用(select/poll/epoll)
2.1 传统的基于阻塞IO的多进程/所线程模型
传统的socket编程我们的服务器每接受到一个请求都会建立socket连接然后将其交给一个线程或者进程来处理,传统的socket编程就是基于阻塞IO实现的,阻塞IO的实现过程如下:
-
用户线程发起read系统调用,用户线程此时阻塞
-
内核准备数据
-
内核将数据拷贝到用户进程
-
拷贝完毕,read系统调用返回
整个过程我们的用户线程都是阻塞的,阻塞IO网络socket编程的实现过程如下:
-
服务端绑定端口监听
-
客户端发起连接请求
-
服务端将与客户端建立好的socket放入到一个队列中
-
当服务端调用accept函数的时候就会创建一个线程成接受这个socket并处理客户端的读写(read和write)请求
存在的问题:
-
当请求量非常多时需要创建很多的线程,这样线程上下文切换的开销非常大
-
通过线程池创建线程会导致无法同时处理大量的请求
2.2 IO多路复用
由于传统的socket编程我们每要处理一个socket就得创建一个线程与之对应,所以人们想到能否让一个线程监听多个socket,一个线程处理多个socket的请求,这就是我们的IO多路复用,这里说明一下,多路是说多个TCP连接,或者说多个socket,复用指的是多个socket被一个或者一个线程组来处理。IO多路复用涉及到三个函数,分别是select,poll和epoll,介绍如下:
select/poll
select IO多路复用的步骤如下:
-
将已连接的socket放到一个文件描述符集合中,然后将其拷贝到内核中,由内核来帮我们监听这些socket
-
内核监听socket的方式非常简单,直接遍历真个文件描述符集合,将发生网络事件的socket进行读写标记
-
内核将标记后的socket文件描述符集合拷贝到用户空间
-
用户程序对socket文件描述符集合进行再次遍历,处理有读写标记的socket
select和poll的区别:
二者的主要区别就是对于socket文件描述符集合的存储方式上,select是将文件描述符存储在一个固定大小的BitMap中,所以文件描述符集合的大小有限制;poll将文件描述符存储在一个动态数组中,底层是一个链表,也就解决了select函数socket文件描述符集合有大小限制的问题;
epoll
epoll(epoll对象通过epoll_create()函数创建)不再采用线性结构来组织socket文件描述符集合,而是将socket文件描述符通过红黑树组织起来(epoll_ctl()函数),当某个socket上有事件发生时,会将该socket取出后放到一个链表中,然后用户再通过epoll_wait()系统调用就可以得到这个就绪事件链表然后处理事件即可,这样就避免了和select/poll一样拷贝全部socket文件描述符集合并进行遍历,大大提高了事件处理效率。
事件触发机制的不同
select/poll只支持水平触发,epoll还支持边缘触发
水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了,使用边缘出触发时我们必须保证一次读取数据时就全部读取完毕。
3 reactor模式
传统的IO多路复用接口编程都是面向过程的方式,为了简化编程,所以基于面向对象的思想创建了reactor模式,这个模式有三个主要类型:
-
单reactor单进程/线程 redis
-
reactor通过selectIO多路复用监听事件
-
监听到事件后将事件进行dispatch分发
-
连接请求的时间分配给acceptor处理并创建一个handler处理后序事件;其他请求则直接分配给handler
-
handler通过read -> 业务处理 -> send的方式来完成业务流程
-
-
单reactor多线程
-
reactor通过select监听事件
-
发生事件后进行dispatch分发
-
将连接请求交给acceptor来处理;其他请求直接交给我们的handler处理
-
handler会read -> 创建一个processor线程处理业务逻辑 -> processor处理完后handler进行send
-
-
多reactor多线程 netty
-
mainReactor通过select监听事件
-
监听到事件后将请求dispatch
-
连接请求交由acceptor,并将建立好的连接分配给subReactor
-
subReactor会对这些连接进行监听,有事件发生则dispatch给handler进行处理
-
handler通过read -> 业务处理 -> send的方式处理业务逻辑
-