Linux网络协议栈:关闭一个还有没发送数据完的TCP连接

监视和调整Linux网络协议栈:接收数据

监控和调整Linux网络协议栈的图解指南:接收数据

Linux网络 - 数据包的接收过程

Linux网络协议栈:网络包接收过程

Linux内核网络协议栈:udp数据包发送(源码解读)

TCP/IP网络协议栈面试经典题目

TCP/IP网络协议栈:以太网数据包结构、802.3

TCP/IP网络协议栈:ARP协议详解

TCP / IP攻击:ARP缓存中毒的基本原理、TCP序列号预测和TCP重置攻击

TCP/IP网络协议栈:IP协议

TCP/IP网络协议栈:以太网数据包结构、802.3

TCP/IP网络协议栈:ARP协议详解

TCP / IP攻击:ARP缓存中毒的基本原理、TCP序列号预测和TCP重置攻击

目录

背景

关闭 TCP 连接过程

 sys_close() 

sock_release() 

inet_release() 

tcp_close()

tcp_send_fin() 


 

背景


有一次,光神 在群问了个问题:

当 close 一个 TCP 连接时,如果还有没发送完的数据在缓冲区中,内核会怎么处理?

当时我认为,因为关闭 TCP 连接会触发四次挥手过程,而为了让四次挥手能够快速完成,应该会把发送缓冲区的数据清空,然后发送四次挥手的数据包。

带着疑问,我去查阅 Linux 源码的实现,下面就是关闭一个 TCP 连接的过程。

 

关闭 TCP 连接过程


关闭一个 TCP 连接可以使用 close() 系统调用,我们来分析一下当调用 close() 关闭一个 TCP 连接时会发生什么事情。

当调用 close() 系统调用时,会触发调用 sys_close() 内核函数,其实现如下:

 

 sys_close() 


asmlinkage long sys_close(unsigned int fd)
{
    struct file * filp;
    struct files_struct *files = current->files;
    ...
    return filp_close(filp, files);
    ...
}

sys_close() 函数最终会调用 filp_close() 函数来关闭文件(由于在 Linux 中 socket 是一种特殊的文件),我们接着分析 filp_close() 函数的实现:

int filp_close(struct file *filp, fl_owner_t id)
{
    ...
    fput(filp);
    return retval;
}

void fput(struct file * file)
{
    ...
    if (atomic_dec_and_test(&file->f_count)) {
        ...
        if (file->f_op && file->f_op->release)
            file->f_op->release(inode, file);
        ...
    }
}

可以看到,最终会调用文件系统对应的 release() 方法来处理关闭操作。对于 socket 文件系统,release() 方法对应的是 sock_close() 函数,而 sock_close() 函数最终会调用 sock_release() 函数,所以我们来看看 sock_release() 函数的实现:

 

sock_release() 


void sock_release(struct socket *sock)
{
    if (sock->ops)
        sock->ops->release(sock);
    ...
}

sock_release() 函数也很简单,就是调用对应 协议族 的 release() 方法,因为 Linux 的 socket 文件系统可以支持多种协议族,比如 INETUnix Domain SocketNetlink 等。而对应 INET协议族(网络) 来说,这个 release() 方法对应的是 inet_release() 函数,inet_release() 函数实现如下:

 

inet_release() 


int inet_release(struct socket *sock)
{
    struct sock *sk = sock->sk;

    if (sk) {
        long timeout;
        ...
        timeout = 0;
        if (sk->linger && !(current->flags & PF_EXITING))
            timeout = sk->lingertime;
        sock->sk = NULL;
        sk->prot->close(sk, timeout);
    }
    return(0);
}

inet_release() 函数最终会调用对应 传输层(TCP或者UDP) 的 close() 方法,对于 TCP协议 来说,close() 方法对应的是 tcp_close() 函数,tcp_close() 就是关闭 TCP 连接的最后站点。

 

tcp_close()


由于 tcp_close() 函数比较复杂,我们这里只分析当发生缓冲区还有数据的情况下,内核会怎么处理缓冲区的数据。


void tcp_close(struct sock *sk, long timeout)
{
    struct sk_buff *skb;
    int data_was_unread = 0;

    ...
    // 如果接收缓冲区有数据, 那么先清空接收缓冲区的数据
    while((skb= __skb_dequeue(&sk->receive_queue)) != NULL) {
        u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq - skb->h.th->fin;
        data_was_unread += len;
        __kfree_skb(skb);
    }

    ...
    if (data_was_unread != 0) {                // 如果接收缓冲区有数据没有处理
        tcp_set_state(sk, TCP_CLOSE);          // 把socket状态设置为TCP_CLOSE
        tcp_send_active_reset(sk, GFP_KERNEL); // 发送一个reset包给对端连接
    } else if (sk->linger && sk->lingertime==0) {
        ...
    } else if (tcp_close_state(sk)) {
        tcp_send_fin(sk); // 开始发生四次挥手包
    }
    ...
}

从 tcp_close() 函数的实现可以看出,关闭过程主要有两种情况:

  • 如果接收缓冲区还有数据没有被用户处理,那么就先把接收缓冲区的数据清空,并且发送一个 reset 包给对端连接。

  • 如果接收缓冲区没有数据,那么就调用 tcp_send_fin() 函数开始进行四次挥手过程。

四次挥手过程如下图:

 

tcp_send_fin() 


接下来,我们分析 tcp_send_fin() 函数的实现:

void tcp_send_fin(struct sock *sk)
{
    struct tcp_opt *tp = &(sk->tp_pinfo.af_tcp);
    struct sk_buff *skb = skb_peek_tail(&sk->write_queue); // 发送缓冲区列表最后一个缓冲块
    unsigned int mss_now;
    ...
    if (tp->send_head != NULL) {                         // 如果发送缓冲区不为空
        TCP_SKB_CB(skb)->flags |= TCPCB_FLAG_FIN;        // 把最后一个发送缓冲块设置FIN标志
        TCP_SKB_CB(skb)->end_seq++;
        tp->write_seq++;
    } else {                                             // 如果发送缓冲区为空
        for (;;) {
            skb = alloc_skb(MAX_TCP_HEADER, GFP_KERNEL); // 申请一个新的缓冲块
            if (skb)
                break;
            current->policy |= SCHED_YIELD;
            schedule();
        }

        skb_reserve(skb, MAX_TCP_HEADER);
        skb->csum = 0;
        TCP_SKB_CB(skb)->flags = (TCPCB_FLAG_ACK | TCPCB_FLAG_FIN); // 设置FIN标志
        TCP_SKB_CB(skb)->sacked = 0;

        TCP_SKB_CB(skb)->seq = tp->write_seq;
        TCP_SKB_CB(skb)->end_seq = TCP_SKB_CB(skb)->seq + 1;
        tcp_send_skb(sk, skb, 1, mss_now); // 发送给对端连接
    }
    ...
}

在 tcp_send_fin() 函数我们终于找到了当发送缓冲区不为空的处理,当发送缓冲区不为空时,首先会获取发送缓冲区的最后一个缓冲块,然后把这个缓冲区的 FIN标志位 设置上。

所以我前面的想法是错的,当关闭一个 TCP 连接时,如果发送缓冲区还有数据没发送完,那么内核只会把发送缓冲区最后一个缓冲块设置上 FIN标志,而不是把发送缓冲区清空。

#define TCPCB_FLAG_FIN		0x01
#define TCPCB_FLAG_SYN		0x02
#define TCPCB_FLAG_RST		0x04
#define TCPCB_FLAG_PSH		0x08
#define TCPCB_FLAG_ACK		0x10
#define TCPCB_FLAG_URG		0x20
#define TCPCB_FLAG_ECE		0x40
#define TCPCB_FLAG_CWR		0x80

 

https://mp.weixin.qq.com/s/zCwSGGt__HB8wUri_xwd0g

 

http://vger.kernel.org/~davem/tcp_skbcb.html

The TCP SKB control block is defined in as follows:

TCP SKB控制块在 如下:


struct tcp_skb_cb {
	union {
		struct inet_skb_parmh4;
		struct inet6_skb_parmh6;
	} header;	/* For incoming frames */
	...

Before input TCP packets are processed by TCP, the upper layer (ipv4 or ipv6) examine the packet first. They also use the SKB control block area to record various bits of per-packet information. For example, ipv4 records the IP header options parsed from the protocol header. In order to not corrupt the protocol level data stored here by ipv4/ipv6, we define this union at the front of the TCP control block.

在TCP处理输入的TCP数据包之前,上层(ipv4或ipv6)首先检查该数据包。他们还使用SKB控制块区域来记录每个数据包信息的各个位。例如,ipv4记录从协议标头解析的IP标头选项。为了不破坏ipv4 / ipv6此处存储的协议级别数据,我们在TCP控制块的前面定义了这个并集。


	...
	__u32		seq;		/* Starting sequence number	*/
	__u32		end_seq;	/* SEQ + FIN + SYN + datalen	*/
	...

These define the TCP sequence numbers covered by the packet. The seq value is simply the sequence number in the TCP packet header on input. As suggested by the comment, the end_seq member is calculated as seq plus the number of data bytes in the TCP packet, plus 1 if the FIN bit is set, and plus 1 if the SYN bit is set.

这些定义了数据包覆盖的TCP序列号。的SEQ值是简单地在输入的TCP数据包报头中的序列号。正如评论所建议的那样, end_seq成员的计算方式为seq加上TCP数据包中的数据字节数,如果FIN位置1,则加1;如果SYN位置1,则加1。


	...
	__u32		when;		/* used to compute rtt's	*/
	...

When a TCP packet is sent, we record the current jiffies value here. It is used to later calculate the round trip time estimates, if necessary.

发送TCP数据包时,我们在 此处记录当前的吉菲斯值。如有必要,它可用于稍后计算往返时间估计。


	...
	__u8		flags;		/* TCP header flags.		*/

	/* NOTE: These must match up to the flags byte in a
	 *	 real TCP header.
	 */
#define TCPCB_FLAG_FIN		0x01
#define TCPCB_FLAG_SYN		0x02
#define TCPCB_FLAG_RST		0x04
#define TCPCB_FLAG_PSH		0x08
#define TCPCB_FLAG_ACK		0x10
#define TCPCB_FLAG_URG		0x20
#define TCPCB_FLAG_ECE		0x40
#define TCPCB_FLAG_CWR		0x80
	...

This member records the raw TCP header flags field we will use in the packet we send out. It is used by tcp_transmit_skb() to fill in the TCP header properly.

该成员记录了我们将在发送的数据包中使用的原始TCP标头标志字段。tcp_transmit_skb()使用它 正确填充TCP头。


	...
	__u8		sacked;		/* State flags for SACK/FACK.	*/
#define TCPCB_SACKED_ACKED	0x01	/* SKB ACK'd by a SACK block	*/
#define TCPCB_SACKED_RETRANS	0x02	/* SKB retransmitted		*/
#define TCPCB_LOST		0x04	/* SKB is lost			*/
#define TCPCB_TAGBITS		0x07	/* All tag bits			*/

#define TCPCB_EVER_RETRANS	0x80	/* Ever retransmitted frame	*/
#define TCPCB_RETRANS		(TCPCB_SACKED_RETRANS|TCPCB_EVER_RETRANS)

#define TCPCB_URG		0x20	/* Urgent pointer advenced here	*/

#define TCPCB_AT_TAIL		(TCPCB_URG)
	...

The sacked field holds the retransmission state for a packet. It also indicates whether the packet contains urgent data or not. As SACK packets arrive from the receiver, they are inspected and the TCPCB_TAGBITS fields are updated as needed. Then, the retransmission engine decides whether the retransmit packets or not.

If a retransmit timeout occurs, all of the SACK state tage bits are cleared, and we forget that state.

If an SKB is retransmitted in any way (either via timeout, or via fast retransmit), the TCPCB_EVER_RETRANS bit is set. Both TCPCB_SACK_RETRANS and TCPCB_EVER_RETRANS are set by tcp_retransmit_skb(). The TCPCB_SACK_RETRANS bit is selectively cleared by routines such as tcp_enter_loss().

该落马字段保存重发状态的数据包。它还指示数据包是否包含紧急数据。当SACK数据包从接收器到达时,将对其进行检查,并 根据需要更新TCPCB_TAGBITS字段。然后,重发引擎决定是否重发数据包。

如果发生重传超时,则会清除所有SACK状态有效位,而我们会忘记该状态。

如果以任何方式(通过超时或通过快速重传)重传SKB,则TCPCB_EVER_RETRANS位置1。无论TCPCB_SACK_RETRANS和TCPCB_EVER_RETRANS 被设置tcp_retransmit_skb() 。所述 TCPCB_SACK_RETRANS位被选择性地通过例程,如清除tcp_enter_loss() 。


	...
	__u16		urg_ptr;	/* Valid w/URG flags is set.	*/
	__u32		ack_seq;	/* Sequence number ACK'd	*/
};

#define TCP_SKB_CB(__skb)((struct tcp_skb_cb *)&((__skb)->cb[0]))

The urg_ptr states the URG pointer TCP header value to use if TCPCB_FLAG_URG is set in the flags. The ack_seq is the ACK sequence from the TCP header on input packets.

所述urg_ptr陈述了URG指针TCP报头值,如果使用TCPCB_FLAG_URG在标志中设置。的 ACK_SEQ是从上输入数据包的TCP报头中的ACK序列。

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 酷酷鲨 设计师:CSDN官方博客 返回首页