Linux: 网络数据收发流程简析

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 背景

本文基于 linux-4.14.132 内核代码进行分析。

3. 网卡数据收发流程

我们先来明确下,Linux 下网络数据通信流程:

             Host A                          Host B
     ---------------------           ---------------------
    |       应用数据       |         |       应用数据      |
    |        |    ^       |         |        |    ^       |
    |        v    |       |         |        v    |       |
    |  -----------------  |         |  -----------------  |
L4  | | 传输层(TCP/UDP) |  | <- - -> | | 传输层(TCP/UDP) | |
    |  -----------------  |         |  -----------------  |
    |       |     ^       |         |       |     ^       |
    |       v     |       |         |       v     |       |
    |  -----------------  |         |  -----------------  |
L3  | |    网络层(IP)   |  | <- - -> | |    网络层(IP)   | |
    |  -----------------  |         |  -----------------  |
    |       |     ^       |         |       |     ^       |
    |       v     |       |         |       v     |       |
    |  -----------------  |         |  -----------------  |
L2  | | 数据链路层(MAC) |  | <- - -> | | 数据链路层(MAC) |  |
    |  -----------------  |         |  -----------------  |
    |       |     ^       |         |       |     ^       |
    |       v     |       |         |       v     |       |
    |  -----------------  |         |  -----------------  |
L1  | |   物理层(PHY)   | | <- - ->  | |   物理层(PHY)   | |
    |  -----------------  |         |  -----------------  |
     ---------------------           --------------------- 
               /\                              /\
               ||                              ||
               ||______________________________||
               |________________________________|          

Host A 和 B 的数据通信流经各自的物理层(L1的PHY)数据链路层(L2的MAC)网络层(L3)传输层(L4),最后到达应用层的缓冲

3.1 网络数据接收流程

网络收据的接收流程,分为两个部分:第1部分,是网卡数据接收流程;第2部分,是网卡将接收的数据,向上传递给协议栈的网络层(L3),再由网络层向上传递给协议栈的传输层(L4)处理

3.1.1 网卡数据接收流程

当数据通过网线,再经物理层(网卡PHY),最后传递给数据链路层的MAC,此时会生成中断,分析流程正是从网卡中断开始。还是以一个实际的网卡驱动例子,来开始分析:

/* drivers/net/ethernet/stmicro/stmmac/dwmac-sun8i.c */

static int sun8i_dwmac_probe(struct platform_device *pdev)
{
	...
	ret = stmmac_dvr_probe(&pdev->dev, plat_dat, &stmmac_res);
	...
	return ret;
}
/* drivers/net/ethernet/stmicro/stmmac/stmmac-main.c */

static const struct net_device_ops stmmac_netdev_ops = {
	.ndo_open = stmmac_open,
	.ndo_start_xmit = stmmac_xmit,
	.ndo_stop = stmmac_release,
	.ndo_change_mtu = stmmac_change_mtu,
	...
	.ndo_do_ioctl = stmmac_ioctl,
	...
	.ndo_set_mac_address = stmmac_set_mac_address,
};

int stmmac_dvr_probe(struct device *device,
		     struct plat_stmmacenet_data *plat_dat,
		     struct stmmac_resources *res)
{
	struct net_device *ndev = NULL;

	/* 创建网卡对象 */
	ndev = alloc_etherdev_mqs(sizeof(struct stmmac_priv),
				  MTL_MAX_TX_QUEUES,
				  MTL_MAX_RX_QUEUES);
	
	...

	/* Init MAC and get the capabilities */
 	ret = stmmac_hw_init(priv);
 	...

	ndev->netdev_ops = &stmmac_netdev_ops; /* 配置网卡接口 */

	...

	/* 网卡使用的所有接收队列(也叫 RING BUFFER)的 NAPI 初始化 */
	for (queue = 0; queue < priv->plat->rx_queues_to_use; queue++) {
		struct stmmac_rx_queue *rx_q = &priv->rx_queue[queue];

		/*
		 * 为每个接收队列 @rx_q 添加一个 数据接收工作处理对象 napi_struct 。
		 * napi_struct 可以理解成一个带权重值的 tasklet 。
		 */
		netif_napi_add(ndev, &rx_q->napi, stmmac_poll,
			       (8 * priv->plat->rx_queues_to_use));
	}

	...
	
	ret = register_netdev(ndev); /* 注册网卡设备 */
	...

	return ret;
}

void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
		    int (*poll)(struct napi_struct *, int), int weight)
{
	INIT_LIST_HEAD(&napi->poll_list);
	hrtimer_init(&napi->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED);
	napi->timer.function = napi_watchdog;
	...
	napi->poll = poll; /* stmmac_poll() */
	list_add(&napi->dev_list, &dev->napi_list); /* 添加到网络设备的 napi_struct 对象列表 */
	napi->dev = dev; /* 设定 napi_struct 对象关联的网络设备 */
	...
	set_bit(NAPI_STATE_SCHED, &napi->state); /* 标记 napi_struct 对象为已调度状态(NAPI_STATE_SCHED) */
	napi_hash_add(napi); /* 添加到 napi_struct 对象 全局哈希表 */
}

上面的代码,创建网卡设备对象,然后设置了其操作接口,接着是所有接收队列(RING BUFFER)的NAPI初始化,最后注册网卡设备对象到系统。应用程序使用网卡设备时,先 打开(或启动)网卡设备 (如 ip link set dev eth0 up,ifconfig eth0 up),并在此时注册网卡数据接收处理中断接口

/*
 * 启动网卡:
 * ip link set dev eth0 up
 * ifconfig eth0 up
 */
sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
ioctl(sockfd, SIOCSIFFLAGS, {ifr_name="eth0", ifr_flags=IFF_UP|IFF_BROADCAST|IFF_RUNNING|IFF_MULTICAST})
	sock_ioctl()
		sock_do_ioctl()
			dev_ioctl()
				dev_ifsioc()
					dev_change_flags()
						__dev_change_flags()
							__dev_open()
								/* 调用网卡驱动 open (启动)接口 */
								ops->ndo_open(dev) = stmmac_open(dev)
struct stmmac_rx_queue {
	...
	/* 使用 NAPI */
	struct napi_struct napi ____cacheline_aligned_in_smp;
};

static int stmmac_open(struct net_device *dev)
{
	struct stmmac_priv *priv = netdev_priv(dev);
	int ret;

	...

	ret = alloc_dma_desc_resources(priv); /* 分配 DMA 收发缓冲等 */
	...

	ret = init_dma_desc_rings(dev, GFP_KERNEL); /* 网卡 DMA 硬件相关配置等 */
	...

	ret = stmmac_hw_setup(dev, true); /* 配置 MAC 硬件准备数据收发 */
	
	...
	
	ret = request_irq(dev->irq, stmmac_interrupt,
			  IRQF_SHARED, dev->name, dev);

	/* 启动 MAC 收发 */
	stmmac_enable_all_queues(priv);
	stmmac_start_all_queues(priv);

	return 0;
}

static void stmmac_enable_all_queues(struct stmmac_priv *priv)
{
	u32 rx_queues_cnt = priv->plat->rx_queues_to_use;
	u32 queue;

	for (queue = 0; queue < rx_queues_cnt; queue++) {
		struct stmmac_rx_queue *rx_q = &priv->rx_queue[queue];

		napi_enable(&rx_q->napi);
	}
}

static void stmmac_start_all_queues(struct stmmac_priv *priv)
{
	u32 tx_queues_cnt = priv->plat->tx_queues_to_use;
	u32 queue;

	for (queue = 0; queue < tx_queues_cnt; queue++)
		netif_tx_start_queue(netdev_get_tx_queue(priv->dev, queue));
}

static inline void napi_enable(struct napi_struct *n)
{
	...
	clear_bit(NAPI_STATE_SCHED, &n->state);
	clear_bit(NAPI_STATE_NPSVC, &n->state);
}

上面代码流程完成了网卡接收中断接口的注册网卡收发数据队列(RING BUFFER)的启动NAPI 的使能。网络数据到达网卡的硬件缓冲(FIFO)后,接着网卡 DMA 将数据送到网卡驱动的 RING BUFFER,并产生一个 DMA 中断。接下来,我们看在网卡的 (DMA) 中断接口中,是怎样来处理处理数据接收的:

static irqreturn_t stmmac_interrupt(int irq, void *dev_id)
{
	struct stmmac_priv *priv = netdev_priv(dev);

	...

	/* To handle DMA interrupts */
	stmmac_dma_interrupt(priv);

	return IRQ_HANDLED;
}

static void stmmac_dma_interrupt(struct stmmac_priv *priv)
{
	u32 tx_channel_count = priv->plat->tx_queues_to_use;
	int status;
	u32 chan;

	/* 查询网卡 MAC 芯片的所有 DMA 通道的数据传输状态 */
	for (chan = 0; chan < tx_channel_count; chan++) {
		struct stmmac_rx_queue *rx_q = &priv->rx_queue[chan]; /* 用于第 @chan 号 DMA 通道 的 接收队列 */

		/* 读取 MAC 芯片寄存器,查询数据传输的 DMA 中断状态 */
		status = priv->hw->dma->dma_interrupt(priv->ioaddr,
						      &priv->xstats, chan);
		if (likely((status & handle_rx)) || (status & handle_tx)) { /* 有完成的 DMA 数据传输 */
			if (likely(napi_schedule_prep(&rx_q->napi))) { /* DMA 通道 @chan 对应数据接收队列可进行 NAPI 调度 */
				stmmac_disable_dma_irq(priv, chan); /* 禁用 DMA 通道 @chan 的中断 */
				__napi_schedule(&rx_q->napi); /* 调度(添加)一个网卡数据接收工作到当前 cpu 网络数据接收工作队列 */
			}
		}
		
		...
	}
}
void __napi_schedule(struct napi_struct *n)
{
	unsigned long flags;

	local_irq_save(flags);
	____napi_schedule(this_cpu_ptr(&softnet_data)/*当前CPU的softnet_data*/, n);
	local_irq_restore(flags);
}

/*
 * 调度(添加)一个 网卡数据接收工作(@n) 到 网络数据接收工作队列(@sd).
 * 
 * 添加 napi_struct(@napi) 到 softnet_data(@sd) 的轮询列表,
 * 然后触发 NET_RX_SOFTIRQ 软中断:
 * 在软中断中将触发 napi_struct::poll(), 完成网络包收取。
 */
static inline void ____napi_schedule(struct softnet_data *sd,
				     struct napi_struct *napi)
{
	/*
	 * 添加到 softnet_data 的 napi_struct 轮询列表:
	 *
	 * . 传统的 netif_rx() 收包方式
	 *	 a. 初始化 backlog
	 *		net_dev_init()
	 *			for_each_possible_cpu(i) {
	 *				struct softnet_data *sd = &per_cpu(softnet_data, i);
	 *				...
	 *				sd->backlog.poll = process_backlog;
	 *				sd->backlog.weight = weight_p;
	 *			}
	 *   b. 触发 poll 接口 process_backlog()
	 *	 netif_rx()
	 *		netif_rx_internal()
	 *			enqueue_to_backlog()
	 *				___napi_schedule(sd, &sd->backlog) // 本函数
	 *
	 * . NAPI 方式
	 *   a. 注册 NAPI 接口
	 *		netif_napi_add(ndev, &priv->napi, sun8i_emac_poll, 64)
	 *			list_add(&napi->dev_list, &dev->napi_list); // 添加的网络设备的 napi_struct 列表
	 *			napi->dev = dev; // 设定 napi_struct 关联的网络设备
	 *	 b. 触发 NAPI 接口
	 *		stmmac_interrupt()
	 *			stmmac_dma_interrupt()
	 *				__napi_schedule()
	 */
	list_add_tail(&napi->poll_list, &sd->poll_list);
	/*
	 * 触发 NET_RX_SOFTIRQ 软中断,NET_RX_SOFTIRQ 软中断 net_rx_action() 处理接口
	 * 来触发 napi_struct::poll():
	 * process_backlog() / stmmac_poll() 完成网络包收取 
	 */
	__raise_softirq_irqoff(NET_RX_SOFTIRQ); 
}

从前面的代码了解到,进入网卡MAC数据接收中断后,最后会调度 NAPI 来收取数据,收取数据流程接下来会进入到 softirq 的流程:

/* 注册软中断 NET_RX_SOFTIRQ 处理接口 net_rx_action() */
// net/core/dev.c
net_dev_init()
	open_softirq(NET_RX_SOFTIRQ, net_rx_action)

/* 退出中断时调用,调用 NET_RX_SOFTIRQ 软中断接口 net_rx_action() */
irq_exit()
	__do_softirq()
		...
		softirq_vec[nr].action(h) = net_rx_action()
// net/core/dev.c
static __latent_entropy void net_rx_action(struct softirq_action *h)
{
	struct softnet_data *sd = this_cpu_ptr(&softnet_data);
	unsigned long time_limit = jiffies +
		usecs_to_jiffies(netdev_budget_usecs);
	int budget = netdev_budget;
	LIST_HEAD(list);
	LIST_HEAD(repoll);

	local_irq_disable();
	list_splice_init(&sd->poll_list, &list); /* 链表拼接: sd->poll_list(napi_struct列表) + list */
	local_irq_enable();

	for (;;) {
		struct napi_struct *n;

		if (list_empty(&list)) {
			if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll))
				goto out;
			break;
		}

		n = list_first_entry(&list, struct napi_struct, poll_list); /* 取当前 CPU 的 softnet_data 的 napi_struct 列表首节点 */
		budget -= napi_poll(n, &repoll);

		/* If softirq window is exhausted then punt.
		 * Allow this to run for 2 jiffies since which will allow
		 * an average latency of 1.5/HZ.
		 */
		if (unlikely(budget <= 0 ||
			     time_after_eq(jiffies, time_limit))) {
			sd->time_squeeze++;
			break;
		}
	}

	local_irq_disable();

	list_splice_tail_init(&sd->poll_list, &list);
	list_splice_tail(&repoll, &list);
	list_splice(&list, &sd->poll_list);
	if (!list_empty(&sd->poll_list))
		__raise_softirq_irqoff(NET_RX_SOFTIRQ);

	net_rps_action_and_irq_enable(sd);
out:
	__kfree_skb_flush();
}

static int napi_poll(struct napi_struct *n, struct list_head *repoll)
{
	void *have;
	int work, weight;

	list_del_init(&n->poll_list); /* 从所属列表移出 */
	
	...

	/* This NAPI_STATE_SCHED test is for avoiding a race
	 * with netpoll's poll_napi().  Only the entity which
	 * obtains the lock and sees NAPI_STATE_SCHED set will
	 * actually make the ->poll() call.  Therefore we avoid
	 * accidentally calling ->poll() when NAPI is not scheduled.
	 */
	work = 0;
	if (test_bit(NAPI_STATE_SCHED, &n->state)) {
		/* 触发 napi_struct::poll(), 如:
		 * . 默认的 softnet_data::backlog 的 poll 接口 process_backlog()
		 * . 使用了 NAPI 的 stmmac_poll() 
		 */
		work = n->poll(n, weight);
		...
	}

	...
	
	return work;
}

兜兜转转,终于进入 MAC 驱动NAPI 接收数据 poll 处理接口 stmmac_poll()

static int stmmac_poll(struct napi_struct *napi, int budget)
{
	struct stmmac_rx_queue *rx_q =
		container_of(napi, struct stmmac_rx_queue, napi);
	...
	u32 chan = rx_q->queue_index;
	int work_done = 0;
	...

	...

	/* 收取数据 */
	work_done = stmmac_rx(priv, budget, rx_q->queue_index);
	/*
	 * 本次轮询目标量为 @budget, 但实际只取了 @work_done , 
	 * 说明当前没有更多数据,因此标记此处数据轮询结束,并
	 * 重启 DMA 通道 @chan 的中断,以便继续接收数据。
	 */
	if (work_done < budget) {
		napi_complete_done(napi, work_done); /*  */
		stmmac_enable_dma_irq(priv, chan);
	}
	return work_done;
}

到此,网络数据接收流程的第1部分:网卡数据接收流程分析完毕。接下来,看处在L2层MAC数据沿着协议栈,流经L3网络层,最终到达L4传输层的过程。

3.1.2 网卡数据向上传递给L3,L4的流程

static int stmmac_rx(struct stmmac_priv *priv, int limit, u32 queue)
{
	struct stmmac_rx_queue *rx_q = &priv->rx_queue[queue];
	unsigned int entry = rx_q->cur_rx; /* 网卡接收缓冲当前位置索引 */
	...
	unsigned int count = 0;

	...
	while (count < limit) {
		struct dma_desc *p; /* 网卡接收缓冲 当前位置 使用的 DMA 接收描述符 */
		struct dma_desc *np; /* 网卡接收缓冲 下一位置 使用的 DMA 接收描述符 */

		if (priv->extend_desc)
			p = (struct dma_desc *)(rx_q->dma_erx + entry);
		else
			p = rx_q->dma_rx + entry;
		
		...
		
		/* 
		 * 计算 网卡接收缓冲 下一位置.
		 * 从宏 STMMAC_GET_ENTRY() 的如下定义
		 * #define STMMAC_GET_ENTRY(x, size) ((x + 1) & (size - 1))
		 * 可以看到接收缓冲位置到达最末后回卷到开始位置,这也是经常会看到
		 * 很多资料里,将网卡接收缓冲叫做 RING BUFFER 原因。
		 */
  		rx_q->cur_rx = STMMAC_GET_ENTRY(rx_q->cur_rx, DMA_RX_SIZE);
		next_entry = rx_q->cur_rx;

		...

		if (unlikely(status == discard_frame)) {
			...
		}  else {
			int frame_len;
			
			frame_len = priv->hw->desc->get_rx_frame_len(p, coe);
			...
			/* 分配 skb 缓冲 */
			skb = netdev_alloc_skb_ip_align(priv->dev, frame_len);
			...
			/* 拷贝接收 RING BUFFER 中的数据到新分配 skb 缓冲 */
			skb_copy_to_linear_data(skb,
						rx_q->rx_skbuff[entry]->data,
						frame_len);
			...
			
			/* 提取数据帧的 协议类型 */
			skb->protocol = eth_type_trans(skb, priv->dev);
			...
			/* 将数据传递给 网络层 (L3) 处理:L2 -> L3 */
			napi_gro_receive(&rx_q->napi, skb);
			...
		}
		...
	}

	...
	
	return count;
}

/* net/core/dev.c */
gro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb)
{
	...
	return napi_skb_finish(dev_gro_receive(napi, skb), skb);
}

static gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb)
{
	switch (ret) {
	case GRO_NORMAL:
		if (netif_receive_skb_internal(skb))
			ret = GRO_DROP;
		break;
	...
	}
	
	return ret;
}

static int netif_receive_skb_internal(struct sk_buff *skb)
{
	/* 这里不列举 XDP 和 RPS 情形,仅说明数据传输流程 */
	...

	rcu_read_lock();
	ret = __netif_receive_skb(skb);
	rcu_read_unlock();
	return ret;
}

static int __netif_receive_skb(struct sk_buff *skb)
{
	int ret;
	
	...
	ret = __netif_receive_skb_core(skb, false);

	return ret;
}

static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc)
{
	struct packet_type *ptype, *pt_prev;
	...
	int ret = NET_RX_DROP;
	...

	/* 
	 * 这里会根据数据报不同协议类型,进入对应协议类型数据包的处理接口。
	 * 我们这里以 ETH_P_IP 协议类型数据报为例,来说明接下来的类型。
	 */
	// net/ipv4/ip_input.c:
	// ip_packet_type.func = ip_rcv() 
	ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
	...

	return ret;
}

ip_rcv() 进入 网络层(L3) 的数据处理流程:

/* net/ipv4/ip_input.c */
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev)
{
	const struct iphdr *iph;

	...
	iph = ip_hdr(skb); /* IP 协议数据报头 */
	...

	len = ntohs(iph->tot_len);
	...
	skb->transport_header = skb->network_header + iph->ihl*4; /* 传出层(L4) 数据报头部指针 */

	/* Remove any debris in the socket control block */
	memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
	IPCB(skb)->iif = skb->skb_iif;

	...

	/* 
	 * 在这里将应用 pre routing 路由规则。
	 * 像用户层 ip, iptables 等工具,可以通过这里,加入规则过滤数据报。
	 */
	return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING,
		       net, NULL, skb, dev, NULL,
		       ip_rcv_finish);
}

static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	...
	/* 
	 * 为收到数据包 @skb 设置路由管理信息和路由处理接口:
	 * (ip_local_deliver(), ip_forward(), ...) 
	 */
	if (!skb_valid_dst(skb)) {
		err = ip_route_input_noref(skb, iph->daddr, iph->saddr,
					   iph->tos, dev);
		...
	}
	
	...
	/* 数据报路由投递处理 */
	return dst_input(skb);
}

/* Input packet from network to transport.  */
static inline int dst_input(struct sk_buff *skb)
{
	/* 这里假设数据报的目标地址是当前机器,所以不走 ip_forward() 数据报转发流程 */
	return skb_dst(skb)->input(skb); /* ip_local_deliver(), ip_forward(), ... */
}

int ip_local_deliver(struct sk_buff *skb)
{
	/*
	 *	Reassemble IP fragments.
	 */
	struct net *net = dev_net(skb->dev);
	
	...
	/* 
	 * 在这里将应用 local in 路由规则。
	 * 像用户层 ip, iptables 等工具,可以通过这里,加入规则过滤数据报。
	 */
	return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
		       net, NULL, skb, skb->dev, NULL,
		       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)); /* 移动 skb 数据指针,指向传输层(L4)的数据报 */

	rcu_read_lock();
	{
		int protocol = ip_hdr(skb)->protocol; /* 获取传输层数据报的协议类型 */
		...
		ipprot = rcu_dereference(inet_protos[protocol]);
		if (ipprot) {
			...
			/* 
			 * 这里按传输层(L4)的数据报协议类型,进入不同协议数据报处理接口。 
			 * 我们这里以 TCP/IP v4 数据报为例,说明接下来的流程。
			 */
			ret = ipprot->handler(skb); /* tcp_v4_rcv() */
		} else {
			...
		}
	}
out:
	rcu_read_unlock();

	return 0;
}

网络层(L3)的数据报处理流程结束,接下来进入传输层(L4)的数据报处理入口 tcp_v4_rcv()

/* net/ipv4/tcp_ipv4.c */
int tcp_v4_rcv(struct sk_buff *skb)
{
	...
	const struct tcphdr *th;
	...
	struct sock *sk;
	
	...
	th = (const struct tcphdr *)skb->data; /* TCP协议数据报头 */
	iph = ip_hdr(skb); /* IP协议数据报头 */
lookup:
	/* 用 【源、目标IP】和【源、目标端口】等找到接收 @skb 的目标通信套接字 */
	sk = __inet_lookup_skb(&tcp_hashinfo, skb, __tcp_hdrlen(th), th->source,
			       th->dest, sdif, &refcounted);
	...

	/* 
	 * 我们假设已经完成了 socket 链接的 3 次握手过程,
	 * 本次通信,为普通数据传输。
	 */
	...

	/* IPsec 包过滤 */
	if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
		goto discard_and_relse;
	
	...
	/* 运行 eBPF 包过滤程序 */
	if (tcp_filter(sk, skb))
		goto discard_and_relse;
	th = (const struct tcphdr *)skb->data;
	iph = ip_hdr(skb);
	tcp_v4_fill_cb(skb, iph, th); /* 从 TCP 数据报头提取序列号、flags、时间戳到 TCP skb 控制块 (tcp_skb_cb) */

	skb->dev = NULL;
	
	/* 目标套接字处于监听状态: 通常是用作 server 端的套接字 */
	if (sk->sk_state == TCP_LISTEN) {
		ret = tcp_v4_do_rcv(sk, skb); /* 接收 TCP 数据报 */
		goto put_and_return;
	}

	...
	if (!sock_owned_by_user(sk)) {
		ret = tcp_v4_do_rcv(sk, skb); /* 接收 TCP 数据报 */
	} else if (tcp_add_backlog(sk, skb)) {
		goto discard_and_relse;
	}
}

/* @sk: 接收 @skb 的目标套接字 */ 
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb)
{
	/* 发送数据 @skb 的源头,已经和 @sk 建立了连接 */
	if (sk->sk_state == TCP_ESTABLISHED) { /* Fast path */
		tcp_rcv_established(sk, skb, tcp_hdr(skb)); /* @sk 接收 @skb 数据 */
		return 0;
	}

	/* socket 尚未建立连接情形,读者可自行分析 */
	...
}

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
			 const struct tcphdr *th)
{
	...
	/* Bulk data transfer: receiver */
	eaten = tcp_queue_rcv(sk, skb, tcp_header_len,
			      &fragstolen);
	...
}

static int __must_check tcp_queue_rcv(struct sock *sk, struct sk_buff *skb, int hdrlen,
		  bool *fragstolen)
{
	int eaten;
	struct sk_buff *tail = skb_peek_tail(&sk->sk_receive_queue);

	__skb_pull(skb, hdrlen);
	eaten = (tail &&
		 tcp_try_coalesce(sk, RCV_QUEUE, tail,
				  skb, fragstolen)) ? 1 : 0;
	tcp_rcv_nxt_update(tcp_sk(sk), TCP_SKB_CB(skb)->end_seq);
	if (!eaten) {
		__skb_queue_tail(&sk->sk_receive_queue, skb); /* 将接收的数据,添加到 socket 接收 skb 缓冲队列 */
		skb_set_owner_r(skb, sk);
	}
	return eaten;
}

到此,我们接收的数据终于到达了 socket 接收 skb 缓冲队列,整个流程总结如下:

            DMA                              NAPI COPY            
网卡硬件缓冲 --> MAC 驱动接收缓冲(RING BUFFER) ---------> 新分配的 skb -----------------------
                                                                                           |
                  COPY: recv()/read()                                                      |
socket 应用层缓冲 <------------------- socket 接收 skb 缓冲队列 <-- TCP(L4) <-- 网络层(L3) <--

注意,其中的 NAPI 拷贝 这一步,并非直接在中断上下文完成的,其首先将一个网卡数据接收 poll 工作添加到当前 CPU 的网卡数据接收工作队列(softnet_data::poll_list),而后抛出软中断 NET_RX_SOFTIRQ,并最终对软中端 NET_RX_SOFTIRQ 的处理调用链 net_rx_action() -> napi_poll() -> ... -> stmmac_poll() 完成网卡硬件接收缓冲中数据的接收、并将数据一路顺着网络协议栈向上传送并最终到达 socket 接收 skb 缓冲队列。详情见前面的 __napi_schedule()net_rx_action() 分析。
到此,网络数据的接收流程已经分析完毕,接下来我们分析网络数据的发送流程

3.2 网卡数据发送流程

网络数据的发送流程,对比 网络数据的接收流程 ,数据流动方向刚好是相反的:

socket 缓冲 -> TCP(L4) -> 网络层(L3) -> 网卡发送缓冲

所以发送流程,从 socket 的发送接口开始,我们仍然以 TCP/IP v4 数据报为例,来分析网络数据报的发送流程。我们假设通信双方的 socket 已经经历了三次握手的连接建立过程。从系统调用 sys_send() 开始:

/* net/socket.c */
sys_send(fd, buff, len, flags)
	sys_sendto(fd, buff, len, flags, NULL, 0)
		sock_sendmsg(sock, &msg)
			sock_sendmsg_nosec(sock, msg)
				sock->ops->sendmsg(sock, msg, msg_data_left(msg)) = inet_sendmsg()
					sk->sk_prot->sendmsg(sk, msg, size) = tcp_sendmsg()
						tcp_sendmsg_locked(sk, msg, size)
/* net/ipv4/tcp.c */
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size)
{
	...
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT); /* 发送超时时间 */
	...
	out:
	if (copied) {
		tcp_tx_timestamp(sk, sockc.tsflags, tcp_write_queue_tail(sk));
		tcp_push(sk, flags, mss_now, tp->nonagle, size_goal); /* 传递数据到 网络层 */
	}
	...
	return copied + copied_syn;

	...
}

static void tcp_push(struct sock *sk, int flags, int mss_now,
		     int nonagle, int size_goal)
{
	...
	__tcp_push_pending_frames(sk, mss_now, nonagle);
}
/* net/ipv4/tcp_output.c */

void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
			       int nonagle)
{
	...
	if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
			   sk_gfp_mask(sk, GFP_ATOMIC)))
		tcp_check_probe_timer(sk);
}

/* 将 sock @sk 发送队列的数据报向下传递给 网络层 */ 
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
			   int push_one, gfp_t gfp)
{
	...
	max_segs = tcp_tso_segs(sk, mss_now);
	while ((skb = tcp_send_head(sk))) {
		...
		if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) /* 将数据报向下传递给 网络层 */
			break;
		...
	}
	...
}

/* 将数据报向下传递给 网络层 */
static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it,
			    gfp_t gfp_mask)
{
	return __tcp_transmit_skb(sk, skb, clone_it, gfp_mask,
				  tcp_sk(sk)->rcv_nxt);
}

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
			      int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
	...
	/* 将数据报向下传递给 网络层 */
	err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); /* ip_queue_xmit() */
	...
	return err;
}

好,数据已经从传输层(L4)向下传送给了网络层(L3)接口 ip_queue_xmit() ,我们来看网络层(L3)数据报向外发送的流程。

/* net/ipv4/ip_output.c */
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl)
{
	struct inet_sock *inet = inet_sk(sk);
	...
	struct rtable *rt;
	struct iphdr *iph;
	int res;
	
	...
	rt = skb_rtable(skb);
	...
	
	/* Make sure we can route this packet. */
	rt = (struct rtable *)__sk_dst_check(sk, 0);
	if (!rt) {
		...
		/* If this fails, retransmit mechanism of transport layer will
		 * keep trying until route appears or the connection times
		 * itself out.
		 */
		/* 输出路由 */ 
		rt = ip_route_output_ports(net, fl4, sk,
					   daddr, inet->inet_saddr,
					   inet->inet_dport,
					   inet->inet_sport,
					   sk->sk_protocol,
					   RT_CONN_FLAGS(sk),
					   sk->sk_bound_dev_if);
		...
	}

	/* OK, we know where to send it, allocate and build IP header. */
	skb_push(skb, sizeof(struct iphdr) + (inet_opt ? inet_opt->opt.optlen : 0));
	skb_reset_network_header(skb);
	iph = ip_hdr(skb);
	...
	iph->protocol = sk->sk_protocol;
	ip_copy_addrs(iph, fl4);
	...
	
	res = ip_local_out(net, sk, skb); /* 将数据包传递给网络设备 */
	...
	return res;

	...
}

int ip_local_out(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	int err;

	err = __ip_local_out(net, sk, skb);
	if (likely(err == 1))
		err = dst_output(net, sk, skb);

	return err;
}

/* Output packet to network from transport.  */
static inline int dst_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	return skb_dst(skb)->output(net, sk, skb); /* ip_output() */
}

int ip_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	struct net_device *dev = skb_dst(skb)->dev;

	skb->dev = dev;
	skb->protocol = htons(ETH_P_IP);

	/* 
	 * 在这里将应用 post routing 路由规则。
	 * 像用户层 ip, iptables 等工具,可以通过这里,加入规则过滤数据报。
	 */
	return NF_HOOK_COND(NFPROTO_IPV4, NF_INET_POST_ROUTING,
			    net, sk, skb, NULL, dev,
			    ip_finish_output,
			    !(IPCB(skb)->flags & IPSKB_REROUTED));
}

static int ip_finish_output(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	unsigned int mtu;
	int ret;

	ret = BPF_CGROUP_RUN_PROG_INET_EGRESS(sk, skb); /* 运行发送包的 eBPF 钩子 */
	...

	...
	return ip_finish_output2(net, sk, skb);
}

static int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb)
{
	...
	neigh = __ipv4_neigh_lookup_noref(dev, nexthop);
	if (unlikely(!neigh)) /* 第1次??? */
		neigh = __neigh_create(&arp_tbl, &nexthop, dev, false);
	if (!IS_ERR(neigh)) {
		int res;

		sock_confirm_neigh(skb, neigh);
		res = neigh_output(neigh, skb); /* 输出数据 @skb: neigh_direct_output(), neigh_resolve_output(), ... */

		...
		return res;
	}
}

int neigh_direct_output(struct neighbour *neigh, struct sk_buff *skb)
{
	return dev_queue_xmit(skb);
}

int dev_queue_xmit(struct sk_buff *skb)
{
	return __dev_queue_xmit(skb, NULL);
}

static int __dev_queue_xmit(struct sk_buff *skb, void *accel_priv)
{
	struct net_device *dev = skb->dev;

	/* 没有 Qdisc 队列, 直接调用驱动接口 ndo_start_xmit() 传送数据  */ 
	if (dev->flags & IFF_UP) {
		int cpu = smp_processor_id(); /* ok because BHs are off */
		if (txq->xmit_lock_owner != cpu) {
			...
			if (!netif_xmit_stopped(txq)) {
				...
				skb = dev_hard_start_xmit(skb, dev, txq, &rc); /* 调用网卡驱动接口 .ndo_start_xmit = stmmac_xmit() 传送数据 */
				...
				if (dev_xmit_complete(rc)) { /* 传输完成 */
					...
					goto out;
				}
			}
			...
		}
	}
}

到此为止,网络子系统数据发送流程的公共代码流程已经完成,接下来的是网卡MAC驱动的数据发送逻辑。接着看 stmmac_xmit()

static netdev_tx_t stmmac_xmit(struct sk_buff *skb, struct net_device *dev)
{
	/* 使用 DMA 通道来传送 @skb 缓冲数据 */
	...

	return NETDEV_TX_OK;
}

到此,网络数据包的发送流程也分析完毕了。

4. 番外:嵌入式平台下网卡的典型结构

在 PC 平台,一张网卡通常集成了 MAC + PHY 两部分硬件;而对于嵌入式平台,为了更好的灵活性,通常 MAC 和 PHY 是彼此独立的硬件模块。一个典型的情景是:SoC 集成 MAC 硬件部分,而外围电路提供 PHY 硬件部分。对于 SoC 集成的 MAC 部分,多数 SoC 厂家也是通过对现有的 IP(Intellectual Property) 进程封装来实现,呈现如下图所示的层次结构:

 ------------------------- \
| SoC 厂家对 MAC IP 的实现 |  \
|  (硬件寄存器,电路等等)   |   \ MAC
|-------------------------|   / 芯片
|         MAC IP          |  /
 ------------------------- /

如本文提到的 MAC ,就是全志通过对 STMicroMAC IP(Intellectual Property) 进行封装实现,从驱动代码组织上也可以看到这一点:

/* 全志 SoC 厂家 MAC 驱动部分 */
drivers/net/ethernet/stmicro/stmmac/dwmac-sun8i.c

/* STMicro MAC IP 驱动公共部分 */
drivers/net/ethernet/stmicro/stmmac/stmmac_*
  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux 服务器端网络收发程序可以使用各种编程语言编写, 常见的有 C/C++, Python, Java 等. 例如, 使用 C 语言编写的常用网络库有 libevent, libev, libuv 等; 使用 Python 编写的常用网络库有 Twisted, asyncio 等. ### 回答2: Linux服务器端网络收发程序是指在Linux操作系统上运行的一种网络应用程序,用于实现服务器端与客户端之间的数据传输和通信。它通常使用TCP/IP协议栈作为底层网络通信协议。 Linux服务器端网络收发程序的主要功能包括监听指定的网络端口,接收客户端的连接请求,以及处理和回复客户端发送的数据。 首先,服务器程序通常会使用socket创建一个套接字,并通过bind函数将套接字绑定到指定的IP地址和端口上。然后,使用listen函数进入监听状态,等待客户端的连接请求。 当有客户端连接时,服务器程序会调用accept函数接受连接请求,并创建一个新的套接字来处理与该客户端的通信。通过recv函数可以从客户端接收数据,并使用send函数将响应数据发送给客户端。 在多线程或多进程的服务器程序中,为了支持同时处理多个客户端连接,通常会使用多个线程或进程来处理不同的客户端请求,并对套接字进行合理的资源管理。 在服务器端的网络收发程序中,还可以实现一些高级的功能,例如基于事件驱动的异步IO模型,可以提高程序的性能和并发处理能力。 此外,为了保证服务器程序的稳定性和安全性,还需要考虑一些网络编程中的常见问题,如对数据包进行解析和校验、防止拒绝服务攻击、实现访问控制等。 总之,Linux服务器端网络收发程序是一种基于Linux操作系统的网络应用程序,能够实现服务器与客户端之间的数据传输和通信,并提供一系列的功能来支持并发处理和保障数据安全。 ### 回答3: Linux服务器端的网络收发程序是指在Linux操作系统上运行的一种程序,其主要功能是通过网络进行数据的传输和接收。 在服务器端,我们可以使用C语言或者其他编程语言编写网络收发程序。其中常用的技术包括套接字(Socket)、TCP/IP协议和网络编程等。 首先,我们需要创建一个服务器端的套接字,通过指定IP地址和端口号来绑定服务器。服务器端套接字用于监听客户端的连接请求,并在有连接请求时进行响应。 一旦有客户端连接到服务器,服务器可以通过accept()函数接受连接,并创建一个新的套接字用于处理与该客户端的通信。服务器可以使用recv()函数接收客户端发送的数据,使用send()函数向客户端发送数据。 服务器端网络收发程序也可以使用多线程或者多进程的方式实现并发处理多个客户端的连接请求。通过创建多个子进程或子线程,每个子进程或子线程负责处理一个客户端的连接请求,实现并发处理。 此外,服务器端网络收发程序还可以通过使用select()函数实现多路复用,从而监控多个套接字的状态,实现同时接收多个客户端的数据。 对于网络收发程序的具体实现,我们还需要考虑异常处理、数据包的拆分和重组、粘包问题以及网络安全等方面的内容。 总而言之,Linux服务器端网络收发程序是一种基于套接字的程序,通过监听和接受客户端的连接请求,并实现数据收发,以实现服务器和客户端之间的网络通信。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值