从头到脚: 跟踪内核态包接收到用户态的数据读取完整流程

编写网络真实一个有趣并且枯燥的体验。当你看到另外一台机器发送过来的消息,或者你发送另外一台机器的消息被正确接收,你总是会非常的开心。如果你还没有编写过网络程序,你可以尝试 Redis 作者的示例程序:antirez/smallchat[1],或者我的 Go 语言/Rust 语言版:smallnest/smallchat[2]

同时,编写网络程序也是一件枯燥的事情,尤其是你编写的程序并没有达到你的预期,或者数据没有发送出去、或者对端没有接收到数据,总之你需要花费大量的时间去 debug, 这需要相当多的经验的积累。

同时 debug 的工具也五花八门,需要你去学习和积累经验。

这篇文档从实战的角度,带你了解探索跟踪网络数据从网卡接收后,在 Linux 协议栈中每一个函数的处理,写入到 socket 的缓冲区,然后被 Go 的网络处理库接收后,被一个 goroutine 读取的完整过程。它提供了一种跟踪网络数据处理过程的方法,可以推广到网络发送流程、不同协议的处理流程场景中。

当然在介绍跟踪网络接收处理过程之前,我先介绍一下 Go 语言中的底层网络编程背景知识,如何使用 Go 语言实现 raw socket 网络编程。

  1. 背景

我在实现我厂物理网络监控的程序中,很大部分都是使用 raw socket 实现的,我们组的其他同学实现的代码中,也大多采用 raw socket 实现,因为这样我们可以实现更底层的控制,比如在 socket 很少的情况下构建非常多的五元组、可以设置 TOS 等网络参数、可以构建 VXLAN 的数据包模拟公有云的数据等等。

为了提升 raw socket 性能,在 Go 语言网络群中经过咨询一些大神,我还了解了对 raw socket 设置 cBPF 过滤器的功能,最早应用 Lidar 项目中,后来也在我们的 raw socket 应用中广泛应用,可以提升程序数倍的性能。

Raw socket 有两种:IP 层的 raw socket 和链路层(Ethernet)的 raw socket。网上有一本非常好的小册子,详细介绍各种操作系统的 raw socket 的支持和系统调用: Introduction to RAW-sockets[3]。它把 IP 层的 raw socket 称之为 Raw-Socket, 链路层 (Ethernet) 的 raw socket 叫做 Packet-Socket。

图片来源:https://0xbharath.github.io/art-of-packet-crafting-with-scapy/networking/socket_interface/index.html

Raw-Socket 能接收到 IP 层的数据,也意味着 IP Header 数据你都可以读取,发送的时候也需要手动设置 IP header。

图片来源:维基百科

Packet-Socket 可以访问到链路层的数据,包括源目的 Mac 地址。同时也意味着发送数据的时候你也需要填空这些信息。

根据你选择的 Socket 不同,每一层的数据你都需要填充,不过谷歌维护的这个库 gopacket [4] 提供了很好的方法进行数据封装。

图片来源:维基百科

1.1 Go 语言使用 RAW-socket

使用 Go 语言的net.ListenPacket函数可以创建一个 IP 层的 RAW-socket,然后使用WriteTo函数发送数据包。比如下面的例子,发送 UDP 数据包:

package main
import (
 "fmt"
 "net"
)
func main() {
 conn, err := net.ListenPacket("ip4:udp", "127.0.0.1")
 if err != nil {
  fmt.Println("DialIP failed:", err)
  return
 }
 data, _ := encodeUDPPacket("127.0.0.1", "192.168.0.1", []byte("hello world"))
 if _, err := conn.WriteTo(data, &net.IPAddr{IP: net.ParseIP("192.168.0.1")}); err != nil {
  panic(err)
 }
 buf := make([]byte, 1024)
 n, peer, err := conn.ReadFrom(buf)
 if err != nil {
  panic(err)
 }
 fmt.Printf("received response from %s: %sn", peer.String(), buf[8:n])
}

注意哈,一定要注意的是,Go 标准库对于 RAW-socket 这种类型的数据做了特殊的处理,你WriteTo发送的数据(上面例子中的data)不包含 IP 头部,Go 会自动帮你添加 IP 头部,所以你只需要构造 UDP 数据包即可。同时你ReadFrom读取的数据也是不包含 IP 头部的,Go 会自动帮你去掉 IP 头部,所以你只需要处理 UDP 数据包即可。

某种程度上来讲,这是一件好事,这种处理方法简化了很多繁琐的网络编程,让我们聚焦于业务逻辑。

但是对于我们来说,我们吸引想要构造 IP 头部,或者你想要读取 IP 头部,因为我们需要为网络监控和探测设置一些底层的参数,这个时候我们就需要设置 IP Header 数据,我们可能就得另外想办法了。这个时候我常用的就是 Go 语言的扩展包golang.org/x/net/ipv4,它提供了一些接口可以让我们设置 IP 头部数据。

package main
import (
 "fmt"
 "net"
 "golang.org/x/net/ipv4"
)
func main() {
 conn, err := net.ListenPacket("ip4:udp", "127.0.0.1")
 if err != nil {
  fmt.Println("DialIP failed:", err)
  return
 }
 rc, err := ipv4.NewRawConn(conn)
 if err != nil {
  panic(err)
 }
 data, _ := encodeUDPPacket("127.0.0.1", "192.168.0.1", []byte("hello world"))
 if _, err := rc.WriteToIP(data, &net.IPAddr{IP: net.ParseIP("192.168.0.1")}); err != nil {
  panic(err)
 }
 rbuf := make([]byte, 1024)
 _, payload, _, err := rc.ReadFrom(rbuf)
 if err != nil {
  panic(err)
 }
 fmt.Printf("received response: %sn", payload[8:])
}

这个时候我使用ipv4.NewRawConn(conn)包装一下,就可以实现读取 IP 头部数据了,发送的数据也需要包含 IP Header 数据。同时,读取的时候,我们还可以读取带外数据,比如软硬件的时间戳。

1.2 Go 语言使用 Packet-socket

我们可以使用 syscall 包来创建 Packet-Socket,然后使用 gopacket 库来构建数据包,最后使用 syscall 包发送数据包。

package main
import (
    "log"
    "net"
    "syscall"
    "github.com/google/gopacket"
    "github.com/google/gopacket/layers"
)
func main() {
    // 1. 创建 Packet Socket
    fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_ALL)))
    if err != nil {
        log.Fatalf("Error creating socket: %v", err)
    }
    defer syscall.Close(fd)
    // 2. 获取网络接口信息
    iface, err := net.InterfaceByName("eth0") // 修改为你实际的网络接口名称
    if err != nil {
        log.Fatalf("Error getting interface: %v", err)
    }
    // 3. 构建以太网层、IP 层和 UDP 层的数据包
    ethernetLayer := &layers.Ethernet{
        SrcMAC:       iface.HardwareAddr,
        DstMAC:       net.HardwareAddr{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // 广播 MAC 地址
        EthernetType: layers.EthernetTypeIPv4,
    }
    ipLayer := &layers.IPv4{
        Version:  4,
        IHL:      5,
        TTL:      64,
        SrcIP:    net.IP{192, 168, 1, 1}, // 修改为实际的源 IP 地址
        DstIP:    net.IP{192, 168, 1, 255}, // 修改为实际的目的 IP 地址
        Protocol: layers.IPProtocolUDP,
    }
    udpLayer := &layers.UDP{
        SrcPort: 12345, // 源端口
        DstPort: 80,    // 目的端口
    }
    udpLayer.SetNetworkLayerForChecksum(ipLayer)
    // 创建 gopacket 序列化缓冲区
    buffer := gopacket.NewSerializeBuffer()
    options := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}
    // 序列化数据包
    if err := gopacket.SerializeLayers(buffer, options, ethernetLayer, ipLayer, udpLayer, gopacket.Payload([]byte("Hello, World!"))); err != nil {
        log.Fatalf("Error serializing packet: %v", err)
    }
    outPacket := buffer.Bytes()
    // 4. 将 Packet Socket 绑定到接口
    addr := syscall.SockaddrLinklayer{
        Protocol: htons(syscall.ETH_P_ALL), // 接收所有协议类型的包
        Ifindex:  iface.Index,
    }
    if err := syscall.Bind(fd, &addr); err != nil {
        log.Fatalf("Error binding socket to interface: %v", err)
    }
    // 5. 发送数据包
    if err := syscall.Sendto(fd, outPacket, 0, &addr); err != nil {
        log.Fatalf("Error sending packet: %v", err)
    }
    log.Println("Packet sent successfully!")
}
// htons 将主机字节序转化为网络字节序
func htons(i uint16) uint16 {
    return (i<<8)&0xff00 | i>>8
}

代码说明

  • Packet Socket 创建:使用 syscall.Socket 创建 AF_PACKET 类型的原始套接字,设置为 SOCK_RAW 类型和 ETH_P_ALL 协议,以便处理所有类型的以太网帧。

  • 获取网络接口:net.InterfaceByName("eth0") 获取指定网络接口的信息,并提取 MAC 地址用于构造以太网层的数据包。请根据需要更改接口名称。

  • 数据包构造:

  • 使用 gopacket 的 layers 包创建以太网层、IP 层和 UDP 层。

  • 设置以太网层的源 MAC 和目标 MAC 地址,以及 EthernetType。

  • 设置 IP 层的源和目的 IP 地址、TTL 和协议类型。

  • 设置 UDP 层的源和目的端口,并通过 SetNetworkLayerForChecksum 函数来计算 UDP 校验和。

  • 使用 gopacket.Payload 传入自定义的 UDP 负载数据 Hello, World!。

  • Packet Socket 绑定:通过 syscall.SockaddrLinklayer 绑定 Packet Socket 到指定的接口,便于发送数据包。

  • 发送数据包:使用 syscall.Sendto 发送数据包

注意上面的代码只是一个示例,在生产环墫中,你需要根据实际情况修改 源 MAC 地址、目的 MAC 地址、源 IP、目的 IP、源端口、目的端口等信息。

2. 既有的网络知识回顾

网上有很多介绍 Linux 网络协议栈的文章,包括从 9x 年开始的一些 Linux 网络栈的分析,并且画出了非常好看的流程图,可能有一些和现在的 Linux 的代码不一致的地方,这是因为 Linux 内核协议栈的代码也在不断的重构和优化,但是大体的流程是不会变的。也有部分书籍介绍 Linux 网络协议栈,比如《Understanding Linux Network Internals》、《Linux 内核网络: 实现和理论》、《Linux 网络体系结构-Linux 内核中网络协议的设计与实现》、以及张彦飞最近出版的《深入理解 Linux 网络:修炼底层内功,掌握高性能原理》等等。

这里列出了网上的一些资料,肯定不会是最全的,但是可以帮助你更好的理解网络协议栈的处理流程(如果你更多的资料,欢迎在留言中补充),排名部分先后:

  • The Journey of a Packet Through the Linux Network Stack[5]
  • Path of a Packet in the Linux Kernel Stack[6]
  • Linux Kernel Networking[7]
  • A Map of the Networking Code in Linux Kernel 2.4.20[8]
  • Linux Network Stack Walkthrough (2.4.20)[9]
  • 图解 Linux 网络包接收过程[10]
  • Monitoring and Tuning the Linux Networking Stack: Sending Data[11]
  • Monitoring and Tuning the Linux Networking Stack: Receiving Data[12]
  • Linux 网络栈原理、监控与调优:前言(2022)[13]
  • Linux 网络栈接收数据(RX):原理及内核实现(2022)[14]
  • Queueing in the Linux Network Stack[15]
  • Scaling in the Linux Networking Stack[16]
  • Linux Networking Stack tutorial: Receiving Data[17]
  • Linux Networking Stack tutorial: Sending Data[18]
  • Linux network ring buffers[19]
  • Linux Network Performance Ultimate Guide[20]
  • How to receive a million packets per second[21]
  • Network stack[22]
  • Linux IP Networking[23]
  • Linux 网卡数据收发过程分析
  • 浅析 Linux 如何接收网络帧[24]
  • 计算机网络学习笔记(7)—— Linux 网络包收发详解[25]
  • 当 eBPF 遇上 Linux 内核网络[26]
  • 万字图解 | 深入揭秘 Linux 接收网络数据包[27]

    图解分析 Linux 网络包发送过程[28]

    2.1 Linux 网络协议栈处理过程鸟瞰

在上面的资料中,有很多关于 Linux 网络协议栈处理流程的图,这里列出几个典型的图,我们先了解一下 Linux 内核网络协议栈的处理流程,这样我们才能更好的跟踪数据包的处理过程。有些图可能传来换去,也找不到最初的出处了。

下面这张图来源自《# Linux network ring buffers》,它揭示了数据从网卡到生成 skb, 之后就交给协议栈去处理了:

这是上图非常简化的处理过程:

  • 传入数据包到达网络接口卡(NIC)。NIC 通过 DMA 将其保存到内存中的 RX 缓冲区(此时还不涉及 CPU)
  • 当数据已保存在 RAM 中时,NIC 会发送一个硬件中断,告诉 CPU 新来了数据。
  • CPU 非常快速地回复以释放该中断,并发送另外一个信号(软中断信号 NET_RX_SOFTIRQ)给ksoftirqd
  • NIC 可以接收另一个数据包,CPU 现在也可以处理其他中断,同时 ksoftirqd 开始在后台消费 RX 缓冲区的数据,而不会阻塞整个过程。主要思想是应尽快处理硬件中断。软件中断实际处理数据包。

接下来就软中断就是处理 RX 缓冲区的数据包,交给netif_receive_skb_internal函数去处理,它会根据数据包的类型,交给不同的处理函数去处理,比如 ARP、IP、TCP、UDP 等等。那么就可以接上下面这张图(来自李迟 http://www.latelee.org), 不同的处理函数后续的函数调用,最后到了 recv/recvfrom 系统调用到了应用层:

一个简略的全貌如下图所示 (TCP 的处理,来源自 https://myaut.github.io/dtrace-stap-book/kernel/net.html)

3. 跟踪数据包的接收

当然,网上还有一些类似的图。阐述了数据包的处理过程,绘制函数的调用关系,但是 Linux 网络协议栈的处理非常复杂,每个函数中可能都包含一些检查和分支,所以实际上一张图不足以把 Linux 所有的调用关系都描述全。另外 Linux 内核的代码也在不断的重构和优化,所以这些图也可能不是最新的,或者说不能和你使用的 Linux 版本完全一致。

所以我们要掌握的是一种方法,一种跟踪数据包的处理过程的方法,这样我们可以根据我们的需求,实际在我们自己的环境中去跟踪数据包的处理过程,这样我们就可以更好的理解数据包的处理过程,也可以更好的定位问题。

要想去跟踪数据包的处理过程,我们需要使用一些工具,比如 strace、gdb、perf、bpftrace 等等,这些工具可以帮助我们跟踪数据包的处理过程,比如系统调用、数据包的接收、发送、处理、转发等等。

接来下我们尝试使用这些工具,跟踪数据包的接收过程,从网卡接收到数据包任务处理函数的分发,到协议栈的处理、 到应用层的 recv 系统调用,Go 网络包的处理,我们将会跟踪整个过程,了解每一个函数的处理过程。

首先声明,以下方法不是唯一的跟踪数据包的方法(比如也有人使用 SystemTrap),也不一定是最好的方法,只是我最近在分析网络处理中一个可行的的方法。

我最近主要在研究 Raw Socket 的瓶颈,所以我会以 Raw Socket 为例,来跟踪数据包的处理过程,如果你关注普通的 UDP、TCP 数据包的处理过程,也可以使用类似的方法。

我主要将数据收取的过程分成三个部分,不同部分我们使用不同的工具来跟踪,主要通过调用堆栈(Call Stack)来获取函数的调用关系(内核态的处理),或者通过火焰图来查看函数的调用关系(用户态的处理)。

3.1 示例程序

我们使用的程序很简单,一个简单的 Raw Socket 程序,接收数据包, 数据包接收后也不进行处理直接忽略了,代码如下:

package main
import (
 "flag"
 "net"
)
var (
 addr = flag.String("s", "127.0.0.1", "ip address")
)
func main() {
 flag.Parse()
 conn, err := net.ListenPacket("ip4:udp", *addr) // ①
 if err != nil {
  panic(err)
 }
 buf := make([]byte, 65535)
 for {
  conn.ReadFrom(buf) // ②
 }
}

net.ListenPacket("ip4:udp", *addr) 创建了一个 IP 层的 Raw Socket,监听指定的 IP 地址,然后我们循环调用conn.ReadFrom(buf)来接收数据包,但是我们并没有对数据包进行处理,只是简单的接收数据包。

后面我们基于这个程序进行数据包接收的分析,主要分析 ② 相关的函数调用。

另外,说个题外话,① 这个地方 Go 是怎么处理的,最后怎么调用到内核态的函数的,这个作为我们的开胃小菜,先尝试分析下。

利用 perf 不容易分析到这个 Go 函数的调用,因为它就执行了一次,不一定采样能抓取它的调用,所以我们使用

3.2 跟踪 net.ListenPacket 的调用

dlv(Delve)是 Go 语言的一个强大的调试工具,适合用于跟踪代码执行、设置断点、监控变量等。不过第一步我们需要安装它:

go install github.com/go-delve/delve/cmd/dlv@latest

然后把 dlv 加入到 PATH 中方便后续使用,然后我们可以使用 dlv 来调试我们的程序,比如我们可以使用dlv debug来调试我们的程序,比如下面的命令:

dlv debug server.go

然后设置断点:

(dlv) b net.ListenPacket

使用 c 或 continue 命令让程序继续执行,直到命中断点。

在断点处,可以使用以下命令来逐步执行代码:

  • nnext:执行到下一行代码,跳过函数调用。
  • sstep:进入函数调用(例如,进入 net.ListenPacket 的实现代码)。
  • sostepout:执行完当前函数,返回到调用函数。

以下是一步步跟踪的主要函数调用的过程:

> net.ListenPacket() /usr/local/go/src/net/dial.go:869 (PC: 0x516b61)
> net.(*ListenConfig).ListenPacket() /usr/local/go/src/net/dial.go:782 (PC: 0x515df6)
> net.(*sysListener).listenIP() /usr/local/go/src/net/iprawsock_posix.go:138 (PC: 0x52a7b6)
> net.internetSocket() /usr/local/go/src/net/ipsock_posix.go:159 (PC: 0x52ed33)
> net.socket() /usr/local/go/src/net/sock_posix.go:18 (PC: 0x53ba13)
> net.sysSocket() /usr/local/go/src/net/sock_cloexec.go:19 (PC: 0x53b413)
> syscall.Socket() /usr/local/go/src/syscall/syscall_unix.go:499 (PC: 0x4ad6ae)
> syscall.socket() /usr/local/go/src/syscall/zsyscall_linux_amd64.go:1497 (PC: 0x4aec2e)
> syscall.RawSyscall() /usr/local/go/src/syscall/syscall_linux.go:54 (PC: 0x4af2a4)
> syscall.RawSyscall6() /usr/local/go/src/syscall/syscall_linux.go:62 (PC: 0x4af344)
> internal/runtime/syscall.Syscall6() /usr/local/go/src/internal/runtime/syscall/asm_linux_amd64.s:30 (PC: 0x47f1a0)

可以看到最终通过系统调用创建的 Socket, 这个 Socket 主要的三个参数如下:

不过都是数字,翻译过来就是:

  • domain 的值是 2, 代表AF_INET,表示 IPv4 网络协议。
  • type 的值是 526339, 代表SOCK_DGRAM|SOCK_NONBLOCK|SOCK_CLOEXEC,表示数据报套接字。SOCK_NONBLOCK 确保 fd 的 I/O 操作立即返回,适合非阻塞网络 I/O。SOCK_CLOEXEC 确保如果通过 exec 执行新程序时,fd 会自动关闭,防止文件描述符泄露给新进程。
  • protocol 的值是 17, 代表IPPROTO_UDP,表示 UDP 协议。

这是我们跟踪应用层的方法,知道了net.ListenPacket函数的调用过程。

接下来我们看看内核态的处理 Socket 监听的过程。我们使用strace跟踪系统调用。strace 是一个 Linux 系统工具,用于跟踪进程与内核之间的系统调用和信号传递。它可以显示进程如何与操作系统交互,包括文件操作、网络通信、内存管理等。strace 对调试、性能分析、问题排查非常有用,特别是在理解进程的系统层行为时。

你可以执行strace ./server, 然后就可以看到系统调用的过程,不过系统调用比较多, 下图只截取了最后的部分:

你也可以值关注特定的网络调用,比如socketbindlistenaccept等等,比如下面的命令:

可以看到这个程序调用了 socket 系统调用,参数为 AF_INETSOCK_RAW|SOCK_CLOEXEC|SOCK_NONBLOCKIPPROTO_UDP,和我们应用态使用 dlv 跟踪的调用参数是一样的。那必须一样呀。

好了,我们的热身运动已经完成了哈,接下来我们开始跟踪数据接收的过程。

3.2 从系统调用到应用层的处理

如果我们有一点点经验,或者看过上面互联网上分析,比如上面图中的 Linux 内核处理网络数据包的流程,就可以知道我们的应用程序是通过 recv 或者 recvfrom 这样的系统调用读取数据的。

事实上我们使用 strace 时,输出中已经显示了相应的系统调用:

但是,Go 应用程序从这个系统调用调用后的数据接收过程是怎么样子的呢?

我们可以使用 Go 的 cpu profile 去采集和分析,也可以直接使用 perf 工具,这里我们就使用 perf 工具,就不需要修改代码了。

首先我们先安装火焰图工具:

git clone https://github.com/brendangregg/FlameGraph

然后我们使用 perf 工具采集此应用程序的数据,比如下面的命令:

perf record -g -aR -p 75039 -- sleep 60

并生成火焰图:

perf script | ./stackcollapse-perf.pl > out.perf-folded
./flamegraph.pl out.perf-folded > perf.svg

查看这个火焰图,我们从最底层往上找 ReadFrom 函数,然后就可以看到整个函数调用链了,比如下面的火焰图:

我之所以使用 perf 工具而不是 Go 的 cpu profile, 是因为这个火焰图还可以把 Linux 内核中的调用关系采集出来,可以看到到了 Linux 内核之后,就沿着 socket_recvmsg ->inet_recvmsg 这个调用关系走下去,当然 inet_recvmsg 函数中还有多个内核函数的调用和处理:

截止到目前,我们把应用层的读取、或者说从内核 socket buffer 中读取数据包的调用关系我们都通过 perf + flamegraph 展示出来了,下一步我们就跟踪数据包进入网卡,然后协议栈有哪些函数参与处理,数据最后落入到 socket 的 buffer 这一过程跟踪起来,分成两块内容。

3.3 从网卡到协议栈

这一块的跟踪我们可以使用 bpftrace 工具,而且前面的 2.1 节中我们也已经知道,数据交给了 netif_receive_skb_internal 函数后,有不同的协议栈函数去处理,所以我们使用 bpftrace 工具跟踪这个函数,把它的堆栈打印出来,我们就知道它之前的调用关系了。不过它不是一个 tracepoint,不好跟踪,我们跟踪 netif_receive_skb 这个 tracepoint:

bpftrace -e 'tracepoint:net:netif_receive_skb { printf("%sn", kstack); }'
__netif_receive_skb_core.constprop.0+1506
__netif_receive_skb_core.constprop.0+1506
__netif_receive_skb_list_core+253
netif_receive_skb_list_internal+419
napi_complete_done+116
rtl8169_poll+94
__napi_poll+51
net_rx_action+385
handle_softirqs+219
__irq_exit_rcu+217
irq_exit_rcu+14
common_interrupt+164
asm_common_interrupt+39
cpuidle_enter_state+218
cpuidle_enter+46
call_cpuidle+35
cpuidle_idle_call+285
do_idle+135
cpu_startup_entry+42
start_secondary+297
secondary_startup_64_no_verify+388

这是一个完整的调用链,从中断处理开始 asm_common_interrupthandle_softirqs, 到 net_rx_action, 到 napi_complete_done, 到 netif_receive_skb_list_internal,到 __netif_receive_skb_list_core,到 netif_receive_skb

当然你也可以使用 perf 工具,生成火焰图,执行下面的命令:

perf record -e net:netif_receive_skb -g -aR sleep 10
perf script | ./stackcollapse-perf.pl > out.perf-folded
./flamegraph.pl out.perf-folded > perf.svg

额外说一句, 如果你想查看当前 Linux 支持的 tracepoint, 你可以使用命令"cat /proc/kallsyms|grep "netif_receive_skb"或者更好的使用 perf list "net":

基本上,这一段的 Linux 内核调用关系我们都理清楚了。如果更近一步,可以看到这个函数继续调用__netif_receive_skb_core -> deliver_skb, deliver_skb会根据注册的rx_handler把数据交给不同 handler 去处理:

3.4 协议栈的处理

Linux 协议栈中数据处理流程的跟踪我们还是可以使用 perf 工具,但是如何我们跟踪net:*事件,执行的命令如下:

perf record -e net:* -g -aR sleep 60
perf script | ./stackcollapse-perf.pl > out.perf-folded
./flamegraph.pl out.perf-folded > perf.svg

其实并不能得到协议栈的处理过程。

这里来个作弊的方法,你可以跟踪sock_queue_rcv_skb_reason或者sock_queue_rcv_skb,依照你 Linux 版本的不同而不同。不要问我为什么,我用作弊手法知道的。当然你可以通过cat /proc/kallsyms |grep sock_queue_rcv_skb来确定你操作系统中具体的函数。

另外这个 sock_queue_rcv_skb_reason 还不是默认的 probe, 你需要手动开启,比如下面的命令:

用完之后删除:

perf probe --del 'sock_queue_rcv_skb_reason'

增加这个 probe 之后, 你就可以使用 perf record 进行采样了:

perf probe --add 'sock_queue_rcv_skb_reason'
perf record -e 'probe:sock_queue_rcv_skb_reason' -g -aR sleep 60
perf script | ./stackcollapse-perf.pl > out.perf-folded
./flamegraph.pl out.perf-folded > perf.svg

最后生成的火焰图如下:

可以看到从上一节的 netif_receive_skb_list_internal 开始,函数调用进入 ip_list_rcv -> ip_sublist_rcv -> …->raw_local_deliver->raw_v 4_input, 最终进入 sock_queue_rcv_skb_reason。

同样的,如果你的机器能运行 bpftrace,你也可以使用 bpftrace 来跟踪这个函数的调用关系,比如下面的命令:

bpftrace -e 'kprobe:sock_queue_rcv_skb_reason { printf("%sn", kstack); }'
        ...
        sock_queue_rcv_skb_reason+1
        raw_v4_input+498
        raw_local_deliver+60
        ip_protocol_deliver_rcu+85
        ip_local_deliver_finish+119
        ip_local_deliver+110
        ip_sublist_rcv_finish+111
        ip_sublist_rcv+376
        ip_list_rcv+258
        __netif_receive_skb_list_core+557
        netif_receive_skb_list_internal+419
        napi_complete_done+116
        rtl8169_poll+94
        __napi_poll+51
        net_rx_action+385
        handle_softirqs+219
        __irq_exit_rcu+217
        irq_exit_rcu+14
        sysvec_apic_timer_interrupt+64
        asm_sysvec_apic_timer_interrupt+27
        ...

最后,我想说刚才说的作弊的方法,其实是我问 chatgpt, 她告诉我:

参考资料

[1]antirez/smallchat: https://github.com/antirez/smallchat

[2]smallnest/smallchat: https://github.com/smallnest/smallchat

[3]Introduction to RAW-sockets: https://tuprints.ulb.tu-darmstadt.de/6243/1/TR-18.pdf

[4]gopacket: https://github.com/google/gopacket

[5]The Journey of a Packet Through the Linux Network Stack: https://www.cs.dartmouth.edu/~sergey/me/netreads/path-of-packet/Lab9_modified.pdf

[6]Path of a Packet in the Linux Kernel Stack: https://www.cs.dartmouth.edu/~sergey/me/netreads/path-of-packet/Network_stack.pdf

[7]Linux Kernel Networking: https://www.cs.dartmouth.edu/~sergey/me/netreads/path-of-packet/netLec.pdf

[8]A Map of the Networking Code in Linux Kernel 2.4.20: https://www.cs.dartmouth.edu/~sergey/me/netreads/path-of-packet/tr-datatag-2004-1.pdf

[9]Linux Network Stack Walkthrough (2.4.20): https://web.archive.org/web/20080714111103/gicl.cs.drexel.edu/people/sevy/network/Linux_network_stack_walkthrough.html

[10]图解 Linux 网络包接收过程: https://zhuanlan.zhihu.com/p/256428917

[11]Monitoring and Tuning the Linux Networking Stack: Sending Data: https://blog.packagecloud.io/monitoring-tuning-linux-networking-stack-sending-data/

[12]Monitoring and Tuning the Linux Networking Stack: Receiving Data: https://blog.packagecloud.io/monitoring-tuning-linux-networking-stack-receiving-data/

[13]Linux 网络栈原理、监控与调优:前言(2022): https://arthurchiao.art/blog/linux-net-stack-zh/

[14]Linux 网络栈接收数据(RX):原理及内核实现(2022): https://arthurchiao.art/blog/linux-net-stack-implementation-rx-zh/

[15]Queueing in the Linux Network Stack: https://www.coverfire.com/articles/queueing-in-the-linux-network-stack/

[16]Scaling in the Linux Networking Stack: https://docs.kernel.org/networking/scaling.html

[17]Linux Networking Stack tutorial: Receiving Data: https://maxnilz.com/docs/004-network/005-linux-rx/

[18]Linux Networking Stack tutorial: Sending Data: https://maxnilz.com/docs/004-network/006-linux-tx/

[19]Linux network ring buffers: https://tungdam.medium.com/linux-network-ring-buffers-cea7ead0b8e8

[20]Linux Network Performance Ultimate Guide: https://ntk148v.github.io/posts/linux-network-performance-ultimate-guide/

[21]How to receive a million packets per second: https://blog.cloudflare.com/how-to-receive-a-million-packets/

[22]Network stack: https://myaut.github.io/dtrace-stap-book/kernel/net.html

[23]Linux IP Networking: https://www.cs.unh.edu/cnrg/people/gherrin/linux-net.html

[24]浅析 Linux 如何接收网络帧: https://waynerv.com/posts/how-linux-process-input-frames/

[25]计算机网络学习笔记(7)—— Linux 网络包收发详解: https://pandasign.cn/archives/ji-suan-ji-wang-luo-xue-xi-bi-ji-7linuxwang-luo-bao-shou-fa-xiang-jie

[26]当 eBPF 遇上 Linux 内核网络: https://www.eet-china.com/mp/a144670.html

[27]万字图解 | 深入揭秘 Linux 接收网络数据包: https://xie.infoq.cn/article/33e631876d1ff361726b6c257

[28]图解分析 Linux 网络包发送过程: https://blog.csdn.net/weiqifa0/article/details/120878058

1