linux操作系统:接收网络包

从硬件网卡到IP层

设备驱动层

网卡作为一个硬件,接收到网络包,应该怎么通知操作系统,这个网络包到达了呢?可以触发一个中断。但是问题是,网络包的到来,往往是很难预期的。网络吞吐量比较大的时候,网络包的到达会十分频繁。这个时候,如果非常频繁的去触发中断,详细就觉得是个灾难。

比如说,CPU正在做某个事情,一些网络包来了,触发了中断,CPU停下手里的事情,去处理这些网络包,处理完毕后按照中断处理的逻辑,应该回去继续处理其他事情。这个时候,另一些网络包又来了,由触发了中断,CPU手里的事情还没有焐热,又要停下来去处理网络包。能不能大家要来的时候一起来,把网络包好好处理一把,然后在回去几种处理其他事情呢?

网络包能不能一起来,这个我们没法儿控制,但是我们可以有一种机制,就是当一些网络包到来触发了中断,内核处理完这些网络包之后,我们可以先进入到主动轮询poll网卡的方式,主动去接收到来的网络包。如果一直有,就一直处理,等处理告一段落,就返回干其他的事情。当有下一批网络包到来的时候,在中断,再轮询poll。这样就会大大减少中断的数量,提升网络处理的效率,这种处理方式叫做NAPI

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");
MODULE_VERSION(DRV_VERSION);
 
/**
 * ixgb_init_module - Driver Registration Routine
 *
 * ixgb_init_module is the first routine called when the driver is
 * loaded. All it does is register with the PCI subsystem.
 **/
 
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_init_module,注册一个驱动ixgb_driver,并且调用它的probe函数ixgb_probe。

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_probe中,我们会创建一个struct net_device表示这个网络设备,并且netif_napi_add函数为这个网络设备注册一个轮询poll函数ixgb_clean,将来一旦出现网络包的时候,就是通过它来轮询。

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

int
ixgb_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 - Interrupt Handler
 * @irq: interrupt number
 * @data: pointer to a network interface device structure
 **/
 
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;
}

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

/**
 * __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);
}

__napi_schedule 是处理中断的关键部分,在它被调用的时候,中断是暂时关闭的,但是处理网络包是个复杂的过程,需要到延迟处理部分,所以__napi_schedule将当前设备放到struct softnet_data结构的poll_list里面,说明在延迟处理部分可以接着处理这个poll_list里面的网络设备。

然后__napi_schedule触发一个软中断NET_RX_SOFTIRQ,通过软中断触发中断处理的延迟处理部分。

NET_RX_SOFTIRQ对应的中断处理函数是net_tx_action。

static __latent_entropy void net_rx_action(struct softirq_action *h)
{
	struct softnet_data *sd = this_cpu_ptr(&softnet_data);
    LIST_HEAD(list);
    list_splice_init(&sd->poll_list, &list);
......
	for (;;) {
		struct napi_struct *n;
......
		n = list_first_entry(&list, struct napi_struct, poll_list);
		budget -= napi_poll(n, &repoll);
	}
......
}

在net_rx_action中,会得到struct softnet_data结构,这个结构在发送的时候我们也遇到过。当时它的 output_queue 用于网络包的发送,这里的 poll_list 用于网络包的接收。

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

在 net_rx_action 中,接下来是一个循环,在 poll_list 里面取出网络包到达的设备,然后调用 napi_poll 来轮询这些设备,napi_poll 会调用最初设备初始化的时候,注册的 poll 函数,对于 ixgb_driver,对应的函数是 ixgb_clean。

ixgb_clean 会调用 ixgb_clean_rx_irq。

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;
......
}

在网络设备的驱动层,有一个用于接收网络包的 rx_ring。它是一个环,从网卡硬件接收的包会放在这个环里面。这个环里面的 buffer_info[] 是一个数组,存放的是网络包的内容。i 和 j 是这个数组的下标,在 ixgb_clean_rx_irq 里面的 while 循环中,依次处理环里面的数据。在这里面,我们看到了 i 和 j 加一之后,如果超过了数组的大小,就跳回下标 0,就说明这是一个环。

ixgb_check_copybreak 函数将 buffer_info 里面的内容,拷贝到 struct sk_buff *skb,从而可以作为一个网络包进行后续的处理,然后调用 netif_receive_skb。

网络协议栈的二层逻辑

从netif_receive_skb函数开始,我们就进入了内核的网络处理栈。

接下来的调用链为:netif_receive_skb->netif_receive_skb_internal->__netif_receive_skb->__netif_receive_skb_core。

在__netif_receive_skb_core中,我们先是处理了二层的一些逻辑。比如,对于VLAN的处理,接下来要想办法交给第三层。

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
	struct packet_type *ptype, *pt_prev;
......
	type = skb->protocol;
......
	deliver_ptype_list_skb(skb, &pt_prev, orig_dev, type,
			       &orig_dev->ptype_specific);
	if (pt_prev) {
		ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
	}
......
}
 
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;
}

在网络包struct sk_buff里面二层的头里面有一个protocol,表示里面一层,也就是三层是什么协议。deliver_ptype_list_skb在一个协议列表中逐个匹配,如果能够匹配到,就返回。

这些协议的注册在网络协议栈初始化的时候,inet_init函数调用dev_add_pack(&ip_packet_type),添加IP协议。协议被放在一个链表里面。

void dev_add_pack(struct packet_type *pt)
{
    struct list_head *head = ptype_head(pt);
    list_add_rcu(&pt->list, head);
}
 
static inline struct list_head *ptype_head(const struct packet_type *pt)
{
    if (pt->type == htons(ETH_P_ALL))
        return pt->dev ? &pt->dev->ptype_all : &ptype_all;
    else
        return pt->dev ? &pt->dev->ptype_specific : &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}

假设这个时候的网络包是一个 IP 包,则在这个链表里面一定能够找到 ip_packet_type,在 __netif_receive_skb_core 中会调用 ip_packet_type 的 func 函数。

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

从上面的定义我们可以看出,接下来,ip_rcv 会被调用。

网络协议栈的IP层

从 ip_rcv 函数开始,我们的处理逻辑就从二层到了三层,IP 层。

int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
	const struct iphdr *iph;
	struct net *net;
	u32 len;
......
	net = dev_net(dev);
......
	iph = ip_hdr(skb);
	len = ntohs(iph->tot_len);
	skb->transport_header = skb->network_header + iph->ihl*4;
......
	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
		       net, NULL, skb, dev, NULL,
		       ip_rcv_finish);
......
}

在ip_rcv中,得到IP头,然后又遇到了我们见过多次的NF_HOOK,这次因为是接收网络包,第一个hook点是NF_INET_PRE_ROUTING,也就是iptables的PREROUTING链。如果里面有规则,则执行规则,然后调用ip_rcv_finish。

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	const struct iphdr *iph = ip_hdr(skb);
	struct net_device *dev = skb->dev;
	struct rtable *rt;
	int err;
......
	rt = skb_rtable(skb);
.....
	return dst_input(skb);
}
 
static inline int dst_input(struct sk_buff *skb)
{
	return skb_dst(skb)->input(skb);
 

ip_rcv_finish 得到网络包对应的路由表,然后调用 dst_input,在 dst_input 中,调用的是 struct rtable 的成员的 dst 的 input 函数。在 rt_dst_alloc 中,我们可以看到,input 函数指向的是 ip_local_deliver。

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 函数中,如果 IP 层进行了分段,则进行重新的组合。接下来就是我们熟悉的 NF_HOOK。hook 点在 NF_INET_LOCAL_IN,对应 iptables 里面的 INPUT 链。在经过 iptables 规则处理完毕后,我们调用 ip_local_deliver_finish。

static int ip_local_deliver_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	__skb_pull(skb, skb_network_header_len(skb));
 
	int protocol = ip_hdr(skb)->protocol;
	const struct net_protocol *ipprot;
 
	ipprot = rcu_dereference(inet_protos[protocol]);
	if (ipprot) {
		int ret;
		ret = ipprot->handler(skb);
......
	}
......
}

在 IP 头中,有一个字段 protocol 用于指定里面一层的协议,在这里应该是 TCP 协议。于是,从 inet_protos 数组中,找出 TCP 协议对应的处理函数。这个数组的定义如下,里面的内容是 struct net_protocol。

struct net_protocol __rcu *inet_protos[MAX_INET_PROTOS] __read_mostly;
 
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol)
{
......
	return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
			NULL, prot) ? 0 : -1;
}
 
static int __init inet_init(void)
{
......
	if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
		pr_crit("%s: Cannot add UDP protocol\n", __func__);
	if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
		pr_crit("%s: Cannot add TCP protocol\n", __func__);
......
}
 
static struct net_protocol tcp_protocol = {
	.early_demux	=	tcp_v4_early_demux,
	.early_demux_handler =  tcp_v4_early_demux,
	.handler	=	tcp_v4_rcv,
	.err_handler	=	tcp_v4_err,
	.no_policy	=	1,
	.netns_ok	=	1,
	.icmp_strict_tag_validation = 1,
};
 
static struct net_protocol udp_protocol = {
	.early_demux =	udp_v4_early_demux,
	.early_demux_handler =	udp_v4_early_demux,
	.handler =	udp_rcv,
	.err_handler =	udp_err,
	.no_policy =	1,
	.netns_ok =	1,
};

在系统初始化的时候,网络协议栈的初始化调用的是 inet_init,它会调用 inet_add_protocol,将 TCP 协议对应的处理函数 tcp_protocol、UDP 协议对应的处理函数 udp_protocol,放到 inet_protos 数组中。

在上面的网络包的接收过程中,会取出 TCP 协议对应的处理函数 tcp_protocol,然后调用 handler 函数,也即 tcp_v4_rcv 函数。

小结

接收网络包:

  • 硬件网卡接收到网络包之后,通过DMA技术,将网络包放入Ring Buffer
  • 硬件网卡通过中断通知CPU新的网络包的到来
  • 网卡驱动程序会注册中断处理函数ixgb_intr
  • 中断处理函数处理完需要暂时屏蔽中断的核心流程之后,通过软中断NET_RX_SOFTIRQ 触发接下来的处理过程
  • NET_RX_SOFTIRQ 软中断处理函数net_rx_action,net_rx_action会调用napi_poll,进而调用ixbg_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。

在这里插入图片描述

从TCP层到socket层

网络协议栈的TCP层

从 tcp_v4_rcv 函数开始,我们的处理逻辑就从 IP 层到了 TCP 层。

int tcp_v4_rcv(struct sk_buff *skb)
{
	struct net *net = dev_net(skb->dev);
	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);
......
	TCP_SKB_CB(skb)->seq = ntohl(th->seq);
	TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin + skb->len - th->doff * 4);
	TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
	TCP_SKB_CB(skb)->tcp_flags = tcp_flag_byte(th);
	TCP_SKB_CB(skb)->tcp_tw_isn = 0;
	TCP_SKB_CB(skb)->ip_dsfield = ipv4_get_dsfield(iph);
	TCP_SKB_CB(skb)->sacked	 = 0;
 
lookup:
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source, th->dest, &refcounted);
 
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);
 
	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)) {
		if (!tcp_prequeue(sk, skb))
			ret = tcp_v4_do_rcv(sk, skb);
	} else if (tcp_add_backlog(sk, skb)) {
		goto discard_and_relse;
	}
......
}

在tcp_v4_rcv中,得到TCP的头以后,我们可以开始处理TCP层的事情。因为TCP层是分状态的,状态被维护在数据结构struct sock里面,因而我们要根据IP地址以及TCP头里面的内容,在tcp_hasinfo中找到这个包对应的struct sock,从而得到这个包对应的连接的状态。

接下来,我们就根据不同的状态做不同的处理。上面的TCP_LISTEN、TCP_NEW_SYN_RECV 状态属于连接建立的过程,TCP_TIME_WAIT 是连接结束状态,与接收网络包无关。

接下来,我们来分析最主流的网络包的接收过程,这里面涉及三个队列:

  • backlog队列
  • prequeue队列
  • sk_receive_queue队列

为什么接收网络包的过程,需要在这三个队列里面折腾过来,倒腾过去呢?这是因为,同样一个网络包要在三个主体之间交接。

  • 第一个主体是软中断的处理过程。我们在执行tcp_v4_rcv函数的时候,依然处理软中断的处理逻辑里,所以必然会占用这个软中断
  • 第二个主体是用户态进程。如果用户态触发系统调用read读取网络包,这要从队列里面找
  • 第三个主体是内核协议栈。哪怕用户进程没有read读取网络包,当网络包到来的时候,也需要有一个地方收着

也就是说,sock_owned_by_user 的意思是看,当前这个sock是不是正有一个用户态进程等着读数据:

  • 如果没有,内核协议栈就调用tcp_add_backlog,暂存在backlog队列中,并且抓紧离开1软中断的处理逻辑。
  • 如果有,就调用tcp_prequeue,也就是放入prequeue队列,并且离开软中断的处理过程。在这个函数里面,我们会看到对于sysctl_tcp_low_latency 的判断,也就是是不是要低时延的处理网络包。

如果把sysctl_tcp_low_latency设置为0,那就要放在prequeue队列中暂存,这样不用等待网络包处理完毕,就可以离开软中断的处理过程,但是会造成比较长的时延。如果把sysctl_tcp_low_latency 设置为1,我们还是要调用tcp_v4_do_rcv

int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
	struct sock *rsk;
 
	if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
		struct dst_entry *dst = sk->sk_rx_dst;
......
		tcp_rcv_established(sk, skb, tcp_hdr(skb), skb->len);
		return 0;
	}
......
	if (tcp_rcv_state_process(sk, skb)) {
......
	}
	return 0;
......
}

在tcp_v4_do_rcv里面,分两种情况,一种情况是连接已经建立,处于TCP_ESTABLISHED状态,调用tcp_rcv_established。另一种情况,就是其他的状态,调用tcp_rcv_state_process

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);
	const struct tcphdr *th = tcp_hdr(skb);
	struct request_sock *req;
	int queued = 0;
	bool acceptable;
 
	switch (sk->sk_state) {
	case TCP_CLOSE:
......
	case TCP_LISTEN:
......
	case TCP_SYN_SENT:
......
	}
......
	switch (sk->sk_state) {
	case TCP_SYN_RECV:
......
	case TCP_FIN_WAIT1: 
......
	case TCP_CLOSING:
......
	case TCP_LAST_ACK:
......
    }
 
	/* step 7: process the segment text */
	switch (sk->sk_state) {
	case TCP_CLOSE_WAIT:
	case TCP_CLOSING:
	case TCP_LAST_ACK:
......
	case TCP_FIN_WAIT1:
	case TCP_FIN_WAIT2:
......
	case TCP_ESTABLISHED:
......
	}
}

在 tcp_rcv_state_process 中,如果我们对着 TCP 的状态图进行比对,能看到,对于 TCP 所有状态的处理,其中和连接建立相关的状态,咱们已经分析过,所以我们重点关注连接状态下的工作模式。

在这里插入图片描述

在连接状态下,我们会调用 tcp_rcv_established。在这个函数里面,我们会调用 tcp_data_queue,将其放入 sk_receive_queue 队列进行处理。

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);
}

在 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 队列中。

例如,客户端发送的网络包序号为 5、6、7、8、9。在 5 还没有到达的时候,服务端的 rcv_nxt 应该是 5,也即期望下一个网络包是 5。但是由于中间网络通路的问题,5、6 还没到达服务端,7、8 已经到达了服务端了,这就出现了乱序。

乱序的包不能进入sk_receive_queue队列。因为一旦进入到这个队列,意味着可以发送给用户进程。然而,按照TCP的定义,用户进程应该是按顺序收到包的,没有排好序,就不能给用户进程。所以,7、8不能进入sk_receive_queue队列,只能暂时放在out_of_order_queue乱序队列中。

当 5、6 到达的时候,5、6 先进入 sk_receive_queue 队列。这个时候我们再来看 out_of_order_queue 乱序队列中的 7、8,发现能够接上。于是,7、8 也能进入 sk_receive_queue 队列了。tcp_ofo_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,这说明从 seq 到 rcv_nxt 这部分网络包原来的 ACK 客户端没有收到,所以重新发送了一次,从 rcv_nxt 到 end_seq 时新发送的,可以放入 sk_receive_queue 队列。

当前四种情况都排除掉了,说明网络包一定是一个乱序包了。这里有点儿难理解,我们还是用上面那个乱序的例子仔细分析一下 rcv_nxt=5。

我们假设 tcp_receive_window 也是 5,也即超过 10 服务端就接收不了了。当前来的这个网络包既不在 rcv_nxt 之前(不是 3 这种),也不在 rcv_nxt + tcp_receive_window 之后(不是 11 这种),说明这正在我们期望的接收窗口里面,但是又不是 rcv_nxt(不是我们马上期望的网络包 5),这正是上面的例子中网络包 7、8 的情况。

对于网络包 7、8,我们只好调用 tcp_data_queue_ofo 进入 out_of_order_queue 乱序队列,但是没有关系,当网络包 5、6 到来的时候,我们会走第一种情况,把 7、8 拿出来放到 sk_receive_queue 队列中。

至此,网络协议栈的处理过程就结束了。

socket层

当接收的网络包进入各种队列之后,接下来我们就要等待用户进程去读取它们了。

读取一个 socket,就像读取一个文件一样,读取 socket 的文件描述符,通过 read 系统调用。

read 系统调用对于一个文件描述符的操作,大致过程都是类似的,最终它会调用到用来表示一个打开文件的结构 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_read_iter。

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;
 
	if (file->f_flags & O_NONBLOCK)
		msg.msg_flags = MSG_DONTWAIT;
......
	res = sock_recvmsg(sock, &msg, msg.msg_flags);
	*to = msg.msg_iter;
	return res;
}

在sock_read_iter中,通过VFS中的struct file,将创建号的socket结构拿出来,然后调用sock_recvmsg,sock_recvmsg会调用sock_recvmsg_nosec。

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);
}

这里调用了 socket 的 ops 的 recvmsg。根据 inet_stream_ops 的定义,这里调用的是 inet_recvmsg。

int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
		 int flags)
{
	struct sock *sk = sock->sk;
	int addr_len = 0;
	int err;
......
	err = sk->sk_prot->recvmsg(sk, msg, size, flags & MSG_DONTWAIT,
				   flags & ~MSG_DONTWAIT, &addr_len);
......
}

这里面,从 socket 结构,我们可以得到更底层的 sock 结构,然后调用 sk_prot 的 recvmsg 方法。根据 tcp_prot 的定义,调用的是 tcp_recvmsg。

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;
	int target;		/* Read at least this many bytes */
	long timeo;
	struct task_struct *user_recv = NULL;
	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;
......
		}
......
		if (!sysctl_tcp_low_latency && tp->ucopy.task == user_recv) {
			/* Install new reader */
			if (!user_recv && !(flags & (MSG_TRUNC | MSG_PEEK))) {
				user_recv = current;
				tp->ucopy.task = user_recv;
				tp->ucopy.msg = msg;
			}
 
			tp->ucopy.len = len;
			/* Look: we have the following (pseudo)queues:
			 *
			 * 1. packets in flight
			 * 2. backlog
			 * 3. prequeue
			 * 4. receive_queue
			 *
			 * Each queue can be processed only if the next ones
			 * are empty. 
			 */
			if (!skb_queue_empty(&tp->ucopy.prequeue))
				goto do_prequeue;
		}
 
		if (copied >= target) {
			/* Do not sleep, just process backlog. */
			release_sock(sk);
			lock_sock(sk);
		} else {
			sk_wait_data(sk, &timeo, last);
		}
 
		if (user_recv) {
			int chunk;
			chunk = len - tp->ucopy.len;
			if (chunk != 0) {
				len -= chunk;
				copied += chunk;
			}
 
			if (tp->rcv_nxt == tp->copied_seq &&
			    !skb_queue_empty(&tp->ucopy.prequeue)) {
do_prequeue:
				tcp_prequeue_process(sk);
 
				chunk = len - tp->ucopy.len;
				if (chunk != 0) {
					len -= chunk;
					copied += chunk;
				}
			}
		}
		continue;
	found_ok_skb:
		/* Ok so how much can we use? */
		used = skb->len - offset;
		if (len < used)
			used = len;
 
		if (!(flags & MSG_TRUNC)) {
			err = skb_copy_datagram_msg(skb, offset, msg, used);
......
		}
 
		*seq += used;
		copied += used;
		len -= used;
 
		tcp_rcv_space_adjust(sk);
......
	} while (len > 0);
......
}

tcp_recvmsg 这个函数比较长,里面逻辑也很复杂,好在里面有一段注释概扩了这里面的逻辑。注释里面提到了三个队列,receive_queue 队列、prequeue 队列和 backlog 队列。这里面,我们需要把前一个队列处理完毕,才处理后一个队列。

tcp_recvmsg的整个逻辑也是这样执行的:这里有一个while循环,不断不读网络包。

这里,我们会先处理sk_receive_queue队列。如果找到了网络包,就调到found_ok_skb这里,这里会调用skb_copy_datagram_msg,将网络包拷贝到用户进程中,然后直接进入下一层循环。

直到 sk_receive_queue 队列处理完毕,我们才到了 sysctl_tcp_low_latency 判断。如果不需要低时延,则会有 prequeue 队列。于是,我们能就跳到 do_prequeue 这里,调用 tcp_prequeue_process 进行处理。

如果 sysctl_tcp_low_latency 设置为 1,也即没有 prequeue 队列,或者 prequeue 队列为空,则需要处理 backlog 队列,在 release_sock 函数中处理。

release_sock 会调用 __release_sock,这里面会依次处理队列中的网络包。

void release_sock(struct sock *sk)
{
......
	if (sk->sk_backlog.tail)
		__release_sock(sk);
......
}
 
static void __release_sock(struct sock *sk)
	__releases(&sk->sk_lock.slock)
	__acquires(&sk->sk_lock.slock)
{
	struct sk_buff *skb, *next;
 
	while ((skb = sk->sk_backlog.head) != NULL) {
		sk->sk_backlog.head = sk->sk_backlog.tail = NULL;
		do {
			next = skb->next;
			prefetch(next);
			skb->next = NULL;
			sk_backlog_rcv(sk, skb);
			cond_resched();
			skb = next;
		} while (skb != NULL);
	}
......
}

最后,哪里都没有网络包,我们只好调用 sk_wait_data,继续等待在哪里,等待网络包的到来。

至此,网络包的接收过程到此结束。

总结

  • 硬件网卡接收到网络包之后,通过 DMA 技术,将网络包放入 Ring Buffer;
  • 硬件网卡通过中断通知 CPU 新的网络包的到来;
  • 网卡驱动程序会注册中断处理函数 ixgb_intr;
  • 中断处理函数处理完需要暂时屏蔽中断的核心流程之后,通过软中断 NET_RX_SOFTIRQ 触发接下来的处理过程;
  • NET_RX_SOFTIRQ 软中断处理函数 net_rx_action,net_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 队列。

在这里插入图片描述

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值