<audio id="audio" title="45 | 发送网络包（上）：如何表达我们想让合作伙伴做什么？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d7/c6/d7a052488b1ddd0e4e4a70f6832f33c6.mp3"></audio>

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

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

## 解析socket的Write操作

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

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

对于socket来讲，它的file_operations定义如下：

```
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。

```
static ssize_t sock_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
	struct file *file = iocb-&gt;ki_filp;
	struct socket *sock = file-&gt;private_data;
	struct msghdr msg = {.msg_iter = *from,
			     .msg_iocb = iocb};
	ssize_t res;
......
	res = sock_sendmsg(sock, &amp;msg);
	*from = msg.msg_iter;
	return res;
}

```

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

```
static inline int sock_sendmsg_nosec(struct socket *sock, struct msghdr *msg)
{
	int ret = sock-&gt;ops-&gt;sendmsg(sock, msg, msg_data_left(msg));
......
}

```

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

```
int inet_sendmsg(struct socket *sock, struct msghdr *msg, size_t size)
{
	struct sock *sk = sock-&gt;sk;
......
	return sk-&gt;sk_prot-&gt;sendmsg(sk, msg, size);
}

```

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

## 解析tcp_sendmsg函数

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

```
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, &amp;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-&gt;ip_summed == CHECKSUM_NONE)
				max = mss_now;
			copy = max - skb-&gt;len;
		}

		if (copy &lt;= 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(&amp;sk-&gt;sk_write_queue);
			skb = sk_stream_alloc_skb(sk,
						  select_size(sk, sg, first_skb),
						  sk-&gt;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 &gt; msg_data_left(msg))
			copy = msg_data_left(msg);

		/* Where to copy to? */
		if (skb_availroom(skb) &gt; 0) {
			/* We have some space in skb head. Superb! */
			copy = min_t(int, copy, skb_availroom(skb));
			err = skb_add_data_nocache(sk, skb, &amp;msg-&gt;msg_iter, copy);
......
		} else {
			bool merge = true;
			int i = skb_shinfo(skb)-&gt;nr_frags;
			struct page_frag *pfrag = sk_page_frag(sk);
......
			copy = min_t(int, copy, pfrag-&gt;size - pfrag-&gt;offset);
......
			err = skb_copy_to_page_nocache(sk, &amp;msg-&gt;msg_iter, skb,
						       pfrag-&gt;page,
						       pfrag-&gt;offset,
						       copy);
......
			pfrag-&gt;offset += copy;
		}

......
		tp-&gt;write_seq += copy;
		TCP_SKB_CB(skb)-&gt;end_seq += copy;
		tcp_skb_pcount_set(skb, 0);

		copied += copy;
		if (!msg_data_left(msg)) {
			if (unlikely(flags &amp; MSG_EOR))
				TCP_SKB_CB(skb)-&gt;eor = 1;
			goto out;
		}

		if (skb-&gt;len &lt; max || (flags &amp; MSG_OOB) || unlikely(tp-&gt;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-&gt;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。

```
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 &amp; 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-&gt;data 的值，来逐步剥离协议首部。而要发送报文时，各协议会创建 sk_buff{}，在经过各下层协议时，通过减少 skb-&gt;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]，会指向一个数组的页面，就不能保证连续了。

<img src="https://static001.geekbang.org/resource/image/9a/b8/9ad34c3c748978f915027d5085a858b8.png" alt="">

于是我们就有了**第四步**。在注释/* 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是如何发送网络包的。

```
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 &gt; 1 &amp;&amp; !tcp_urg_mode(tp))
            limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle);

		if (skb-&gt;len &gt; limit &amp;&amp;
		    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-&gt;len, mss_now)。也就是sk_buff的长度除以mss_now，应该分成几个段。如果算出来要分成多个段，接下来就是要看，是在这里（协议栈的代码里面）分好，还是等待到了底层网卡再分。

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

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

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

<img src="https://static001.geekbang.org/resource/image/40/1f/404a6c5041452c0641ae3cba5319dc1f.png" alt="">

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

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

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

<img src="https://static001.geekbang.org/resource/image/97/65/9791e2f9ff63a9d8f849df7cd55fe965.png" alt="">

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

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

<img src="https://static001.geekbang.org/resource/image/b6/31/b62eea403e665bb196dceba571392531.png" alt="">

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

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

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

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

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

```
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)-&gt;seq;
        max_len = mss_now * max_segs;

        if (likely(max_len &lt;= window &amp;&amp; skb != tcp_write_queue_tail(sk)))
                return max_len;

        needed = min(skb-&gt;len, window);

        if (max_len &lt;= needed)
                return max_len;
......
        return needed;
}

```

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

在一个循环的最后，是调用tcp_transmit_skb，真的去发送一个网络包。

```
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-&gt;skb_mstamp = tp-&gt;tcp_mstamp;
    inet = inet_sk(sk);
    tcb = TCP_SKB_CB(skb);
    memset(&amp;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-&gt;data;
    th-&gt;source      = inet-&gt;inet_sport;
    th-&gt;dest        = inet-&gt;inet_dport;
    th-&gt;seq         = htonl(tcb-&gt;seq);
    th-&gt;ack_seq     = htonl(tp-&gt;rcv_nxt);
    *(((__be16 *)th) + 6)   = htons(((tcp_header_size &gt;&gt; 2) &lt;&lt; 12) |
                    tcb-&gt;tcp_flags);

    th-&gt;check       = 0;
    th-&gt;urg_ptr     = 0;
......
    tcp_options_write((__be32 *)(th + 1), tp, &amp;opts);
    th-&gt;window  = htons(min(tp-&gt;rcv_wnd, 65535U));
......
    err = icsk-&gt;icsk_af_ops-&gt;queue_xmit(sk, skb, &amp;inet-&gt;cork.fl);
......
}

```

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

<img src="https://static001.geekbang.org/resource/image/be/0e/be225a97816a664367f29be9046aa30e.png" alt="">

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

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

```
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,
};

```

## 总结时刻

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

<img src="https://static001.geekbang.org/resource/image/dc/44/dc66535fa7e1a10fd6d728865f6c9344.png" alt="">

这个过程分成几个层次。

- 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的包，看看里面的结构。

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


