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

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

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数据结构中。如果找不到,则丢弃。

五、应用程序处理

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

六、发送响应数据

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


相关文章
|
1月前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
72 2
|
1月前
|
Linux iOS开发 网络架构
如何使用 Ping 命令监测网络丢包情况?
如何使用 Ping 命令监测网络丢包情况?
143 48
|
29天前
|
安全 Windows
【Azure Cloud Service】在Windows系统中抓取网络包 ( 不需要另外安全抓包工具)
通常,在生产环境中,为了保证系统环境的安全和纯粹,是不建议安装其它软件或排查工具(如果可以安装,也是需要走审批流程)。 本文将介绍一种,不用安装Wireshark / tcpdump 等工具,使用Windows系统自带的 netsh trace 命令来获取网络包的步骤
67 32
|
10天前
|
Web App开发 网络协议 安全
网络编程懒人入门(十六):手把手教你使用网络编程抓包神器Wireshark
Wireshark是一款开源和跨平台的抓包工具。它通过调用操作系统底层的API,直接捕获网卡上的数据包,因此捕获的数据包详细、功能强大。但Wireshark本身稍显复杂,本文将以用抓包实例,手把手带你一步步用好Wireshark,并真正理解抓到的数据包的各项含义。
46 2
|
2月前
|
运维 监控 网络协议
|
1月前
|
安全 算法 网络安全
量子计算与网络安全:保护数据的新方法
量子计算的崛起为网络安全带来了新的挑战和机遇。本文介绍了量子计算的基本原理,重点探讨了量子加密技术,如量子密钥分发(QKD)和量子签名,这些技术利用量子物理的特性,提供更高的安全性和可扩展性。未来,量子加密将在金融、政府通信等领域发挥重要作用,但仍需克服量子硬件不稳定性和算法优化等挑战。
|
1月前
|
存储 安全 网络安全
云计算与网络安全:保护数据的新策略
【10月更文挑战第28天】随着云计算的广泛应用,网络安全问题日益突出。本文将深入探讨云计算环境下的网络安全挑战,并提出有效的安全策略和措施。我们将分析云服务中的安全风险,探讨如何通过技术和管理措施来提升信息安全水平,包括加密技术、访问控制、安全审计等。此外,文章还将分享一些实用的代码示例,帮助读者更好地理解和应用这些安全策略。
|
22天前
|
弹性计算 安全 容灾
阿里云DTS踩坑经验分享系列|使用VPC数据通道解决网络冲突问题
阿里云DTS作为数据世界高速传输通道的建造者,每周为您分享一个避坑技巧,助力数据之旅更加快捷、便利、安全。本文介绍如何使用VPC数据通道解决网络冲突问题。
77 0
|
1月前
|
安全 网络安全 数据安全/隐私保护
网络安全与信息安全:从漏洞到加密,保护数据的关键步骤
【10月更文挑战第24天】在数字化时代,网络安全和信息安全是维护个人隐私和企业资产的前线防线。本文将探讨网络安全中的常见漏洞、加密技术的重要性以及如何通过提高安全意识来防范潜在的网络威胁。我们将深入理解网络安全的基本概念,学习如何识别和应对安全威胁,并掌握保护信息不被非法访问的策略。无论你是IT专业人士还是日常互联网用户,这篇文章都将为你提供宝贵的知识和技能,帮助你在网络世界中更安全地航行。
|
1月前
|
网络协议 安全 算法
网络空间安全之一个WH的超前沿全栈技术深入学习之路(9):WireShark 简介和抓包原理及实战过程一条龙全线分析——就怕你学成黑客啦!
实战:WireShark 抓包及快速定位数据包技巧、使用 WireShark 对常用协议抓包并分析原理 、WireShark 抓包解决服务器被黑上不了网等具体操作详解步骤;精典图示举例说明、注意点及常见报错问题所对应的解决方法IKUN和I原们你这要是学不会我直接退出江湖;好吧!!!
网络空间安全之一个WH的超前沿全栈技术深入学习之路(9):WireShark 简介和抓包原理及实战过程一条龙全线分析——就怕你学成黑客啦!
下一篇
DataWorks