Linux操作系统学习笔记(二十三)网络通信之收包

一. 简介

  本文将分析网络协议栈收包的整个流程,收包和发包是刚好相反的过程。根据顺序我们将依次介绍硬件设备驱动层、数据链路层、网络层、传输层、套接字文件系统的相关发包处理流程,内容较多较复杂,主要掌握整个流程即可。

二. 网卡驱动层

  网卡作为一个硬件,接收到网络包后靠中断来通知操作系统。但是这里有个问题:网络包的到来往往是很难预期的。网络吞吐量比较大的时候,网络包的到达会十分频繁。这个时候,如果非常频繁地去触发中断,会造成频繁的上下文切换,带来极大的开销。因此硬件处理厂商设计了一种机制,就是当一些网络包到来触发了中断,内核处理完这些网络包之后,我们可以先进入主动轮询 poll 网卡的方式主动去接收到来的网络包。如果一直有,就一直处理,等处理告一段落,就返回干其他的事情。当再有下一批网络包到来的时候,再中断,再轮询 poll。这样就会大大减少中断的数量,提升网络处理的效率,这种处理方式我们称为 NAPI

  本文以 Intel(R) PRO/10GbE 网卡驱动为例,在网卡驱动程序初始化的时候,我们会调用 ixgb_init_module()注册一个驱动 ixgb_driver,并且调用它的 probe 函数 ixgb_probe()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static struct pci_driver ixgb_driver = {
.name = ixgb_driver_name,
.id_table = ixgb_pci_tbl,
.probe = ixgb_probe,
.remove = ixgb_remove,
.err_handler = &ixgb_err_handler
};
MODULE_AUTHOR("Intel Corporation, <linux.nics@intel.com>");
MODULE_DESCRIPTION("Intel(R) PRO/10GbE Network Driver");
MODULE_LICENSE("GPL v2");
MODULE_VERSION(DRV_VERSION);

static int __init
ixgb_init_module(void)
{
pr_info("%s - version %s\n", ixgb_driver_string, ixgb_driver_version);
pr_info("%s\n", ixgb_copyright);
return pci_register_driver(&ixgb_driver);
}
module_init(ixgb_init_module);

   ixgb_probe() 会创建一个 struct net_device 表示这个网络设备,并且调用 netif_napi_add() 函数为这个网络设备注册一个轮询 poll 函数 ixgb_clean(),将来一旦出现网络包的时候,就通过该函数来轮询。

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
static int
ixgb_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct net_device *netdev = NULL;
struct ixgb_adapter *adapter;
......
netdev = alloc_etherdev(sizeof(struct ixgb_adapter));
SET_NETDEV_DEV(netdev, &pdev->dev);

pci_set_drvdata(pdev, netdev);
adapter = netdev_priv(netdev);
adapter->netdev = netdev;
adapter->pdev = pdev;
adapter->hw.back = adapter;
adapter->msg_enable = netif_msg_init(debug, DEFAULT_MSG_ENABLE);
adapter->hw.hw_addr = pci_ioremap_bar(pdev, BAR_0);
......
netdev->netdev_ops = &ixgb_netdev_ops;
ixgb_set_ethtool_ops(netdev);
netdev->watchdog_timeo = 5 * HZ;
netif_napi_add(netdev, &adapter->napi, ixgb_clean, 64);

strncpy(netdev->name, pci_name(pdev), sizeof(netdev->name) - 1);

adapter->bd_number = cards_found;
adapter->link_speed = 0;
adapter->link_duplex = 0;
......
}

  网卡被激活的时候会调用函数 ixgb_open()->ixgb_up(),在这里面注册一个硬件的中断处理函数。

1
2
3
4
5
6
7
intixgb_up(struct ixgb_adapter *adapter)
{
struct net_device *netdev = adapter->netdev;
......
err = request_irq(adapter->pdev->irq, ixgb_intr, irq_flags, netdev->name, netdev);
......
}

  如果一个网络包到来,触发了硬件中断,就会调用 ixgb_intr(),这里面会调用 __napi_schedule()

1
2
3
4
5
6
7
8
9
10
11
12
13
static irqreturn_t ixgb_intr(int irq, void *data)
{
struct net_device *netdev = data;
struct ixgb_adapter *adapter = netdev_priv(netdev);
struct ixgb_hw *hw = &adapter->hw;
......
if (napi_schedule_prep(&adapter->napi))
{
IXGB_WRITE_REG(&adapter->hw, IMC, ~0);
__napi_schedule(&adapter->napi);
}
return IRQ_HANDLED;
}

  __napi_schedule() 处于中断处理的关键部分,在被调用的时候,中断是暂时关闭的。处理网络包是个复杂的过程,需要到中断处理的延迟处理部分执行,所以 ____napi_schedule() 将当前设备放到 struct softnet_data 结构的 poll_list 里面,说明在延迟处理部分可以接着处理这个 poll_list 里面的网络设备。然后 ____napi_schedule() 触发一个软中断 NET_RX_SOFTIRQ,通过软中断触发中断处理的延迟处理部分,也是常用的手段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* __napi_schedule - schedule for receive
* @n: entry to schedule
*
* The entry's receive function will be scheduled to run.
* Consider using __napi_schedule_irqoff() if hard irqs are masked.
*/
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(this_cpu_ptr(&softnet_data), n);
local_irq_restore(flags);
}

static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list);
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

软中断 NET_RX_SOFTIRQ 对应的中断处理函数是 net_rx_action(),其逻辑为

  • 调用this_cpu_ptr(),得到 struct softnet_data 结构,这个结构在发送的时候我们也遇到过。当时它的 output_queue 用于网络包的发送,这里的 poll_list 用于网络包的接收。
  • 进入循环,从 poll_list 里面取出有网络包到达的设备,然后调用 napi_poll() 来轮询这些设备,napi_poll() 会调用最初设备初始化的时候注册的 poll 函数,对于 ixgb_driver对应的函数是 ixgb_clean()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
......
for (;;) {
struct napi_struct *n;
......
n = list_first_entry(&list, struct napi_struct, poll_list);
budget -= napi_poll(n, &repoll);
......
}
}

struct softnet_data
{
struct list_head poll_list;
......
struct Qdisc *output_queue;
struct Qdisc **output_queue_tailp;
......
}

  ixgb_clean() 实际调用ixgb_clean_rx_irq()。在网络设备的驱动层,有一个用于接收网络包的 rx_ring。它是一个环,从网卡硬件接收的包会放在这个环里面。这个环里面的 buffer_info[]是一个数组,存放的是网络包的内容。ij 是这个数组的下标,在 ixgb_clean_rx_irq() 里面的 while 循环中,依次处理环里面的数据。在这里面,我们看到了 ij 加一之后,如果超过了数组的大小,就跳回下标 0,就说明这是一个环。ixgb_check_copybreak() 函数将 buffer_info 里面的内容拷贝到 struct sk_buff *skb,从而可以作为一个网络包进行后续的处理,然后调用 netif_receive_skb()进入MAC层继续进行收包的解析处理。

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
static int ixgb_clean(struct napi_struct *napi, int budget)
{
struct ixgb_adapter *adapter = container_of(napi, struct ixgb_adapter, napi);
int work_done = 0;
ixgb_clean_tx_irq(adapter);
ixgb_clean_rx_irq(adapter, &work_done, budget);
......
return work_done;
}

static bool
ixgb_clean_rx_irq(struct ixgb_adapter *adapter, int *work_done, int work_to_do)
{
struct ixgb_desc_ring *rx_ring = &adapter->rx_ring;
struct net_device *netdev = adapter->netdev;
struct pci_dev *pdev = adapter->pdev;
struct ixgb_rx_desc *rx_desc, *next_rxd;
struct ixgb_buffer *buffer_info, *next_buffer, *next2_buffer;
u32 length;
unsigned int i, j;
int cleaned_count = 0;
bool cleaned = false;

i = rx_ring->next_to_clean;
rx_desc = IXGB_RX_DESC(*rx_ring, i);
buffer_info = &rx_ring->buffer_info[i];

while (rx_desc->status & IXGB_RX_DESC_STATUS_DD) {
struct sk_buff *skb;
u8 status;

status = rx_desc->status;
skb = buffer_info->skb;
buffer_info->skb = NULL;

prefetch(skb->data - NET_IP_ALIGN);

if (++i == rx_ring->count)
i = 0;
next_rxd = IXGB_RX_DESC(*rx_ring, i);
prefetch(next_rxd);

j = i + 1;
if (j == rx_ring->count)
j = 0;
next2_buffer = &rx_ring->buffer_info[j];
prefetch(next2_buffer);

next_buffer = &rx_ring->buffer_info[i];
......
length = le16_to_cpu(rx_desc->length);
rx_desc->length = 0;
......
ixgb_check_copybreak(&adapter->napi, buffer_info, length, &skb);

/* Good Receive */
skb_put(skb, length);

/* Receive Checksum Offload */
ixgb_rx_checksum(adapter, rx_desc, skb);

skb->protocol = eth_type_trans(skb, netdev);

netif_receive_skb(skb);
......
/* use prefetched values */
rx_desc = next_rxd;
buffer_info = next_buffer;
}

rx_ring->next_to_clean = i;
......
}

三. MAC层

  从 netif_receive_skb() 函数开始,我们就进入了内核的网络协议栈。接下来的调用链为:netif_receive_skb()->netif_receive_skb_internal()->__netif_receive_skb()->__netif_receive_skb_core()。在 __netif_receive_skb_core() 中,我们先是处理了二层的一些逻辑,如对于 VLAN 的处理,如果不是则调用deliver_ptype_list_skb() 在一个协议列表中逐个匹配在网络包 struct sk_buff 里面定义的 skb->protocol,该变量表示三层使用的协议类型。

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
static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
struct packet_type *ptype, *pt_prev;
......
if (skb_vlan_tag_present(skb)) {
if (pt_prev) {
ret = deliver_skb(skb, pt_prev, orig_dev);
pt_prev = NULL;
}
if (vlan_do_receive(&skb))
goto another_round;
else if (unlikely(!skb))
goto out;
}
......
type = skb->protocol;
......
deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
&orig_dev->ptype_specific);
......
}

static inline void deliver_ptype_list_skb(struct sk_buff *skb,
struct packet_type **pt,
struct net_device *orig_dev,
__be16 type,
struct list_head *ptype_list)
{
struct packet_type *ptype, *pt_prev = *pt;

list_for_each_entry_rcu(ptype, ptype_list, list) {
if (ptype->type != type)
continue;
if (pt_prev)
deliver_skb(skb, pt_prev, orig_dev);
pt_prev = ptype;
}
*pt = pt_prev;
}

无论是VLAN还是普通的包,最后的发送均会调用deliver_skb(),该函数会调用协议定义好的函数进行网络层解析。对于IP协议即为ip_rcv()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline int deliver_skb(struct sk_buff *skb,
struct packet_type *pt_prev,
struct net_device *orig_dev)
{
if (unlikely(skb_orphan_frags_rx(skb, GFP_ATOMIC)))
return -ENOMEM;
refcount_inc(&skb->users);
return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}

static struct packet_type ip_packet_type __read_mostly = {
.type = cpu_to_be16(ETH_P_IP),
.func = ip_rcv,
};

四. 网络层

  在ip_rcv()中,我们又看到了熟悉的Netfilter,这次对应的是PREROUTING状态,执行完定义的钩子函数后,会继续执行ip_rcv_finish()

1
2
3
4
5
6
7
8
9
10
11
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt,
struct net_device *orig_dev)
{
struct net *net = dev_net(dev);
skb = ip_rcv_core(skb, net);
if (skb == NULL)
return NET_RX_DROP;
return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
net, NULL, skb, dev, NULL,
ip_rcv_finish);
}

  ip_rcv_finish() 首先调用ip_rcv_finish_core(),该函数会先检测是否为广播、组播,如果不是则得到网络包对应的路由表,然后调用 dst_input(),在 dst_input() 中,调用的是 struct rtable 的成员的 dstinput() 函数。在 rt_dst_alloc() 中,我们可以看到input 函数指向的是 ip_local_deliver()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
struct net_device *dev = skb->dev;
int ret;
/* if ingress device is enslaved to an L3 master device pass the
* skb to its handler for processing
*/
skb = l3mdev_ip_rcv(skb);
if (!skb)
return NET_RX_SUCCESS;
ret = ip_rcv_finish_core(net, sk, skb, dev);
if (ret != NET_RX_DROP)
ret = dst_input(skb);
return ret;
}

static inline int dst_input(struct sk_buff *skb)
{
return skb_dst(skb)->input(skb);
}

  进入ip_local_deliver()意味着从PREROUTING确认进入本机处理,所以进入了状态INPUT,如果 IP 层进行了分段,则进行重新的组合。接下来就是我们熟悉的 NF_HOOK。在经过 iptables 规则处理完毕后,会调用 ip_local_deliver_finish()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int ip_local_deliver(struct sk_buff *skb)
{
/*
* Reassemble IP fragments.
*/
struct net *net = dev_net(skb->dev);
if (ip_is_fragment(ip_hdr(skb))) {
if (ip_defrag(net, skb, IP_DEFRAG_LOCAL_DELIVER))
return 0;
}
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}

  ip_local_deliver_finish()首先调用__skb_pull()sk_buff中取下一个,接着调用ip_protocol_deliver_rcu(),该函数会从inet_protos[protocol]中找寻对应的处理函数进一步对收到的数据包进行解析。对应TCP的是tcp_v4_rcv(),UDP则是udp_rcv()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
__skb_pull(skb, skb_network_header_len(skb));
rcu_read_lock();
ip_protocol_deliver_rcu(net, skb, ip_hdr(skb)->protocol);
rcu_read_unlock();
return 0;
}

static struct net_protocol tcp_protocol = {
......
.handler = tcp_v4_rcv,
......
};

static struct net_protocol udp_protocol = {
......
.handler = udp_rcv,
......
};

五. 传输层

  在 tcp_v4_rcv() 中,首先会获取 TCP 的头部,接着就开始处理 TCP 层的事情。因为 TCP 层是分状态的,状态被维护在数据结构 struct sock 里面,因而要根据 IP 地址以及 TCP 头里面的内容,在 tcp_hashinfo 中找到这个包对应的 struct sock,从而得到这个包对应的连接的状态。接下来就根据不同的状态做不同的处理。如在前文三次握手的分析中已经剖析了TCP_NEW_SYN_RECV后续的逻辑。对于正常通信包,则会涉及到三条队列的操作。

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
int tcp_v4_rcv(struct sk_buff *skb)
{
struct net *net = dev_net(skb->dev);
int sdif = inet_sdif(skb);
const struct iphdr *iph;
const struct tcphdr *th;
bool refcounted;
struct sock *sk;
int ret;
......
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
lookup:
sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
th->dest, sdif, &refcounted);
if (!sk)
goto no_tcp_socket;
process:
if (sk->sk_state == TCP_TIME_WAIT)
goto do_time_wait;
if (sk->sk_state == TCP_NEW_SYN_RECV) {
......
}
......
th = (const struct tcphdr *)skb->data;
iph = ip_hdr(skb);
tcp_v4_fill_cb(skb, iph, th);
skb->dev = NULL;
if (sk->sk_state == TCP_LISTEN) {
ret = tcp_v4_do_rcv(sk, skb);
goto put_and_return;
}
......
if (!sock_owned_by_user(sk)) {
ret = tcp_v4_do_rcv(sk, skb);
} else if (tcp_add_backlog(sk, skb)) {
goto discard_and_relse;
}
......
case TCP_TW_SYN: {
......
}
/* to ACK */
/* fall through */
case TCP_TW_ACK:
......
case TCP_TW_RST:
......
case TCP_TW_SUCCESS:;
}
goto discard_it;
}

  网络包接收过程中常见的三个队列为

  • backlog 队列:软中断过程中的数据包处理队列
  • prequeue 队列:用户态进程读队列
  • sk_receive_queue 队列:内核态数据包缓存队列

  存在三个队列的原因是运行至tcp_v4_rcv()时,依然处于软中断的处理逻辑里,所以必然会占用这个软中断。如果用户态使用了系统调用read()读取数据包,则放入prequeue队列等待读取,如果暂时没有读取请求,则放入内核态的缓存队列sk_receive_queue中等候用户态请求。

  tcp_v4_rcv()调用sock_owned_by_user()判断该包现在是否正在被用户态进行读操作,如果没有则调用tcp_add_backlog()暂存在 backlog 队列中,并且抓紧离开软中断的处理过程,如果是则调用 tcp_prequeue(),将数据包放入 prequeue 队列并且离开软中断的处理过程。在这个函数里面,会对 sysctl_tcp_low_latency 进行判断,也即是不是要低时延地处理网络包。如果把 sysctl_tcp_low_latency 设置为 0,那就要放在 prequeue 队列中暂存,这样不用等待网络包处理完毕,就可以离开软中断的处理过程,但是会造成比较长的时延。如果把 sysctl_tcp_low_latency 设置为 1,则调用 tcp_v4_do_rcv()立即处理。

  特别注意:在2017年的一个patch中,有大佬提出取消prequeue队列以顺应新的TCP需求。但是我们这里依然以三条队列进行分析,实际上代码中较新的版本已经没有了tcp_prequeue()函数。之所以取消prequeue,是因为在大多使用事件驱动(epoll)的当下,已经很少有阻塞在recvfrom()或者read()的服务端代码了。下面分析中会加上prequeue相关功能,但是实际代码中不一定有

  在 tcp_v4_do_rcv() 中会分两种情况处理,一种情况是连接已经建立,处于 TCP_ESTABLISHED 状态,调用 tcp_rcv_established()。另一种情况,就是未建立连接的状态,调用 tcp_rcv_state_process()。关于tcp_rcv_state_process()在三次握手中已分析过了,这里重点看tcp_rcv_established()。该函数会调用 tcp_data_queue(),将数据包放入 sk_receive_queue 队列进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
const struct tcphdr *th = (const struct tcphdr *)skb->data;
struct tcp_sock *tp = tcp_sk(sk);
unsigned int len = skb->len;
......
tcp_data_queue(sk, skb);
tcp_data_snd_check(sk);
tcp_ack_snd_check(sk);
return;
......
}

  在 tcp_data_queue() 中,对于收到的网络包,我们要分情况进行处理。

  • 第一种情况,seq == tp->rcv_nxt,说明来的网络包正是我服务端期望的下一个网络包。
    • 调用 sock_owned_by_user()判断用户进程是否正在等待读取,如果是则直接调用 skb_copy_datagram_msg(),将网络包拷贝给用户进程就可以了。如果用户进程没有正在等待读取,或者因为内存原因没有能够拷贝成功,tcp_queue_rcv() 里面还是将网络包放入 sk_receive_queue 队列。
    • 调用tcp_rcv_nxt_update()tp->rcv_nxt 设置为 end_seq,也即当前的网络包接收成功后,更新下一个期待的网络包
    • 判断一下另一个队列out_of_order_queue,即乱序队列的情况,看看乱序队列里面的包会不会因为这个新的网络包的到来,也能放入到 sk_receive_queue 队列中。
  • 第二种情况,end_seq 小于 rcv_nxt,也即服务端期望网络包 5。但是,来了一个网络包 3,怎样才会出现这种情况呢?肯定是服务端早就收到了网络包 3,但是 ACK 没有到达客户端,中途丢了,那客户端就认为网络包 3 没有发送成功,于是又发送了一遍,这种情况下,要赶紧给客户端再发送一次 ACK,表示早就收到了。
  • 第三种情况,seq 大于 rcv_nxt + tcp_receive_window。这说明客户端发送得太猛了。本来 seq 肯定应该在接收窗口里面的,这样服务端才来得及处理,结果现在超出了接收窗口,说明客户端一下子把服务端给塞满了。这种情况下,服务端不能再接收数据包了,只能发送 ACK 了,在 ACK 中会将接收窗口为 0 的情况告知客户端,客户端就知道不能再发送了。这个时候双方只能交互窗口探测数据包,直到服务端因为用户进程把数据读走了,空出接收窗口,才能在 ACK 里面再次告诉客户端,又有窗口了,又能发送数据包了。
  • 第四种情况,seq 小于 rcv_nxt,但是 end_seq 大于 rcv_nxt,这说明从 seqrcv_nxt 这部分网络包原来的 ACK 客户端没有收到,所以重新发送了一次,从 rcv_nxtend_seq 是新发送的,可以放入 sk_receive_queue 队列。
  • 第五种情况,是正好在接收窗口内但是不是期望接收的下一个包,则说明发生了乱序,调用tcp_data_queue_ofo()加入乱序队列中。
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
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
struct tcp_sock *tp = tcp_sk(sk);
bool fragstolen = false;
......
if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
if (tcp_receive_window(tp) == 0)
goto out_of_window;

/* Ok. In sequence. In window. */
if (tp->ucopy.task == current &&
tp->copied_seq == tp->rcv_nxt && tp->ucopy.len &&
sock_owned_by_user(sk) && !tp->urg_data) {
int chunk = min_t(unsigned int, skb->len, tp->ucopy.len);

__set_current_state(TASK_RUNNING);

if (!skb_copy_datagram_msg(skb, 0, tp->ucopy.msg, chunk)) {
tp->ucopy.len -= chunk;
tp->copied_seq += chunk;
eaten = (chunk == skb->len);
tcp_rcv_space_adjust(sk);
}
}

if (eaten <= 0) {
queue_and_out:
......
eaten = tcp_queue_rcv(sk, skb, 0, &fragstolen);
}
tcp_rcv_nxt_update(tp, TCP_SKB_CB(skb)->end_seq);
......
if (!RB_EMPTY_ROOT(&tp->out_of_order_queue))
tcp_ofo_queue(sk);
......
return;
}
if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
/* A retransmit, 2nd most common case. Force an immediate ack. */
tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq);

out_of_window:
tcp_enter_quickack_mode(sk);
inet_csk_schedule_ack(sk);
drop:
tcp_drop(sk, skb);
return;
}

/* Out of window. F.e. zero window probe. */
if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt + tcp_receive_window(tp)))
goto out_of_window;

tcp_enter_quickack_mode(sk);

if (before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {
/* Partial packet, seq < rcv_next < end_seq */
tcp_dsack_set(sk, TCP_SKB_CB(skb)->seq, tp->rcv_nxt);
/* If window is closed, drop tail of packet. But after
* remembering D-SACK for its head made in previous line.
*/
if (!tcp_receive_window(tp))
goto out_of_window;
goto queue_and_out;
}

tcp_data_queue_ofo(sk, skb);
}

六. 套接字层

  当接收的网络包进入各种队列之后,接下来我们就要等待用户进程去读取它们了。读取一个 socket,就像读取一个文件一样,读取 socket 的文件描述符,通过 read 系统调用。read 系统调用对于一个文件描述符的操作,大致过程都是类似的,在文件系统那一节,我们已经详细解析过。最终它会调用到用来表示一个打开文件的结构 stuct file 指向的 file_operations 操作。

1
2
3
4
5
static const struct file_operations socket_file_ops = {
......
.read_iter = sock_read_iter,
......
};

  sock_read_iter()首先从虚拟文件系统中获取对应的文件,然后通过file获取对应的套接字sock,接着调用sock_recvmsg()读取该套接字对应的连接的数据包缓存队列。

1
2
3
4
5
6
7
8
9
10
11
12
static ssize_t sock_read_iter(struct kiocb *iocb, struct iov_iter *to)
{
struct file *file = iocb->ki_filp;
struct socket *sock = file->private_data;
struct msghdr msg = {.msg_iter = *to,
.msg_iocb = iocb};
ssize_t res;
......
res = sock_recvmsg(sock, &msg, msg.msg_flags);
*to = msg.msg_iter;
return res;
}

  sock_recvmsg()实际调用sock_recvmmsg_nosec(),该函数会调用套接字对应的读操作,即inet_recvmsg()

1
2
3
4
5
6
7
8
9
10
11
int sock_recvmsg(struct socket *sock, struct msghdr *msg, int flags)
{
int err = security_socket_recvmsg(sock, msg, msg_data_left(msg), flags);
return err ?: sock_recvmsg_nosec(sock, msg, flags);
}

static inline int sock_recvmsg_nosec(struct socket *sock, struct msghdr *msg,
int flags)
{
return sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);
}

  inet_recvmsg()会调用协议对应的读操作,即tcp_recvmsg()进行读操作。

1
2
3
4
5
6
7
8
9
int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
int flags)
{
struct sock *sk = sock->sk;
......
err = sk->sk_prot->recvmsg(sk, msg, size, flags & MSG_DONTWAIT,
flags & ~MSG_DONTWAIT, &addr_len);
......
}

  tcp_recvmsg()通过一个循环读取队列中的数据包,直至读完。循环内的逻辑为:

  • 处理sk_receive_queue队列:调用skb_peek_tail()获取队列中的一项,并调用skb_queue_walk()处理。如果找到了网络包,就跳到 found_ok_skb 这里。这里会调用 skb_copy_datagram_msg()将网络包拷贝到用户进程中,然后直接进入下一层循环。
  • 处理prequeue队列(已废弃):直到 sk_receive_queue 队列处理完毕才到了 sysctl_tcp_low_latency 判断。如果不需要低时延,则会有 prequeue 队列。于是跳到 do_prequeue 这里,调用 tcp_prequeue_process() 进行处理。
  • 处理backlog队列:调用release_sock()完成。release_sock() 会调用 __release_sock(),这里面会依次处理队列中的网络包。
  • 处理完所有队列后,调用 sk_wait_data(),继续等待在哪里,等待网络包的到来。
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

int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
int flags, int *addr_len)
{
struct tcp_sock *tp = tcp_sk(sk);
int copied = 0;
u32 peek_seq;
u32 *seq;
unsigned long used;
int err, inq;
int target; /* Read at least this many bytes */
long timeo;
struct sk_buff *skb, *last;
......
do {
u32 offset;
......
/* Next get a buffer. */
last = skb_peek_tail(&sk->sk_receive_queue);
skb_queue_walk(&sk->sk_receive_queue, skb) {
last = skb;
......
offset = *seq - TCP_SKB_CB(skb)->seq;
......
if (offset < skb->len)
goto found_ok_skb;
......
}
/* Well, if we have backlog, try to process it now yet. */
if (copied >= target && !sk->sk_backlog.tail)
break;
if (copied) {
if (sk->sk_err ||
sk->sk_state == TCP_CLOSE ||
(sk->sk_shutdown & RCV_SHUTDOWN) ||
!timeo ||
signal_pending(current))
break;
}
......
tcp_cleanup_rbuf(sk, copied);
if (copied >= target) {
/* Do not sleep, just process backlog. */
release_sock(sk);
lock_sock(sk);
} else {
sk_wait_data(sk, &timeo, last);
}
......
found_ok_skb:
/* Ok so how much can we use? */
used = skb->len - offset;
if (len < used)
used = len;
/* Do we have urgent data here? */
if (tp->urg_data) {
u32 urg_offset = tp->urg_seq - *seq;
if (urg_offset < used) {
if (!urg_offset) {
if (!sock_flag(sk, SOCK_URGINLINE)) {
++*seq;
urg_hole++;
offset++;
used--;
if (!used)
goto skip_copy;
}
} else
used = urg_offset;
}
}
if (!(flags & MSG_TRUNC)) {
err = skb_copy_datagram_msg(skb, offset, msg, used);
if (err) {
/* Exception. Bailout! */
if (!copied)
copied = -EFAULT;
break;
}
}
*seq += used;
copied += used;
len -= used;
tcp_rcv_space_adjust(sk);
......
} while (len > 0);
......
}

总结

  至此网络协议栈的收发流程都已经分析完毕了。收包流程可以总结为以下过程

  • 硬件网卡接收到网络包之后,通过 DMA 技术,将网络包放入 Ring Buffer;
  • 硬件网卡通过中断通知 CPU 新的网络包的到来;
  • 网卡驱动程序会注册中断处理函数 ixgb_intr;
  • 中断处理函数处理完需要暂时屏蔽中断的核心流程之后,通过软中断 NET_RX_SOFTIRQ 触发接下来的处理过程;
  • NET_RX_SOFTIRQ 软中断处理函数 net_rx_actionnet_rx_action 会调用 napi_poll,进而调用 ixgb_clean_rx_irq,从 Ring Buffer 中读取数据到内核 struct sk_buff
  • 调用 netif_receive_skb 进入内核网络协议栈,进行一些关于 VLAN 的二层逻辑处理后,调用 ip_rcv 进入三层 IP 层;
  • 在 IP 层,会处理 iptables 规则,然后调用 ip_local_deliver 交给更上层 TCP 层;
  • 在 TCP 层调用 tcp_v4_rcv,这里面有三个队列需要处理,如果当前的 Socket 不是正在被读取,则放入 backlog 队列,如果正在被读取,不需要很实时的话,则放入 prequeue 队列,其他情况调用 tcp_v4_do_rcv
  • tcp_v4_do_rcv 中,如果是处于 TCP_ESTABLISHED 状态,调用 tcp_rcv_established,其他的状态,调用 tcp_rcv_state_process
  • tcp_rcv_established 中,调用 tcp_data_queue,如果序列号能够接的上,则放入 sk_receive_queue 队列;
  • 如果序列号接不上,则暂时放入 out_of_order_queue 队列,等序列号能够接上的时候,再放入 sk_receive_queue 队列。

至此内核接收网络包的过程到此结束,接下来就是用户态读取网络包的过程,这个过程分成几个层次。

  • VFS 层:read 系统调用找到 struct file,根据里面的 file_operations 的定义,调用 sock_read_iter 函数。sock_read_iter 函数调用 sock_recvmsg 函数。
  • Socket 层:从 struct file 里面的 private_data 得到 struct socket,根据里面 ops的定义,调用 inet_recvmsg 函数。
  • Sock 层:从 struct socket 里面的 sk 得到 struct sock,根据里面 sk_prot 的定义,调用 tcp_recvmsg 函数。
  • TCP 层:tcp_recvmsg 函数会依次读取 receive_queue 队列、prequeue 队列和 backlog 队列。

img

源码资料

[1] ixgb_init_module()

[2] __napi_schedule()

[3] ip_rcv()

[4] tcp_v4_rcv()

[5] sock_read_iter()

[6] inet_recvmsg()

[7] tcp_recvmsg()

参考资料

[1] wiki

[2] elixir.bootlin.com/linux

[3] woboq

[4] Linux-insides

[5] 深入理解Linux内核

[6] Linux内核设计的艺术

[7] 极客时间 趣谈Linux操作系统

[8] 深入理解Linux网络技术内幕

坚持原创,坚持分享,谢谢鼓励和支持