面试:网络/IO

IO基础知识

无论是读磁盘还是读网络,都涉及到两个过程:

  1. 从磁盘/网卡,到 内核缓冲区
  2. 从内核缓存中,到 应用层内存(设计到系统调用和上下文切换)

以当前典型的DMA控制器为例,可以用下图描述:从磁盘读,并写到网卡涉及到2次系统调用(共四次上下文切换)、2次cpu拷贝(内核空间和用户空间之间)、2次DMA拷贝(外设和内核空间之间)

有DMA控制器时,从硬件外设复制数据到内核缓冲区是由DMA控制器执行的,不用CPU耗费大量时间,见图中的蓝色部分。

IO模型

5种IO模型就是如何进行上述的两个过程,所以理解IO模型的前提是理解IO的两个阶段。IO模型一共有5种

  • 阻塞式IO:两个阶段都阻塞,代表:java BIO
  • 非阻塞式IO:第一个阶段轮训是否完成,第二个阶段阻塞,效率很低,无使用
  • IO复用(事件驱动IO):epoll、select。两个阶段都阻塞,但是第一个阶段使用一个线程处理多个fd。
  • 信号驱动IO:第一个阶段完成时,用信号通知进程
  • 异步IO:两个阶段都异步

零拷贝(描述的过程都是从文件读,并写到socket的过程)

深入剖析Linux IO原理和几种零拷贝机制的实现

传统模式下有2次系统调用(read+write)(共四次上下文切换)、2次cpu拷贝(内核空间和用户空间之间)、2次DMA拷贝(外设和内核空间之间)。快速记忆:三个二

2次DMA拷贝是避免不了的,想办法减少系统调用和cpu拷贝就是零拷贝技术。

用户态直接IO

linux中默认是缓存IO,即先写入page cache,再fsync到磁盘中;读文件先读page cache,没有在读磁盘。缓存IO就涉及到我们上面说的两个过程:1. 从磁盘到内核缓冲区;2. 从内核缓冲区到用户态。像kafka、redis的aof都用到了page cache来加速磁盘读写。但是对于某些应用例如数据库管理系统,就不想用默认的page cache,而是需要自己来实现cache。他们就会使用用户态直接IO。

用户态直接IO的特征是open文件时模式带有o_direct的tag。避免了用户态和内核态之间的cpu拷贝。

mmap + write

用户态和内核态之间进行映射,从而避免内核态和用户态之间的一次拷贝。

两次系统调用(mmap+write),1次cpu拷贝,2次DMA拷贝。减少了一次CPU拷贝

sendfile

一次系统调用、一次cpu拷贝、2次DMA拷贝。减少一次系统调用、一次cpu拷贝

sendfile + DMA gather copy

在sendfile的基础上,再减少一次CPU拷贝,真正的0次拷贝。一次系统调用、0次cpu拷贝、2次DMA拷贝。

需要硬件支持。减少一次系统调用,两次cpu拷贝

splice

sendfile + DMA gather copy需要硬件支持,并且只适用于从文件读,并写入到网卡的情况。

splice在接受缓冲区和发送缓冲区之间建立“管道”,而不是进行cpu拷贝。所以也是0次拷贝。

OSI七层模型和TCP/IP四层模型

先有TCP/IP的四层模型,后面再有OSI的七层模型。

OSI七层模型将应用层拆解为会话层、表现层、应用层,但是实际的应用层协议中,连接管理、编码转换和应用层处理往往密不可分,所以OSI的这三层可以仅当成一个概念。

OSI七层模型,增加了物理层,补充了TCP/IP四层中没有对物理硬件描述的问题。

四层代理是对TCP的代理。七层代理则需要解析应用层协议,比如HTTP的域名,端口,URL再进行代理。

  • MAC 层(数据链路层)的传输单位是帧(frame)
  • IP 层的传输单位是包(packet)
  • TCP 层的传输单位是段(segment)
  • HTTP 的传输单位则是消息或报文(message)

他们没有本质区别,可以统称为数据包。

TCP

TCP报文

header的固定部分长度是20字节,可扩展部分是40字节,所以tcp头的长度范围是20~60字节,并且是4字节的整数倍。

  • Source Port(源端口):源端口号 (占用16位),发送端程序端口

  • Destination Port(目的端口):目的端口号(占用16位),接收端程序端口

  • Sequence Number(序列号):用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一个数据字节在数据流中的序号;主要用来解决网络报乱序的问题;(占用32位)

  • Acknowledgment Number (确认号): 只有控制位ACK=1,该字段才有效。表示我希望你接下来发送的第一的报文的seq为此确认号。

  • Data Offset(数据偏移量) : data相对起始点的偏移量,就是tcp头部的长度,为20~60字节。

  • Reserved(保留字段):保留字段,目前还没有使用。

  • TCP Flags(控制位) :TCP控制位(6位),每一位代表一个控制位,依次为URG,ACK,PSH,RST,SYN,FIN。每个标志位的意思如下:

    • URG:此标志表示TCP包的紧急指针域(后面马上就要说到)有效,用来保证TCP连接不被中断,并且督促中间层设备要尽快处理这些数据;
    • ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0;
    • PSH:这个标志位表示Push操作。所谓Push操作就是指在数据包到达接收端以后,立即传送给应用程序,而不是在缓冲区中排队;
    • RST:这个标志表示连接复位请求。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包;
    • SYN:表示同步序号,用来建立连接。SYN标志位和ACK标志位搭配使用,当连接请求的时候,SYN=1,ACK=0;连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送一个只有SYN的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口;但是由于这种扫描方式只是进行TCP三次握手的第一次握手,因此这种扫描的成功表示被扫描的机器不很安全,一台安全的主机将会强制要求一个连接严格的进行TCP的三次握手;
    • FIN: 表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志位的TCP数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。
  • Window(窗口) :窗口大小(16位),表示接收端可用缓冲区大小,根据缓冲区大小和每次包大小,就可以计算出同时处理的 TCP 包的个数。同时处理的包个数越多,则网速越快。

  • Checksum (校验和): 用来检查 TCP 包是否完整(16位)

  • Urgent Pointer (紧急指针):表示应紧急处理的数据位置(16位)。路由器可以把紧急的数据包优先处理。

  • Options(可选字段):可选字段,可变长度,最长为40字节。(因为 Data Offset 最多能表示60个字节长度的 TCP 头信息,固定的 TCP 头部为 20 字节)

  • Padding (填充):填充位。因为 Data Offset 只能表示 TCP 头部的长度 必须是 4 字节的整倍数。如果 Options 选项不足 4字节的整倍数,就需要 Padding 填充为 4 字节的整倍数。

常见的TCP option:

  • 窗口扩大系数:固定部分的窗口最大为65536字节。通过这个系数来继续扩大窗口
  • MSS:最大Segement长度(TCP的数据包被称为segment段)。默认546byte。

MTU和MSS

  • MTU: 最大传输单元。工作在数据链路层,定义了更上层协议的最大长度,例如ip头 + tcp头 + tcp数据
  • MSS: 最大报文段。TCP协议的概念。

三次握手

服务端是listen的那一方,对于建连来说,很明确的有client/server的区别。

  1. 客户端:syn
  2. 服务端: syn+ack
  3. 客户端:ack

也可以看作是4个过程:

  1. 客户端:syn
  2. 服务端:ack
  3. 服务端:syn
  4. 客户端:ack

只是2和3完全可以合并为一个tcp消息,所以是TCP三次握手

全链接队列满/半连接队列

  1. 服务端接收到客户端的syn时,该fd会进入半连接队列。
  2. 服务端接收到客户端的ack时,该fd会进入全连接队列。此时应用程序accept就是从全连接队列取fd。

全链接队列和半连接队列满的默认策略都是丢弃该连接。全连接队列长度可以通过内核参数和应用参数共同控制。半连接队列长度无法控制。

四次挥手

  1. 主动方:fin
  2. 被动方:ack。此时进入close_wait状态(被动方)
  3. 被动方:fin
  4. 主动方:ack。此时进入time_wait状态(主动方)
  • close_wait:被动关闭方的状态,指等待处理完最后的消息,而后发送fin的这段时间。
  • time_wait:等待2MSL,(MSL是报文最大存活时间)
    1. 四次挥手的最后一步“主动方ack”的报文可能丢包,此时被动方会重传fin,为了接受到这个fin,并再次ack。
    2. 保证重复的报文在网络中消亡,以防干扰另一个有同样标识的连接。

close_wait过多往往意味着应用的处理能力不足。

TCP分包

  • 头部记录长度字段
  • 固定分割符,例如CRLF

为什么说TCP是可靠的

网络工程师(8):TCP为什么可靠

乱序重排

乱序:因为网络传输的不确定性,接收方不一定能按照发送报文的顺序来接收报文

如何解决?通过TCP头部的序列号

应答确认

发送方没发送一个数据包时,接收方会应答一个ack,告知发送方收到了这个数据包,解决丢包的问题。ack的确认号为序列号+1,表示发送方下一个包应该是序列号+1。

实际情况下,并不是每个数据包都会ack一次,TCP也会在滑动窗口中去攒批,多个数据包进行一次ack,在后面的滑动窗口和空洞中去介绍。

报文重传

在应答确认的机制下,就有了重传,分为超时重传和快速重传:

  • (发送方主动)超时重传。发送方长时间没收到ack,则主动重传
  • (接口方控制)快速重传。因为接受的数据包可能是乱序的,接收方发出的ack是n+1,但先收到的是n+2, n+3。此时,接口方会再发出ack=n+1,要求发送方重传n+1的数据包。

流量控制

发送和接受的处理能力是不一样的,发送的速度超过处理的速度就会溢出。考虑生产者消费者模式,也需要通过锁等同步机制来写作。对于tcp来说,接收方会调整窗口大小来控制发送方的发送速率。

上图是快速重传和批量ack的图示,2号的包丢失了,收到了345时都会发送ack=2,要求重发2号包,这就是快速重传。收到的345并不会被丢弃,而是存在接收方的滑动窗口里。此时滑动窗口里有一个2号的“空洞”。当收到2号后,滑动窗口里2345都有了,此时会累积应答ack=6。也可见下图:

TCP和UDP区别

  • TCP是面向连接的、可靠的、基于字节流传输层通信协议
  • UDP是无连接、无状态、面向包的通信协议。UDP更快

HTTP/2

特性:

  1. 连接复用:stream的概念,一个tcp上有多个stream
  2. 二进制分帧层
  3. 头部压缩
  4. stream优先级
  5. serverpush
  6. 拥塞控制

相关术语:

  • stream:tcp连接上承载的流,一个tcp连接上可以有多个stream,每个stream有唯一的streamId
  • message:完整的请求或响应。由frame组成
  • frame:最小单位

frame的格式:

  • 固定9字节的header
    • payload长度
    • 帧类型(header/data)
    • flag,用于控制
    • 所属的streamId
  • payload

读取frame的伪代码:

loop
read 9 byte
payload_Length=first 3 bytes
read payload
swith type: (header/data)
Take action
end loop