Linux是如何收发网络包的

网络模型

为了使得多种设备能通过网络相互通信,和为了解决各种不同设备在网络互联中的兼容性问题,国际标准化组织制定了开放式系统互联通信参考模型(Open System Interconnection Reference Model),也就是 OSI 网络模型,该模型主要有 7 层,分别是应用层、表示层、会话层、传输层、网络层、数据链路层以及物理层。

每一层负责的职能都不同,如下:

  • 应用层,负责给应用程序提供统一的接口;
  • 表示层,负责把数据转换成兼容另一个系统能识别的格式;
  • 会话层,负责建立、管理和终止表示层实体之间的通信会话;
  • 传输层,负责端到端的数据传输;
  • 网络层,负责数据的路由、转发、分片;
  • 数据链路层,负责数据的封帧和差错检测,以及 MAC 寻址;
  • 物理层,负责在物理网络中传输数据帧;

由于 OSI 模型实在太复杂,提出的也只是概念理论上的分层,并没有提供具体的实现方案。

事实上,我们比较常见,也比较实用的是四层模型,即 TCP/IP 网络模型,Linux 系统正是按照这套网络模型来实现网络协议栈的。

TCP/IP 网络模型共有 4 层,分别是应用层、传输层、网络层和网络接口层,每一层负责的职能如下:

  • 应用层,负责向用户提供一组应用程序,比如 HTTP、DNS、FTP 等;
  • 传输层,负责端到端的通信,比如 TCP、UDP 等;
  • 网络层,负责网络包的封装、分片、路由、转发,比如 IP、ICMP 等;
  • 网络接口层,负责网络包在物理网络中的传输,比如网络包的封帧、 MAC 寻址、差错检测,以及通过网卡传输网络帧等;

TCP/IP 网络模型相比 OSI 网络模型简化了不少,也更加易记,它们之间的关系如下图:

 

Linux 网络协议栈

我们可以把自己的身体比作应用层中的数据,打底衣服比作传输层中的 TCP 头,外套比作网络层中 IP 头,帽子和鞋子分别比作网络接口层的帧头和帧尾。

在冬天这个季节,当我们要从家里出去玩的时候,自然要先穿个打底衣服,再套上保暖外套,最后穿上帽子和鞋子才出门,这个过程就好像我们把 TCP 协议通信的网络包发出去的时候,会把应用层的数据按照网络协议栈层层封装和处理。

你从下面这张图可以看到,应用层数据在每一层的封装格式。

 

 

 

其中:

  • 传输层,给应用数据前面增加了 TCP 头;
  • 网络层,给 TCP 数据包前面增加了 IP 头;
  • 网络接口层,给 IP 数据包前后分别增加了帧头和帧尾;

这些新增的头部和尾部,都有各自的作用,也都是按照特定的协议格式填充,这每一层都增加了各自的协议头,那自然网络包的大小就增大了,但物理链路并不能传输任意大小的数据包,所以在以太网中,规定了最大传输单元(MTU)是 1500 字节,也就是规定了单次传输的最大 IP 包大小。

当网络包超过 MTU 的大小,就会在网络层分片,以确保分片后的 IP 包不会超过 MTU 大小,如果 MTU 越小,需要的分包就越多,那么网络吞吐能力就越差,相反的,如果 MTU 越大,需要的分包就越小,那么网络吞吐能力就越好。

知道了 TCP/IP 网络模型,以及网络包的封装原理后,那么 Linux 网络协议栈的样子,你想必猜到了大概,它其实就类似于 TCP/IP 的四层结构:

从上图的的网络协议栈,你可以看到:

  • 应用程序需要通过系统调用,来跟 Socket 层进行数据交互;
  • Socket 层的下面就是传输层、网络层和网络接口层;
  • 最下面的一层,则是网卡驱动程序和硬件网卡设备;

 

Linux 接收网络包的流程

网卡是计算机里的一个硬件,专门负责接收和发送网络包,当网卡接收到一个网络包后,会通过 DMA 技术,将网络包放入到 Ring Buffer,这个是一个环形缓冲区。

那接收到网络包后,应该怎么告诉操作系统这个网络包已经到达了呢?

最简单的一种方式就是触发中断,也就是每当网卡收到一个网络包,就触发一个中断告诉操作系统。

但是,这存在一个问题,在高性能网络场景下,网络包的数量会非常多,那么就会触发非常多的中断,要知道当 CPU 收到了中断,就会停下手里的事情,而去处理这些网络包,处理完毕后,才会回去继续其他事情,那么频繁地触发中断,则会导致 CPU 一直没玩没了的处理中断,而导致其他任务可能无法继续前进,从而影响系统的整体效率。

所以为了解决频繁中断带来的性能开销,Linux 内核在 2.6 版本中引入了 NAPI 机制,它是混合「中断和轮询」的方式来接收网络包,它的核心概念就是不采用中断的方式读取数据,而是首先采用中断唤醒数据接收的服务程序,然后 poll 的方法来轮询数据。

比如,当有网络包到达时,网卡发起硬件中断,于是会执行网卡硬件中断处理函数,中断处理函数处理完需要「暂时屏蔽中断」,然后唤醒「软中断」来轮询处理数据,直到没有新数据时才恢复中断,这样一次中断处理多个网络包,于是就可以降低网卡中断带来的性能开销。

软中断是怎么处理网络包的呢?它会从 Ring Buffer 中拷贝数据到内核 struct sk_buff 缓冲区中,从而可以作为一个网络包交给网络协议栈进行逐层处理。

  1. 首先,会先进入到网络接口层,在这一层会检查报文的合法性,如果不合法则丢弃,合法则会找出该网络包的上层协议的类型,比如是 IPv4,还是 IPv6,接着再去掉帧头和帧尾,
  2. 然后交给网络层。到了网络层,则取出 IP 包,判断网络包下一步的走向,比如是交给上层处理还是转发出去。当确认这个网络包要发送给本机后,就会从 IP 头里看看上一层协议的类型是 TCP 还是 UDP,接着去掉 IP 头,然后交给传输层。
  3. 传输层取出 TCP 头或 UDP 头,根据四元组「源 IP、源端口、目的 IP、目的端口」 作为标识,找出对应的 Socket,并把数据拷贝到 Socket 的接收缓冲区。
  4. 最后,应用层程序调用 Socket 接口,从内核的 Socket 接收缓冲区读取新到来的数据到应用层。

至此,一个网络包的接收过程就已经结束了,你也可以从下图左边部分看到网络包接收的流程,右边部分刚好反过来,它是网络包发送的流程。

 

 

 

Linux 发送网络包的流程

如上图的有半部分,发送网络包的流程正好和接收流程相反。

  1. 首先,应用程序会调用 Socket 发送数据包的接口,由于这个是系统调用,所以会从用户态陷入到内核态中的 Socket 层,Socket 层会将应用层数据拷贝到 Socket 发送缓冲区中。
  2. 接下来,网络协议栈从 Socket 发送缓冲区中取出数据包,并按照 TCP/IP 协议栈从上到下逐层处理。
  3. 如果使用的是 TCP 传输协议发送数据,那么会在传输层增加 TCP 包头,然后交给网络层,网络层会给数据包增加 IP 包,然后通过查询路由表确认下一跳的 IP,并按照 MTU 大小进行分片。
  4. 分片后的网络包,就会被送到网络接口层,在这里会通过 ARP 协议获得下一跳的 MAC 地址,然后增加帧头和帧尾,放到发包队列中。

这一些准备好后,会触发软中断告诉网卡驱动程序,这里有新的网络包需要发送,最后驱动程序通过 DMA,从发包队列中读取网络包,将其放入到硬件网卡的队列中,随后物理网卡再将它发送出去。

 

 

总结

电脑与电脑之间通常都是通话网卡、交换机、路由器等网络设备连接到一起,那由于网络设备的异构性,

国际标准化组织定义了一个七层的 OSI 网络模型,但是这个模型由于比较复杂,实际应用中并没有采用,而是采用了更为简化的 TCP/IP 模型,Linux 网络协议栈就是按照了该模型来实现的。

TCP/IP 模型主要分为应用层、传输层、网络层、网络接口层四层,每一层负责的职责都不同,这也是 Linux 网络协议栈主要构成部分。

当应用程序通过 Socket 接口发送数据包,数据包会被网络协议栈从上到下进行逐层处理后,才会被送到网卡队列中,随后由网卡将网络包发送出去

而在接收网络包时,同样也要先经过网络协议栈从下到上的逐层处理,最后才会被送到应用程序。

 

posted @ 2022-05-06 18:02  一只艾米果  阅读(146)  评论(0编辑  收藏  举报