分享

全面了解linux TCP/IP协议栈

 waston 2019-04-29

简要说明

 自从熟悉了linux socket编程(主要做posix socket的TCP/IP)之后,就一直以来就想写一篇对TCP/IP有一个比较全面的涵盖用户空间、内核以及网卡的文章,以便帮助大家在遇到基于socket的TCP/IP问题或困惑时能进行有目的的、恰当的分析以便解决问题。只是一是本人上学时是个“不学无术”的不良少年。工作后自己也是不断学习中,还有比较忙(都是懒的借口吧,不然怎么还有时间看龙珠),一直没动笔,今天就花一下午时间来做一个介绍。如果大家发现了谬误之处,请及时留言,我好更正之、学习之。话不多说,下面就进入正题。
 
 现如今的internet services可以说就是基于TCP/IP构建的。理解数据是如何通过network传输的,无论对你调试net IO的性能还是解决问题还是学习新的技术都是有很大助益的。本文将会全面的,尽力细致的通过内核及硬件中的数据流和控制流来介绍这方面的知识。
 
 PS: 有一个我自认为实现的还不错的项目,有兴趣的童鞋可以参与github c/c++连接复用库实现

TCP/IP的关键特征

 我们如何设计一个数据传输协议以便保证数据快速、有序、无误?TCP/IP正是为了这样的需求被创造的。下面的几个特征用于帮助了解什么是TCP/IP协议(栈)。由于对于TCP来讲IP是紧密相关的,我们放到一起介绍。更多的内容,大家可以参考大学教材《计算机网络》(谢希仁著)以及《TCP/IP协议》三卷(国外)。

  • 面向连接的(Connection-oriented)
    一个tcp connection有两个端(endpoint),每一个endpoint可以用一个(ip、port)来表达,所以两端的话就可以用(local IP address, local port number, remote IP address, remote port number)来表达。

  • 数据是双向流动的
    双向的传递二进制流。

  • 按序传送的
    接受者接收数据一定是会按照发送者发送数据的顺序的。通过一个32-bit integer做标记。通过ACK来保证可靠性,如果发送者收不到接受者的ACK,则会重新发送。

  • 流量控制
    发送方会根据接收方提供的的窗口大小来决定如何发送数据,不会超过接收方的缓冲能力。

  • 拥塞控制
    拥塞窗口(congestion window)区别于receive window,是发送方自己根据包ACK的状态结合特定的拥塞算法计算出的一个window。它表达的当前的网络状态。发送发发送的数据上限受到流量控制和拥塞控制共同的作用。

数据传送

这里写图片描述
 
 数据通过网络协议栈发送,如下图1。
 
 这里写图片描述
             图1:数据发送的流动过程

 借用于国外大神的图(下文也会借用很多,不一一说明了),其表达了数据的流动过程。这里为了防止大家不认真看,我要强调一下右侧黑色方块表达用户write的新的数据,而灰色的代表发送缓冲区中已有的数据,大括号圈的灰黑两块结合代表了一个TCP报文段。整个过程可以分为三个区域,user、kernel和device,其中user和kernel的部分要吃CPU的。这里的device就是我们说的网卡(Network Interface Card)。

 内核socket关联了两个缓冲区:

一个发送缓冲区为了数据发送。
一个接收缓冲区为了接收数据。

 在内核中有一个TCP control block(TCB)关联到socket。TCB包含了连接需要处理的一系列数据,这里面包含了TCP的state(LISTEN, ESTABLISHED, TIME_WAIT),receive window, congestion window, sequence number, resending timer等等。
 
 内核中如果当前的TCP状态允许数据发送,则一个新的TCP报文段(或者说包)就会被创建。
 这里写图片描述
             图2:TCP报文段

 之后报文段流向IP层。IP层在TCP的报文段上加上IP头并执行IP路由。IP路由是寻找到达目的IP的下一跳的一个程序。IP层计算完并加上IP头的checksum之后就会把数据发送到链路层。链路层通过ARP和下一跳的IP地址查找到下一跳的MAC地址,之后链路层把其头加到数据中。至此主机端数据包完成。之后就是调用网卡驱动了。此时如果有包捕获程序比如tcpdump或者Wireshark处于运行中,内核会把数据包拷贝给它们一份。
 
 驱动根据硬件厂商定义的协议请求传送数据。网卡在接到数据传送请求之后把数据包从主存拷贝到它的存储空间中,之后把数据打到网线。这时,为了遵从以太网标准,网卡会添加IFG(帧间隔)到数据包以便区分数据包的开始。网卡发送完数据包之后就会产生一个CPU中断,每一个中断都一个特定的中断号,OS根据中断号选择合适的驱动对中断进行处理(驱动启动的时候会注册一个对应中断号的处理函数)。

数据接收

这里写图片描述

 现在我们来看看是怎么接收数据的,如图3。
 
 这里写图片描述
              图3:数据流入过程
 首先网卡把接收到的数据包写入到它的内存之中。然后对其进行校验,通过后发送到主机的主存之中。主存中的buffer是驱动分配好的,驱动会把分配好的buffer描述告诉网卡,如果没有足够的buffer接受网卡的数据包,网卡会将数据包丢弃。一旦数据包拷贝到主存完成,网卡会通过中断告知主机OS。
 之后驱动会检查它是否能处理这个新的包。如果能处理,驱动会把数据包包装成OS认识的结构(linux sk_buffer)并推送到上层。
 链路层接收到帧后检查通过的话会按照协议解帧并推送至IP层。
 IP层会在解包之后根据包中包含的IP信息决定推送至上层还是转发到其他IP。如果判断需要推送至上层,则会解掉IP包头并推送至TCP层。
 TCP在解报之后会根据其四元组找到对应的TCB,之后通过TCP协议处理这个报文。在接收到报文后,会把报文加到接受报文,之后根据TCP的状态发送一个ACK给对端。
 当然上述过程会受到NAT等等Netfilter的作用,这里不谈了,也没深研究过。当然为了性能,大牛们方方面面也做了很多努力,比如大到RDMA、DPDK等大的软硬件技术,小到zero-copy、checksum offload等。

数据结构

 下面介绍一下关键数据结构sk_buff(skb)。
 
这里写图片描述
         图4:sk_buffer(意为socket buffer?)
 
 一个skb就是一个发送缓冲区可发送的数据包。从图4中可以看到其各个指针。不同层级的数据包头的添加和删除、数据包的联合和分割都是通过控制这些指针来实现的。真正的数据结构可能比这复杂很多,但是基本思路是一致的。

  • TCP control block
     一个TCB代表了一个connection,这里TCB是一个抽象,linux用tcp_sock这个结构表达。下图5可以看出tcp_sock和fd、socket之间的关系。
     这里写图片描述
                图5:TCP connection结构
     
     当调用系统调用的时候,OS先找到file结构。对于类unix系统,socket、本地file、device都被抽象成file。因此file拥有最少的信息。对于socket,有其自己的结构关联到file,tcp_sock也会关联到socket。tcp_sock只是socket的一类,其他还有诸如inet_sock等支持各种协议的sock。所有TCP相关的信息都在tcp_sock中,比如序号啊,各种窗口等。
     
     发送和接收缓冲区就是sk_buffer的list。dst_entry就是路由的结果,为了避免太频繁的路由,他们是sock关联的。dst_entry允许简单的ARP查找,它也是路由表的一部分。tcp_sock通过对四元组进行hash来索引。

驱动和网卡的交互

 这一部分的知识可能是网上最难搜索到的部分,很大一部分原因应该是很少有人关注吧,但是了解了这部分知识会让你更通透。
 
 驱动和网卡之间是异步通信。驱动在请求发送数据之后CPU就去干别的事情去了。网卡发送完包之后通过中断通知CPU,CPU再通过驱动程序了解到结果。和发送数据一样,接收数据也是异步的。网卡把数据倒腾到主存之后再通过中断通知CPU。

 因此,预留一些空间来缓存发送和接受的buffer是必要的。大多数情况下,网卡使用环结构,这个环基本上就是一个队列,它具有固定的条目数,每一个条目存储一个发送或者接受的数据。条目被顺序的轮流使用,可以复用。如下图6,可以看到数据传送过程。
 这里写图片描述
           图6:驱动与网卡发送数据流

 驱动接收上层的数据并创建一个网卡可以理解的数据包描述(send descriptor),包含了主存地址和大小。由于网卡只认识物理地址,所以驱动还需将虚拟地址转换成物理地址,之后把send descriptor放到Tx ring之中。下一步通过通知网卡有新的数据了,之后网卡通过DMA(直接内存访问)获取元数据和数据发送出去。发送完之后通过DMA把结果写回,之后发送中断通知。

 数据的接收和发送反推过程差不多,自己看图7说话吧;-)。
 这里写图片描述
           图7:驱动与网卡接收数据流

协议栈buffer和控制流

 协议栈中的控制流分为几个阶段。图8显示了buffer的发送过程。
         这里写图片描述
              图8:buffer发送流

 首先应用程序创建数据并加入到发送缓冲区。如果缓冲区不足则调用失败或者阻塞调用线程。因此应用程序向内核灌入数据的速率收到缓冲区大小的限制。
 
 之后TCP创建包并通过传输队列(qdisc)发送给驱动。qdisc是一个FIFO结构并且是固定大小,这个大小可以通过ifconfig命令查看,其中的txqueuelen便是,一般情况下它是千级别的。

 在驱动和网卡之间是TX ring。之前提到它是定长的,如果它没有足够的空间,那么当传输队列(qdisc)也满了之后包就会被drop,就形成了之下而上的反压。

 下图9表现了buffer接收流。
          这里写图片描述
               图9:buffer接收流

 很容易通过发送流反推。值得注意的是驱动和协议栈之间没有了队列,数据是通过poll直接获取的。如果主机处理的速度没有网卡接收的快,则Rx ring会满,就会有包被丢弃。一般情况下丢弃不会是因为TCP连接导致的,因为TCP连接有流量控制,但是UDP是没有的。可以通过ifconfig命令看到很多信息,比如drop、error等包的数量。

最后

 现代的软硬件TCP/IP协议栈单链接发送速率到1~2GiB/s完全没有任何问题(经过实测)。如果你想探索更优秀的性能,你可以尝试RMDA等技术,他们通过绕过内核以减少拷贝等方式优化了性能,当然可能依赖硬件。


深入浅出 TCP/IP 协议栈

TCP/IP 协议栈是一系列网络协议的总和,是构成网络通信的核心骨架,它定义了电子设备如何连入因特网,以及数据如何在它们之间进行传输。TCP/IP 协议采用4层结构,分别是应用层、传输层、网络层和链路层,每一层都呼叫它的下一层所提供的协议来完成自己的需求。由于我们大部分时间都工作在应用层,下层的事情不用我们操心;其次网络协议体系本身就很复杂庞大,入门门槛高,因此很难搞清楚TCP/IP的工作原理,通俗一点讲就是,一个主机的数据要经过哪些过程才能发送到对方的主机上。 接下来,我们就来探索一下这个过程。

0、物理介质

物理介质就是把电脑连接起来的物理手段,常见的有光纤、双绞线,以及无线电波,它决定了电信号(0和1)的传输方式,物理介质的不同决定了电信号的传输带宽、速率、传输距离以及抗干扰性等等。

TCP/IP协议栈分为四层,每一层都由特定的协议与对方进行通信,而协议之间的通信最终都要转化为 0 和 1 的电信号,通过物理介质进行传输才能到达对方的电脑,因此物理介质是网络通信的基石。

下面我们通过一张图先来大概了解一下TCP/IP协议的基本框架:

当通过http发起一个请求时,应用层、传输层、网络层和链路层的相关协议依次对该请求进行包装并携带对应的首部,最终在链路层生成以太网数据包,以太网数据包通过物理介质传输给对方主机,对方接收到数据包以后,然后再一层一层采用对应的协议进行拆包,最后把应用层数据交给应用程序处理。

网络通信就好比送快递,商品外面的一层层包裹就是各种协议,协议包含了商品信息、收货地址、收件人、联系方式等,然后还需要配送车、配送站、快递员,商品才能最终到达用户手中。

一般情况下,快递是不能直达的,需要先转发到对应的配送站,然后由配送站再进行派件。

配送车就是物理介质,配送站就是网关, 快递员就是路由器,收货地址就是IP地址,联系方式就是MAC地址。 

快递员负责把包裹转发到各个配送站,配送站根据收获地址里的省市区,确认是否需要继续转发到其他配送站,当包裹到达了目标配送站以后,配送站再根据联系方式找到收件人进行派件。  

有了整体概念以后,下面我们详细了解一下各层的分工。

1、链路层

网络通信就是把有特定意义的数据通过物理介质传送给对方,单纯的发送 0 和 1 是没有意义的,要传输有意义的数据,就需要以字节为单位对 0 和 1 进行分组,并且要标识好每一组电信号的信息特征,然后按照分组的顺序依次发送。以太网规定一组电信号就是一个数据包,一个数据包被称为一帧, 制定这个规则的协议就是以太网协议。一个完整的以太网数据包如下图所示:

整个数据帧由首部数据尾部三部分组成,首部固定为14个字节,包含了目标MAC地址、源MAC地址和类型;数据最短为46个字节,最长为1500个字节,如果需要传输的数据很长,就必须分割成多个帧进行发送;尾部固定为4个字节,表示数据帧校验序列,用于确定数据包在传输过程中是否损坏。因此,以太网协议通过对电信号进行分组并形成数据帧,然后通过物理介质把数据帧发送给接收方。那么以太网如何来识接收方的身份呢?

以太网规协议定,接入网络的设备都必须安装网络适配器,即网卡, 数据包必须是从一块网卡传送到另一块网卡。而网卡地址就是数据包的发送地址和接收地址,也就是帧首部所包含的MAC地址,MAC地址是每块网卡的身份标识,就如同我们身份证上的身份证号码,具有全球唯一性。MAC地址采用十六进制标识,共6个字节, 前三个字节是厂商编号,后三个字节是网卡流水号,例如 4C-0F-6E-12-D2-19

有了MAC地址以后,以太网采用广播形式,把数据包发给该子网内所有主机,子网内每台主机在接收到这个包以后,都会读取首部里的目标MAC地址,然后和自己的MAC地址进行对比,如果相同就做下一步处理,如果不同,就丢弃这个包。

所以链路层的主要工作就是对电信号进行分组并形成具有特定意义的数据帧,然后以广播的形式通过物理介质发送给接收方。

2、网络层

对于上面的过程,有几个细节问题值得我们思考:

发送者如何知道接收者的MAC地址?

发送者如何知道接收者和自己同属一个子网?

如果接收者和自己不在同一个子网,数据包如何发给对方?

为了解决这些问题,网络层引入了三个协议,分别是IP协议ARP协议路由协议。

【1】IP协议

通过前面的介绍我们知道,MAC地址只与厂商有关,与所处的网络无关,所以无法通过MAC地址来判断两台主机是否属于同一个子网。

因此,网络层引入了IP协议,制定了一套新地址,使得我们能够区分两台主机是否同属一个网络,这套地址就是网络地址,也就是所谓的IP地址。

IP地址目前有两个版本,分别是IPv4IPv6,IPv4是一个32位的地址,常采用4个十进制数字表示。IP协议将这个32位的地址分为两部分,前面部分代表网络地址,后面部分表示该主机在局域网中的地址。由于各类地址的分法不尽相同,以C类地址192.168.24.1为例其中前24位就是网络地址,后8位就是主机地址。因此, 如果两个IP地址在同一个子网内,则网络地址一定相同。为了判断IP地址中的网络地址,IP协议还引入了子网掩码, IP地址和子网掩码通过按位与运算后就可以得到网络地址。

由于发送者和接收者的IP地址是已知的(应用层的协议会传入), 因此我们只要通过子网掩码对两个IP地址进行AND运算后就能够判断双方是否在同一个子网了。

【2】ARP协议

即地址解析协议,是根据IP地址获取MAC地址的一个网络层协议。其工作原理如下:

ARP首先会发起一个请求数据包,数据包的首部包含了目标主机的IP地址,然后这个数据包会在链路层进行再次包装,生成以太网数据包,最终由以太网广播给子网内的所有主机,每一台主机都会接收到这个数据包,并取出标头里的IP地址,然后和自己的IP地址进行比较,如果相同就返回自己的MAC地址,如果不同就丢弃该数据包。ARP接收返回消息,以此确定目标机的MAC地址;与此同时,ARP还会将返回的MAC地址与对应的IP地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。cmd输入 arp -a 就可以查询本机缓存的ARP数据。

【3】路由协议

通过ARP协议的工作原理可以发现,ARP的MAC寻址还是局限在同一个子网中,因此网络层引入了路由协议,首先通过IP协议来判断两台主机是否在同一个子网中,如果在同一个子网,就通过ARP协议查询对应的MAC地址,然后以广播的形式向该子网内的主机发送数据包;如果不在同一个子网,以太网会将该数据包转发给本子网的网关进行路由。网关是互联网上子网与子网之间的桥梁,所以网关会进行多次转发,最终将该数据包转发到目标IP所在的子网中,然后再通过ARP获取目标机MAC,最终也是通过广播形式将数据包发送给接收方。

而完成这个路由协议的物理设备就是路由器,在错综复杂的网络世界里,路由器扮演者交通枢纽的角色,它会根据信道情况,选择并设定路由,以最佳路径来转发数据包。

【4】IP数据包

在网络层被包装的数据包就叫IP数据包,IPv4数据包的结构如下图所示:

IP数据包由首部和数据两部分组成,首部长度为20个字节,主要包含了目标IP地址和源IP地址,目标IP地址是网关路由的线索和依据;数据部分的最大长度为65515字节,理论上一个IP数据包的总长度可以达到65535个字节,而以太网数据包的最大长度是1500个字符,如果超过这个大小,就需要对IP数据包进行分割,分成多帧发送。

所以,网络层的主要工作是定义网络地址,区分网段,子网内MAC寻址,对于不同子网的数据包进行路由。

3、传输层

链路层定义了主机的身份,即MAC地址, 而网络层定义了IP地址,明确了主机所在的网段,有了这两个地址,数据包就从可以从一个主机发送到另一台主机。但实际上数据包是从一个主机的某个应用程序发出,然后由对方主机的应用程序接收。而每台电脑都有可能同时运行着很多个应用程序,所以当数据包被发送到主机上以后,是无法确定哪个应用程序要接收这个包。

因此传输层引入了UDP协议来解决这个问题,为了给每个应用程序标识身份,UDP协议定义了端口,同一个主机上的每个应用程序都需要指定唯一的端口号,并且规定网络中传输的数据包必须加上端口信息。 这样,当数据包到达主机以后,就可以根据端口号找到对应的应用程序了。UDP定义的数据包就叫做UDP数据包,结构如下所示:

UDP数据包由首部和数据两部分组成,首部长度为8个字节,主要包括源端口和目标端口;数据最大为65527个字节,整个数据包的长度最大可达到65535个字节。

UDP协议比较简单,实现容易,但它没有确认机制, 数据包一旦发出,无法知道对方是否收到,因此可靠性较差,为了解决这个问题,提高网络可靠性,TCP协议就诞生了,TCP即传输控制协议,是一种面向连接的、可靠的、基于字节流的通信协议。简单来说TCP就是有确认机制的UDP协议,每发出一个数据包都要求确认,如果有一个数据包丢失,就收不到确认,发送方就必须重发这个数据包。

为了保证传输的可靠性,TCP 协议在 UDP 基础之上建立了三次对话的确认机制,也就是说,在正式收发数据前,必须和对方建立可靠的连接。由于建立过程较为复杂,我们在这里做一个形象的描述:

主机A:我想发数据给你,可以么?

主机B:可以,你什么时候发?

主机A:我马上发,你接着!

经过三次对话之后,主机A才会向主机B发送正式数据,而UDP是面向非连接的协议,它不与对方建立连接,而是直接就把数据包发过去了。所以 TCP 能够保证数据包在传输过程中不被丢失,但美好的事物必然是要付出代价的,相比 UDP,TCP 实现过程复杂,消耗连接资源多,传输速度慢。

TCP 数据包和 UDP 一样,都是由首部和数据两部分组成,唯一不同的是,TCP 数据包没有长度限制,理论上可以无限长,但是为了保证网络的效率,通常 TCP 数据包的长度不会超过IP数据包的长度,以确保单个 TCP 数据包不必再分割。

总结一下,传输层的主要工作是定义端口,标识应用程序身份,实现端口到端口的通信,TCP协议可以保证数据传输的可靠性

4、应用层

理论上讲,有了以上三层协议的支持,数据已经可以从一个主机上的应用程序传输到另一台主机的应用程序了,但此时传过来的数据是字节流,不能很好的被程序识别,操作性差。因此,应用层定义了各种各样的协议来规范数据格式,常见的有 HTTP、FTP、SMTP 等,HTTP 是一种比较常用的应用层协议,主要用于B/S架构之间的数据通信,其报文格式如下:

在 Resquest Headers 中,Accept 表示客户端期望接收的数据格式,而 ContentType 则表示客户端发送的数据格式;在 Response Headers 中,ContentType 表示服务端响应的数据格式,这里定义的格式,一般是和  Resquest Headers 中 Accept 定义的格式是一致的。

有了这个规范以后,服务端收到请求以后,就能正确的解析客户端发来的数据,当请求处理完以后,再按照客户端要求的格式返回,客户端收到结果后,按照服务端返回的格式进行解析。

所以应用层的主要工作就是定义数据格式并按照对应的格式解读数据。

5、全流程

首先我们梳理一下每层模型的职责:

  • 链路层:对0和1进行分组,定义数据帧,确认主机的物理地址,传输数据;

  • 网络层:定义IP地址,确认主机所在的网络位置,并通过IP进行MAC寻址,对外网数据包进行路由转发;

  • 传输层:定义端口,确认主机上应用程序的身份,并将数据包交给对应的应用程序;

  • 应用层:定义数据格式,并按照对应的格式解读数据。

然后再把每层模型的职责串联起来,用一句通俗易懂的话讲就是:

当你输入一个网址并按下回车键的时候,首先,应用层协议对该请求包做了格式定义;紧接着传输层协议加上了双方的端口号,确认了双方通信的应用程序;然后网络协议加上了双方的IP地址,确认了双方的网络位置;最后链路层协议加上了双方的MAC地址,确认了双方的物理位置,同时将数据进行分组,形成数据帧,采用广播方式,通过传输介质发送给对方主机。而对于不同网段,该数据包首先会转发给网关路由器,经过多次转发后,最终被发送到目标主机。目标机接收到数据包后,采用对应的协议,对帧数据进行组装,然后再通过一层一层的协议进行解析,最终被应用层的协议解析并交给服务器处理。

6、总结

以上内容是对TCP/IP四层模型做了简单的介绍,而实际上每一层模型都有很多协议,每个协议要做的事情也很多,但我们首先得有一个清晰的脉络结构,掌握每一层模型最基本的作用,然后再去丰富细枝末节的东西,也许会更容易理解。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多