网络通信的本质是电磁波高低电位变化在物理介质上的传输,高低变化的电位形成二进制的数据传递给网络硬件设备,硬件网络设备则会根据特定的编码方式讲二进制的数据变成以太网报文,此时,物理网络的信息就被解调制成为了数据帧,在OSI七层网络协议的定义中,二进制的数据处于物理层,而以太网数据帧则处于数据链路层,对于Linux内核来说,物理层和数据链路层之间的转换通常是由硬件设备的驱动完成,Linux内核定义了与硬件设备驱动通信的标准,在完成数据链路层的收包后,硬件网络设备会将数据帧传递给操作系统Linux的内核进行处理,所以Linux网络协议栈的源头是数据链路层的数据帧报文。
Linux内核在处理数据帧报文时按照数据帧的分片,TSO等特性,将数据帧转化为可见的网络层报文,从而进行下一步的操作,内核在处理网络报文的时候,采用一种非常高效的方式:
● 内核通过一个sk_buff结构体,保存所有的报文数据。
● 在网络层处理后传递给传输层的操作中,内核并不会为每一层进行复制,而是通过指针偏移的方式处理同一个sk_buff结构体。
下图是对Linux网络子系统的一个简单示意图:
根据网络报文的流向,我们按照两个不同的方向对报文处理的流程进行概述。
1) 收包方向的报文处理
从上图中可以发现,当内核从网络驱动设备中获取到报文后:
● 首先进行TC子系统的操作,在收包方向,不会有特别复杂的操作。
● 完成了网络层报文的重组后,报文进入了网络层,在网络层的处理中,比较重要的是netfilter框架和路由查找,这里实现了大量的功能,在云原生的场景中,netfilter和路由子系统有着至关重要的作用。
● 网络层完成处理后,会交由传输层进行处理,传输层的协议最常见的是tcp和udp,其中tcp协议在传输层通过重传和序号校验等机制保证了连接的可靠性。
● 传输层完成处理后,内核会将真正的报文数据提取出来并交给socket子系统,socket子系统抽象了网络的所有操作,屏蔽了网络报文的细节,让用户程序可以按照文件io的方式读写网络数据。
● socket子系统的数据最终会被用户程序读取,此时,一些编程语言的底层依赖库会进行一些封装,提供应用层协议的支持,如http,mqtt等,提供用户友好的操作体验。
2) 发包方向的报文处理
发包方向的处理,相比收包方向来说,一个重要的区别是,收包方向的操作是内核的软中断处理线程进行收包操作,而发包方向的操作则是由进程上下文进入内核态进行,因此,发包方向出现由于调度问题产生的偶发延迟抖动的概率比收包方向要小。
应用程序通常都是调用编程语言或者第三方的类库提供的方法进行发包动作,而这些发包动作最终都会调用socket子系统的写入调用来实现发送数据:
● 应用程序写入socket后,在达到一定的条件,如内存充裕,tcp发送窗口足够等情况下,内核会给当前需要发送的数据申请一个sk_buff结构体的内存,并将需要发送的数据填充到sk_buff的数据中。
● sk_buff数据报文首先会经过传输层的处理,根据当前的系统状态,内核会给报文sk_buff的tcp报头进行填充,然后调用网络层提供的方法进行发送。
● 网络层和收包方向类似,也会经历过netfilter框架的选取路由的动作,在这里实现了大量的与网络抽象相关的逻辑。在网络层完成处理后,按照OSI七层网络定义,会进入数据链路层,实际上在Linux中,邻居子系统的信息填充,也会在路由子系统完成路由的选取后一并填入到报文的报头中,随后报文会进入发送方向的TC子系统。
● TC子系统是实现网络流量整形的关键,和收包方向不同的是,发包方向的TC子系统会进行复杂的排序工作,通过数据包的类型与配置的规则进行匹配,实现网络流控的功能。TC子系统会将优先级最高的报文传递给网络设备驱动。
● 网络设备驱动的发送方法的实现逻辑细节已经不再属于Linux内核的范畴,通常在进入网络设备后,我们就可以认为数据包已经完成了发送。