引子
上一篇《从网卡到TCP/IP协议栈的数据流转》,描述了数据从网卡到应用程序的流转过程
应用程序并不直接同内核交互来传递数据,而是通过缓冲区
如果是网络数据缓冲区,那么使用socket。如果是文件缓冲区,那么使用句柄
当网卡接收到数据之后,内核会将数据拷贝到与四元组连接对应的socket缓冲区
那么应用程序如何感知到数据到达呢?
能不能直接读取socket缓冲区中的数据呢?
横梗在应用程序空间与内核空间之间的屏障
linux使用虚拟内存机制,应用进程维护了自己的虚拟内存地址空间,内核也维护了自己的内存地址空间,
应用程序不能直接通过指针来访问到内核地址空间之中的数据,数据的交互需要内核的参与
注:内核可以使用CPU,也可使用DMA。NIO中的零拷贝,就是不通过CPU而是通过DMA拷贝数据。关于零拷贝,后面再讲
应用程序与内核的IO交互
回顾socket监听代码
ServerSocket monitorSocket =new ServerSocket();
//绑定端口,指定需要监听的端口
monitorSocket.bind(8080);
//在此端口上开启监听
//通知内核,此socket需要监听8080端口,实际上是将监听socket信息注册到了内核维护的一个监听列表
//当有客户端与服务端握手时,服务端会先收到syn报文,然后去检查对用的端口上有没有应用程序在监听
//如果用应用程序在监听,才可能(还需要做其它检查)返回ack报文,否则返回rst报文,拒绝连接
monitorSocket.listen();
//通过监听socket调用accept函数,返回一个已经创建好连接的socket
//对应三次握手中的第三次握手,客户端返回ack之后,服务端初始化socket,并创建缓冲区
//内核处理tcp连接时,维护了两个队列,一个是正在握手的syn队列,一个是已经握手完成的accept队列
//accept函数就是从accept队列中获取一个已握手完成的tcp连接所对应的socket
Socket channelSocket = socket.accept();
//读取连接socket上的数据
channelSocket.read();
当应用程序获取到一个channelSocket后,需要不断地去读取socket上的数据
read()函数就会触发向内核询问的操作,因为应用程序不能直接判断socket上是否有数据到达,需要通过内核
内核空间中维护了一个数据结构(select,poll,epoll使用不同的数据结构,后面再讲)
也就是说,应用程序读取数据的过程如下
向内核发起读取请求
内核将数据返回
讲到这里,IO模型就呼之欲出了
IO模型
同步阻塞式IO(blocking IO)
应用程序通过channelSocket向内核请求读取数据,内核发现此channelSocket没有新的数据,那么线程会阻塞,直到有数据返回
同步非阻塞式IO(noblocking IO)
应用程序通过channelSocket向内核请求读取数据,内核检查如果没有数据,则直接返回,告诉应用程序“没有”
多路复用IO(multiplexing IO)
应用程序有多个chanelSocket,把所有的chanelSocket的读取操作交给一个线程去完成
也就是把读取操作分为了两步,向内核查询可读取的socket,向内核执行读取
应用程序使用一个线程通过chanelSocket列表向内核发起读取操作,线程需要等待内核的响应,然后返回可读条件
应用程序根据可读条件向内核发起数据读取请求,此时内核再通过CPU或者DMA将数据拷贝到用户空间
可以看到,如果要支持多路复用,需要内核一次遍历多个ChanelSocket,并返回可读条件,这需要内核的支持
现代内核支持的select,poll,epoll模型都是基于多路复用的
异步IO (asynchronous IO)
应用程序首先给内核发送一个读取信号,内核马上返回,表示收到
当有channelSocket的数据就绪时,内核根据注册的aio信号,直接把数据拷贝到用户空间,并返回aio注册时的标识
这种IO方式时完全异步,非阻塞的,但是需要内核更进一步的支持。内核收到aio信号后,需要监听对应的sockt.
也就是把数据就绪检查的工作交给了内核去实现
现在大多数linux内核都不支持AIO,不过Windows下的IOCP已支持AIO模型
主流IO模型
理论上最快的AIO并不被大多数linux内核所支持,因此使用最多的是 多路复用的IO模型
多路复用IO实际是
同步:调用内核需要的函数需要等待内核的响应
非阻塞的:第一次调用只查询就绪列表,而不用等待数据读取
所以要全面描述的话,叫做同步非阻塞多路复用IO
select &poll &epoll
之前有讲到,select,poll,epoll模型都是多路复用的IO模型,那么它们之间的区别在哪里呢?
多路复用实现的方式不同,具体的说,查询channelSocket就绪列表的实现方式不同
select /poll
应用程序首先调用内核接口,传入一个需要查询的chanelSocket列表
内核收到后调用请求,根据channelSocket列表去遍历内核中的socketChannel列表,逐个检查channelSocket是否可读。然后返回一个可读的chanelScoket列表给到应用程序
select 和poll的区别仅仅在于 :
select使用固定长度bitMap来保存socketChannel列表,固定BiteMap,能维护的sockChannel受限
poll使用动态数据来保存socketChannel列表
总的来说,有两点缺点
chanelSocket列表需要做两次拷贝,第一次从用户态拷贝到内核态,第二此从内核态拷贝到用户态
每一次查询都需要遍历所有的channelSocket
epoll
针对select 和poll的问题,epoll做了以下两点改进
在内核中使用红黑树来维护channelSockt列表,增删改查的时间复杂度都为O(logn)
epoll使用事件驱动的机制,当有channelSocket就绪时,通过回调函数将channelSocket加入到就绪列表中,而不需要遍历
epoll的事件驱动机制,不会随着监听的channeSockt增多而提高延时
什么是零拷贝
定义
这个词在现在大多数高性能架构中都能听到,而且很容易误解
先明确定义:
不需要 CPU 参与的数据拷贝才叫做零拷贝
CPU参与的拷贝
举例
当数据需要从内核态拷贝到用户态时,内核发起CPUI中断,cpu放下手中的事情响应中断请求,完成拷贝的操作
当数据从通过网卡检验,网卡缓冲,需要进入socket缓冲区时,网卡驱动发起中断,CPU响应中断,完成数据拷贝操作
....
可以看到,数据的转移需要拷贝,而拷贝需要CPU响应大量的中断来参与,有如下缺点
CPU 响应中断,有大量的线程切换开销
CPU参与数据拷贝,污染CPU高速缓冲,影响CPU的执行
数据拷贝非常简单,CPU的参与大材小用,无法发挥CPU高速计算的优势
那有没有什么东西可以分担CPU这项数据拷贝操作呢?
DMA参与的拷贝
DMA的全程叫做 direct memory access 直接存储器访问
可以将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器与存储器之间的高速数据传输
不需要CPU的参与,即可完成数据的拷贝操作
DMA在现代计算机中通常作为一个组件集成在CPU上
零拷贝
DMA不仅可以替代CPU的数据拷贝工作,因为可以直接在存储器之间高速数据传输,甚至还可以节省数据拷贝路径
如:一个请求静态资源的过程
DMA参与之前:
磁盘文件->页缓存->mmap到用户空间->socket缓冲区->网卡缓冲区
DMA参与之后: 磁盘文件->页缓存->mmap到用户空间->网卡缓冲区
总结
介绍了很多种IO模型,主流的在使用的时多路复用
介绍了很多种多路复用的实现方式,主流的是epoll
那介绍这么多的必要性有吗?
有,知其然,知其所以然