Linux内核传输层TCP源码分析

embedded/2025/3/17 10:43:44/

一、传输控制协议(TCP)

 1.tcp报头        

        TCP 是 Internet 中最常用的传输协议,很多著名协议都基于 TCP。其中最著名的可能就是 HTTP,但这里有必要提及其它一些著名协议,如 SSH、SMTP、SSL 等,不同于 UDP,TCP 提供面向连接的可靠传输,是通过使用序列号和确认来实现的。

        TCP 内具体报头 20 字节,不过在使用 TCP 选项时它最长可达 60 字节,具体如下:

 2.TCP初始化操作

        定义对象tcp_protocol(net_protocol对象),使用inet_add_protocol()来添加它,具体如下:

  1. 初始化时机
    代码中 inet_init 函数带有 __init 修饰符,这是 Linux 内核初始化阶段的典型标识。因此,TCP 相关初始化(如 tcp_protocol 注册)的时机是 内核初始化阶段,在系统启动过程中执行。

  2. 初始化次数
    __init 修饰的函数仅在系统启动初始化时执行一次。inet_init 中通过 proto_register(&tcp_prot, 1) 完成 TCP 协议相关注册,整个过程只在 内核初始化时执行一次,不会重复触发。

3.TCP定时器以及TCP套接字初始化操作

        TCP 使用定时器有 4 个:重传定时器、延迟确认定时器、存活定时器、持续定时器。
使用 TCP 套接字,用户空间应用程序必须创建一个 SOCK_STREAM 套接字,且调用系统调用 socket (),内核中由回调函数 tcp_v4_init_sock () 来处理,实际完成工作由 tcp_init_sock ()。
TCP 连接的建立和拆除及 TCP 连接的属性都被描述为状态机的状态,在给定时点,TCP 套接字将处于指定的任何一种状态。在 TCP 客户端和 TCP 服务器之间,使用三次握手建立 TCP 连接。

一、TCP 定时器功能

  1. 重传定时器

    • 作用:负责重传在指定时间内未收到确认的数据包,确保数据可靠传输。若发送方发送数据后未及时收到接收方的确认,重传定时器触发重传机制。
  2. 延迟确认定时器

    • 作用:推迟发送确认数据包。接收方收到数据后,不立即发送确认,而是通过延迟确认定时器控制确认发送时机,减少网络中确认包的数量,优化传输效率。
  3. 存活定时器

    • 作用:检查连接是否断开。若在一定时间内未收到对方数据,存活定时器触发,判断连接是否失效,用于检测长期空闲的连接状态。
  4. 零窗口探测定时器(持续定时器)

    • 作用:当接收方缓冲区满并通告发送方 “零窗口”(停止发送数据)后,发送方通过持续定时器定期探测接收方窗口状态,一旦窗口更新(非零),立即恢复数据发送。

二、TCP 套接字初始化流程

  1. 用户空间操作

    • 应用程序需创建 SOCK_STREAM 类型套接字,通过系统调用 socket() 发起请求。
  2. 内核处理

    • 内核通过回调函数 tcp_v4_init_sock() 响应,实际初始化工作由 tcp_init_sock() 完成,主要任务包括:
      • 状态设置:将套接字状态初始化为 TCP_CLOSE(关闭状态)。
      • 定时器初始化:调用 tcp_init_xmit_timers() 初始化 TCP 相关定时器(如上述重传、延迟确认等定时器)。
      • 缓冲区初始化:配置发送缓冲区(sk_sndbuf)和接收缓冲区(sk_rcvbuf),规划数据收发的缓存空间。
      • 队列初始化:初始化套接字的发送队列、接收队列及预备队列,为数据收发和管理做好准备。
      • 参数初始化:设置套接字的各种参数,确保网络通信的基础配置就绪。

3.建立连接和断开连接

  • 三次握手
    • 客户端:客户端调用connect函数发起连接请求,该函数会触发inet_connection_sock_connect,进而调用tcp_connect。在tcp_connect函数中,会构造一个带有SYN标志的 TCP 报文段并发送出去,客户端进入SYN_SENT状态 。
    • 服务器:当服务器的网络接口接收到客户端的SYN报文后,会通过一系列网络层处理,最终由tcp_v4_rcv函数接收并处理。tcp_v4_rcv会调用tcp_rcv_state_process函数,根据当前连接状态机的状态(此时处于监听状态),处理SYN包并构造带有SYN+ACK标志的响应报文发送给客户端,服务器进入SYN_RECV状态。
    • 客户端:客户端收到服务器的SYN+ACK报文后,还是由tcp_v4_rcvtcp_rcv_state_process函数处理,然后发送ACK确认报文,客户端进入ESTABLISHED状态。
    • 服务器:服务器收到客户端的ACK报文后,同样经tcp_v4_rcvtcp_rcv_state_process处理,也进入ESTABLISHED状态,至此三次握手完成。
  • 四次挥手
    • 主动关闭方(假设为客户端):当应用层调用close函数关闭套接字时,会触发inet_shutdown,进而调用tcp_closetcp_close函数会构造一个带有FIN标志的报文段发送给服务器,客户端进入FIN_WAIT1状态。
    • 被动关闭方(服务器):服务器收到FIN报文后,由tcp_v4_rcvtcp_rcv_state_process函数处理,然后发送ACK确认报文,服务器进入CLOSE_WAIT状态。客户端收到ACK后进入FIN_WAIT2状态。
    • 被动关闭方(服务器):当服务器应用层也调用close函数关闭套接字时,同样会触发inet_shutdowntcp_close,构造并发送FIN报文,服务器进入LAST_ACK状态。
    • 主动关闭方(客户端):客户端收到服务器的FIN报文后,经tcp_v4_rcvtcp_rcv_state_process处理,发送ACK确认报文,然后进入TIME_WAIT状态。服务器收到ACK后进入CLOSED状态。在经过两倍的最大报文段生存时间(2MSL)后,客户端也进入CLOSED状态 ,四次挥手完成。

4.接收网络层的TCP数据包tcp_v4_rcv(...)

        方法tcp_v4_rcv是负责接收来自网络层的TCP数据包的主要处理程序,流程如下:

函数功能概述

tcp_v4_rcv 函数是 Linux 内核中用于处理 IPv4 TCP 数据包接收的核心函数。它负责接收来自网络的 TCP 数据包,进行一系列的有效性检查,如数据包类型、头部长度、校验和等,查找对应的套接字,根据套接字的状态进行不同的处理,最后将数据包传递给合适的处理函数进行进一步处理。

参数说明

  • struct sk_buff *skb:指向套接字缓冲区(sk_buff)的指针,该缓冲区包含了接收到的 TCP 数据包以及相关的网络层和传输层头部信息。

重要部分讲解

1. 数据包基本检查
if (skb->pkt_type != PACKET_HOST)goto discard_it;if (!pskb_may_pull(skb, sizeof(struct tcphdr)))goto discard_it;th = (const struct tcphdr *)skb->data;if (unlikely(th->doff < sizeof(struct tcphdr) / 4))goto bad_packet;
if (!pskb_may_pull(skb, th->doff * 4))goto discard_it;if (skb_checksum_init(skb, IPPROTO_TCP, inet_compute_pseudo))goto csum_error;

这部分代码对接收的数据包进行基本检查,包括数据包类型是否为 PACKET_HOST(即是否是发送给本地主机的)、TCP 头部长度是否合法、是否能够提取完整的 TCP 头部以及校验和初始化是否成功。如果检查不通过,则跳转到相应的错误处理标签处。

2. 套接字查找
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,th->dest, sdif, &refcounted);
if (!sk)goto no_tcp_socket;

根据接收到的 TCP 数据包中的源端口、目的端口等信息,在 TCP 哈希表中查找对应的套接字。如果未找到匹配的套接字,则跳转到 no_tcp_socket 标签处进行处理。

3. 套接字状态处理
  • TIME_WAIT 状态
if (sk->sk_state == TCP_TIME_WAIT)goto do_time_wait;

如果套接字处于 TIME_WAIT 状态,则跳转到 do_time_wait 标签处进行特殊处理,如检查策略、校验和,根据 tcp_timewait_state_process 函数的返回值进行不同的操作,如发送 SYN 时尝试查找监听套接字、发送 ACK 或 RST 等。

  • NEW_SYN_RECV 状态
if (sk->sk_state == TCP_NEW_SYN_RECV) {// ...
}

当套接字处于 TCP_NEW_SYN_RECV 状态时,会进行一系列的检查,如 MD5 哈希验证、校验和检查等。如果检查通过,会调用 tcp_check_req 函数来处理 SYN 包,可能会创建新的套接字并将数据包传递给新套接字进行处理。

 
  • LISTEN 状态
if (sk->sk_state == TCP_LISTEN) {ret = tcp_v4_do_rcv(sk, skb);goto put_and_return;
}

如果套接字处于 LISTEN 状态,则调用 tcp_v4_do_rcv 函数来处理接收到的数据包,并跳转到 put_and_return 标签处进行资源释放和返回操作。

4. 数据包处理与入队
bh_lock_sock_nested(sk);
tcp_segs_in(tcp_sk(sk), skb);
ret = 0;
if (!sock_owned_by_user(sk)) {skb_to_free = sk->sk_rx_skb_cache;sk->sk_rx_skb_cache = NULL;ret = tcp_v4_do_rcv(sk, skb);
} else {if (tcp_add_backlog(sk, skb))goto discard_and_relse;skb_to_free = NULL;
}
bh_unlock_sock(sk);

对套接字进行加锁,调用 tcp_segs_in 函数更新 TCP 统计信息。如果套接字没有被用户空间占用,则调用 tcp_v4_do_rcv 函数处理数据包;否则,将数据包添加到套接字的接收队列中。最后解锁套接字。

        

5.发送TCP数据包tcp_sendmsg(...)

        从用户空间中创建的TCP套接字发送数据包,可以使用多个系统调用,包括send()、sendto()、sendmsg()和write(),系统调用最终由方法tcp_sendmsg处理,它将来自用户空间的数据的有效负载复制到内核空间,将其最为TCP数据段进行发送。

函数功能

    tcp_sendmsg 函数是 Linux 内核中用于通过 TCP 套接字发送数据的接口函数。它的主要功能是将用户空间的数据封装成 TCP 数据包并发送出去。该函数首先对套接字进行加锁,然后调用 tcp_sendmsg_locked 函数来实际处理数据发送,最后释放套接字锁。

参数说明

  • struct sock *sk:指向套接字的指针,该套接字表示当前进行 TCP 数据发送操作所使用的套接字对象,其中包含了套接字的各种状态信息和配置。
  • struct msghdr *msg:指向 msghdr 结构体的指针,这个结构体包含了要发送的数据以及目标地址等信息,例如数据缓冲区、目标地址结构体等。
  • size_t size:表示要发送的数据的长度。

重要部分讲解

1. 加锁与解锁
lock_sock(sk);
ret = tcp_sendmsg_locked(sk, msg, size);
release_sock(sk);

这部分代码对套接字进行加锁,以确保在多线程环境下对套接字的操作是线程安全的。在调用 tcp_sendmsg_locked 函数完成数据发送后,释放套接字锁。

2. 零拷贝处理
if (flags & MSG_ZEROCOPY && size && sock_flag(sk, SOCK_ZEROCOPY)) {skb = tcp_write_queue_tail(sk);uarg = sock_zerocopy_realloc(sk, size, skb_zcopy(skb));if (!uarg) {err = -ENOBUFS;goto out_err;}zc = sk->sk_route_caps & NETIF_F_SG;if (!zc)uarg->zerocopy = 0;
}

如果设置了 MSG_ZEROCOPY 标志,并且套接字支持零拷贝,会尝试进行零拷贝操作。如果分配零拷贝所需的资源失败,则返回错误。

3. 等待连接建立
if (((1 << sk->sk_state) & ~(TCPF_ESTABLISHED | TCPF_CLOSE_WAIT)) &&!tcp_passive_fastopen(sk)) {err = sk_stream_wait_connect(sk, &timeo);if (err != 0)goto do_error;
}

如果 TCP 连接尚未建立(除了 TCP Fast Open 被动侧),则等待连接建立。如果等待过程中出现错误,则跳转到错误处理部分。

4. 处理控制消息
if (msg->msg_controllen) {err = sock_cmsg_send(sk, msg, &sockc);if (unlikely(err)) {err = -EINVAL;goto out_err;}
}

如果 msg 中包含控制消息,则调用 sock_cmsg_send 函数处理这些控制消息。如果处理过程中出现错误,则释放相关资源并返回错误码。

5. 数据分段与发送
while (msg_data_left(msg)) {int copy = 0;skb = tcp_write_queue_tail(sk);if (skb)copy = size_goal - skb->len;if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {// 创建新的 SKB...}// 尝试将数据追加到 SKB 末尾...copied += copy;if (!msg_data_left(msg)) {if (unlikely(flags & MSG_EOR))TCP_SKB_CB(skb)->eor = 1;goto out;}if (skb->len < size_goal || (flags & MSG_OOB) || unlikely(tp->repair))continue;if (forced_push(tp)) {tcp_mark_push(tp, skb);__tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);} else if (skb == tcp_send_head(sk))tcp_push_one(sk, mss_now);continue;
}

这部分代码是数据发送的核心部分,它会不断地将用户数据分段并封装成 SKB(套接字缓冲区)。如果当前 SKB 空间不足或无法合并数据,则创建新的 SKB。在数据封装完成后,如果满足一定条件,会调用 tcp_push 或 tcp_push_one 函数将数据发送出去。

6.TCP使用流程 

1. 用户层

用户在主机 192.168.186.138 上运行的应用程序若要向 47.95.193.211 的 8888 端口发送数据,会使用系统提供的套接字 API,像 socket()connect()send() 等。应用程序调用 send() 时,数据会从用户空间复制到内核空间,这时候内核中的 TCP 模块开始发挥作用。

2. 传输层(TCP)

  • 套接字(struct sock
    • 它是内核中代表 TCP 连接的核心数据结构。应用程序创建套接字时,内核会生成对应的 sock 结构体。该结构体存有连接的状态(如 ESTABLISHEDSYN_SENT 等)、源端口、目的端口等信息。
    • 当调用 send() 时,内核借助 sock 结构体找到对应的 TCP 连接,从而进行后续操作。
  • sk_buff(套接字缓冲区)
    • 用于存储待发送的数据和协议头部信息。TCP 模块会把用户数据封装成 TCP 段,为其添加 TCP 头部(包含源端口、目的端口、序列号、确认号等),然后将这些信息填充到 sk_buff 中。
    • 填充好的 sk_buff 会传递给 IP 层进行进一步处理。

3. 网络层(IP)

  • 路由表(rt_table
    • 路由表用于确定数据包的下一跳地址。内核会依据目的 IP 地址 47.95.193.211 查找路由表,找出合适的出口设备(如网卡)和下一跳 IP 地址。
    • 若路由表中没有匹配项,数据包可能会被丢弃,或者发送 ICMP 错误消息。
  • sk_buff 再次处理
    • IP 层会在 sk_buff 中添加 IP 头部(包含源 IP 地址 192.168.186.138、目的 IP 地址 47.95.193.211、协议类型(这里是 TCP)等)。
    • 经过 IP 层处理后的 sk_buff 会被传递到链路层。

4. 链路层

  • ARP 表(neigh_table)和邻居表项(neighbour
    • 由于 IP 地址是网络层地址,而链路层(如以太网)需要使用 MAC 地址进行通信,所以内核需要将下一跳 IP 地址转换为对应的 MAC 地址。
    • 内核会查询 ARP 表(neigh_table),看是否存在下一跳 IP 地址对应的邻居表项(neighbour)。若存在,就可以获取到对应的 MAC 地址;若不存在,内核会发送 ARP 请求广播,询问拥有该 IP 地址的设备的 MAC 地址。
    • 收到 ARP 响应后,内核会更新 ARP 表,并将 MAC 地址记录到对应的邻居表项中。
  • sk_buff 最后处理
    • 链路层会在 sk_buff 中添加链路层头部(如以太网头部,包含源 MAC 地址、目的 MAC 地址、帧类型等),以及尾部(如帧校验序列 FCS)。
    • 处理完成的 sk_buff 会被发送到物理层。

5. 物理层

物理层负责将链路层传递过来的二进制数据转换为电信号、光信号或无线信号,通过物理介质(如网线、光纤、无线信道)传输到目标主机。

6. 目标主机接收数据

  • 物理层和链路层:目标主机的物理层接收到信号后,将其转换为二进制数据,链路层会去除链路层头部和尾部,对数据进行校验。若校验无误,就将数据传递给网络层。
  • 网络网络层会去除 IP 头部,检查目的 IP 地址是否为本机地址。若匹配,就将数据传递给传输层。
  • 传输层(TCP):TCP 层会去除 TCP 头部,根据序列号和确认号对数据进行排序和确认,然后将数据传递给对应的应用程序。
  • 应用层:目标主机上的应用程序通过套接字 API 接收数据,完成整个通信过程。

http://www.ppmy.cn/embedded/173327.html

相关文章

第4节:分类任务

引入&#xff1a; 独热编码&#xff08;one-hot&#xff09;&#xff1a;对于分类任务的输出&#xff0c;也就是是或不是某类的问题&#xff0c;采取独热编码的形式将y由一离散值转化为连续的概率分布&#xff0c;最大值所在下标为预测类 输入的处理&#xff1a;对于任意一张…

DeepSeek:为教培小程序赋能,引领行业变革新潮流

在竞争日益激烈的教培行业中&#xff0c;一款搭载DeepSeek技术的创新小程序正悄然掀起一场变革的浪潮&#xff0c;为教育机构带来前所未有的显著效益。DeepSeek&#xff0c;凭借其强大的AI能力&#xff0c;正在重新定义教培行业的教学方式、学习体验以及运营管理&#xff0c;为…

入门到入土,Java学习 day19(多线程上)

多线程 让程序同时做多件事情 线程&#xff1a;是操作系统能够进行运算调度的最小单位。它被包含在进程之中&#xff0c;是进程中的实际运作单位。 应用场景&#xff1a;拷贝迁移大文件&#xff0c;加载大量的资源文件 并发和并行 并发&#xff1a;同一时刻&#xff0c;多…

Python中在类中创建对象

在 Python 中&#xff0c;在类的定义体&#xff08;class body&#xff09;中直接创建对象是不合理的&#xff0c;但在类方法&#xff08;classmethod&#xff09;或静态方法&#xff08;staticmethod&#xff09;中创建对象是合理的&#xff0c;主要是因为作用域、生命周期和设…

玩转github

me github 可以给仓库添加开发人员吗 4o 是的&#xff0c;GitHub允许仓库管理员为仓库添加开发人员&#xff0c;并设置这些开发人员的角色和权限。这里是一个简单的步骤指导&#xff0c;教你如何给一个 GitHub 仓库添加开发人员&#xff1a; 前提条件 你必须有这个仓库的权限&…

不像人做的题————十四届蓝桥杯省赛真题解析(上)A,B,C,D题解析

题目A&#xff1a;日期统计 思路分析&#xff1a; 本题的题目比较繁琐&#xff0c;我们采用暴力加DFS剪枝的方式去做&#xff0c;我们在DFS中按照8位日期的每一个位的要求进行初步剪枝找出所有的八位子串&#xff0c;但是还是会存在19月的情况&#xff0c;为此还需要在CHECK函数…

C语言结构体全面解析 | 从入门到精通

&#x1f4da; C语言结构体全面解析 | 从入门到精通 整理&#xff1a;算法练习生| 转载请注明出处 &#x1f4d1; 目录 结构体的定义与使用结构体变量的参数传递结构体数组结构体指针typedef关键字结构体初始化 1️⃣ 结构体的定义与使用 为什么需要结构体&#xff1f; 当…

【C++】 —— 笔试刷题day_6

刷题day_6&#xff0c;继续加油哇&#xff01; 今天这三道题全是高精度算法 一、大数加法 题目链接&#xff1a;大数加法 题目解析与解题思路 OK&#xff0c;这道题题目描述很简单&#xff0c;就是给我们两个字符串形式的数字&#xff0c;让我们计算这两个数字的和 看题目我…