kk Blog —— 通用基础

date [-d @int|str] [+%s|"+%F %T"]

TCP状态转换

1、建立连接协议(三次握手)

(1) 客户端发送一个带SYN标志的TCP报文到服务器。这是三次握手过程中的报文1。
(2) 服务器端回应客户端的,这是三次握手中的第2个报文,这个报文同时带ACK标志和SYN标志。因此它表示对刚才客户端SYN报文的回应;同时又标志SYN给客户端,询问客户端是否准备好进行数据通讯。
(3) 客户必须再次回应服务段一个ACK报文,这是报文段3。

2、连接终止协议(四次握手)

由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
(1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送(报文段4)。
(2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1(报文段5)。和SYN一样,一个FIN将占用一个序号。
(3) 服务器关闭客户端的连接,发送一个FIN给客户端(报文段6)。
(4) 客户段发回ACK报文确认,并将确认序号设置为收到序号加1(报文段7)。

tcp状态解释

  1. CLOSED: 表示初始状态。
  2. LISTEN: 表示服务器端的某个SOCKET处于监听状态,可以接受连接了.
  3. SYN_SENT: 客户端通过应用程序调用connect进行active open.于是客户端tcp发送一个SYN以请求建立一个连接.之后状态置为SYN_SENT.
  4. SYN_RECV: 服务端应发出ACK确认客户端的SYN,同时自己向客户端发送一个SYN.之后状态置为SYN_RECV.
  5. ESTABLISHED:代表一个打开的连接,双方可以进行或已经在数据交互了.
  6. FIN_WAIT_1: 主动关闭(active close)端应用程序调用close,于是其TCP发出FIN请求主动关闭连接,之后进入FIN_WAIT1状态.
  7. FIN_WAIT2: 主动关闭端接到ACK后,就进入了FIN-WAIT-2 . 其实FIN_WAIT_1和FIN_WAIT_2状态的真正含义都是表示等待对方的FIN报文。而这两种状态的区别 是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即 进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态.
  8. CLOSE_WAIT: CLOSE_WAIT:被动关闭(passive close)端TCP接到FIN后,就发出ACK以回应FIN请求(它的接收也作为文件结束符传递给上层应用程序),并进入CLOSE_WAIT。接下来还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接.
  9. LAST_ACK: 被动关闭端一段时间后,接收到文件结束符的应用程序将调用CLOSE关闭连接。这导致它的TCP也发送一个 FIN,等待对方的ACK.就进入了LAST-ACK.
  10. TIME_WAIT: 在主动关闭端接收到FIN后,TCP就发送ACK包,并进入TIME-WAIT状态.
  11. CLOSING: 正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,没有收到对方的ACK报文,反而收到了对方的FIN报文。表示双方都正在关闭SOCKET连接.
  12. CLOSED: 被动关闭端在接受到ACK包后,就进入了closed的状态。连接结束.

linux kernel 网络协议栈之GRO(Generic receive offload)


Attention: gro会合并多个gso_size不同的包, 会将gso_size设置成第一个包的gso_size.

http://www.pagefault.info/?p=159

GRO(Generic receive offload)在内核2.6.29之后合并进去的,作者是一个华裔Herbert Xu ,GRO的简介可以看这里:

http://lwn.net/Articles/358910/

先来描述一下GRO的作用,GRO是针对网络接受包的处理的,并且只是针对NAPI类型的驱动,因此如果要支持GRO,不仅要内核支持,而且驱动也必须调用相应的借口,用ethtool -K gro on来设置,如果报错就说明网卡驱动本身就不支持GRO。

GRO类似tso,可是tso只支持发送数据包,这样你tcp层大的段会在网卡被切包,然后再传递给对端,而如果没有gro,则小的段会被一个个送到协议栈,有了gro之后,就会在接收端做一个反向的操作(想对于tso).也就是将tso切好的数据包组合成大包再传递给协议栈。

如果实现了GRO支持的驱动是这样子处理数据的,在NAPI的回调poll方法中读取数据包,然后调用GRO的接口napi_gro_receive或者napi_gro_frags来将数据包feed进协议栈。而具体GRO的工作就是在这两个函数中进行的,他们最终都会调用__napi_gro_receive。下面就是napi_gro_receive,它最终会调用napi_skb_finish以及__napi_gro_receive

1
2
3
4
5
6
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	skb_gro_reset_offset(skb);
 
	return napi_skb_finish(__napi_gro_receive(napi, skb), skb);
}

然后GRO什么时候会将数据feed进协议栈呢,这里会有两个退出点,一个是在napi_skb_finish里,他会通过判断__napi_gro_receive的返回值,来决定是需要将数据包立即feed进协议栈还是保存起来,还有一个点是当napi的循环执行完毕时,也就是执行napi_complete的时候,先来看napi_skb_finish,napi_complete我们后面会详细介绍。

在NAPI驱动中,直接调用netif_receive_skb会将数据feed 进协议栈,因此这里如果返回值是NORMAL,则直接调用netif_receive_skb来将数据送进协议栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
{
	switch (ret) {
	case GRO_NORMAL:
		//将数据包送进协议栈
		if (netif_receive_skb(skb))
			ret = GRO_DROP;
		break;
	//表示skb可以被free,因为gro已经将skb合并并保存起来。
	case GRO_DROP:
	case GRO_MERGED_FREE:
		//free skb
		kfree_skb(skb);
		break;
	//这个表示当前数据已经被gro保存起来,但是并没有进行合并,因此skb还需要保存。
	case GRO_HELD:
	case GRO_MERGED:
		break;
	}
 
	return ret;
}

GRO的主要思想就是,组合一些类似的数据包(基于一些数据域,后面会介绍到)为一个大的数据包(一个skb),然后feed给协议栈,这里主要是利用Scatter-gather IO,也就是skb的struct skb_shared_info域(我前面的blog讲述ip分片的时候有详细介绍这个域)来合并数据包。

在每个NAPI的实例都会包括一个域叫gro_list,保存了我们积攒的数据包(将要被merge的).然后每次进来的skb都会在这个链表里面进行查找,看是否需要merge。而gro_count表示当前的gro_list中的skb的个数。

1
2
3
4
5
6
7
8
9
struct napi_struct {
................................................
	//个数
	unsigned int        gro_count;
......................................
	//积攒的数据包
	struct sk_buff      *gro_list;
	struct sk_buff      *skb;
};

紧接着是gro最核心的一个数据结构napi_gro_cb,它是保存在skb的cb域中,它保存了gro要使用到的一些上下文,这里每个域kernel的注释都比较清楚。到后面我们会看到这些域的具体用途。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct napi_gro_cb {
	/* Virtual address of skb_shinfo(skb)->frags[0].page + offset. */
	void *frag0;
 
	/* Length of frag0. */
	unsigned int frag0_len;
 
	/* This indicates where we are processing relative to skb->data. */
	int data_offset;
 
	/* This is non-zero if the packet may be of the same flow. */
	int same_flow;
 
	/* This is non-zero if the packet cannot be merged with the new skb. */
	int flush;
 
	/* Number of segments aggregated. */
	int count;
 
	/* Free the skb? */
	int free;
};

每一层协议都实现了自己的gro回调函数,gro_receive和gro_complete,gro系统会根据协议来调用对应回调函数,其中gro_receive是将输入skb尽量合并到我们gro_list中。而gro_complete则是当我们需要提交gro合并的数据包到协议栈时被调用的。

下面就是ip层和tcp层对应的回调方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static const struct net_protocol tcp_protocol = {
	.handler =  tcp_v4_rcv,
	.err_handler =  tcp_v4_err,
	.gso_send_check = tcp_v4_gso_send_check,
	.gso_segment =  tcp_tso_segment,
	//gso回调
	.gro_receive =  tcp4_gro_receive,
	.gro_complete = tcp4_gro_complete,
	.no_policy =    1,
	.netns_ok = 1,
};
 
static struct packet_type ip_packet_type __read_mostly = {
	.type = cpu_to_be16(ETH_P_IP),
	.func = ip_rcv,
	.gso_send_check = inet_gso_send_check,
	.gso_segment = inet_gso_segment,
	//gso回调
	.gro_receive = inet_gro_receive,
	.gro_complete = inet_gro_complete,
};

gro的入口函数是napi_gro_receive,它的实现很简单,就是将skb包含的gro上下文reset,然后调用__napi_gro_receive,最终通过napi_skb_finis来判断是否需要讲数据包feed进协议栈。

1
2
3
4
5
6
7
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	//reset gro对应的域
	skb_gro_reset_offset(skb);
 
	return napi_skb_finish(__napi_gro_receive(napi, skb), skb);
}

napi_skb_finish一开始已经介绍过了,这个函数主要是通过判断传递进来的ret(__napi_gro_receive的返回值),来决定是否需要feed数据进协议栈。它的第二个参数是前面处理过的skb。

这里再来看下skb_gro_reset_offset,首先要知道一种情况,那就是skb本身不包含数据(包括头也没有),而所有的数据都保存在skb_shared_info中(支持S/G的网卡有可能会这么做).此时我们如果想要合并的话,就需要将包头这些信息取出来,也就是从skb_shared_info的frags[0]中去的,在 skb_gro_reset_offset中就有做这个事情,而这里就会把头的信息保存到napi_gro_cb 的frags0中。并且此时frags必然不会在high mem,要么是线性区,要么是dma(S/G io)。 来看skb_gro_reset_offset。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void skb_gro_reset_offset(struct sk_buff *skb)
{
	NAPI_GRO_CB(skb)->data_offset = 0;
	NAPI_GRO_CB(skb)->frag0 = NULL;
	NAPI_GRO_CB(skb)->frag0_len = 0;
	//如果mac_header和skb->tail相等并且地址不在高端内存,则说明包头保存在skb_shinfo中,所以我们需要从frags中取得对应的数据包
	if (skb->mac_header == skb->tail &&
		!PageHighMem(skb_shinfo(skb)->frags[0].page)) {
		// 可以看到frag0保存的就是对应的skb的frags的第一个元素的地址
		// frag0的作用是: 有些包的包头会存在skb->frag[0]里面,gro合并时会调用skb_gro_header_slow将包头拉到线性空间中,那么在非线性skb->frag[0]中的包头部分就应该删掉。
			NAPI_GRO_CB(skb)->frag0 =
				page_address(skb_shinfo(skb)->frags[0].page) +
				skb_shinfo(skb)->frags[0].page_offset;
		//然后保存对应的大小。
		NAPI_GRO_CB(skb)->frag0_len = skb_shinfo(skb)->frags[0].size;
	}
}

接下来就是__napi_gro_receive,它主要是遍历gro_list,然后给same_flow赋值,这里要注意,same_flow是一个标记,表示某个skb是否有可能会和当前要处理的skb是相同的流,而这里的相同会在每层都进行判断,也就是在设备层,ip层,tcp层都会判断,这里就是设备层的判断了。这里的判断很简单,有2个条件:
1 设备是否相同
2 mac的头必须相等

如果上面两个条件都满足,则说明两个skb有可能是相同的flow,所以设置same_flow,以便与我们后面合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static gro_result_t
__napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	struct sk_buff *p;
 
	if (netpoll_rx_on(skb))
		return GRO_NORMAL;
	//遍历gro_list,然后判断是否有可能两个skb 相似。
	for (p = napi->gro_list; p; p = p->next) {
		//给same_flow赋值
		NAPI_GRO_CB(p)->same_flow =
			(p->dev == skb->dev) &&
			!compare_ether_header(skb_mac_header(p),
				skb_gro_mac_header(skb));
		NAPI_GRO_CB(p)->flush = 0;
	}
	//调用dev_gro_receiv
	return dev_gro_receive(napi, skb);
}

接下来来看dev_gro_receive,这个函数我们分做两部分来看,第一部分是正常处理部分,第二部份是处理frag0的部分。

来看如何判断是否支持GRO,这里每个设备的features会在驱动初始化的时候被初始化,然后如果支持GRO,则会包括NETIF_F_GRO。 还有要注意的就是,gro不支持切片的ip包,因为ip切片的组包在内核的ip会做一遍,因此这里gro如果合并的话,没有多大意义,而且还增加复杂度。

在dev_gro_receive中会遍历对应的ptype(也就是协议的类链表,以前的blog有详细介绍),然后调用对应的回调函数,一般来说这里会调用文章开始说的ip_packet_type,也就是 inet_gro_receive。

而 inet_gro_receive的返回值表示我们需要立刻feed 进协议栈的数据包,如果为空,则说明不需要feed数据包进协议栈。后面会分析到这里他的详细算法。

而如果当inet_gro_receive正确返回后,如果same_flow没有被设置,则说明gro list中不存在能和当前的skb合并的项,因此此时需要将skb插入到gro list中。这个时候的返回值就是HELD。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
enum gro_result dev_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	struct sk_buff **pp = NULL;
	struct packet_type *ptype;
	__be16 type = skb->protocol;
	struct list_head *head = &ptype_base[ntohs(type) & PTYPE_HASH_MASK];
	int same_flow;
	int mac_len;
	enum gro_result ret;
	//判断是否支持gro
	if (!(skb->dev->features & NETIF_F_GRO))
		goto normal;
	//判断是否为切片的ip包
	if (skb_is_gso(skb) || skb_has_frags(skb))
		goto normal;

	rcu_read_lock();
	//开始遍历对应的协议表
	list_for_each_entry_rcu(ptype, head, list) {
		if (ptype->type != type || ptype->dev || !ptype->gro_receive)
			continue;

		skb_set_network_header(skb, skb_gro_offset(skb));
		mac_len = skb->network_header - skb->mac_header;
		skb->mac_len = mac_len;
		NAPI_GRO_CB(skb)->same_flow = 0;
		NAPI_GRO_CB(skb)->flush = 0;
		NAPI_GRO_CB(skb)->free = 0;
		//调用对应的gro接收函数
		pp = ptype->gro_receive(&napi->gro_list, skb);
		break;
	}
	rcu_read_unlock();
	//如果是没有实现gro的协议则也直接调到normal处理
	if (&ptype->list == head)
		goto normal;
 
	//到达这里,则说明gro_receive已经调用过了,因此进行后续的处理
 
	//得到same_flow
	same_flow = NAPI_GRO_CB(skb)->same_flow;
	//看是否有需要free对应的skb
	ret = NAPI_GRO_CB(skb)->free ? GRO_MERGED_FREE : GRO_MERGED;
	//如果返回值pp部位空,则说明pp需要马上被feed进协议栈
	if (pp) {
		struct sk_buff *nskb = *pp;
 
		*pp = nskb->next;
		nskb->next = NULL;
		//调用napi_gro_complete 将pp刷进协议栈
		napi_gro_complete(nskb);
		napi->gro_count--;
	}
	//如果same_flow有设置,则说明skb已经被正确的合并,因此直接返回。
	if (same_flow)
		goto ok;
	//查看是否有设置flush和gro list的个数是否已经超过限制
	// BUG: 这里是有点不对的,因为这时的skb是比gro_list中的skb更晚到的,但是却被先feed进了协议栈
	if (NAPI_GRO_CB(skb)->flush || napi->gro_count >= MAX_GRO_SKBS)
		goto normal;
 
	//到达这里说明skb对应gro list来说是一个新的skb,也就是说当前的gro list并不存在可以和skb合并的数据包,因此此时将这个skb插入到gro_list的头。
	napi->gro_count++;
	NAPI_GRO_CB(skb)->count = 1;
	skb_shinfo(skb)->gso_size = skb_gro_len(skb);
	//将skb插入到gro list的头
	skb->next = napi->gro_list;
	napi->gro_list = skb;
	//设置返回值
	ret = GRO_HELD;

然后就是处理frag0的部分,以及不支持gro的处理。 frag0的作用是: 有些包的包头会存在skb->frag[0]里面,gro合并时会调用skb_gro_header_slow将包头拉到线性空间中,那么在非线性skb->frag[0]中的包头部分就应该删掉。

这里要需要对skb_shinfo的结构比较了解,我在以前的blog对这个有很详细的介绍,可以去查阅。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
pull:
	//是否需要拷贝头
	if (skb_headlen(skb) < skb_gro_offset(skb)) {
		//得到对应的头的大小
		int grow = skb_gro_offset(skb) - skb_headlen(skb);
 
		BUG_ON(skb->end - skb->tail < grow);
		//开始拷贝
		memcpy(skb_tail_pointer(skb), NAPI_GRO_CB(skb)->frag0, grow);
 
		skb->tail += grow;
		skb->data_len -= grow;
		//更新对应的frags[0]
		skb_shinfo(skb)->frags[0].page_offset += grow;
		skb_shinfo(skb)->frags[0].size -= grow;
		//如果size为0了,则说明第一个页全部包含头,因此需要将后面的页全部移动到前面。
		if (unlikely(!skb_shinfo(skb)->frags[0].size)) {
			put_page(skb_shinfo(skb)->frags[0].page);
			//开始移动。
			memmove(skb_shinfo(skb)->frags,
				skb_shinfo(skb)->frags + 1,
				--skb_shinfo(skb)->nr_frags * sizeof(skb_frag_t));
		}
	}
 
ok:
	return ret;
 
normal:
	ret = GRO_NORMAL;
	goto pull;
}

接下来就是inet_gro_receive,这个函数是ip层的gro receive回调函数,函数很简单,首先取得ip头,然后判断是否需要从frag复制数据,如果需要则复制数据

1
2
3
4
5
6
7
8
9
10
11
12
//得到偏移
off = skb_gro_offset(skb);
//得到头的整个长度(mac+ip)
hlen = off + sizeof(*iph);
//得到ip头
iph = skb_gro_header_fast(skb, off);
//是否需要复制
if (skb_gro_header_hard(skb, hlen)) {
	iph = skb_gro_header_slow(skb, hlen, off);
	if (unlikely(!iph))
		goto out;
}

然后就是一些校验工作,比如协议是否支持gro_reveive,ip头是否合法等等

1
2
3
4
5
6
7
8
9
10
11
12
13
proto = iph->protocol & (MAX_INET_PROTOS - 1);
 
rcu_read_lock();
ops = rcu_dereference(inet_protos[proto]);
//是否支持gro
if (!ops || !ops->gro_receive)
	goto out_unlock;
//ip头是否合法, iph->version = 4, iph->ipl = 5
if (*(u8 *)iph != 0x45)
	goto out_unlock;
//ip头教研
if (unlikely(ip_fast_csum((u8 *)iph, iph->ihl)))
	goto out_unlock;

然后就是核心的处理部分,它会遍历整个gro_list,然后进行same_flow和是否需要flush的判断。

这里ip层设置same_flow是根据下面的规则的:
1 4层的协议必须相同
2 tos域必须相同
3 源,目的地址必须相同

如果3个条件一个不满足,则会设置same_flow为0。 这里还有一个就是判断是否需要flush 对应的skb到协议栈,这里的判断条件是这样子的。
1 ip包的ttl不一样
2 ip包的id顺序不对
3 如果是切片包

如果上面两个条件某一个满足,则说明skb需要被flush出gro。

不过这里要注意只有两个数据包是same flow的情况下,才会进行flush判断。原因很简单,都不是有可能进行merge的包,自然没必要进行flush了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
	//取出id
	id = ntohl(*(__be32 *)&iph->id);
	//判断是否需要切片
	flush = (u16)((ntohl(*(__be32 *)iph) ^ skb_gro_len(skb)) | (id ^ IP_DF));
	id >>= 16;
	//开始遍历gro list
	for (p = *head; p; p = p->next) {
		struct iphdr *iph2;
		//如果上一层已经不可能same flow则直接继续下一个
		if (!NAPI_GRO_CB(p)->same_flow)
			continue;
		//取出ip头
		iph2 = ip_hdr(p);
		//开始same flow的判断
		if ((iph->protocol ^ iph2->protocol) |
			(iph->tos ^ iph2->tos) |
			((__force u32)iph->saddr ^ (__force u32)iph2->saddr) |
			((__force u32)iph->daddr ^ (__force u32)iph2->daddr)) {
			NAPI_GRO_CB(p)->same_flow = 0;
			continue;
		}
		//开始flush的判断。这里注意如果不是same_flow的话,就没必要进行flush的判断。
		/* All fields must match except length and checksum. */
		NAPI_GRO_CB(p)->flush |=
			(iph->ttl ^ iph2->ttl) |
			((u16)(ntohs(iph2->id) + NAPI_GRO_CB(p)->count) ^ id);
 
		NAPI_GRO_CB(p)->flush |= flush;
	}
 
	NAPI_GRO_CB(skb)->flush |= flush;
	//pull ip头进gro,这里更新data_offset
	skb_gro_pull(skb, sizeof(*iph));
	//设置传输层的头的位置
	skb_set_transport_header(skb, skb_gro_offset(skb));
	//调用传输层的reveive方法。
	pp = ops->gro_receive(head, skb);
 
out_unlock:
	rcu_read_unlock();
 
out:
	NAPI_GRO_CB(skb)->flush |= flush;
 
}

然后就是tcp层的gro方法,它的主要实现函数是tcp_gro_receive,他的流程和inet_gro_receiv类似,就是取得tcp的头,然后对gro list进行遍历,最终会调用合并方法。

首先来看gro list遍历的部分,它对same flow的要求就是source必须相同,如果不同则设置same flow为0.如果相同则跳到found部分,进行合并处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//遍历gro list
for (; (p = *head); head = &p->next) {
	//如果ip层已经不可能same flow则直接进行下一次匹配
	if (!NAPI_GRO_CB(p)->same_flow)
		continue;
 
	th2 = tcp_hdr(p);
	//判断源地址
	if (*(u32 *)&th->source ^ *(u32 *)&th2->source) {
		NAPI_GRO_CB(p)->same_flow = 0;
		continue;
	}
 
	goto found;
}

接下来就是当找到能够合并的skb的时候的处理,这里首先来看flush的设置,这里会有4个条件:
1 拥塞状态被设置(TCP_FLAG_CWR).
2 tcp的ack的序列号不匹配 (这是肯定的,因为它只是对tso或者说gso进行反向操作)
3 skb的flag和从gro list中查找到要合并skb的flag 如果他们中的不同位 不包括TCP_FLAG_CWR | TCP_FLAG_FIN | TCP_FLAG_PSH,这三个任意一个域。
4 tcp的option域不同

如果上面4个条件有一个满足,则会设置flush为1,也就是找到的这个skb(gro list中)必须被刷出到协议栈。

这里谈一下flags域的设置问题首先如果当前的skb设置了cwr,也就是发生了拥塞,那么自然前面被缓存的数据包需要马上被刷到协议栈,以便与tcp的拥塞控制马上进行。

而FIN和PSH这两个flag自然不需要一致,因为这两个和其他的不是互斥的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
found:
	flush = NAPI_GRO_CB(p)->flush;
	//如果设置拥塞,则肯定需要刷出skb到协议栈
	flush |= (__force int)(flags & TCP_FLAG_CWR);
	//如果相差的域是除了这3个中的,就需要flush出skb
	flush |= (__force int)((flags ^ tcp_flag_word(th2)) &
		  ~(TCP_FLAG_CWR | TCP_FLAG_FIN | TCP_FLAG_PSH));
	//ack的序列号必须一致
	flush |= (__force int)(th->ack_seq ^ th2->ack_seq);
	//tcp的option头必须一致
	for (i = sizeof(*th); i < thlen; i += 4)
		flush |= *(u32 *)((u8 *)th + i) ^
			 *(u32 *)((u8 *)th2 + i);
 
	mss = skb_shinfo(p)->gso_size;
	// 0-1 = 0xFFFFFFFF, 所以skb的数据部分长度为0的包是不会被合并的
	flush |= (len - 1) >= mss;
	flush |= (ntohl(th2->seq) + skb_gro_len(p)) ^ ntohl(th->seq);
	//如果flush有设置则不会调用 skb_gro_receive,也就是不需要进行合并,否则调用skb_gro_receive进行数据包合并
	if (flush || skb_gro_receive(head, skb)) {
		mss = 1;
		goto out_check_final;
	}
 
	p = *head;
	th2 = tcp_hdr(p);
	//更新p的头。到达这里说明合并完毕,因此需要更新合并完的新包的头。
	tcp_flag_word(th2) |= flags & (TCP_FLAG_FIN | TCP_FLAG_PSH);

从上面我们可以看到如果tcp的包被设置了一些特殊的flag比如PSH,SYN这类的就必须马上把数据包刷出到协议栈。

下面就是最终的一些flags判断,比如第一个数据包进来都会到这里来判断。

1
2
3
4
5
6
7
8
9
10
11
12
out_check_final:
	flush = len < mss;
	//根据flag得到flush
	flush |= (__force int)(flags & (TCP_FLAG_URG | TCP_FLAG_PSH |
					TCP_FLAG_RST | TCP_FLAG_SYN |
					TCP_FLAG_FIN));
 
	if (p && (!NAPI_GRO_CB(skb)->same_flow || flush))
		pp = head;
 
out:
	NAPI_GRO_CB(skb)->flush |= flush;

这里要知道每次我们只会刷出gro list中的一个skb节点,这是因为每次进来的数据包我们也只会匹配一个。因此如果遇到需要刷出的数据包,会在dev_gro_receive中先刷出gro list中的,然后再将当前的skb feed进协议栈。

最后就是gro最核心的一个函数skb_gro_receive,它的主要工作就是合并,它有2个参数,第一个是gro list中和当前处理的skb是same flow的skb,第二个就是我们需要合并的skb。

这里要注意就是farg_list,其实gro对待skb_shared_info和ip层切片,组包很类似,就是frags放Scatter-Gather I/O的数据包,frag_list放线性数据。这里gro 也是这样的,如果过来的skb支持Scatter-Gather I/O并且数据是只放在frags中,则会合并frags,如果过来的skb不支持Scatter-Gather I/O(数据头还是保存在skb中),则合并很简单,就是新建一个skb然后拷贝当前的skb,并将gro list中的skb直接挂载到farg_list。

先来看支持Scatter-Gather I/O的处理部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
//一些需要用到的变量
struct sk_buff *p = *head;
struct sk_buff *nskb;
//当前的skb的 share_ino
struct skb_shared_info *skbinfo = skb_shinfo(skb);
//当前的gro list中的要合并的skb的share_info
struct skb_shared_info *pinfo = skb_shinfo(p);
unsigned int headroom;
unsigned int len = skb_gro_len(skb);
unsigned int offset = skb_gro_offset(skb);
unsigned int headlen = skb_headlen(skb);
//如果有frag_list的话,则直接去非Scatter-Gather I/O部分处理,也就是合并到frag_list.
if (pinfo->frag_list)
	goto merge;
else if (headlen <= offset) {
	//支持Scatter-Gather I/O的处理
	skb_frag_t *frag;
	skb_frag_t *frag2;
	int i = skbinfo->nr_frags;
	//这里遍历是从后向前。
	int nr_frags = pinfo->nr_frags + i;
 
	offset -= headlen;
 
	if (nr_frags > MAX_SKB_FRAGS)
		return -E2BIG;
	//设置pinfo的frags的大小,可以看到就是加上skb的frags的大小
	pinfo->nr_frags = nr_frags;
	skbinfo->nr_frags = 0;
 
	frag = pinfo->frags + nr_frags;
	frag2 = skbinfo->frags + i;
	//遍历赋值,其实就是地址赋值,这里就是将skb的frag加到pinfo的frgas后面。
	do {
		*--frag = *--frag2;
	} while (--i);
	//更改page_offet的值
	frag->page_offset += offset;
	//修改size大小
	frag->size -= offset;
	//更新skb的相关值
	skb->truesize -= skb->data_len;
	skb->len -= skb->data_len;
	skb->data_len = 0;
 
	NAPI_GRO_CB(skb)->free = 1;
	//最终完成
	goto done;
} else if (skb_gro_len(p) != pinfo->gso_size)
	return -E2BIG;

这里gro list中的要被合并的skb我们叫做skb_s.

接下来就是不支持支持Scatter-Gather I/O(skb的头放在skb中)的处理。这里处理也比较简单,就是复制一个新的nskb,然后它的头和skb_s一样,然后将skb_s挂载到nskb的frag_list上,并且把新建的nskb挂在到gro list中,代替skb_s的位置,而当前的skb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
	headroom = skb_headroom(p);
	nskb = alloc_skb(headroom + skb_gro_offset(p), GFP_ATOMIC);
	if (unlikely(!nskb))
		return -ENOMEM;
	//复制头
	__copy_skb_header(nskb, p);
	nskb->mac_len = p->mac_len;
 
	skb_reserve(nskb, headroom);
	__skb_put(nskb, skb_gro_offset(p));
	//设置各层的头
	skb_set_mac_header(nskb, skb_mac_header(p) - p->data);
	skb_set_network_header(nskb, skb_network_offset(p));
	skb_set_transport_header(nskb, skb_transport_offset(p));
 
	__skb_pull(p, skb_gro_offset(p));
	//复制数据
	memcpy(skb_mac_header(nskb), skb_mac_header(p),
		   p->data - skb_mac_header(p));
	//对应的gro 域的赋值
	*NAPI_GRO_CB(nskb) = *NAPI_GRO_CB(p);
	//可以看到frag_list被赋值
	skb_shinfo(nskb)->frag_list = p;
	skb_shinfo(nskb)->gso_size = pinfo->gso_size;
	pinfo->gso_size = 0;
	skb_header_release(p);
	nskb->prev = p;
	//更新新的skb的数据段
	nskb->data_len += p->len;
	nskb->truesize += p->len;  // 应该改成 nskb->truesize += p->truesize; 更准确
	nskb->len += p->len;
	//将新的skb插入到gro list中
	*head = nskb;
	nskb->next = p->next;
	p->next = NULL;
 
	p = nskb;
 
merge:
	if (offset > headlen) {
		skbinfo->frags[0].page_offset += offset - headlen;
		skbinfo->frags[0].size -= offset - headlen;
		offset = headlen;
	}
 
	__skb_pull(skb, offset);
	//将skb插入新的skb的(或者老的skb,当frag list本身存在)fraglist
	// 这里是用p->prev来记录了p->fraglist的最后一个包,所以在gro向协议栈提交时最好加一句skb->prev = NULL;
	p->prev->next = skb;
	p->prev = skb;
	skb_header_release(skb);

拥塞控制模块注意

应用改变sock的拥塞控制算法

1
2
3
4
5
#define SOL_TCP 6
#define TCP_CONGESTION  13

strcpy(name, "cubic");
setsockopt (connfd, SOL_TCP, TCP_CONGESTION, name, strlen(name));
net/socket.c
1
2
3
4
5
6
7
8
9
SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,
		char __user *, optval, int, optlen)
{
	...
			err =
				sock->ops->setsockopt(sock, level, optname, optval,
						  optlen);
	...
}

对于ipv4的tcp,sock->ops指向 net/ipv4/af_inet.c 中的 inet_stream_ops,所以setsockopt等于sock_common_setsockopt。

net/core/sock.c
1
2
3
4
5
6
7
int sock_common_setsockopt(struct socket *sock, int level, int optname,
			   char __user *optval, unsigned int optlen)
{
	struct sock *sk = sock->sk;

	return sk->sk_prot->setsockopt(sk, level, optname, optval, optlen);
}

sk_prot 指向 net/ipv4/tcp_ipv4.c 中的 tcp_prot,所以setsockopt等于tcp_setsockopt

net/ipv4/tcp.c
1
2
3
4
5
6
7
8
9
10
int tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,
		   unsigned int optlen)
{
	struct inet_connection_sock *icsk = inet_csk(sk);

	if (level != SOL_TCP)
		return icsk->icsk_af_ops->setsockopt(sk, level, optname,
							 optval, optlen);
	return do_tcp_setsockopt(sk, level, optname, optval, optlen);
}

因为level = SOL_TCP, optname = TCP_CONGESTION, 所以直接到do_tcp_setsockopt的第一个if里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int do_tcp_setsockopt(struct sock *sk, int level,
		int optname, char __user *optval, unsigned int optlen)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk); 
	int val;    
	int err = 0;

	/* This is a string value all the others are int's */
	if (optname == TCP_CONGESTION) {    
		char name[TCP_CA_NAME_MAX]; 

		if (optlen < 1)
			return -EINVAL;

		val = strncpy_from_user(name, optval,
					min_t(long, TCP_CA_NAME_MAX-1, optlen));
		if (val < 0)
			return -EFAULT;
		name[val] = 0;

		lock_sock(sk);
		err = tcp_set_congestion_control(sk, name);
		release_sock(sk);
		return err;
	}

...

net/ipv4/tcp_cong.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/* Change congestion control for socket */
int tcp_set_congestion_control(struct sock *sk, const char *name)
{
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_congestion_ops *ca;
	int err = 0;

	rcu_read_lock();
	ca = tcp_ca_find(name);

	/* no change asking for existing value */
	if (ca == icsk->icsk_ca_ops)
		goto out;

#ifdef CONFIG_MODULES
	/* not found attempt to autoload module */
	if (!ca && capable(CAP_NET_ADMIN)) {
		rcu_read_unlock();
		request_module("tcp_%s", name);
		rcu_read_lock();
		ca = tcp_ca_find(name);
	}
#endif
	if (!ca)
		err = -ENOENT;

	else if (!((ca->flags & TCP_CONG_NON_RESTRICTED) || capable(CAP_NET_ADMIN)))
		err = -EPERM;

	else if (!try_module_get(ca->owner))
		err = -EBUSY;

	else {
		tcp_cleanup_congestion_control(sk);
		icsk->icsk_ca_ops = ca;

		if (sk->sk_state != TCP_CLOSE && icsk->icsk_ca_ops->init) // 如果sk->sk_state = TCP_CLOSE, 那么不会调用拥塞控制模块的初始化
			icsk->icsk_ca_ops->init(sk);
	}
 out:
	rcu_read_unlock();
	return err;
}

可以看到,如果sk->sk_state = TCP_CLOSE, 那么不会调用拥塞控制模块的初始化。


那么什么时候sk->sk_state == TCP_CLOSE,并且还能调用setsockopt呢?

举一种情况:当收到RST包的时候,tcp_rcv_established()->tcp_validate_incoming()->tcp_reset()->tcp_done()将sk置为TCP_CLOSE。
如果拥塞控制模块中init有申请内存,release中释放内存。那么在上述情况下将会出现没有申请而直接释放的情况,导致panic。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BUG: unable to handle kernel paging request at ffffeba4000002a0

[<ffffffff8115b17e>] kfree+0x6e/0x240
[<ffffffffa0068055>] cong_release+0x35/0x50 [cong]
[<ffffffff81467953>] tcp_cleanup_congestion_control+0x23/0x40
[<ffffffff81465bb9>] tcp_v4_destroy_sock+0x29/0x2d0
[<ffffffff8144e9e3>] inet_csk_destroy_sock+0x53/0x140
[<ffffffff814504c0>] tcp_close+0x340/0x4a0
[<ffffffff814748de>] inet_release+0x5e/0x90
[<ffffffff813f4359>] sock_release+0x29/0x90
[<ffffffff813f43d7>] sock_close+0x17/0x40
[<ffffffff81173ed3>] __fput+0xf3/0x220
[<ffffffff8117401c>] fput+0x1c/0x30
[<ffffffff8116df2d>] filp_close+0x5d/0x90
[<ffffffff8117090c>] sys_close+0xac/0x110
[<ffffffff8100af72>] system_call_fastpath+0x16/0x1b

测试代码

congestion_mod_panic

TCP校验和的原理和实现

http://blog.csdn.net/zhangskd/article/details/11770647

概述

TCP校验和是一个端到端的校验和,由发送端计算,然后由接收端验证。其目的是为了发现TCP首部和数据在发送端到接收端之间发生的任何改动。如果接收方检测到校验和有差错,则TCP段会被直接丢弃。

TCP校验和覆盖TCP首部和TCP数据,而IP首部中的校验和只覆盖IP的首部,不覆盖IP数据报中的任何数据。

TCP的校验和是必需的,而UDP的校验和是可选的。

TCP和UDP计算校验和时,都要加上一个12字节的伪首部。

伪首部

伪首部共有12字节,包含如下信息:源IP地址、目的IP地址、保留字节(置0)、传输层协议号(TCP是6)、TCP报文长度(报头+数据)。

伪首部是为了增加TCP校验和的检错能力:如检查TCP报文是否收错了(目的IP地址)、传输层协议是否选对了(传输层协议号)等。

定义

(1) RFC 793的TCP校验和定义

The checksum field is the 16 bit one’s complement of the one’s complement sum of all 16-bit words in the header and text. If a segment contains an odd number of header and text octets to be checksummed, the last octet is padded on the right with zeros to form a 16-bit word for checksum purposes. The pad is not transmitted as part of the segment. While computing the checksum, the checksum field itself is replaced with zeros.

上述的定义说得很明确:
首先,把伪首部、TCP报头、TCP数据分为16位的字,如果总长度为奇数个字节,则在最后增添一个位都为0的字节。把TCP报头中的校验和字段置为0(否则就陷入鸡生蛋还是蛋生鸡的问题)。

其次,用反码相加法累加所有的16位字(进位也要累加)。

最后,对计算结果取反,作为TCP的校验和。

(2) RFC 1071的IP校验和定义

1.Adjacent octets to be checksummed are paired to form 16-bit integers, and the 1’s complement sum of these 16-bit integers is formed.

2.To generate a checksum, the checksum field itself is cleared, the 16-bit 1’s complement sum is computed over the octets concerned, and the 1’s complement of this sum is placed in the checksum field.

3.To check a checksum, the 1’s complement sum is computed over the same set of octets, including the checksum field. If the result is all 1 bits (-0 in 1’s complement arithmetic), the check succeeds.

可以看到,TCP校验和、IP校验和的计算方法是基本一致的,除了计算的范围不同。

实现

基于2.6.18、x86_64。

csum_tcpudp_nofold()按4字节累加伪首部到sum中。

1
2
3
4
5
6
7
8
9
10
11
12
static inline unsigned long csum_tcpudp_nofold (unsigned long saddr, unsigned long daddr,  
						unsigned short len, unsigned short proto,  
						unsigned int sum)  
{  
	asm("addl %1, %0\n"    /* 累加daddr */  
		"adcl %2, %0\n"    /* 累加saddr */  
		"adcl %3, %0\n"    /* 累加len(2字节), proto, 0*/  
		"adcl $0, %0\n"    /*加上进位 */  
		: "=r" (sum)  
		: "g" (daddr), "g" (saddr), "g" ((ntohs(len) << 16) + proto*256), "0" (sum));  
	return sum;  
}   

csum_tcpudp_magic()产生最终的校验和。

首先,按4字节累加伪首部到sum中。

其次,累加sum的低16位、sum的高16位,并且对累加的结果取反。

最后,截取sum的高16位,作为校验和。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static inline unsigned short int csum_tcpudp_magic(unsigned long saddr, unsigned long daddr,  
							unsigned short len, unsigned short proto,  
							unsigned int sum)  
{  
	return csum_fold(csum_tcpudp_nofold(saddr, daddr, len, proto, sum));  
}  
  
static inline unsigned int csum_fold(unsigned int sum)  
{  
	__asm__(  
		"addl %1, %0\n"  
		"adcl 0xffff, %0"  
		: "=r" (sum)  
		: "r" (sum << 16), "0" (sum & 0xffff0000)   
  
		/* 将sum的低16位,作为寄存器1的高16位,寄存器1的低16位补0。 
		  * 将sum的高16位,作为寄存器0的高16位,寄存器0的低16位补0。 
		  * 这样,addl %1, %0就累加了sum的高16位和低16位。 
		  * 
		 * 还要考虑进位。如果有进位,adcl 0xfff, %0为:0x1 + 0xffff + %0,寄存器0的高16位加1。 
		  * 如果没有进位,adcl 0xffff, %0为:0xffff + %0,对寄存器0的高16位无影响。 
		  */  
  
	);  
  
	return (~sum) >> 16; /* 对sum取反,返回它的高16位,作为最终的校验和 */  
}  

发送校验

1
2
3
4
5
#define CHECKSUM_NONE 0 /* 不使用校验和,UDP可选 */  
#define CHECKSUM_HW 1 /* 由硬件计算报头和首部的校验和 */  
#define CHECKSUM_UNNECESSARY 2 /* 表示不需要校验,或者已经成功校验了 */  
#define CHECKSUM_PARTIAL CHECKSUM_HW  
#define CHECKSUM_COMPLETE CHECKSUM_HW  
@tcp_transmit_skb()
icsk->icsk_af_ops->send_check(sk, skb->len, skb); /* 计算校验和 */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void tcp_v4_send_check(struct sock *sk, int len, struct sk_buff *skb)  
{  
	struct inet_sock *inet = inet_sk(sk);  
	struct tcphdr *th = skb->h.th;  
   
	if (skb->ip_summed == CHECKSUM_HW) {  
		/* 只计算伪首部,TCP报头和TCP数据的累加由硬件完成 */  
		th->check = ~tcp_v4_check(th, len, inet->saddr, inet->daddr, 0);  
		skb->csum = offsetof(struct tcphdr, check); /* 校验和值在TCP首部的偏移 */  
  
	} else {  
		/* tcp_v4_check累加伪首部,获取最终的校验和。 
		 * csum_partial累加TCP报头。 
		 * 那么skb->csum应该是TCP数据部分的累加,这是在从用户空间复制时顺便累加的。 
		 */  
		th->check = tcp_v4_check(th, len, inet->saddr, inet->daddr,  
					csum_partial((char *)th, th->doff << 2, skb->csum));  
	}  
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
unsigned csum_partial(const unsigned char *buff, unsigned len, unsigned sum)  
{  
	return add32_with_carry(do_csum(buff, len), sum);  
}  
  
static inline unsigned add32_with_carry(unsigned a, unsigned b)  
{  
	asm("addl %2, %0\n\t"  
		 "adcl $0, %0"  
		 : "=r" (a)  
		 : "0" (a), "r" (b));  
	return a;  
}   

do_csum()用于计算一段内存的校验和,这里用于累加TCP报头。

具体计算时用到一些技巧:
1.反码累加时,按16位、32位、64位来累加的效果是一样的。
2.使用内存对齐,减少内存操作的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
static __force_inline unsigned do_csum(const unsigned char *buff, unsigned len)  
{  
	unsigned odd, count;  
	unsigned long result = 0;  
  
	if (unlikely(len == 0))  
		return result;  
  
	/* 使起始地址为XXX0,接下来可按2字节对齐 */  
	odd = 1 & (unsigned long) buff;  
	if (unlikely(odd)) {  
		result = *buff << 8; /* 因为机器是小端的 */  
		len--;  
		buff++;  
	}  
	count = len >> 1; /* nr of 16-bit words,这里可能余下1字节未算,最后会处理*/  
  
	if (count) {  
		/* 使起始地址为XX00,接下来可按4字节对齐 */  
		if (2 & (unsigned long) buff) {  
			result += *(unsigned short *)buff;  
			count--;  
			len -= 2;  
			buff += 2;  
		}  
		count >>= 1; /* nr of 32-bit words,这里可能余下2字节未算,最后会处理 */  
  
		if (count) {  
			unsigned long zero;  
			unsigned count64;  
			/* 使起始地址为X000,接下来可按8字节对齐 */  
			if (4 & (unsigned long)buff) {  
				result += *(unsigned int *)buff;  
				count--;  
				len -= 4;  
				buff += 4;  
			}  
			count >>= 1; /* nr of 64-bit words,这里可能余下4字节未算,最后会处理*/  
  
			/* main loop using 64byte blocks */  
			zero = 0;  
			count64 = count >> 3; /* 64字节的块数,这里可能余下56字节未算,最后会处理 */  
			while (count64) { /* 反码累加所有的64字节块 */  
				asm ("addq 0*8(%[src]), %[res]\n\t"    /* b、w、l、q分别对应8、16、32、64位操作 */  
					"addq 1*8(%[src]), %[res]\n\t"    /* [src]为指定寄存器的别名,效果应该等同于0、1等 */  
					"adcq 2*8(%[src]), %[res]\n\t"  
					"adcq 3*8(%[src]), %[res]\n\t"  
					"adcq 4*8(%[src]), %[res]\n\t"  
					"adcq 5*8(%[src]), %[res]\n\t"  
					"adcq 6*8(%[src]), %[res]\n\t"  
					"adcq 7*8(%[src]), %[res]\n\t"  
					"adcq %[zero], %[res]"  
					: [res] "=r" (result)  
					: [src] "r" (buff), [zero] "r" (zero), "[res]" (result));  
				buff += 64;  
				count64--;  
			}  
  
			/* 从这里开始,反序处理之前可能漏算的字节 */  
  
			/* last upto 7 8byte blocks,前面按8个8字节做计算单位,所以最多可能剩下7个8字节 */  
			count %= 8;  
			while (count) {  
				asm ("addq %1, %0\n\t"  
					 "adcq %2, %0\n"  
					 : "=r" (result)  
					 : "m" (*(unsigned long *)buff), "r" (zero), "0" (result));  
				--count;  
				buff += 8;  
			}  
  
			/* 带进位累加result的高32位和低32位 */  
			result = add32_with_carry(result>>32, result&0xffffffff);  
  
			/* 之前始按8字节对齐,可能有4字节剩下 */  
			if (len & 4) {  
				result += *(unsigned int *) buff;  
				buff += 4;  
			}  
		}  
  
	   /* 更早前按4字节对齐,可能有2字节剩下 */  
		if (len & 2) {  
			result += *(unsigned short *) buff;  
			buff += 2;  
		}  
	}  
  
	/* 最早之前按2字节对齐,可能有1字节剩下 */  
	if (len & 1)  
		result += *buff;  
  
	/* 再次带进位累加result的高32位和低32位 */  
	result = add32_with_carry(result>>32, result & 0xffffffff);   
  
	/* 这里涉及到一个技巧,用于处理初始地址为奇数的情况 */  
	if (unlikely(odd)) {  
		result = from32to16(result); /* 累加到result的低16位 */  
		/* result为:0 0 a b 
		 * 然后交换a和b,result变为:0 0 b a 
		 */  
		result = ((result >> 8) & 0xff) | ((result & oxff) << 8);  
	}  
  
	return result; /* 返回result的低32位 */  
}  
1
2
3
4
5
6
7
8
9
static inline unsigned short from32to16(unsigned a)  
{  
	unsigned short b = a >> 16;  
	asm ("addw %w2, %w0\n\t"  
			  "adcw $0, %w0\n"  
			  : "=r" (b)  
			  : "0" (b), "r" (a));  
	return b;  
}  

csum_partial_copy_from_user()用于拷贝用户空间数据到内核空间,同时计算用户数据的校验和,结果保存到skb->csum中(X86_64)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/** 
 * csum_partial_copy_from_user - Copy and checksum from user space. 
 * @src: source address (user space) 
 * @dst: destination address 
 * @len: number of bytes to be copied. 
 * @isum: initial sum that is added into the result (32bit unfolded) 
 * @errp: set to -EFAULT for an bad source address. 
 * 
 * Returns an 32bit unfolded checksum of the buffer. 
 * src and dst are best aligned to 64bits. 
 */  
  
unsigned int csum_partial_copy_from_user(const unsigned char __user *src,  
						unsigned char *dst, int len, unsigned int isum, int *errp)  
{  
	might_sleep();  
	*errp = 0;  
  
	if (likely(access_ok(VERIFY_READ, src, len))) {  
  
		/* Why 6, not 7? To handle odd addresses aligned we would need to do considerable 
		 * complications to fix the checksum which is defined as an 16bit accumulator. The fix 
		 * alignment code is primarily for performance compatibility with 32bit and that will handle 
		 * odd addresses slowly too. 
		 * 处理X010、X100、X110的起始地址。不处理X001,因为这会使复杂度大增加。 
		 */  
		if (unlikely((unsigned long)src & 6)) {  
			while (((unsigned long)src & 6) && len >= 2) {  
				__u16 val16;  
				*errp = __get_user(val16, (__u16 __user *)src);  
				if (*errp)  
					return isum;  
				*(__u16 *)dst = val16;  
				isum = add32_with_carry(isum, val16);  
				src += 2;  
				dst += 2;  
				len -= 2;  
			}  
		}  
  
		/* 计算函数是用纯汇编实现的,应该是因为效率吧 */  
		isum = csum_parial_copy_generic((__force void *)src, dst, len, isum, errp, NULL);  
  
		if (likely(*errp == 0))  
			return isum; /* 成功 */  
	}  
  
	*errp = -EFAULT;  
	memset(dst, 0, len);  
	return isum;  
}  

上述的实现比较复杂,来看下最简单的csum_partial_copy_from_user()实现(um)。

1
2
3
4
5
6
7
8
9
10
11
unsigned int csum_partial_copy_from_user(const unsigned char *src,  
						unsigned char *dst, int len, int sum,  
						int *err_ptr)  
{  
	if (copy_from_user(dst, src, len)) { /* 拷贝用户空间数据到内核空间 */  
		*err_ptr = -EFAULT; /* bad address */  
		return (-1);  
	}  
  
	return csum_partial(dst, len, sum); /* 计算用户数据的校验和,会存到skb->csum中 */  
}  

接收校验

@tcp_v4_rcv
/* 检查校验和 */
if (skb->ip_summed != CHECKSUM_UNNECESSARY && tcp_v4_checksum_init(skb))  
    goto bad_packet;   

接收校验的第一部分,主要是计算伪首部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static int tcp_v4_checksum_init(struct sk_buff *skb)  
{  
	/* 如果TCP报头、TCP数据的反码累加已经由硬件完成 */  
	if (skb->ip_summed == CHECKSUM_HW) {  
  
		/* 现在只需要再累加上伪首部,取反获取最终的校验和。 
		 * 校验和为0时,表示TCP数据报正确。 
		 */  
		if (! tcp_v4_check(skb->h.th, skb->len, skb->nh.iph->saddr, skb->nh.iph->daddr, skb->csum)) {  
			skb->ip_summed = CHECKSUM_UNNECESSARY;  
			return 0; /* 校验成功 */  
  
		} /* 没有else失败退出吗?*/  
	}  
  
	/* 对伪首部进行反码累加,主要用于软件方法 */  
	skb->csum = csum_tcpudp_nofold(skb->nh.iph->saddr, skb->nh.iph->daddr, skb->len, IPPROTO_TCP, 0);  
   
  
	/* 对于长度小于76字节的小包,接着累加TCP报头和报文,完成校验;否则,以后再完成检验。*/  
	if (skb->len <= 76) {  
		return __skb_checksum_complete(skb);  
	}  
}  

接收校验的第二部分,计算报头和报文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
tcp_v4_rcv、tcp_v4_do_rcv()

  | --> tcp_checksum_complete()

      | --> __tcp_checksum_complete()

          | --> __skb_checksum_complete()


tcp_rcv_established()

  | --> tcp_checksum_complete_user()

      | --> __tcp_checksum_complete_user()

          | --> __tcp_checksum_complete()

              | --> __skb_checksum_complete()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned int __skb_checksum_complete(struct sk_buff *skb)  
{  
	unsigned int sum;  
  
	sum = (u16) csum_fold(skb_checksum(skb, 0, skb->len, skb->csum));  
  
	if (likely(!sum)) { /* sum为0表示成功了 */  
		/* 硬件检测失败,软件检测成功了,说明硬件检测有误 */  
		if (unlikely(skb->ip_summed == CHECKSUM_HW))  
			netdev_rx_csum_fault(skb->dev);  
		skb->ip_summed = CHECKSUM_UNNECESSARY;  
	}  
	return sum;  
}  

计算skb包的校验和时,可以指定相对于skb->data的偏移量offset。由于skb包可能由分页和分段,所以需要考虑skb->data + offset是位于此skb段的线性区中、还是此skb的分页中,或者位于其它分段中。这个函数逻辑比较复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/* Checksum skb data. */  
unsigned int skb_checksum(const struct sk_buff *skb, int offset, int len, unsigned int csum)  
{  
	int start = skb_headlen(skb); /* 线性区域长度 */  
	/* copy > 0,说明offset在线性区域中。 
	 * copy < 0,说明offset在此skb的分页数据中,或者在其它分段skb中。 
	 */  
	int i, copy = start - offset;  
	int pos = 0; /* 表示校验了多少数据 */  
  
	/* Checksum header. */  
	if (copy > 0) { /* 说明offset在本skb的线性区域中 */  
		if (copy > len)  
			copy = len; /* 不能超过指定的校验长度 */  
  
		/* 累加copy长度的线性区校验 */  
		csum = csum_partial(skb->data + offset, copy, csum);  
  
		if ((len -= copy) == 0)  
			return csum;  
  
		offset += copy; /* 接下来从这里继续处理 */  
		pos = copy; /* 已处理数据长 */  
	}  
  
	/* 累加本skb分页数据的校验和 */  
	for (i = 0; i < skb_shinfo(skb)->nr_frags; i++) {  
		int end;  
		BUG_TRAP(start <= offset + len);  
	  
		end = start + skb_shinfo(skb)->frags[i].size;  
  
		if ((copy = end - offset) > 0) { /* 如果offset位于本页中,或者线性区中 */  
			unsigned int csum2;  
			u8 *vaddr; /* 8位够吗?*/  
			skb_frag_t *frag = &skb_shinfo(skb)->frags[i];  
   
			if (copy > len)  
				copy = len;  
  
			vaddr = kmap_skb_frag(frag); /* 把物理页映射到内核空间 */  
			csum2 = csum_partial(vaddr + frag->page_offset + offset - start, copy, 0);  
			kunmap_skb_frag(vaddr); /* 解除映射 */  
  
			/* 如果pos为奇数,需要对csum2进行处理。 
			 * csum2:a, b, c, d => b, a, d, c 
			 */  
			csum = csum_block_add(csum, csum2, pos);  
  
			if (! (len -= copy))  
				return csum;  
  
			offset += copy;  
			pos += copy;  
		}  
		start = end; /* 接下来从这里处理 */  
	}  
   
	/* 如果此skb是个大包,还有其它分段 */  
	if (skb_shinfo(skb)->frag_list) {  
		struct sk_buff *list = skb_shinfo(skb)->frag_list;  
  
		for (; list; list = list->next) {  
			int end;  
			BUG_TRAP(start <= offset + len);  
   
			end = start + list->len;  
  
			if ((copy = end - offset) > 0) { /* 如果offset位于此skb分段中,或者分页,或者线性区 */  
				unsigned int csum2;  
				if (copy > len)  
					copy = len;  
  
				csum2 = skb_checksum(list, offset - start, copy, 0); /* 递归调用 */  
				csum = csum_block_add(csum, csum2, pos);  
				if ((len -= copy) == 0)  
					return csum;  
  
				offset += copy;  
				pos += copy;  
			}  
			start = end;  
		}  
	}  
  
	BUG_ON(len);  
	return csum;  
}

重算skb的checksum

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <linux/version.h>
#include <linux/net.h>
#include <linux/ip.h>
#include <linux/tcp.h>
#include <net/tcp.h>

void skbcsum(struct sk_buff *skb)
{
	struct tcphdr *tcph;
	struct iphdr *iph;
	int iphl;
	int tcphl;
	int tcplen;

	iph = (struct iphdr *)skb->data;
	iphl = iph->ihl << 2;
	tcph = (struct tcphdr *)(skb->data + iphl);
	tcphl = tcph->doff << 2;

	iph->check = 0;
	iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);

	tcph->check    = 0;
	tcplen        = skb->len - (iph->ihl << 2);
	if (skb->ip_summed == CHECKSUM_PARTIAL) {
		tcph->check = ~csum_tcpudp_magic(iph->saddr, iph->daddr,
				tcplen, IPPROTO_TCP, 0);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 32)
		skb->csum = offsetof(struct tcphdr, check);
#else
		skb->csum_start    = skb_transport_header(skb) - skb->head;
		skb->csum_offset = offsetof(struct tcphdr, check);
#endif
	}
	else {
		skb->csum = 0;
		skb->csum = skb_checksum(skb, iph->ihl << 2, tcplen, 0);
		tcph->check = csum_tcpudp_magic(iph->saddr, iph->daddr,
				tcplen, IPPROTO_TCP, skb->csum);

	}
}