一、传输控制协议(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()来添加它,具体如下:
初始化时机:
代码中inet_init
函数带有__init
修饰符,这是 Linux 内核初始化阶段的典型标识。因此,TCP 相关初始化(如tcp_protocol
注册)的时机是 内核初始化阶段,在系统启动过程中执行。初始化次数:
__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 定时器功能
重传定时器
- 作用:负责重传在指定时间内未收到确认的数据包,确保数据可靠传输。若发送方发送数据后未及时收到接收方的确认,重传定时器触发重传机制。
延迟确认定时器
- 作用:推迟发送确认数据包。接收方收到数据后,不立即发送确认,而是通过延迟确认定时器控制确认发送时机,减少网络中确认包的数量,优化传输效率。
存活定时器
- 作用:检查连接是否断开。若在一定时间内未收到对方数据,存活定时器触发,判断连接是否失效,用于检测长期空闲的连接状态。
零窗口探测定时器(持续定时器)
- 作用:当接收方缓冲区满并通告发送方 “零窗口”(停止发送数据)后,发送方通过持续定时器定期探测接收方窗口状态,一旦窗口更新(非零),立即恢复数据发送。
二、TCP 套接字初始化流程
用户空间操作
- 应用程序需创建
SOCK_STREAM
类型套接字,通过系统调用socket()
发起请求。内核处理
- 内核通过回调函数
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_rcv
和tcp_rcv_state_process
函数处理,然后发送ACK
确认报文,客户端进入ESTABLISHED
状态。 - 服务器:服务器收到客户端的
ACK
报文后,同样经tcp_v4_rcv
和tcp_rcv_state_process
处理,也进入ESTABLISHED
状态,至此三次握手完成。
- 客户端:客户端调用
- 四次挥手:
- 主动关闭方(假设为客户端):当应用层调用
close
函数关闭套接字时,会触发inet_shutdown
,进而调用tcp_close
。tcp_close
函数会构造一个带有FIN
标志的报文段发送给服务器,客户端进入FIN_WAIT1
状态。 - 被动关闭方(服务器):服务器收到
FIN
报文后,由tcp_v4_rcv
和tcp_rcv_state_process
函数处理,然后发送ACK
确认报文,服务器进入CLOSE_WAIT
状态。客户端收到ACK
后进入FIN_WAIT2
状态。 - 被动关闭方(服务器):当服务器应用层也调用
close
函数关闭套接字时,同样会触发inet_shutdown
和tcp_close
,构造并发送FIN
报文,服务器进入LAST_ACK
状态。 - 主动关闭方(客户端):客户端收到服务器的
FIN
报文后,经tcp_v4_rcv
和tcp_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
结构体。该结构体存有连接的状态(如ESTABLISHED
、SYN_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. 目标主机接收数据