2019独角兽企业重金招聘Python工程师标准>>>

概述

出于对Linux内核的兴趣,也得益于学院老师的支持,我和我的小伙伴们完成了一份内核TCP源码的分析文档。基于CC协议开源,项目托管在了Github上:欢迎关注^_^

目前网络上已有大量关于TCP的分析,但是很多博文分析的版本还停留在很早的阶段。而且一般都是TCP中很小的一个部分。我们希望能够更系统地分析目前较新版本的内核TCP实现。

当然了,作为学生作品,难免有各种不足,希望能够和大家一道,共同改进这一不成熟的作品。issues、star、watch、pull-request等我们都欢迎~

正文节选

作品较长,我们节选了一部分,以供预览。由于是节选,因而行文逻辑并不连贯。部分的字符画由于从latex转换到markdown问题,难以对齐,显示不出原来的效果。大家可以看我们Github上生成好的完整PDF版本。

RFC

在分析TCP的过程中会遇到很多RFC,在这里,我们将可能会碰到的RFC罗列出来, 并进行一定的讨论,便于后面的分析。

RFC793 : Transmission Control Protocol

该RFC正是定义了TCP协议的那份RFC。在该RFC中,可以查到TCP的很多细节,帮助后续的代码分析。

TCP状态图

在RFC793中,给出了TCP协议的状态图,图中的TCB代表TCP控制块。原图如下所示:

                              +---------+ ---------\      active OPEN  |  CLOSED |            \    -----------  +---------+<---------\   \   create TCB  |     ^              \   \  snd SYN    passive OPEN |     |   CLOSE        \   \           ------------ |     | ----------       \   \         create TCB  |     | delete TCB         \   \       V     |                      \   \     +---------+            CLOSE    |    \   |  LISTEN |          ---------- |     |  +---------+          delete TCB |     |  rcv SYN      |     |     SEND              |     |  -----------   |     |    -------            |     V  +---------+      snd SYN,ACK  /       \   snd SYN          +---------+|         |<-----------------           ------------------>|         ||   SYN   |                    rcv SYN                     |   SYN   ||   RCVD  |<-----------------------------------------------|   SENT  ||         |                    snd ACK                     |         ||         |------------------           -------------------|         |+---------+   rcv ACK of SYN  \       /  rcv SYN,ACK       +---------+|           --------------   |     |   -----------                  |                  x         |     |     snd ACK                    |                            V     V                                |  CLOSE                   +---------+                              | -------                  |  ESTAB  |                              | snd FIN                  +---------+                              |                   CLOSE    |     |    rcv FIN                     V                  -------   |     |    -------                     +---------+          snd FIN  /       \   snd ACK          +---------+|  FIN    |<-----------------           ------------------>|  CLOSE  || WAIT-1  |------------------                              |   WAIT  |+---------+          rcv FIN  \                            +---------+| rcv ACK of FIN   -------   |                            CLOSE  |  | --------------   snd ACK   |                           ------- |  V        x                   V                           snd FIN V  +---------+                  +---------+                   +---------+|FINWAIT-2|                  | CLOSING |                   | LAST-ACK|+---------+                  +---------+                   +---------+|                rcv ACK of FIN |                 rcv ACK of FIN |  |  rcv FIN       -------------- |    Timeout=2MSL -------------- |  |  -------              x       V    ------------        x       V  \ snd ACK                 +---------+delete TCB         +---------+------------------------>|TIME WAIT|------------------>| CLOSED  |+---------+                   +---------+TCP Connection State Diagram

这张图对于后面的分析有很强的指导意义。

连接部分分为了主动连接和被动连接。主动连接是指客户端从CLOSED状态主动发出连接请求, 进入SYN-SENT状态,之后收到服务端的SYN+ACK包,进入ESTAB状态(即连接建立状态), 然后回复ACK包,完成三次握手。这一部分的代码我们将在[sec:tcp_connect_client] 中进行详细分析。被动连接是从listen状态开始,监听端口。随后收到SYN包,进入SYN-RCVD状态, 同时发送SYN+ACK包,最后,收到ACK后,进入ESTAB状态,完成被动连接的三次握手过程。 这一部分的详细讨论在[sec:tcp_server_connect]中完成。

连接终止的部分也被分为了两部分进行实现,主动终止和被动终止。主动终止是上图中从ESTAB状态 主动终止连接,发送FIN包的过程。可以看到,主动终止又分为两种情况,一种是FIN发出后,收到了 发来的FIN(即通信双方同时主动关闭连接),此时转入CLOSING状态并发送ACK包。收到ACK后, 进入TIME WAIT状态。另一种是收到了ACK包,转入FINWAIT-2状态,最后收到FIN后发送ACK, 完成四次握手,进入TIME WAIT状态。最后等数据发送完或者超时后,删除TCB,进入CLOSED状态。 被动终止则是接收到FIN包后,发送了ACK包,进入CLOSE WAIT状态。之后,当这一端的数据 也发送完成后,发送FIN包,进入LAST-ACK状态,接收到ACK后,进入CLOSED状态。

TCP头部格式

RFC793中,对于TCP头部格式的描述摘录如下:

    0                   1                   2                   3   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|          Source Port          |       Destination Port        |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                        Sequence Number                        |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                    Acknowledgment Number                      |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|  Data |           |U|A|P|R|S|F|                               || Offset| Reserved  |R|C|S|S|Y|I|            Window             ||       |           |G|K|H|T|N|N|                               |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|           Checksum            |         Urgent Pointer        |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                    Options                    |    Padding    |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+|                             data                              |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+TCP Header FormatNote that one tick mark represents one bit position.

这张图可以很方便地读出各个位占多长,它上面的标识是十进制的,很容易读。 这里我们挑出我们比较关心的Options字段来解读。因为很多TCP的扩展都是通过新增 选项来实现的。

选项总是在TCP头部的最后,且长度是 8 位的整数倍。全部选项都被包括在 校验和中。选项可从任何字节边界开始。选项的格式有 2 种情况:

  1. 单独的一个字节,代表选项的类型例如:

      End of Option List+--------+|00000000| +--------+ Kind=0No-Operation+--------+|00000001|+--------+Kind=1
    
  2. 第一个字节代表选项的类型,紧跟着的一个字节代表选项的长度,后面跟着 选项的数据。例如:

      Maximum Segment Size+--------+--------+---------+--------+ |00000010|00000100| max seg size     | +--------+--------+---------+--------+ Kind=2  Length=4
    

RFC1337 : TIME-WAIT Assassination Hazards in TCP

在TCP连接中,存在这样一个阶段。该阶段会等待2MSL的 时间,以使得属于当前连接的所有的包都消失掉。这样可以保证再次用相同端口建立连接时, 不会有属于上一个连接的滞留在网络中的包对连接产生干扰。

TIME-WAIT Assassination(TWA)现象

TCP的连接是由四元组(源IP地址,源端口,目的IP地址,目的端口)唯一决定的。但是,存在 这样一种情况,当一个TCP连接关闭,随后,客户端又使用相同的IP和端口号向服务端发起连接, 即产生了和之前的连接一模一样的四元组。此时,如果网络中还存在上一个连接遗留下来的包, 就会出现各类的问题。对于这一问题,RFC793中定义了相关的机制进行应对。

  1. 三次握手时会拒绝旧的SYN段,以避免重复建立连接。

  2. 通过判断序列号可以有效地拒绝旧的或者重复的段被错误地接受。

  3. 通过选择合适的ISN(Initial Sequence Number)可以避免旧的连接和新的连接的段 的序列号空间发生重叠。

  4. TIME-WAIT状态会等待足够长的时间,让旧的滞留在网络上的段因超过其生命周期而消失。

  5. 在系统崩溃后,在系统启动时的静默时间可以使旧的段在连接开始前消失。

然而,其中的状态的相关机制却是不可靠的。网络中滞留的段 有可能会使得状态被意外结束。这一现象即是TIME-WAIT Assassination现象。RFC1337中给出了一个实例:

       TCP A                                                TCP B1.  ESTABLISHED                                          ESTABLISHED(Close)2.  FIN-WAIT-1  --> <SEQ=100><ACK=300><CTL=FIN,ACK>  --> CLOSE-WAIT3.  FIN-WAIT-2  <-- <SEQ=300><ACK=101><CTL=ACK>      <-- CLOSE-WAIT(Close)4.  TIME-WAIT   <-- <SEQ=300><ACK=101><CTL=FIN,ACK>  <-- LAST-ACK5.  TIME-WAIT   --> <SEQ=101><ACK=301><CTL=ACK>      --> CLOSED- - - - - - - - - - - - - - - - - - - - - - - - - - - -5.1. TIME-WAIT   <--  <SEQ=255><ACK=33> ... old duplicate5.2  TIME-WAIT   --> <SEQ=101><ACK=301><CTL=ACK>    -->  ????5.3  CLOSED      <-- <SEQ=301><CTL=RST>             <--  ????(prematurely)

可以看到,TCP A收到了一个遗留的ACK包,之后响应了这个ACK。TCP B收到这个莫名其妙的响应后, 会发出RST报,因为它认为发生了错误。收到RST包后,TCP A的TIME-WAIT状态被终止了。然而, 此时还没有到2MSL的时间。

TWA的危险性及现有的解决方法

RFC1337中列举了三种TWA现象带来的危险。

  1. 滞留在网络上的旧的数据段可能被错误地接受。

  2. 新的连接可能陷入到不同步的状态中。如接收到一个旧的ACK包等情况。

  3. 新的连接可能被滞留在网络中的旧的FIN包关闭。或者是SYN-SENT状态下出现了 意料之外的ACK包等。都可能导致新的连接被终止。

而解决TWA问题的方法较为简单,直接在TIME-WAIT阶段忽略掉所有的RST段即可。 在[subsec:time_wait]中的代码中可以看到,Linux正是采用了这种方法来解决该问题。

第一次握手:构造并发送SYN包

基本调用关系

tcp_v4_connect

的主要作用是进行一系列的判断,初始化传输控制块 并调用相关函数发送SYN包。

/*
Location:net/ipv4/tcp_ipv4.cFunction:这个函数会初始化一个的连接。Parameters:sk:传输控制块uaddr:通用地址结构,包含所属协议字段和相应的地址字段。addr_len :目的地址长度
*/
int tcp_v4_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{/*sockaddr_in结构体用于描述一个Internet (IP) 套接字的地址     */struct sockaddr_in *usin = (struct sockaddr_in *)uaddr; struct inet_sock *inet = inet_sk(sk);struct tcp_sock *tp = tcp_sk(sk);/*网络协议主要使用大端存储,be16 means 16 bits stored with big-endianbe32,the same.*/__be16 orig_sport, orig_dport;__be32 daddr, nexthop;                  struct flowi4 *fl4;struct rtable *rt;int err;struct ip_options_rcu *inet_opt;/* 检验目的地址长度是否有效 */if (addr_len < sizeof(struct sockaddr_in))return -EINVAL;                 //Invalid argument 错误码为22/* 检验协议族是否正确 */if (usin->sin_family != AF_INET)  //IPV4地址域return -EAFNOSUPPORT;     //Address family not supported by protocol/* 将下一跳地址和目的地址暂时设置为用户传入的IP地址 */nexthop = daddr = usin->sin_addr.s_addr;inet_opt = rcu_dereference_protected(inet->inet_opt,sock_owned_by_user(sk));/* 如果选择源地址路由,则将下一跳地址设置为IP选项中的faddr-first hop address*/if (inet_opt && inet_opt->opt.srr) {if (!daddr)return -EINVAL;nexthop = inet_opt->opt.faddr;}

上面 rcu_dereference_protected函数使用了RCU锁,RCU锁的基本介绍参见[Appendix:RCU].

源地址路由是一种特殊的路由策略。一般路由都是通过目的地址来进行的。而有时也需要 通过源地址来进行路由,例如在有多个网卡等情况下,可以根据源地址来决定走哪个网卡等等。

        orig_sport = inet->inet_sport;orig_dport = usin->sin_port;fl4 = &inet->cork.fl.u.ip4;     //对应于ipv4的流/* 获取目标的路由缓存项,如果路由查找命中,则生成一个相应的路由缓存项,这个缓存项不但可以用于当前待发送的SYN段,而且对后续的所有数据包都可以起到一个加速路由查找的作用。*/rt = ip_route_connect(fl4, nexthop, inet->inet_saddr,RT_CONN_FLAGS(sk), sk->sk_bound_dev_if,IPPROTO_TCP,orig_sport, orig_dport, sk);/*判断指针是否有效*/        if (IS_ERR(rt)) {err = PTR_ERR(rt);if (err == -ENETUNREACH)    //Network is unreachableIP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);return err;}

对于函数IS_ERR、PTR_ERR的具体介绍,请参见[Appendix:ERR].

        /*TCP不能使用类型为组播或多播的路由缓存项*/if (rt->rt_flags & (RTCF_MULTICAST | RTCF_BROADCAST)) {ip_rt_put(rt);return -ENETUNREACH;        //Network is unreachable}/* 如果IP选项为空或者没有开启源路由功能,则采用查找到的缓存项 */if (!inet_opt || !inet_opt->opt.srr)daddr = fl4->daddr;/* 如果没有设置源地址,则设置为缓存项中的源地址 */if (!inet->inet_saddr)inet->inet_saddr = fl4->saddr;sk_rcv_saddr_set(sk, inet->inet_saddr);/* 如果该传输控制块的时间戳已被使用过,则重置各状态 rx_opt:  tcp_options_received*/if (tp->rx_opt.ts_recent_stamp && inet->inet_daddr != daddr) {/* Reset inherited state *//*下一个待发送的TCP段中的时间戳回显值*/tp->rx_opt.ts_recent       = 0;     /*从接收到的段中取出时间戳*/tp->rx_opt.ts_recent_stamp = 0;/*What does it means repair*/       if (likely(!tp->repair))tp->write_seq      = 0;}/*  在启用了tw_recycle:time wait recycle的情况下,重设时间戳 它用来快速回收TIME_WAIT连接.*/if (tcp_death_row.sysctl_tw_recycle &&!tp->rx_opt.ts_recent_stamp && fl4->daddr == daddr)tcp_fetch_timewait_stamp(sk, &rt->dst);/* 设置传输控制块 */inet->inet_dport = usin->sin_port;sk_daddr_set(sk, daddr);inet_csk(sk)->icsk_ext_hdr_len = 0;if (inet_opt)inet_csk(sk)->icsk_ext_hdr_len = inet_opt->opt.optlen;/* 设置MSS大小 */tp->rx_opt.mss_clamp = TCP_MSS_DEFAULT;/* Socket identity is still unknown (sport may be zero).* However we set state to SYN-SENT and not releasing socket* lock select source port, enter ourselves into the hash tables and* complete initialization after this.*//* 将TCP的状态设置为SYN_SENT */tcp_set_state(sk, TCP_SYN_SENT);err = inet_hash_connect(&tcp_death_row, sk);if (err)goto failure;sk_set_txhash(sk);/*如果源端口或者目的端口发生改变,则需要重新查找路由,并用新的路由缓存项更新sk中保存的路由缓存项。*/rt = ip_route_newports(fl4, rt, orig_sport, orig_dport,inet->inet_sport, inet->inet_dport, sk);if (IS_ERR(rt)) {err = PTR_ERR(rt);rt = NULL;goto failure;}/* 将目的地址提交到套接字  */sk->sk_gso_type = SKB_GSO_TCPV4;sk_setup_caps(sk, &rt->dst);/*  如果没有设置序号,则计算初始序号 序号与双方的地址与端口号有关系*/if (!tp->write_seq && likely(!tp->repair))tp->write_seq = secure_tcp_sequence_number(inet->inet_saddr,inet->inet_daddr,inet->inet_sport,usin->sin_port);/*  计算IP首部的id域的值 全局变量jiffies用来记录自系统启动以来产生的节拍的总数       */inet->inet_id = tp->write_seq ^ jiffies;/* 调用tcp_connect构造并发送SYN包*/err = tcp_connect(sk);rt = NULL;if (err)goto failure;return 0;

总结起来,是在根据用户提供的目的地址, 设置好了传输控制块,为传输做好准备。如果在这一过程中出现错误,则会跳到 错误处理代码。

failure:/* 将状态设定为TCP_CLOSE,释放端口,并返回错误值。*/tcp_set_state(sk, TCP_CLOSE);ip_rt_put(rt);sk->sk_route_caps = 0;inet->inet_dport = 0;return err;

tcp_connect

上面的会进行一系列的判断,之后真正构造SYN包的部分 被放在了中。接下来,我们来分析这个函数。

/*
Location:net/ipv4/tcp_output.cFunction:该函数用于构造并发送SYN包。Parameter:sk:传输控制块。*/
int tcp_connect(struct sock *sk)
{struct tcp_sock *tp = tcp_sk(sk);struct sk_buff *buff;int err;/* 初始化tcp连接 */tcp_connect_init(sk);

关于tcp_connect_init更多的内容,请参见[TCPInitialize:tcp_connect_init]。

        if (unlikely(tp->repair)) {             //what does it mean repair?/* 如果repair位被置1,那么结束TCP连接 */tcp_finish_connect(sk, NULL);return 0;}/* 分配一个sk_buff */buff = sk_stream_alloc_skb(sk, 0, sk->sk_allocation, true);if (unlikely(!buff))return -ENOBUFS;                //No buffer space available/* 初始化skb,并自增write_seq的值 */tcp_init_nondata_skb(buff, tp->write_seq++, TCPHDR_SYN);

关于tcp_init_nondata_skb更多的内容,请参见[TCPInitialize:tcp_init_nondata_skb]。

        /* 设置时间戳 */tp->retrans_stamp = tcp_time_stamp;/* 将当前的sk_buff添加到发送队列中 */tcp_connect_queue_skb(sk, buff);/* ECN (Explicit Congestion Notification,)支持显式拥塞控制*/tcp_ecn_send_syn(sk, buff);         //what does this function do?/* 发送SYN包,这里同时还考虑了Fast Open的情况,意思就是可以在连接建立的时候同时发数据。*/err = tp->fastopen_req ? tcp_send_syn_data(sk, buff) :tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);if (err == -ECONNREFUSED)           /*Connection refused*/return err;/* We change tp->snd_nxt after the tcp_transmit_skb() call* in order to make this packet get counted in tcpOutSegs.*/tp->snd_nxt = tp->write_seq;     //send next 下一个待发送字节的序列号/*pushed_seq means Last pushed seq, required to talk to windows???*/        tp->pushed_seq = tp->write_seq;TCP_INC_STATS(sock_net(sk), TCP_MIB_ACTIVEOPENS);/* 设定超时重传定时器 */inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,inet_csk(sk)->icsk_rto, TCP_RTO_MAX);return 0;
}

转载于:https://my.oschina.net/fzyz999/blog/704510

Linux4.4 TCP源码分析相关推荐

  1. 【Linux4.1.12源码分析】协议栈gro收包之TCP处理

    TCP gro实现定义在tcpv4_offload对象 static const struct net_offload tcpv4_offload = {.callbacks = {.gso_segm ...

  2. 【Linux4.1.12源码分析】VXLAN报文内核协议栈处理

    4.1.12内核已经支持vxlan报文的gro功能,意味着vxlan报文交给协议栈之前,已经被聚合过了,而在早期的内核中聚合逻辑是在encap_rcv函数之后实现的. 之前分析的UDP报文处理中,可以 ...

  3. 【Linux4.1.12源码分析】协议栈gro收包之MAC层处理

    <span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255) ...

  4. TCP拥塞控制算法BBR源码分析

      BBR是谷歌与2016年提出的TCP拥塞控制算法,在Linux4.9的patch中正式加入.该算法一出,瞬间引起了极大的轰动.在CSDN上也有众多大佬对此进行分析讨论,褒贬不一.   本文首先对源 ...

  5. tcp/ip 协议栈Linux内核源码分析15 udp套接字接收流程二

    内核版本:3.4.39 上篇我们分析了UDP套接字如何接收数据的流程,最终它是在内核套接字的接收队列里取出报文,剩下的问题就是谁会去写入这个队列,当然,这部分工作由内核来完成,本篇剩下的文章主要分析内 ...

  6. activeMQ的源码分析 -TCP通讯机制

    2019独角兽企业重金招聘Python工程师标准>>> activeMQ的源码分析 -TCP通讯机制 博客分类: MQ <IGNORE_JS_OP style="WO ...

  7. 基于TCP网络通信的自动升级程序源码分析-客户端接收文件

    升级程序客户端接收文件 /// <summary>/// 文件数据缓存 索引是 ConnectionInfo对象 数据包的顺序号 值是数据/// </summary>Dicti ...

  8. tcpip四层源码分析(Linux)

    tcp/ip四层源码分析(Linux)之socket.c 文章目录 tcp/ip四层源码分析(Linux)之socket.c 1.socket层 2.INET socket层 socket结构体解释 ...

  9. JDK源码分析 NIO实现

    总列表:http://hg.openjdk.java.net/ 小版本:http://hg.openjdk.java.net/jdk8u jdk:http://hg.openjdk.java.net/ ...

最新文章

  1. 16位汇编 call调用函数 通过栈来传递参数
  2. PAT1047 编程团体赛 (20 分)
  3. 学习笔记-error LNK2019
  4. 微信微调助手WeChatTweak for mac(微信多开和防撤回工具)最新版
  5. pdf打印机怎么把PDF文件打印成JPG
  6. 常用的Unicode码表(汉字从A到Z、数字、英文)
  7. C、C++编程学习资料收藏
  8. php的rps,如何理解RPS的本质
  9. 从Altium官方网站下载库文件
  10. golden ticket和sliver ticket的区别是什么?
  11. Ubuntu20.04安装nvidia显卡驱动并解决重启后黑屏问题
  12. 实验二 单隐层神经网络
  13. XTF格式侧扫声呐数据格式解析
  14. 飞飞cms模板,飞飞cms自适应模板,飞飞cms影视模板
  15. seneca mysql_Package - seneca-mysql-store-ex
  16. SpeedMent入门集成SpringBootStream常见操作
  17. 原来多多输入法生成器可以换图标
  18. 屏蔽浏览器f1帮助,启用自己的帮助
  19. python抢票代码_如何使用python爬取抢票?
  20. Jquery实现花瓣随机飘落(收藏自慕课网)

热门文章

  1. 关于做Android+J2ee系统集成开发的一点心得
  2. 聪明的程序员用Delphi,真正的程序员用C++,偷懒的程序员用PowerShell
  3. httpruner学习--安装和认识
  4. JS实现HashMap
  5. Fedora26 tftp-server设置
  6. 2016年十大存储预测
  7. Oracle Clustered Table
  8. SCOM2012部署系列之九:部署审核收集报告(ACSReporting)
  9. Quartz.Net 1.30的一些设置说明
  10. COM组件开发实践(七)---多线程ActiveX控件和自动调整ActiveX控件大小(上)