分享

剖析Linux网络包接收过程:掌握数据如何被捕获和分发的全过程

 深度Linux 2023-08-25 发布于湖南

前言:在Linux网络编程中,网络包接收指的是主机从网络上接收到一个数据包。它可以是来自其他计算机或设备发送的数据包,也可以是回环地址(localhost)上本地发送的数据包。

当一个网络包被接收时,它经过了多个层次的处理:

  1. 首先,在物理层,网卡会检测到数据包,并将其传递给操作系统内核。

  2. 然后,在网络协议栈中,内核会对数据包进行解析和处理。它可能会检查目标IP地址、端口号等信息,并根据规则进行路由、过滤或转发操作。

  3. 最终,当数据包成功被接收并处理后,应用程序可以通过读取套接字(socket)来获取其中的数据内容。通过监听和接收网络包,我们可以实现各种功能,如实时通信、网络监控、报文分析等。

-----------------------------零声白金卡限时活动---------------------------------

我自己学C++,填了一个坑又一个坑,深知新手学习C/C++的重要性和疑难问题,因此特地给C/C++开发的同学精心准备了一份优惠优质学习卡——零声白金卡(https://xxetb.xet.tech/s/3wrN44购买地址),6个项目分别是:基础架构-KV存储项目、spdk文件系统实现项目、Linux内核内存管理实战案例分析、golang云原生、FFmpeg+SDL播放器开发实站QtMP3音乐播放器搜索引擎实战,提供项目源码下载,同时这份资料也包括 C/C++学习路线、简历指导和求职技巧等。

一、网卡接收

当网络包到达网卡时,网卡会将数据包存储到接收缓冲区中。网卡通常使用DMA(Direct Memory Access)来直接将数据复制到主内存,减少CPU的参与。

网卡本身是有内存的,每个网卡一般都有4K以上的内存,用来发送,接收数据。

数据在从主内存搬到网卡之后,不是立即就能被发送出去的,而是要先在网卡自身的内存中排队,再按照先后顺序发送;同样的,数据从以太网传递到网卡时,网卡也是先把数据存储到自身的内存中,等到收到一帧数据了,再经过中断的方式,告诉主CPU(不是网卡本身的微处理器)把网卡内存的数据读走,而读走后的内存,又被清空,再次被使用,用来接收新的数据,如此循环往复。

而网卡本身的内存,又多是按照256字节为1页的方式,把所有内存分页,之后把这些页组成队列,大致的结构如图:

蓝色部分为发送数据用的页面总和,总共只有6个页面用于发送数据(40h~45h);剩余的46h~80h都是接收数据用的,而在接收数据内存中,只有红色部分是有数据的,当接收新的数据时,是向红色部分前面的绿色中的256字节写入数据,同时“把当前指针”移动到+256字节的后面(网卡自动完成),而现在要读的数据,是在“边界指针”那里开始的256字节(紫色部分),下一个要读的数据,是在“下一包指针”的位置开始的256字节,当256字节被读出来了,就变成了重新可以使用的内存,即绿色所表示,而接收数据,就是把可用的内存拿来用,即变成了红色,当数据写到了0x80h后,又从0x46h开始写数据,这样循环,如果数据满了,则网卡就不能再接收数据,必须等待数据被读出去了,才能再继续接收。

下面是一些网卡常用的寄存器:

  • CR(command register)---命令寄存器

  • TSR(transmit state register)---发送状态寄存器

  • ISR(interrupt state register)----中断状态寄存器

  • RSR(receive state register)---接收状态寄存器

  • RCR(receive configure register)---接收配置寄存器

  • TCR(transmit configure register)---发送配置寄存器

  • DCR(data configure register)---数据配置寄存器

  • IMR(interrupt mask register)---中断屏蔽寄存器

  • NCR(non-coding region)---包发送期间碰撞次数

  • FIFO(first in first out)

  • CNTR0(counter register)--- 帧同步错总计数器

  • CNTR1---CRC错总计数器

  • CNTR2---丢包总计数器

  • PAR0~5(physical address register)---本地MAC地址

  • MAR0~7(multiple address register)---多播地址匹配

  • PSTOP(page stop register)---结束页面寄存器

  • PSTART(page start register)---开始页面寄存器

  • BNRY(boundary register)----边界页寄存器

  • CURR(current page register)---当前页面寄存器

  • CLDA0,1(Current Local DMA Address)---当前本地DMA寄存器

  • TPSR(Transmit page start register)---传送页面开始寄存器

  • TBCR0,1(transmit byte counter register)---传送字节计数寄存器

  • CRDA0,1(current remote DMA address)---当前远程DMA寄存器

  • RSAR0,1(remote start address register)---远程DMA起始地址寄存器

  • RBCR0,1(remote byte counter register)---远程字节计数寄存器

  • BPAGE(BROM page register)---BROM页面寄存器

1.1框架

网络子系统中,在本文中我们关注的是驱动和内核的交互。也就是网卡收到数据包后怎么交给内核,内核收到数据包后怎么交给协议栈处理。

在内核中,网卡设备是被net_device结构体描述的。驱动需要通过net_device向内核注册一组操作网卡硬件的函数,这样内核便可以使用网卡了。而所有的数据包在内核空间都是使用sk_buff结构体来表示,所以将网卡硬件收到的数据转换成内核认可的skb_buff也是驱动的工作。

在这之后,还有两个结构体也发挥了非常重要的作用。一个是为struct softnet_data,另一个是struct napi_struct。为软中断的方式处理数据包提供了支持。

1.2初始化

一切的起源都是上电那一刻,当系统初始化完毕后,我们的系统就应该是可用的了。网络子模块的初始化也是在Linux启动经历两阶段的混沌boost自举后,进入的第一个C函数start_kernel。在这之前是Bootloader和Linux的故事,在这之后,便是Linux的单人秀了。

网络子设备初始化调用链:start_kernel->rest_init->kernel_init->kernel_init_freeable->do_basic_setup->do_initcalls->do_initcalls->net_dev_init。

上面调用关系中的kernel_init是一个内核子线程中调用的:

pid = kernel_thread(kernel_init, NULL, CLONE_FS);

然后再一个问题就是当进入do_initcalls后我们会发现画风突变:

static void __init do_initcalls(void)
{
int level;

for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}

我是谁,我来自哪,我要到哪去。

如果do_initcalls还给了我们一丝看下去的希望,点开do_initcall_level可能就真的绝望了。

static void __init do_initcall_level(int level)
{
initcall_t *fn;

strcpy(initcall_command_line, saved_command_line);
parse_args(initcall_level_names[level],
initcall_command_line, __start___param,
__stop___param - __start___param,
level, level,
NULL, &repair_env_string);

trace_initcall_level(initcall_level_names[level]);
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}

全局一个fn指针,实现调用全靠猜。反正我不管,我说调用了net_dev_init就是调用了。伟大的google告诉我只要被下面这些宏定义包裹的函数就会被do_one_initcall调用,用了什么黑科技,先不管:

#file:include/linux/init.h

#define pure_initcall(fn) __define_initcall(fn, 0)

#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)

在net_dev_init的定义下面,我们可以找到subsys_initcall(net_dev_init);。Ok,网络子系统的初始化入口已找到到。

static int __init net_dev_init(void)
{
int i, rc = -ENOMEM;

BUG_ON(!dev_boot_phase);

if (dev_proc_init())
goto out;

if (netdev_kobject_init())
goto out;

INIT_LIST_HEAD(&ptype_all);
for (i = 0; i < PTYPE_HASH_SIZE; i++)
INIT_LIST_HEAD(&ptype_base[i]);

INIT_LIST_HEAD(&offload_base);

if (register_pernet_subsys(&netdev_net_ops))
goto out;

/*
* Initialise the packet receive queues.
*/

for_each_possible_cpu(i) {
struct work_struct *flush = per_cpu_ptr(&flush_works, i);
struct softnet_data *sd = &per_cpu(softnet_data, i);

INIT_WORK(flush, flush_backlog);

skb_queue_head_init(&sd->input_pkt_queue);
skb_queue_head_init(&sd->process_queue);
#ifdef CONFIG_XFRM_OFFLOAD
skb_queue_head_init(&sd->xfrm_backlog);
#endif
INIT_LIST_HEAD(&sd->poll_list);
sd->output_queue_tailp = &sd->output_queue;
#ifdef CONFIG_RPS
sd->csd.func = rps_trigger_softirq;
sd->csd.info = sd;
sd->cpu = i;
#endif

sd->backlog.poll = process_backlog;
sd->backlog.weight = weight_p;
}

dev_boot_phase = 0;

/* The loopback device is special if any other network devices
* is present in a network namespace the loopback device must
* be present. Since we now dynamically allocate and free the
* loopback device ensure this invariant is maintained by
* keeping the loopback device as the first device on the
* list of network devices. Ensuring the loopback devices
* is the first device that appears and the last network device
* that disappears.
*/
if (register_pernet_device(&loopback_net_ops))
goto out;

if (register_pernet_device(&default_device_ops))
goto out;

open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

rc = cpuhp_setup_state_nocalls(CPUHP_NET_DEV_DEAD, "net/dev:dead",
NULL, dev_cpu_dead);
WARN_ON(rc < 0);
rc = 0;
out:
return rc;
}

在net_dev_init中,初始化了内核收发包队列,开启了对应的软中断NET_TX_SOFTIRQ和NET_RX_SOFTIRQ。在其中,该函数为每个CPU初始化了一个softnet_data来挂载需要处理设备的napi_struct。这个结构非常重要,软中断的处理就是从这个链表上取napi_struct,然后收包的。这也是内核和驱动的接口之一。

再就是开启的两个软中断,当驱动在硬终端完成必要的上半部工作后,就会拉起对应的软中断。让数据包下半部软中断中处理。

net_dev_init执行完后,我们内核就有了处理数据包的能力,只要驱动能向softnet_data挂载需要收包设备的napi_struct。内核子线程ksoftirqd便会做后续的处理。接下来就是网卡驱动的初始化了。

各种网卡肯定有不同的驱动,各驱动封装各自硬件的差异,给内核提供一个统一的接口。我们这不关心,网卡驱动是怎么把数据发出去的,如何收回来的。而是探究网卡收到数据了,要怎么交给内核,内核如何将要发的数据给网卡。总之,驱动需要给内核提供哪些接口,内核又需要给网卡哪些支持。我们以e1000网卡为例子。看看它和内核的缠绵故事。

e1000网卡是一块PCI设备。所以它首先得要让内核能通过PCI总线探测到,需要向内核注册一个pci_driver结构,PCI设备的使用是另一个话题,这里不会探究,我也不知道:

static struct pci_driver e1000_driver = {
.name = e1000_driver_name,
.id_table = e1000_pci_tbl,
.probe = e1000_probe,
.remove = e1000_remove,
#ifdef CONFIG_PM
/* Power Management Hooks */
.suspend = e1000_suspend,
.resume = e1000_resume,
#endif
.shutdown = e1000_shutdown,
.err_handler = &e1000_err_handler
};

其中e1000_probe就是给内核的探测回调函数,算是网卡的初始化函数吧,驱动需要在这里初始化网卡设备。去掉总线相关的代码,错误处理的代码,硬件相关的代码:

static int e1000_probe(struct pci_dev *pdev, const struct pci_device_id *ent)
{
struct net_device *netdev;
netdev = alloc_etherdev(sizeof(struct e1000_adapter));//申请net_device设备
netdev->netdev_ops = &e1000_netdev_ops; //注册操作设备的回调函数
e1000_set_ethtool_ops(netdev);
netdev->watchdog_timeo = 5 * HZ;
netif_napi_add(netdev, &adapter->napi, e1000_clean, 64);//软中断里会调用poll钩子函数
strncpy(netdev->name, pci_name(pdev), sizeof(netdev->name) - 1);
err = register_netdev(netdev);
}

每一个网络设备都有一个对应的net_devie结构体来描述。其中像设备文件操作一样,保存了一种操作设备的接口函数netdev_ops,对e1000网卡是e1000_netdev_ops。当通过终端输入ifup,ifdowm命令操作网卡时,对应的open,close函数就会被调用。这段代码最重要的还是netif_napi_add的调用,它向内核注册了e1000_clean函数,用来给上面的CPU收包队列调用。

通过初始化,驱动注册了网卡描述net_device, 内核可以通过它操作到网卡设备。通过e1000_clean函数内核软中断也可以收包了。

1.3驱动收包

前面有一个内核软中断来收包,但这个软中断怎么触发呢?硬中断。当有数据到网卡时,会产生一个硬中断。这中断的注册是上面,e1000_netdev_ops中的e1000_up函数调用的。也就是网卡up时会注册这个硬中断处理函数e1000_intr。

/**
* e1000_intr - Interrupt Handler
* @irq: interrupt number
* @data: pointer to a network interface device structure
**/
static irqreturn_t e1000_intr(int irq, void *data)
{
struct net_device *netdev = data;
struct e1000_adapter *adapter = netdev_priv(netdev);
struct e1000_hw *hw = &adapter->hw;
u32 icr = er32(ICR);

/* disable interrupts, without the synchronize_irq bit */
ew32(IMC, ~0);
E1000_WRITE_FLUSH();

if (likely(napi_schedule_prep(&adapter->napi))) {
adapter->total_tx_bytes = 0;
adapter->total_tx_packets = 0;
adapter->total_rx_bytes = 0;
adapter->total_rx_packets = 0;
__napi_schedule(&adapter->napi);
} else {
/* this really should not happen! if it does it is basically a
* bug, but not a hard error, so enable ints and continue
*/
if (!test_bit(__E1000_DOWN, &adapter->flags))
e1000_irq_enable(adapter);
}

return IRQ_HANDLED;
}

去掉unlikely的代码,其中通过if (likely(napi_schedule_prep(&adapter->napi)))测试,网卡设备自己的napi是否正在被CPU使用。没有就调用__napi_schedule将自己的napi挂载到CPU的softnet_data上。这样软中断的内核线程就能轮询到这个软中断。

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

/* Called with irq disabled */
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
}

这里的softnet_data就是前面net_dev_init函数为每个CPU初始化的。到这里硬件中断就处理完了,但我们依然没有发现任何有关数据包的处理,只知道了有一个napi被挂载。这是因为硬件中断不能显然太长,的确不会去做数据的处理工作。这些都交给软中断的内核线程来处理的。

1.4内核处理

硬中断将一个napi结构体甩给了内核,内核要怎么根据它来接收数据呢?前面说到,内核为每个CPU核心都运行了一个内核线程ksoftirqd。软中断也就是在这线程中处理的。上面的硬件中断函数设置了NET_RX_SOFTIRQ软中断标志,这个字段处理函数还记得在哪注册的么?是的,net_dev_init中。

 open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

显然,后续处理肯定是由net_rx_action来完成。

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);
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);
budget -= napi_poll(n, &repoll); //在这回调驱动的poll函数,这个函数在napi中

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

上面看到budget -= napi_poll(n, &repoll);他会去调用我们驱动初始化时注册的poll函数,在e1000网卡中就是e1000_clean函数。

/**
* e1000_clean - NAPI Rx polling callback
* @adapter: board private structure
**/
static int e1000_clean(struct napi_struct *napi, int budget)
{
struct e1000_adapter *adapter = container_of(napi, struct e1000_adapter,
napi);
int tx_clean_complete = 0, work_done = 0;

tx_clean_complete = e1000_clean_tx_irq(adapter, &adapter->tx_ring[0]);

adapter->clean_rx(adapter, &adapter->rx_ring[0], &work_done, budget);//将数据发给协议栈来处理。

if (!tx_clean_complete)
work_done = budget;

/* If budget not fully consumed, exit the polling mode */
if (work_done < budget) {
if (likely(adapter->itr_setting & 3))
e1000_set_itr(adapter);
napi_complete_done(napi, work_done);
if (!test_bit(__E1000_DOWN, &adapter->flags))
e1000_irq_enable(adapter);
}

return work_done;
}

e1000_clean函数通过调用clean_rx函数指针来处理数据包。

/**
* e1000_clean_jumbo_rx_irq - Send received data up the network stack; legacy
* @adapter: board private structure
* @rx_ring: ring to clean
* @work_done: amount of napi work completed this call
* @work_to_do: max amount of work allowed for this call to do
*
* the return value indicates whether actual cleaning was done, there
* is no guarantee that everything was cleaned
*/
static bool e1000_clean_jumbo_rx_irq(struct e1000_adapter *adapter,
struct e1000_rx_ring *rx_ring,
int *work_done, int work_to_do)
{
struct net_device *netdev = adapter->netdev;
struct pci_dev *pdev = adapter->pdev;
struct e1000_rx_desc *rx_desc, *next_rxd;
struct e1000_rx_buffer *buffer_info, *next_buffer;
u32 length;
unsigned int i;
int cleaned_count = 0;
bool cleaned = false;
unsigned int total_rx_bytes = 0, total_rx_packets = 0;

i = rx_ring->next_to_clean;
rx_desc = E1000_RX_DESC(*rx_ring, i);
buffer_info = &rx_ring->buffer_info[i];
e1000_receive_skb(adapter, status, rx_desc->special, skb);
napi_gro_frags(&adapter->napi);

return cleaned;
}

/**
* e1000_receive_skb - helper function to handle rx indications
* @adapter: board private structure
* @status: descriptor status field as written by hardware
* @vlan: descriptor vlan field as written by hardware (no le/be conversion)
* @skb: pointer to sk_buff to be indicated to stack
*/
static void e1000_receive_skb(struct e1000_adapter *adapter, u8 status,
__le16 vlan, struct sk_buff *skb)
{
skb->protocol = eth_type_trans(skb, adapter->netdev);

if (status & E1000_RXD_STAT_VP) {
u16 vid = le16_to_cpu(vlan) & E1000_RXD_SPC_VLAN_MASK;

__vlan_hwaccel_put_tag(skb, htons(ETH_P_8021Q), vid);
}
napi_gro_receive(&adapter->napi, skb);
}

这个函数太长,我就保留了e1000_receive_skb函数的调用,它调用了napi_gro_receive,这个函数同样是NAPI提供的函数,我们的skb从这里调用到netif_receive_skb协议栈的入口函数。调用路径是napi_gro_receive->napi_frags_finish->netif_receive_skb_internal->__netif_receive_skb。具体的流程先放放。

毕竟NAPI是内核为了提高网卡收包性能而设计的一套框架。这就可以让我先挖个坑以后在分析NAPI的时候在填上。总之有了NAPI后的收包流程和之前的区别如图:

到这里,网卡驱动到协议栈入口的处理过程就写完了。

1.5接收网络包

硬件网卡接收到网络包之后,通过 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。

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

硬件网卡接收到网络包之后,通过 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 队列。

二、中断处理

一旦网卡接收完成,它会向CPU发送一个中断信号以通知数据包的到达。操作系统内核会相应地触发一个中断处理程序,并暂停当前正在执行的任务。

  • Linux内核网络收包过程函数调用分析

  • 数据帧首先到达网卡的接收队列,分配RingBuffer

  • DMA把数据搬运到网卡关联的内存

  • 网卡向CPU发起硬中断,通知CPU有数据

  • 调用驱动注册的硬中断处理函数

  • 启动NAPI,触发软中断

2.1网卡驱动注册硬中断处理函数

网卡驱动注册中断处理函数igb_msix_ring()。

igb_open() - drivers/net/ethernet/intel/igb/igb_main.c
igb_request_irq - drivers/net/ethernet/intel/igb/igb_main.c
igb_request_msix - drivers/net/ethernet/intel/igb/igb_main.c
igb_msix_ring() - drivers/net/ethernet/intel/igb/igb_main.c

系统启动时注册软中断处理函数

NET_RX_SOFTIRQ的软中断处理函数为net_rx_action()。

subsys_initcall(net_dev_init) - net/core/dev.c
net_dev_init() - net/core/dev.c
open_softirq(NET_RX_SOFTIRQ, net_rx_action) - net/core/dev.c

系统启动时注册协议栈处理函数

在网络层,以IPv4为例,注册的协议处理函数为ip_rcv()。在传输层,根据协议注册其处理函数upd_rcv()、tcp_v4_rcv()、icmp_rcv()等。

fs_initcall(inet_init) - net/ipv4/af_inet.c
inet_init() - net/ipv4/af_inet.c
inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) - net/ipv4/af_inet.c
inet_add_protocol(&udp_protocol, IPPROTO_UDP) - net/ipv4/af_inet.c
inet_add_protocol(&tcp_protocol, IPPROTO_TCP) - net/ipv4/af_inet.c
dev_add_pack(&ip_packet_type) - net/ipv4/af_inet.c

2.2硬中断处理函数

当有数据包到达网卡时,DMA把数据映射到内存,通知CPU硬中断,执行注册的硬中断处理函数igb_msix_ring(),简单处理后,发出软中断NET_RX_SOFTIRQ。

igb_msix_ring() - drivers/net/ethernet/intel/igb/igb_main.c
__napi_schedule() - net/core/dev.c
____napi_schedule() - net/core/dev.c
__raise_softirq_irqoff(NET_RX_SOFTIRQ) - net/core/dev.c

2.3软中断处理函数

ksoftirqd为软中断处理进程,ksoftirqd收到NET_RX_SOFTIRQ软中断后,执行软中断处理函数net_rx_action(),调用网卡驱动poll()函数收包。最后通过调用注册的ip协议处理函数ip_rcv()将数据包送往协议栈。

run_ksoftirqd() - kernel/softirqd.c
__do_softirq() - kernel/softirqd.c
h->action(h) - kernel/softirqd.c
net_rx_action() - net/core/dev.c
napi_poll() - net/core/dev.c
__napi_poll - net/core/dev.c
work = n->poll(n, weight) - net/core/dev.c
igb_poll() - drivers/net/ethernet/intel/igb/igb_main.c
igb_clean_rx_irq() - drivers/net/ethernet/intel/igb/igb_main.c
napi_gro_receive() - net/core/gro.c
napi_skb_finish() - net/core/gro.c
netif_receive_skb_list_internal() - net/core/dev.c
__netif_receive_skb_list() - net/core/dev.c
__netif_receive_skb_list_core - net/core/dev.c
__netif_receive_skb_core - net/core/dev.c
deliver_skb() - net/core/dev.c
pt_prev->func(skb, skb->dev, pt_prev, orig_dev)

协议栈处理函数-L3

在软中断处理的最后,调用的pt_prev->func()函数即为协议栈注册ipv4处理函数ip_rcv()。网络层处理完成之后,根据传输协议执行注册的传输层处理函数tcp_v4_rcv或者udp_rcv()。

ip_rcv() - net/ipv4/ip_input.c
ip_rcv_finish() - net/ipv4/ip_input.c
dst_input() - include/net/dst.h
ip_local_deliver() - net/ipv4/ip_input.c
ip_local_deliver_finish() - net/ipv4/ip_input.c
ip_protocol_deliver_rcu() - net/ipv4/ip_input.c
ret = INDIRECT_CALL_2(ipprot->handler, tcp_v4_rcv, udp_rcv, skb)

协议栈处理函数-L4

这里以udp协议为例说明处理过程,tcp协议处理过程更复杂一些。最后将数据包添加到socket的接收队列。然后进入用户空间应用层面处理。

udp_rcv() - net/ipv4/udp.c
udp_unicast_rcv_skb() - net/ipv4/udp.c
udp_queue_rcv_skb() - net/ipv4/udp.c
udp_queue_rcv_one_skb() - net/ipv4/udp.c
__udp_queue_rcv_skb() - net/ipv4/udp.c
__udp_enqueue_schedule_skb() - net/ipv4/udp.c
__skb_queue_tail() - net/ipv4/udp.c

最终调用 gro_normal_list将数据发送到网络协议栈

三、包分类

中断处理程序开始运行后,它会根据网络包的协议类型(如TCP、UDP等)和目标IP地址进行分类。这样可以确保每个数据包被传送给正确的协议栈。

四、协议栈处理

对于需要进一步处理的数据包,操作系统内核将其传递给相应的网络层、传输层和应用层协议栈。例如,在IPv4上运行TCP/IP时,数据包将经过IPv4模块、TCP模块等依次处理。

4.1图解以IPv4分组为例

4.2处理过程

1) ip_rcv()

skb被送到ip_rcv()函数进行处理。首先ip_rcv函数验证IP分组。比如目的地是否是本机地址,校验和是否正确等等。若正确,则交给netfilter的NF_IP_ROUTING;否则,丢弃

2)ip_rcv_finish()

随后将分组发送到ip_rcv_finish()函数处理。根据skb结构的目的或路由信息发送到不同的处理函数。

ip_rcv_finish()函数的具体处理过程如下:

从 skb->nh ( IP 头,由 netif_receive_skb 初始化)结构得到 IP 地址

struct net_device *dev = skb->dev;
struct iphdr *iph = skb->nh.iph;

而 skb->dst 或许包含了数据分组到达目的地的路由信息,如果没有,则需要查找路由,如果最后结果显示目的地不可达,那么就丢弃该数据包:

if (skb->dst == NULL) {
if (ip_route_input(skb, iph->daddr, iph->saddr, iph->tos, dev))
goto drop; //丢弃
}

ip_rcv_finish() 函数最后执行 dst_input ,决定数据包的下一步的处理。

本机分组则由ip_local_deliver处理;
需要转发的数据则由ip_forward()函数处理;
组播数据包则由ip_mr_input()函数处理。

4.3 ip_forward()转发数据包

  1. 处理IP头选项。如果需要,会记录本地IP地址和时间戳;

  2. 确认分组可以被转发

  3. 将TTL减1,如果TTL为0,则丢弃分组,TTL是 Time To Live的缩写,该字段指定IP包被路由器丢弃之前允许通过的最大网段数量。TTL是IPv4报头的一个8 bit字段。

  4. 根据MTU大小和路由信息,对数据分组进行分片。MTU即最大传输单元(Maximum Transmission Unit,MTU)用来通知对方所能接受数据服务单元的最大尺寸,说明发送方能够接受的有效载荷大小。

  5. 将数据分组送往外出设备。如果因为某种原因分组转发失败,则回应ICMP消息,来回复不能转发的原因。如果对转发的分组进行各种检查无误后。

  6. 执行ip_forward_finish()函数,准备发送。然后执行dst_output(skb)将分组发到转发的目的主机或本地主机。dst_output(skb) 函数要执行虚函数 output (单播的话为 ip_output ,多播为 ip_mc_output )。

  7. 最后, 调用ip_finish_output() 进入邻居子系统。邻居子系统:在数据链接层,必须要获取发送方和接收方的MAC地址,这样数据才能正确到达接收方。邻居子系统的作用就是把IP地址转换成对应的MAC地址。如果目的主机不是和发送发位于同一局域网时,解析的MAC地址就是下一跳网关地址

4.4ip_local_deliver本地处理

ip_local_deliver中对ip分片进行重组,经过LOCAL_IN钩子点,然后调用ip_local_deliver_finish;

/*
* Deliver IP Packets to the higher protocol layers.
*/
int ip_local_deliver(struct sk_buff *skb)
{
/*
* 重组 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;
}

/* 经过LOCAL_IN钩子点 */
return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN,
net, NULL, skb, skb->dev, NULL,
ip_local_deliver_finish);
}

最后调用ip_local_deliver_finish()函数:ip_local_deliver_finish函数处理原始套接字的数据接收,并调用上层协议的包接收函数,将数据包传递到传输层;

4.5传输层处理

TCP处理过程如图:


接收到的分组由ip_local_deliver进入。

发送分组或者响应分组有ip_queue_xmit()函数出口出去:

发送时,ip_queue_xmit()函数检查socket结构体中是否有路由信息,如果没有则执行ip_route_flow()查找,并存储到skb数据结构中。如果找不到,则丢弃。

五、应用程序处理

协议处理程序处理完成后,会将数据包存储到应用层缓冲区中,等待应用程序处理。应用程序可以从应用层缓冲区中读取数据包,并进行相应的处理。

六、发送响应数据

当应用程序处理完数据包后,会将响应数据返回给协议栈。协议栈会将响应数据封装成数据包,并通过网卡发送出去。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多