上一节,我们通过 socket 函数、bind 函数、listen 函数、accept 函数以及 connect 函数,在内核建立好了数据结构,并完成了 TCP 连接建立的三次握手过程。

这一节,我们接着来分析,发送一个网络包的过程。

解析 socket 的 Write 操作

socket 对于用户来讲,是一个文件一样的存在,拥有一个文件描述符。因而对于网络包的发送,我们可以使用对于 socket 文件的写入系统调用,也就是 write 系统调用。

write 系统调用对于一个文件描述符的操作,大致过程都是类似的。在文件系统那一节,我们已经详细解析过,这里不再多说。对于每一个打开的文件都有一个 struct file 结构,write 系统调用会最终调用 stuct file 结构指向的 file_operations 操作。

对于 socket 来讲,它的 file_operations 定义如下:

 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 const struct file_operations socket_file_ops = {

	.owner =	THIS_MODULE,

	.llseek =	no_llseek,

	.read_iter =	sock_read_iter,

	.write_iter =	sock_write_iter,

	.poll =		sock_poll,

	.unlocked_ioctl = sock_ioctl,

	.mmap =		sock_mmap,

	.release =	sock_close,

	.fasync =	sock_fasync,

	.sendpage =	sock_sendpage,

	.splice_write = generic_splice_sendpage,

	.splice_read =	sock_splice_read,

};

按照文件系统的写入流程,调用的是 sock_write_iter。

 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 ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)

{

	struct file *file = iocb->ki_filp;

	struct socket *sock = file->private_data;

	struct msghdr msg = {.msg_iter = *from,

			     .msg_iocb = iocb};

	ssize_t res;

......

	res = sock_sendmsg(sock, &msg);

	*from = msg.msg_iter;

	return res;

}

在 sock_write_iter 中,我们通过 VFS 中的 struct file,将创建好的 socket 结构拿出来,然后调用 sock_sendmsg。而 sock_sendmsg 会调用 sock_sendmsg_nosec。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10

static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)

{

	int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));

......

}

这里调用了 socket 的 ops 的 sendmsg,我们在上一节已经遇到它好几次了。根据 inet_stream_ops 的定义,我们这里调用的是 inet_sendmsg。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12

int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)

{

	struct sock *sk = sock->sk;

......

	return sk->sk_prot->sendmsg(sk, msg, size);

}

这里面,从 socket 结构中,我们可以得到更底层的 sock 结构,然后调用 sk_prot 的 sendmsg 方法。这个我们同样在上一节遇到好几次了。

解析 tcp_sendmsg 函数

根据 tcp_prot 的定义,我们调用的是 tcp_sendmsg。

  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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172

int tcp_sendmsg(struct sock *sk, struct msghdr *msg, size_t size)

{

	struct tcp_sock *tp = tcp_sk(sk);

	struct sk_buff *skb;

	int flags, err, copied = 0;

	int mss_now = 0, size_goal, copied_syn = 0;

	long timeo;

......

	/* Ok commence sending. */

	copied = 0;

restart:

	mss_now = tcp_send_mss(sk, &size_goal, flags);

 	while (msg_data_left(msg)) {

		int copy = 0;

		int max = size_goal;

 		skb = tcp_write_queue_tail(sk);

		if (tcp_send_head(sk)) {

			if (skb->ip_summed == CHECKSUM_NONE)

				max = mss_now;

			copy = max - skb->len;

		}

 		if (copy <= 0 || !tcp_skb_can_collapse_to(skb)) {

			bool first_skb;

 new_segment:

			/* Allocate new segment. If the interface is SG,

			 * allocate skb fitting to single page.

			 */

			if (!sk_stream_memory_free(sk))

				goto wait_for_sndbuf;

......

			first_skb = skb_queue_empty(&sk->sk_write_queue);

			skb = sk_stream_alloc_skb(sk,

						  select_size(sk, sg, first_skb),

						  sk->sk_allocation,

						  first_skb);

......

			skb_entail(sk, skb);

			copy = size_goal;

			max = size_goal;

......

		}

 		/* Try to append data to the end of skb. */

		if (copy > msg_data_left(msg))

			copy = msg_data_left(msg);

 		/* Where to copy to? */

		if (skb_availroom(skb) > 0) {

			/* We have some space in skb head. Superb! */

			copy = min_t(int, copy, skb_availroom(skb));

			err = skb_add_data_nocache(sk, skb, &msg->msg_iter, copy);

......

		} else {

			bool merge = true;

			int i = skb_shinfo(skb)->nr_frags;

			struct page_frag *pfrag = sk_page_frag(sk);

......

			copy = min_t(int, copy, pfrag->size - pfrag->offset);

......

			err = skb_copy_to_page_nocache(sk, &msg->msg_iter, skb,

						       pfrag->page,

						       pfrag->offset,

						       copy);

......

			pfrag->offset += copy;

		}

 ......

		tp->write_seq += copy;

		TCP_SKB_CB(skb)->end_seq += copy;

		tcp_skb_pcount_set(skb, 0);

 		copied += copy;

		if (!msg_data_left(msg)) {

			if (unlikely(flags & MSG_EOR))

				TCP_SKB_CB(skb)->eor = 1;

			goto out;

		}

 		if (skb->len < max || (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;

......

	}

......

}

tcp_sendmsg 的实现还是很复杂的,这里面做了这样几件事情。

msg 是用户要写入的数据,这个数据要拷贝到内核协议栈里面去发送;在内核协议栈里面,网络包的数据都是由 struct sk_buff 维护的,因而第一件事情就是找到一个空闲的内存空间,将用户要写入的数据,拷贝到 struct sk_buff 的管辖范围内。而第二件事情就是发送 struct sk_buff。

在 tcp_sendmsg 中,我们首先通过强制类型转换,将 sock 结构转换为 struct tcp_sock,这个是维护 TCP 连接状态的重要数据结构。

接下来是 tcp_sendmsg 的第一件事情,把数据拷贝到 struct sk_buff。

我们先声明一个变量 copied,初始化为 0,这表示拷贝了多少数据。紧接着是一个循环,while (msg_data_left(msg)),也即如果用户的数据没有发送完毕,就一直循环。循环里声明了一个 copy 变量,表示这次拷贝的数值,在循环的最后有 copied += copy,将每次拷贝的数量都加起来。

我们这里只需要看一次循环做了哪些事情。

第一步,tcp_write_queue_tail 从 TCP 写入队列 sk_write_queue 中拿出最后一个 struct sk_buff,在这个写入队列中排满了要发送的 struct sk_buff,为什么要拿最后一个呢?这里面只有最后一个,可能会因为上次用户给的数据太少,而没有填满。

第二步,tcp_send_mss 会计算 MSS,也即 Max Segment Size。这是什么呢?这个意思是说,我们在网络上传输的网络包的大小是有限制的,而这个限制在最底层开始就有。

MTU(Maximum Transmission Unit,最大传输单元)是二层的一个定义。以以太网为例,MTU 为 1500 个 Byte,前面有 6 个 Byte 的目标 MAC 地址,6 个 Byte 的源 MAC 地址,2 个 Byte 的类型,后面有 4 个 Byte 的 CRC 校验,共 1518 个 Byte。

在 IP 层,一个 IP 数据报在以太网中传输,如果它的长度大于该 MTU 值,就要进行分片传输。

在 TCP 层有个MSS(Maximum Segment Size,最大分段大小),等于 MTU 减去 IP 头,再减去 TCP 头。也就是,在不分片的情况下,TCP 里面放的最大内容。

在这里,max 是 struct sk_buff 的最大数据长度,skb->len 是当前已经占用的 skb 的数据长度,相减得到当前 skb 的剩余数据空间。

第三步,如果 copy 小于 0,说明最后一个 struct sk_buff 已经没地方存放了,需要调用 sk_stream_alloc_skb,重新分配 struct sk_buff,然后调用 skb_entail,将新分配的 sk_buff 放到队列尾部。

struct sk_buff 是存储网络包的重要的数据结构,在应用层数据包叫 data,在 TCP 层我们称为 segment,在 IP 层我们叫 packet,在数据链路层称为 frame。在 struct sk_buff,首先是一个链表,将 struct sk_buff 结构串起来。

接下来,我们从 headers_start 开始,到 headers_end 结束,里面都是各层次的头的位置。这里面有二层的 mac_header、三层的 network_header 和四层的 transport_header。

 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

struct sk_buff {

	union {

		struct {

			/* These two members must be first. */

			struct sk_buff		*next;

			struct sk_buff		*prev;

......

		};

		struct rb_node	rbnode; /* used in netem & tcp stack */

	};

......

	/* private: */

	__u32			headers_start[0];

	/* public: */

......

	__u32			priority;

	int			skb_iif;

	__u32			hash;

	__be16			vlan_proto;

	__u16			vlan_tci;

......

	union {

		__u32		mark;

		__u32		reserved_tailroom;

	};

 	union {

		__be16		inner_protocol;

		__u8		inner_ipproto;

	};

 	__u16			inner_transport_header;

	__u16			inner_network_header;

	__u16			inner_mac_header;

 	__be16			protocol;

	__u16			transport_header;

	__u16			network_header;

	__u16			mac_header;

 	/* private: */

	__u32			headers_end[0];

	/* public: */

 	/* These elements must be at the end, see alloc_skb() for details.  */

	sk_buff_data_t		tail;

	sk_buff_data_t		end;

	unsigned char		*head,

				*data;

	unsigned int		truesize;

	refcount_t		users;

};

最后几项,head 指向分配的内存块起始地址。data 这个指针指向的位置是可变的。它有可能随着报文所处的层次而变动。当接收报文时,从网卡驱动开始,通过协议栈层层往上传送数据报,通过增加 skb->data 的值,来逐步剥离协议首部。而要发送报文时,各协议会创建 sk_buff{},在经过各下层协议时,通过减少 skb->data 的值来增加协议首部。tail 指向数据的结尾,end 指向分配的内存块的结束地址。

要分配这样一个结构,sk_stream_alloc_skb 会最终调用到 __alloc_skb。在这个函数里面,除了分配一个 sk_buff 结构之外,还要分配 sk_buff 指向的数据区域。这段数据区域分为下面这几个部分。

第一部分是连续的数据区域。紧接着是第二部分,一个 struct skb_shared_info 结构。这个结构是对于网络包发送过程的一个优化,因为传输层之上就是应用层了。按照 TCP 的定义,应用层感受不到下面的网络层的 IP 包是一个个独立的包的存在的。反正就是一个流,往里写就是了,可能一下子写多了,超过了一个 IP 包的承载能力,就会出现上面 MSS 的定义,拆分成一个个的 Segment 放在一个个的 IP 包里面,也可能一次写一点,一次写一点,这样数据是分散的,在 IP 层还要通过内存拷贝合成一个 IP 包。

为了减少内存拷贝的代价,有的网络设备支持分散聚合(Scatter/Gather)I/O,顾名思义,就是 IP 层没必要通过内存拷贝进行聚合,让散的数据零散的放在原处,在设备层进行聚合。如果使用这种模式,网络包的数据就不会放在连续的数据区域,而是放在 struct skb_shared_info 结构里面指向的离散数据,skb_shared_info 的成员变量 skb_frag_t frags[MAX_SKB_FRAGS],会指向一个数组的页面,就不能保证连续了。

于是我们就有了第四步。在注释 /* Where to copy to? */ 后面有个 if-else 分支。if 分支就是 skb_add_data_nocache 将数据拷贝到连续的数据区域。else 分支就是 skb_copy_to_page_nocache 将数据拷贝到 struct skb_shared_info 结构指向的不需要连续的页面区域。

第五步,就是要发生网络包了。第一种情况是积累的数据报数目太多了,因而我们需要通过调用 __tcp_push_pending_frames 发送网络包。第二种情况是,这是第一个网络包,需要马上发送,调用 tcp_push_one。无论 __tcp_push_pending_frames 还是 tcp_push_one,都会调用 tcp_write_xmit 发送网络包。

至此,tcp_sendmsg 解析完了。

解析 tcp_write_xmit 函数

接下来我们来看,tcp_write_xmit 是如何发送网络包的。

 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

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp)

{

	struct tcp_sock *tp = tcp_sk(sk);

	struct sk_buff *skb;

	unsigned int tso_segs, sent_pkts;

	int cwnd_quota;

......

	max_segs = tcp_tso_segs(sk, mss_now);

	while ((skb = tcp_send_head(sk))) {

		unsigned int limit;

......

		tso_segs = tcp_init_tso_segs(skb, mss_now);

......

		cwnd_quota = tcp_cwnd_test(tp, skb);

......

		if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) {

			is_rwnd_limited = true;

			break;

		}

......

		limit = mss_now;

        if (tso_segs > 1 && !tcp_urg_mode(tp))

            limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle);

 		if (skb->len > limit &&

		    unlikely(tso_fragment(sk, skb, limit, mss_now, gfp)))

			break;

......

		if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))

			break;

 repair:

		/* Advance the send_head.  This one is sent out.

		 * This call will increment packets_out.

		 */

		tcp_event_new_data_sent(sk, skb);

 		tcp_minshall_update(tp, mss_now, skb);

		sent_pkts += tcp_skb_pcount(skb);

 		if (push_one)

			break;

	}

......

}

这里面主要的逻辑是一个循环,用来处理发送队列,只要队列不空,就会发送。

在一个循环中,涉及 TCP 层的很多传输算法,我们来一一解析。

第一个概念是TSO(TCP Segmentation Offload)。如果发送的网络包非常大,就像上面说的一样,要进行分段。分段这个事情可以由协议栈代码在内核做,但是缺点是比较费 CPU,另一种方式是延迟到硬件网卡去做,需要网卡支持对大数据包进行自动分段,可以降低 CPU 负载。

在代码中,tcp_init_tso_segs 会调用 tcp_set_skb_tso_segs。这里面有这样的语句:DIV_ROUND_UP(skb->len, mss_now)。也就是 sk_buff 的长度除以 mss_now,应该分成几个段。如果算出来要分成多个段,接下来就是要看,是在这里(协议栈的代码里面)分好,还是等待到了底层网卡再分。

于是,调用函数 tcp_mss_split_point,开始计算切分的 limit。这里面会计算 max_len = mss_now * max_segs,根据现在不切分来计算 limit,所以下一步的判断中,大部分情况下 tso_fragment 不会被调用,等待到了底层网卡来切分。

第二个概念是拥塞窗口的概念(cwnd,congestion window),也就是说为了避免拼命发包,把网络塞满了,定义一个窗口的概念,在这个窗口之内的才能发送,超过这个窗口的就不能发送,来控制发送的频率。

那窗口大小是多少呢?就是遵循下面这个著名的拥塞窗口变化图。

一开始的窗口只有一个 mss 大小叫作 slow start(慢启动)。一开始的增长速度的很快的,翻倍增长。一旦到达一个临界值 ssthresh,就变成线性增长,我们就称为拥塞避免。什么时候算真正拥塞呢?就是出现了丢包。一旦丢包,一种方法是马上降回到一个 mss,然后重复先翻倍再线性对的过程。如果觉得太过激进,也可以有第二种方法,就是降到当前 cwnd 的一半,然后进行线性增长。

在代码中,tcp_cwnd_test 会将当前的 snd_cwnd,减去已经在窗口里面尚未发送完毕的网络包,那就是剩下的窗口大小 cwnd_quota,也即就能发送这么多了。

第三个概念就是接收窗口rwnd 的概念(receive window),也叫滑动窗口。如果说拥塞窗口是为了怕把网络塞满,在出现丢包的时候减少发送速度,那么滑动窗口就是为了怕把接收方塞满,而控制发送速度。

滑动窗口,其实就是接收方告诉发送方自己的网络包的接收能力,超过这个能力,我就受不了了。因为滑动窗口的存在,将发送方的缓存分成了四个部分。

  • 第一部分:发送了并且已经确认的。这部分是已经发送完毕的网络包,这部分没有用了,可以回收。
  • 第二部分:发送了但尚未确认的。这部分,发送方要等待,万一发送不成功,还要重新发送,所以不能删除。
  • 第三部分:没有发送,但是已经等待发送的。这部分是接收方空闲的能力,可以马上发送,接收方收得了。
  • 第四部分:没有发送,并且暂时还不会发送的。这部分已经超过了接收方的接收能力,再发送接收方就收不了了。

因为滑动窗口的存在,接收方的缓存也要分成了三个部分。

  • 第一部分:接受并且确认过的任务。这部分完全接收成功了,可以交给应用层了。
  • 第二部分:还没接收,但是马上就能接收的任务。这部分有的网络包到达了,但是还没确认,不算完全完毕,有的还没有到达,那就是接收方能够接受的最大的网络包数量。
  • 第三部分:还没接收,也没法接收的任务。这部分已经超出接收方能力。

在网络包的交互过程中,接收方会将第二部分的大小,作为 AdvertisedWindow 发送给发送方,发送方就可以根据他来调整发送速度了。

在 tcp_snd_wnd_test 函数中,会判断 sk_buff 中的 end_seq 和 tcp_wnd_end(tp) 之间的关系,也即这个 sk_buff 是否在滑动窗口的允许范围之内。如果不在范围内,说明发送要受限制了,我们就要把 is_rwnd_limited 设置为 true。

接下来,tcp_mss_split_point 函数要被调用了。

 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

static unsigned int tcp_mss_split_point(const struct sock *sk,

                                        const struct sk_buff *skb,

                                        unsigned int mss_now,

                                        unsigned int max_segs,

                                        int nonagle)

{

        const struct tcp_sock *tp = tcp_sk(sk);

        u32 partial, needed, window, max_len;

         window = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;

        max_len = mss_now * max_segs;

         if (likely(max_len <= window && skb != tcp_write_queue_tail(sk)))

                return max_len;

         needed = min(skb->len, window);

         if (max_len <= needed)

                return max_len;

......

        return needed;

}

这里面除了会判断上面讲的,是否会因为超出 mss 而分段,还会判断另一个条件,就是是否在滑动窗口的运行范围之内,如果小于窗口的大小,也需要分段,也即需要调用 tso_fragment。

在一个循环的最后,是调用 tcp_transmit_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

static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,

                gfp_t gfp_mask)

{

    const struct inet_connection_sock *icsk = inet_csk(sk);

    struct inet_sock *inet;

    struct tcp_sock *tp;

    struct tcp_skb_cb *tcb;

    struct tcphdr *th;

    int err;

     tp = tcp_sk(sk);

     skb->skb_mstamp = tp->tcp_mstamp;

    inet = inet_sk(sk);

    tcb = TCP_SKB_CB(skb);

    memset(&opts, 0, sizeof(opts));

     tcp_header_size = tcp_options_size + sizeof(struct tcphdr);

    skb_push(skb, tcp_header_size);

     /* Build TCP header and checksum it. */

    th = (struct tcphdr *)skb->data;

    th->source      = inet->inet_sport;

    th->dest        = inet->inet_dport;

    th->seq         = htonl(tcb->seq);

    th->ack_seq     = htonl(tp->rcv_nxt);

    *(((__be16 *)th) + 6)   = htons(((tcp_header_size >> 2) << 12) |

                    tcb->tcp_flags);

     th->check       = 0;

    th->urg_ptr     = 0;

......

    tcp_options_write((__be32 *)(th + 1), tp, &opts);

    th->window  = htons(min(tp->rcv_wnd, 65535U));

......

    err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);

......

}

tcp_transmit_skb 这个函数比较长,主要做了两件事情,第一件事情就是填充 TCP 头,如果我们对着 TCP 头的格式。

这里面有源端口,设置为 inet_sport,有目标端口,设置为 inet_dport;有序列号,设置为 tcb->seq;有确认序列号,设置为 tp->rcv_nxt。我们把所有的 flags 设置为 tcb->tcp_flags。设置选项为 opts。设置窗口大小为 tp->rcv_wnd。

全部设置完毕之后,就会调用 icsk_af_ops 的 queue_xmit 方法,icsk_af_ops 指向 ipv4_specific,也即调用的是 ip_queue_xmit 函数。

 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

const struct inet_connection_sock_af_ops ipv4_specific = {

        .queue_xmit        = ip_queue_xmit,

        .send_check        = tcp_v4_send_check,

        .rebuild_header    = inet_sk_rebuild_header,

        .sk_rx_dst_set     = inet_sk_rx_dst_set,

        .conn_request      = tcp_v4_conn_request,

        .syn_recv_sock     = tcp_v4_syn_recv_sock,

        .net_header_len    = sizeof(struct iphdr),

        .setsockopt        = ip_setsockopt,

        .getsockopt        = ip_getsockopt,

        .addr2sockaddr     = inet_csk_addr2sockaddr,

        .sockaddr_len      = sizeof(struct sockaddr_in),

        .mtu_reduced       = tcp_v4_mtu_reduced,

};

总结时刻

这一节,我们解析了发送一个网络包的一部分过程,如下图所示。

这个过程分成几个层次。

  • VFS 层:write 系统调用找到 struct file,根据里面的 file_operations 的定义,调用 sock_write_iter 函数。sock_write_iter 函数调用 sock_sendmsg 函数。
  • Socket 层:从 struct file 里面的 private_data 得到 struct socket,根据里面 ops 的定义,调用 inet_sendmsg 函数。
  • Sock 层:从 struct socket 里面的 sk 得到 struct sock,根据里面 sk_prot 的定义,调用 tcp_sendmsg 函数。
  • TCP 层:tcp_sendmsg 函数会调用 tcp_write_xmit 函数,tcp_write_xmit 函数会调用 tcp_transmit_skb,在这里实现了 TCP 层面向连接的逻辑。
  • IP 层:扩展 struct sock,得到 struct inet_connection_sock,根据里面 icsk_af_ops 的定义,调用 ip_queue_xmit 函数。

课堂练习

如果你对 TCP 协议的结构不太熟悉,可以使用 tcpdump 命令截取一个 TCP 的包,看看里面的结构。

欢迎留言和我分享你的疑惑和见解,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。