文章目录
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 ,就是全志通过对 STMicro
的 MAC IP(Intellectual Property)
进行封装实现,从驱动代码组织上也可以看到这一点:
/* 全志 SoC 厂家 MAC 驱动部分 */
drivers/net/ethernet/stmicro/stmmac/dwmac-sun8i.c
/* STMicro MAC IP 驱动公共部分 */
drivers/net/ethernet/stmicro/stmmac/stmmac_*