从IO模型到epoll

引子

上一篇《从网卡到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使用不同的数据结构,后面再讲)

也就是说,应用程序读取数据的过程如下

  1. 向内核发起读取请求

  2. 内核将数据返回

讲到这里,IO模型就呼之欲出了

IO模型

同步阻塞式IO(blocking IO)

阻塞式IO模型.png

应用程序通过channelSocket向内核请求读取数据,内核发现此channelSocket没有新的数据,那么线程会阻塞,直到有数据返回

同步非阻塞式IO(noblocking IO)

非阻塞式IO模型.png

应用程序通过channelSocket向内核请求读取数据,内核检查如果没有数据,则直接返回,告诉应用程序“没有”

多路复用IO(multiplexing IO)

IO多路复用模型.png

应用程序有多个chanelSocket,把所有的chanelSocket的读取操作交给一个线程去完成

也就是把读取操作分为了两步,向内核查询可读取的socket,向内核执行读取

  1. 应用程序使用一个线程通过chanelSocket列表向内核发起读取操作,线程需要等待内核的响应,然后返回可读条件

  2. 应用程序根据可读条件向内核发起数据读取请求,此时内核再通过CPU或者DMA将数据拷贝到用户空间

可以看到,如果要支持多路复用,需要内核一次遍历多个ChanelSocket,并返回可读条件,这需要内核的支持

现代内核支持的select,poll,epoll模型都是基于多路复用的

异步IO (asynchronous IO)

异步IO模型.png
  1. 应用程序首先给内核发送一个读取信号,内核马上返回,表示收到

  2. 当有channelSocket的数据就绪时,内核根据注册的aio信号,直接把数据拷贝到用户空间,并返回aio注册时的标识

这种IO方式时完全异步,非阻塞的,但是需要内核更进一步的支持。内核收到aio信号后,需要监听对应的sockt.

也就是把数据就绪检查的工作交给了内核去实现

现在大多数linux内核都不支持AIO,不过Windows下的IOCP已支持AIO模型

主流IO模型

理论上最快的AIO并不被大多数linux内核所支持,因此使用最多的是 多路复用的IO模型

多路复用IO实际是

  1. 同步:调用内核需要的函数需要等待内核的响应

  2. 非阻塞的:第一次调用只查询就绪列表,而不用等待数据读取

所以要全面描述的话,叫做同步非阻塞多路复用IO

select &poll &epoll

之前有讲到,select,poll,epoll模型都是多路复用的IO模型,那么它们之间的区别在哪里呢?

多路复用实现的方式不同,具体的说,查询channelSocket就绪列表的实现方式不同

select /poll

  1. 应用程序首先调用内核接口,传入一个需要查询的chanelSocket列表

  2. 内核收到后调用请求,根据channelSocket列表去遍历内核中的socketChannel列表,逐个检查channelSocket是否可读。然后返回一个可读的chanelScoket列表给到应用程序

select 和poll的区别仅仅在于 :

  • select使用固定长度bitMap来保存socketChannel列表,固定BiteMap,能维护的sockChannel受限

  • poll使用动态数据来保存socketChannel列表

总的来说,有两点缺点

  1. chanelSocket列表需要做两次拷贝,第一次从用户态拷贝到内核态,第二此从内核态拷贝到用户态

  2. 每一次查询都需要遍历所有的channelSocket

epoll

针对select 和poll的问题,epoll做了以下两点改进

  1. 在内核中使用红黑树来维护channelSockt列表,增删改查的时间复杂度都为O(logn)

  2. epoll使用事件驱动的机制,当有channelSocket就绪时,通过回调函数将channelSocket加入到就绪列表中,而不需要遍历

epoll的事件驱动机制,不会随着监听的channeSockt增多而提高延时

什么是零拷贝

定义

这个词在现在大多数高性能架构中都能听到,而且很容易误解

先明确定义:

不需要 CPU 参与的数据拷贝才叫做零拷贝

CPU参与的拷贝

举例

  1. 当数据需要从内核态拷贝到用户态时,内核发起CPUI中断,cpu放下手中的事情响应中断请求,完成拷贝的操作

  2. 当数据从通过网卡检验,网卡缓冲,需要进入socket缓冲区时,网卡驱动发起中断,CPU响应中断,完成数据拷贝操作

  3. ....

可以看到,数据的转移需要拷贝,而拷贝需要CPU响应大量的中断来参与,有如下缺点

  1. CPU 响应中断,有大量的线程切换开销

  2. CPU参与数据拷贝,污染CPU高速缓冲,影响CPU的执行

  3. 数据拷贝非常简单,CPU的参与大材小用,无法发挥CPU高速计算的优势

那有没有什么东西可以分担CPU这项数据拷贝操作呢?

DMA参与的拷贝

DMA的全程叫做 direct memory access 直接存储器访问

可以将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器与存储器之间的高速数据传输

不需要CPU的参与,即可完成数据的拷贝操作

DMA在现代计算机中通常作为一个组件集成在CPU上

零拷贝

DMA不仅可以替代CPU的数据拷贝工作,因为可以直接在存储器之间高速数据传输,甚至还可以节省数据拷贝路径

如:一个请求静态资源的过程

DMA参与之前:

磁盘文件->页缓存->mmap到用户空间->socket缓冲区->网卡缓冲区

DMA参与之后: 磁盘文件->页缓存->mmap到用户空间->网卡缓冲区

总结

介绍了很多种IO模型,主流的在使用的时多路复用

介绍了很多种多路复用的实现方式,主流的是epoll

那介绍这么多的必要性有吗?

有,知其然,知其所以然

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容