TCP协议接受IP层数据包过程

IP层函数ip_local_deliver函数处理对数据包处理接受后根据iphdr->protocol数据域中协议号在inet_protocol全局变量中查找传输层的接受函数,TCP协议的接受函数是tcp_v4_rcv,tcp_v4_rcv函数的功能主要包含两个方面:

(1)、数据包合法性检查

(2)、确定数据包是快速路径处理还是慢速路径处理

下面分析tcp_v4_rcv函数的处理分析。

1、数据包正确性检查

(1)、skb->pkt_type判断目的地是否是本机,如果不是本机就扔掉数据包。

(2)、调用pskb_may_pull查看TCP协议头的正确性。

(3)、保存TCP协议头部到局部变量th中,然后检查协议头长度th->doff是否正确,TCP协议头的每个单元占32位也就是4字节,               所以sizeof(struct tcphdr)/4为TCP协议头占用的单元数。

(4)、检查TCP数据段校验和的正确信息,不正确就扔掉数据包。

int tcp_v4_rcv(struct sk_buff *skb)
{
	const struct iphdr *iph;
	struct tcphdr *th;
	struct sock *sk;
	int ret;
	struct net *net = dev_net(skb->dev);

	//数据包目的不是本地就扔掉
	if (skb->pkt_type != PACKET_HOST)
		goto discard_it;

	/* Count it even if it's bad */
	TCP_INC_STATS_BH(net, TCP_MIB_INSEGS);

	//检查TCP协议头的正确性
	if (!pskb_may_pull(skb, sizeof(struct tcphdr)))
		goto discard_it;

	th = tcp_hdr(skb);

	//头部长度检查
	if (th->doff < sizeof(struct tcphdr) / 4)
		goto bad_packet;
	if (!pskb_may_pull(skb, th->doff * 4))
		goto discard_it;

	/* An explanation is required here, I think.
	 * Packet length and doff are validated by header prediction,
	 * provided case of th->doff==0 is eliminated.
	 * So, we defer the checks. */
	 //检查校验和
	if (!skb_csum_unnecessary(skb) && tcp_v4_checksum_init(skb))
		goto bad_packet;
...
}

2、保存协议头控制信息

将TCP协议的控制信息保存到skb的控制缓冲区,TCP控制缓冲区结构体tcp_skb_cb通过宏TCP_SKB_CB(skb)来访问,主要保存有:

(1)、TCP数据段起始序列号seq。

(2)、TCP回答序列号ack_seq。

(3)、when用于数据包重传计算时间

...

th = tcp_hdr(skb);
	iph = ip_hdr(skb);
	//起始序列号
	TCP_SKB_CB(skb)->seq = ntohl(th->seq);
	TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
				    skb->len - th->doff * 4);
	//回答序列号
	TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
	//重传时间
	TCP_SKB_CB(skb)->when	 = 0;
	TCP_SKB_CB(skb)->flags	 = iph->tos;
	//选择回答
	TCP_SKB_CB(skb)->sacked	 = 0;

...

3、查找套接字

调用函数__inet_lookup_skb查找数据段所属的套接字,根据数据包的网络端口、源端口号、目的端口号在哈希表中查找

...

//根据端口在哈希表中查找套接字
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
	if (!sk)
		goto no_tcp_socket;

...

如果没有找到打开的套接字就调转到no_tcp_socket标签处理,没有打开的套接字也要对数据包完成校验和检查,如果数据包不正确就更新接受的错误信息,如果数据包完好无损表名对端方数据段给一个已经关闭或者没有打开的套接字,这时要回复一个复位请求来告诉对端关闭连接,然后扔掉数据包,释放内存。

...

no_tcp_socket:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
		goto discard_it;
	//校验和检查
	if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
bad_packet:
		TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
	} else {
		//没有打开的套接字就发送一个复位请求来告诉对端关闭连接
		tcp_v4_send_reset(NULL, skb);
	}

...

4、数据段处理

数段处理主要有三个方面:

(1)、看套接字的状态是否是TIME_WAIT,如果是TIME_WAIT就调转到do_time_wait标签处理。

(2)、检查是否配置了IPsec安全策略,如果配置就要对IPsec策略检查,如果检查为通过就扔到数据包。

(3)、获取锁防止并发访问套接字,如果获取锁失败就调用sk_add_backlog函数把数据段加入到backlog_queue队列中,如果获取锁成功就调用tcp_prequeue函数把数据段放入prequeue队列中,prequeue队列存放在用户进程管理复制数据的struct ucopy数据结构中(struct ucopy是tcp_sock数据结构的一部分),数据段加入到prequeue队列后就交给应用程序处理。如果tcp_prequeue返回0,说明当前没有用户进程处理这个套接字,这时调用tcp_v4_do_rcv函数继续数据段的Slow Path接受处理,这个函数主要是把数据加入到sk_receive_queue队列中。

...

process:
	//套接字状态是TIME_WAIT就调转到超时处理
	if (sk->sk_state == TCP_TIME_WAIT)
		goto do_time_wait;

	if (unlikely(iph->ttl < inet_sk(sk)->min_ttl)) {
		NET_INC_STATS_BH(net, LINUX_MIB_TCPMINTTLDROP);
		goto discard_and_relse;
	}

	//IPsec策略检查处理
	if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
		goto discard_and_relse;
	nf_reset(skb);

	if (sk_filter(sk, skb))
		goto discard_and_relse;

	skb->dev = NULL;

	//加锁
	bh_lock_sock_nested(sk);
	ret = 0;
	if (!sock_owned_by_user(sk)) {
#ifdef CONFIG_NET_DMA
		//DMA方式处理
		struct tcp_sock *tp = tcp_sk(sk);
		if (!tp->ucopy.dma_chan && tp->ucopy.pinned_list)
			tp->ucopy.dma_chan = dma_find_channel(DMA_MEMCPY);
		if (tp->ucopy.dma_chan)
			ret = tcp_v4_do_rcv(sk, skb);
		else
#endif
		{	//将数据包加入prequeue队列按Past Path路径处理
			if (!tcp_prequeue(sk, skb))
				//不成功调用tcp_v4_do_rcv按照Slow Path处理
				ret = tcp_v4_do_rcv(sk, skb);
		}
	//加锁不成功将数据段加入backlog_queue队列
	} else if (unlikely(sk_add_backlog(sk, skb))) {
		bh_unlock_sock(sk);
		NET_INC_STATS_BH(net, LINUX_MIB_TCPBACKLOGDROP);
		goto discard_and_relse;
	}
	bh_unlock_sock(sk);
	//应用计数减1,并是否套接字
	sock_put(sk);

	return ret;

...

5、TIME_WAIT处理

首先调用tcp_timewait_state_process函数获取数据包的类型,是SYN数据包、FIN数据包、数据段数据包,然后分别处理,各种状态处理如下:

TCP_TW_SUCCESS:延迟数据包或者源ACK的复制,处理方式是扔掉数据包。

TCP_TW_RST:结束连接的FIN,发送一个复位给对端关闭连接。

TCP_TW_ACK:收到最后一个ACK,回复对端一个ACK并关闭连接。

TCP_TW_SYN:收到建立连接请求,调用tcp_v4_lookup_listener函数来创建监听连接进程并创建新的连接。

...

do_time_wait:
	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
		inet_twsk_put(inet_twsk(sk));
		goto discard_it;
	}

	if (skb->len < (th->doff << 2) || tcp_checksum_complete(skb)) {
		TCP_INC_STATS_BH(net, TCP_MIB_INERRS);
		inet_twsk_put(inet_twsk(sk));
		goto discard_it;
	}
	//获取数据包类型
	switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
	case TCP_TW_SYN: {
		//如果是SYN请求,就打开套接字连接
		struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
							&tcp_hashinfo,
							iph->daddr, th->dest,
							inet_iif(skb));
		if (sk2) {
			inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
			inet_twsk_put(inet_twsk(sk));
			sk = sk2;
			goto process;
		}
		/* Fall through to ACK */
	}
	//收到最后一个ACK,也回复对端一个ACK并关闭连接
	case TCP_TW_ACK:
		tcp_v4_timewait_ack(sk, skb);
		break;
		//接受连接FIN就回复一个复位给对端
	case TCP_TW_RST:
		goto no_tcp_socket;
		//延迟数据段直接扔掉数据包
	case TCP_TW_SUCCESS:;
	}

...

6、tcp_v4_rcv函数流程图

tcp_v4_do_rcv函数处理流程

猜你喜欢

转载自blog.csdn.net/City_of_skey/article/details/84593654