
网络模型Linux 网络协议栈逐层封装Linux 网络协议栈Linux 接收网络包的流程网卡是计算机里的一个硬件专门负责接收和发送网络包当网卡接收到一个网络包后会通过 DMA 技术将网络包写入到指定的内存地址也就是写入到 Ring Buffer 这个是一个环形缓冲区接着就会告诉操作系统这个网络包已经到达。那应该怎么告诉操作系统这个网络包已经到达了呢最简单的一种方式就是触发中断也就是每当网卡收到一个网络包就触发一个中断告诉操作系统。但是这存在一个问题在高性能网络场景下网络包的数量会非常多那么就会触发非常多的中断那么频繁地触发中断则会导致 CPU 一直没完没了的处理中断而导致其他任务可能无法继续前进从而影响系统的整体效率。所以为了解决频繁中断带来的性能开销Linux 内核在 2.6 版本中引入了 NAPI 机制它是混合「中断和轮询」的方式来接收网络包它的核心概念就是不采用中断的方式读取数据而是首先采用中断唤醒数据接收的服务程序然后 poll 的方法来轮询数据。NAPI类似边沿触发即一次中断通知下从网卡取一批包放入网卡接收队列。全部取完后再次开启中断通知。无法取完延迟到ksoftirqd继续取取完后再次开启中断。因此当有网络包到达时会通过 DMA 技术将网络包写入到指定的内存地址接着网卡向 CPU 发起硬件中断当 CPU 收到硬件中断请求后根据中断表调用已经注册的中断处理函数。硬件中断处理函数会做如下的事情需要先「暂时屏蔽中断」接着对包进行初步处理发起「软中断」然后恢复刚才屏蔽的中断。至此硬件中断处理函数的工作就已经完成。硬件中断处理函数做的事情很少主要耗时的工作都交给软中断处理函数了。软中断的处理在硬件中断处理返回前会执行软中断处理正常情况下在软中断处理中会完成所有协议栈处理并将数据放入套接字接收缓存软中断处理后剩余部分会转入cpu的 ksoftirqd 线程继续负责软中断的后续处理。网络协议栈首先会先进入到网络接口层在这一层会检查报文的合法性如果不合法则丢弃合法则会找出该网络包的上层协议的类型比如是 IPv4还是 IPv6接着再去掉帧头和帧尾然后交给网络层。到了网络层则取出 IP 包判断网络包下一步的走向比如是交给上层处理还是转发出去。当确认这个网络包要发送给本机后就会从 IP 头里看看上一层协议的类型是 TCP 还是 UDP接着去掉 IP 头然后交给传输层。传输层取出 TCP 头或 UDP 头根据四元组「源 IP、源端口、目的 IP、目的端口」 作为标识找出对应的 Socket并把数据放到 Socket 的接收缓冲区。最后应用层程序调用 Socket 接口将内核的 Socket 接收缓冲区的数据「拷贝」到应用层的缓冲区然后唤醒用户进程。Linux 发送网络包的流程如上图的右半部分发送网络包的流程正好和接收流程相反。首先应用程序会调用 Socket 发送数据包的接口由于这个是系统调用所以会从用户态陷入到内核态中的 Socket 层内核会申请一个内核态的 sk_buff 内存将用户待发送的数据拷贝到 sk_buff 内存并将其加入到发送缓冲区。接下来网络协议栈从 Socket 发送缓冲区中取出 sk_buff并按照 TCP/IP 协议栈从上到下逐层处理。如果使用的是 TCP 传输协议发送数据**那么先移动拷贝一个新的 sk_buff 副本 **这是因为 sk_buff 后续在调用网络层最后到达网卡发送完成的时候这个 sk_buff 会被释放掉。而 TCP 协议是支持丢失重传的在收到对方的 ACK 之前这个 sk_buff 不能被删除。所以内核的做法就是每次调用网卡发送的时候实际上传递出去的是 sk_buff 的一个拷贝等收到 ACK 再真正删除。接着对 sk_buff 填充 TCP 头。这里提一下sk_buff 可以表示各个层的数据包接着对 sk_buff 填充 TCP 头。这里提一下sk_buff 可以表示各个层的数据包为了在层级之间传递数据时不发生拷贝只用 sk_buff 一个结构体来描述所有的网络包那它是如何做到的呢是通过调整 sk_buff 中 data 的指针比如当接收报文时从网卡驱动开始通过协议栈层层往上传送数据报通过增加 skb-data 的值来逐步剥离协议首部。当要发送报文时创建 sk_buff 结构体数据缓存区的头部预留足够的空间用来填充各层首部在经过各下层协议时通过减少 skb-data 的值来增加协议首部。然后交给网络层在网络层里会做这些工作选取路由确认下一跳的 IP、填充 IP 头、netfilter 过滤、对超过 MTU 大小的数据包进行分片。处理完这些工作后会交给网络接口层处理。网络接口层会通过 ARP 协议获得下一跳的 MAC 地址然后对 sk_buff 填充帧头和帧尾接着将 sk_buff 放到网卡的发送队列中。这一些工作准备好后会触发「软中断」告诉网卡驱动程序这里有新的网络包需要发送驱动程序会从发送队列中读取 sk_buff将这个 sk_buff 挂到 RingBuffer 中接着将 sk_buff 数据映射到网卡可访问的内存 DMA 区域最后触发真实的发送。当数据发送完成以后其实工作并没有结束因为内存还没有清理。当发送完成的时候网卡设备会触发一个硬中断来释放内存主要是释放 sk_buff 内存和清理 RingBuffer 内存。最后当收到这个 TCP 报文的 ACK 应答时传输层就会释放原始的 sk_buff 。正常发送下send/write只产生一次数据拷贝后续tcp分片备份引用一份数据只是克隆控制对象并修改字段特殊场景对数据内存连续性有要求时可能发生额外数据拷贝。