gitbook/趣谈Linux操作系统/docs/106490.md
2022-09-03 22:05:03 +08:00

497 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 45 | 发送网络包(上):如何表达我们想让合作伙伴做什么?
上一节,我们通过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->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。
```
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。
```
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。
```
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。
```
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\],会指向一个数组的页面,就不能保证连续了。
![](https://static001.geekbang.org/resource/image/9a/b8/9ad34c3c748978f915027d5085a858b8.png)
于是我们就有了**第四步**。在注释/\* 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 > 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),也就是说为了避免拼命发包,把网络塞满了,定义一个窗口的概念,在这个窗口之内的才能发送,超过这个窗口的就不能发送,来控制发送的频率。
那窗口大小是多少呢?就是遵循下面这个著名的拥塞窗口变化图。
![](https://static001.geekbang.org/resource/image/40/1f/404a6c5041452c0641ae3cba5319dc1f.png)
一开始的窗口只有一个mss大小叫作slow start(慢启动)。一开始的增长速度的很快的,翻倍增长。一旦到达一个临界值ssthresh,就变成线性增长,我们就称为**拥塞避免**。什么时候算真正拥塞呢?就是出现了丢包。一旦丢包,一种方法是马上降回到一个mss,然后重复先翻倍再线性对的过程。如果觉得太过激进,也可以有第二种方法,就是降到当前cwnd的一半,然后进行线性增长。
在代码中,tcp\_cwnd\_test会将当前的snd\_cwnd,减去已经在窗口里面尚未发送完毕的网络包,那就是剩下的窗口大小cwnd\_quota,也即就能发送这么多了。
第三个概念就是**接收窗口**rwnd的概念(receive window),也叫滑动窗口。如果说拥塞窗口是为了怕把网络塞满,在出现丢包的时候减少发送速度,那么滑动窗口就是为了怕把接收方塞满,而控制发送速度。
![](https://static001.geekbang.org/resource/image/97/65/9791e2f9ff63a9d8f849df7cd55fe965.png)
滑动窗口,其实就是接收方告诉发送方自己的网络包的接收能力,超过这个能力,我就受不了了。因为滑动窗口的存在,将发送方的缓存分成了四个部分。
* 第一部分:发送了并且已经确认的。这部分是已经发送完毕的网络包,这部分没有用了,可以回收。
* 第二部分:发送了但尚未确认的。这部分,发送方要等待,万一发送不成功,还要重新发送,所以不能删除。
* 第三部分:没有发送,但是已经等待发送的。这部分是接收方空闲的能力,可以马上发送,接收方收得了。
* 第四部分:没有发送,并且暂时还不会发送的。这部分已经超过了接收方的接收能力,再发送接收方就收不了了。
![](https://static001.geekbang.org/resource/image/b6/31/b62eea403e665bb196dceba571392531.png)
因为滑动窗口的存在,接收方的缓存也要分成了三个部分。
* 第一部分:接受并且确认过的任务。这部分完全接收成功了,可以交给应用层了。
* 第二部分:还没接收,但是马上就能接收的任务。这部分有的网络包到达了,但是还没确认,不算完全完毕,有的还没有到达,那就是接收方能够接受的最大的网络包数量。
* 第三部分:还没接收,也没法接收的任务。这部分已经超出接收方能力。
在网络包的交互过程中,接收方会将第二部分的大小,作为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)->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,真的去发送一个网络包。
```
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头的格式。
![](https://static001.geekbang.org/resource/image/be/0e/be225a97816a664367f29be9046aa30e.png)
这里面有源端口,设置为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函数。
```
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,
};
```
## 总结时刻
这一节,我们解析了发送一个网络包的一部分过程,如下图所示。
![](https://static001.geekbang.org/resource/image/dc/44/dc66535fa7e1a10fd6d728865f6c9344.png)
这个过程分成几个层次。
* 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的包,看看里面的结构。
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。