openEuler 网络调优之重新认识 Linux 本机网络【华为开发者空间】
写在前面
- 博文涉及 openEuler 跨主机网络IO认知以及本机网络IO分析
- 实验环境使用
华为云开发者空间的云主机,做实验很方便 - 没有调优相关的Demo
- 理解不足小伙伴帮忙指正 :),生活加油
每个人都想成为生活中的重要的人物。事实是,不论他们多么重要,总会出现更重要的人,人们很计较这件事。他们没有意识到,别人是否看重你,根本不重要。重要的是自信,一旦有了自信,人就会赢得一切。—塞缪尔·克罗瑟斯
一、跨主机网络IO认知
在传统的跨主机网络IO中,本机发包和收包的流程是这样的:
发包流程
发包:
- 用户态应用程序(用户态程序发生数据,触发send 系统调用)
- 内核态网络协议栈(拷贝数据到内核态,经过TCP/IP 协议栈处理,TCP封包,IP路由选择获取吓一跳地址,邻居子系统ARP获取MAC地址,网络设备子系统调用网卡驱动进⼊RingBuffer)
- 内核态网络设备驱动(内核态调用驱动对应的函数发送数据包,CPU时间片内没有发送完触发软中断)
- 网卡(数据发送完触发硬中断,清空RingBuffer缓冲区)
1. 用户态应用程序
├─ 核心操作:调用 `send()` 系统调用,发起数据发送请求
├─ 数据位置:用户态缓冲区(如应用程序的 char[] 数组)
└─ 触发动作:从用户态陷入内核态,将数据交给内核协议栈
↓
2. 内核态网络协议栈
├─ 第一步:数据拷贝(用户态 → 内核态)
│ └─ 将用户态缓冲区数据拷贝到内核的 Socket 发送缓冲区(`sk_buff` 结构体)
├─ 第二步:TCP 层处理
│ ├─ 封装 TCP 头部(源端口、目标端口、序列号、确认号、标志位等)
│ └─ 处理流量控制(滑动窗口)、拥塞控制(如 BBR/CUBIC)
├─ 第三步:IP 层处理
│ ├─ 封装 IP 头部(源 IP、目标 IP、TTL、协议类型等)
│ ├─ 路由选择:调用 `ip_route_output_ports` 查找路由表(优先查 local 表,再查 main 表)
│ └─ 确定下一跳 IP(跨网段则为网关,本机则为回环设备)
├─ 第四步:邻居子系统处理
│ ├─ 核心操作:调用 ARP 协议,根据下一跳 IP 获取目标 MAC 地址
│ └─ 缓存结果:将 IP-MAC 映射存入 ARP 缓存表,避免重复查询
├─ 第五步:网络设备子系统处理
│ └─ 调用 `dev_queue_xmit`,将 `sk_buff` 交给对应网卡的发送队列
↓
3. 内核态网络设备驱动
├─ 核心操作1:调用驱动发送函数(如 igb 网卡的 `igb_xmit_frame`)
│ └─ 将 `sk_buff` 挂载到网卡的 RingBuffer(环形缓冲区)
├─ 核心操作2:DMA 映射
│ └─ 调用 `dma_map_single`,将内核虚拟地址转为网卡可访问的物理地址
├─ 特殊场景:若 CPU 时间片内未发送完
│ └─ 触发 `NET_TX_SOFTIRQ` 软中断,后续由软中断线程继续发送
↓
4. 网卡(硬件层)
├─ 核心操作1:读取 RingBuffer 数据
│ └─ 通过 DMA 直接读取物理地址数据,无需 CPU 参与
├─ 核心操作2:发送完成通知
│ └─ 数据发送完毕后,触发 **硬中断**(IRQ),告知内核发送完成
└─ 收尾操作:内核触发软中断清空 RingBuffer 中已发送的数据包缓存
收包流程
收包:
- 网卡(数据写入到网卡缓冲区DMA,触发硬中断通知CPU)
- 内核态网络设备驱动(CPU触发软中断,软中断处理线程收取数据包从网卡缓冲区DMA拷贝到内核态)
- 内核态网络协议栈(内核态协议栈解析数据包拷贝到用户态,唤醒用户进程)
- 用户态应用程序(触发recv 系统调用,获取数据,)
1. 网卡(硬件层)
├─ 核心操作1:接收数据帧
│ └─ 从网线接收以太网帧,校验帧头(如 CRC 校验),过滤无效帧
├─ 核心操作2:DMA 写入
│ └─ 通过 DMA 直接将数据写入内核的 RingBuffer(避免 CPU 拷贝)
└─ 核心操作3:触发中断
└─ 发送 **硬中断**(IRQ),通知 CPU 有新数据待处理
↓
2. 内核态网络设备驱动
├─ 第一步:硬中断处理(快速响应)
│ ├─ 禁用硬中断(避免重复触发)
│ └─ 触发 `NET_RX_SOFTIRQ` 软中断,将复杂处理交给软中断线程
├─ 第二步:软中断处理(核心逻辑)
│ ├─ 调用驱动 `poll` 函数(如 igb 网卡的 `igb_poll`)
│ ├─ 从 RingBuffer 读取数据包,封装为 `sk_buff`
│ └─ 调用 `dma_unmap_single`,释放 DMA 映射
└─ 第三步:交给协议栈
└─ 调用 `netif_rx`,将 `sk_buff` 送入内核网络协议栈
↓
3. 内核态网络协议栈
├─ 第一步:链路层处理
│ └─ 解析以太网帧头,剥离 MAC 地址,判断上层协议(如 IP 协议)
├─ 第二步:IP 层处理
│ ├─ 解析 IP 头部,校验 IP 地址(目标 IP 是否为本机)
│ ├─ 若有分片,重组数据包;若无分片,交给传输层
│ └─ 根据协议类型(如 TCP/UDP),将数据转发到对应传输层
├─ 第三步:TCP 层处理
│ ├─ 解析 TCP 头部,校验序列号、确认号,处理重传、滑动窗口
│ └─ 将数据存入 Socket 接收缓冲区(内核态)
├─ 第四步:唤醒用户进程
│ └─ 若用户进程已调用 `recv()` 阻塞等待,内核通过信号唤醒进程
└─ 第五步:数据拷贝(内核态 → 用户态)
└─ 将 Socket 接收缓冲区的数据,拷贝到用户态缓冲区(如应用程序的 char[])
↓
4. 用户态应用程序
├─ 核心操作:调用 `recv()` 系统调用
├─ 数据获取:从用户态缓冲区读取内核拷贝过来的数据
└─ 后续处理:应用程序解析数据(如业务逻辑处理)
二、本机网络 IO 分析
在本机网络IO中:
首先明确一个核心结论:所有本机网络IO都不会经过物理网卡,而是通过Linux内核的回环设备(lo接口)完成。回环设备是一个纯软件实现的虚拟网卡,其MTU默认高达65535(远大于物理网卡的1500),专门用于本机内进程间的网络通信。
[root@developer ~]# ip addr show lo
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
[root@developer ~]#
与跨机网络IO相比,本机网络IO的关键差异集中在两处:
- 路由选择:优先查询
local路由表,直接匹配回环设备; - 驱动与硬件交互:无需物理网卡驱动,回环设备"驱动"仅做软件层面的数据包转发。
本机网络IO核心流程
下面我们依次来看一下,下面为本机物理网啦的配置信息
[root@developer ~]# ip a show enp3s0
2: enp3s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc tbf state UP group default qlen 1000
link/ether fa:16:3e:11:d1:b1 brd ff:ff:ff:ff:ff:ff
inet 10.4.196.164/20 brd 10.4.207.255 scope global dynamic noprefixroute enp3s0
valid_lft 107999738sec preferred_lft 107999738sec
inet6 fe80::f816:3eff:fe11:d1b1/64 scope link noprefixroute
valid_lft forever preferred_lft forever
[root@developer ~]#
第一步:网络层路由选择:优先匹配local路由表,绑定回环设备
当用户进程发起本机网络请求(如curl 127.0.0.1:8080或curl 10.4.196.164:8080),也就是发包的时候,数据包进入内核协议栈后,首先在网络层完成路由选择,这是决定"是否走回环设备"的关键步骤。
网络层的核心入口函数是ip_queue_xmit,其核心逻辑是"先查缓存路由,无缓存则重新查找
路由查找最终会进入fib_lookup函数,该函数会优先查询local路由表(本地路由表,处理本机内部流量),查询命中则终止,不进入main表(认路由表,处理跨网段 / 外网流量)
通过ip route list table local命令,可查看local路由表的内容(以本机IP为10.4.196.164为例)
[root@developer ~]# ip route list table local
local 10.4.196.164 dev enp3s0 proto kernel scope host src 10.4.196.164
broadcast 10.4.207.255 dev enp3s0 proto kernel scope link src 10.4.196.164
local 127.0.0.0/8 dev lo proto kernel scope host src 127.0.0.1
local 127.0.0.1 dev lo proto kernel scope host src 127.0.0.1
broadcast 127.255.255.255 dev lo proto kernel scope link src 127.0.0.1
local 172.17.0.1 dev docker0 proto kernel scope host src 172.17.0.1
broadcast 172.17.255.255 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
[root@developer ~]#
可以看到上面的路由表中的路由条目都是通过内核自动添加的,内核在初始化IP时,会通过fib_inetaddr_event函数将所有本机IP添加到local路由表,并标记路由类型为RTN_LOCAL(这里很关键)。
其中127.0.0.0/8和127.0.0.1是本机IP,172.17.0.1是Docker容器网段,10.4.196.164是本机物理网卡IP。并且 本机IP 在local路由表中都绑定了lo回环设备,本机物理网卡IP 绑定了 物理网卡, docker 容器相关的IP绑定了docker0设备。这里可能会有小伙伴说如果访问的是物理网卡的IP,是走物理网卡,因为绑定了物理网卡,后面会说明
第一条路由意思访问本机的物理网卡 IP 10.4.196.164 时,虽然绑定了物理网卡,但是走本机内部路由(scope host),不经过物理网卡,直接走回环设备(lo),并且源地址也用本机物理网卡IP 10.4.196.164 发送。
第三/四条为回环设备的路由,访问 127.0.0.0/8 网段(包含 127.0.0.1 ~ 127.255.255.254)的所有流量,都走回环设备 lo,完全在本机内部流转,源地址也用 127.0.0.1 发送。
剩下的一条是 Docker 容器网段的路由,还有一些广播地址的路由。回环网段的广播地址,发送到该地址的广播包,仅在本机内部广播(不会出本机)。10.4.192.0/20 网段的广播地址,发送到该地址的广播包(如 ARP 请求),通过 enp3s0 网卡在当前网段内广播。
这里我们顺便把 main 表的路由配置也看一下
[root@developer ~]# ip route list table main
default via 10.4.192.1 dev enp3s0 proto dhcp metric 100
10.4.192.0/20 dev enp3s0 proto kernel scope link src 10.4.196.164 metric 100
169.254.169.254 via 10.4.207.254 dev enp3s0 proto dhcp metric 100
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown
main 表是 Linux 系统的默认路由表,负责处理 “非本机内部” 的流量(如访问其他网段、外网),上面输出包含 4 条路由:
第一条是默认路由,所有 “找不到匹配网段” 的流量(如访问百度、其他公司的服务器),都会走这条路由:
default via 10.4.192.1:目标 IP 不在任何已知网段时,将流量转发给网关 10.4.192.1(网关是连接当前网段和其他网段的 “桥梁”);dev enp3s0:通过物理网卡 enp3s0 发往网关;proto dhcp:这条默认路由是通过 DHCP 自动获取的(不是手动配置的);metric 100:路由优先级为 100(若有其他默认路由,metric 更小的会被优先使用)
第二条是当前网段的路由,访问 10.4.192.0/20 网段内的设备时,直接通过物理网卡 enp3s0 通信,无需经过网关,
10.4.192.0/20:目标网段(/20 表示子网掩码是 255.255.240.0,该网段包含的 IP 范围是 10.4.192.1 ~ 10.4.207.254);scope link:作用范围是 “当前链路(网段)”,即该网段内的设备在同一物理网络,直接通过二层(MAC 地址)通信;src 10.4.196.164:访问该网段时,源 IP 用 10.4.196.164(这是 enp3s0 网卡的 IP);proto kernel:这条路由是内核自动添加的(当 enp3s0 获取到 IP 后,内核会自动生成当前网段的路由)。
第三条访问特定 IP 169.254.169.254 的流量,需通过网关 10.4.207.254 转发。并且也是DHCP自动生成,一般有一些用户态隧道的服务器会用到
第四条是 Docker 容器网段的路由,但当前 docker0 网卡(Docker 默认的网桥)处于链路断开状态,无法访问该网段。
- 172.17.0.0/16:Docker 默认的容器网段(所有 Docker 容器的 IP 通常在这个网段内,如 172.17.0.2、172.17.0.3);
- dev docker0:访问容器时,通过 Docker 网桥 docker0 通信(docker0 是 Linux 虚拟网桥,连接主机和容器);
- src 172.17.0.1:主机访问容器时,源 IP 用 172.17.0.1(这是 docker0 网桥的 IP);
- linkdown:关键状态 —— 当前 docker0 网桥未启用(可能是 Docker 服务未启动,或网桥被手动禁用),因此无法访问容器网段。
[root@developer ~]# ip addr show docker0
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:b6:ee:db:49 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
[root@developer ~]#
关键误区澄清:
如果访问 127 开头的网段,可以看到直接绑定的回环设备,直接回环设备本机网络,但是对于物理网卡的IP,在网络层路由处理中会强制使用回环设备
当fib_lookup查询到RTN_LOCAL类型的路由后,会进入net/ipv4/route.c的ip_route_output_key函数,明确将输出设备设置为回环设备:
struct rtable *ip_route_output_key(struct net *net, struct flowi4 *fl4) {
struct fib_result res;
if (fib_lookup(net, fl4, &res) == 0) {
if (res.type == RTN_LOCAL) {
dev_out = net->loopback_dev; // 强制使用回环设备
rth = mkroute_output(&res, fl4, orig_oif, dev_out, flags);
return rth;
}
}
// ...
}
所以访问本机IP(如10.4.196.164)并不会经过物理网卡(如enp3s0),而是和127.0.0.1一样,通过lo设备通信——可通过tcpdump -i enp3s0 port 8888验证:即使发起telnet 10.4.196.164 8888,enp3s0上也抓不到任何数据包;而tcpdump -i lo port 8888能清晰看到TCP握手包。所以即使网卡down,本机网络IO仍能正常工作。
同样我们也可以解释127.0.0.1和本机IP速度的一致性,两者的路由流程、设备选择、驱动处理完全一致,唯一差异是127.0.0.1是回环设备的默认IP,而本机IP是物理网卡IP,但最终都走lo设备——性能无任何区别。
第二步:网络设备子系统:回环设备无队列,直接进入驱动
路由选择完成后,数据包进入网络设备子系统,入口函数是net/core/dev.c的dev_queue_xmit。与物理网卡不同,回环设备的处理逻辑极简化:无发送队列,直接调用dev_hard_start_xmit进入驱动。
对于物理网卡(如Intel igb),dev_queue_xmit会经历"选择队列→skb入队→出队发送"的复杂流程,甚至可能触发软中断;但回环设备的q->enqueue为false(无队列),直接跳过队列逻辑
int dev_queue_xmit(struct sk_buff *skb) {
struct netdev_queue *txq = netdev_pick_tx(dev, skb, NULL);
struct Qdisc *q = rcu_dereference_bh(txq->qdisc);
if (q->enqueue) {
// 物理网卡:入队并调度发送(_dev_xmit_skb)
rc = _dev_xmit_skb(skb, q, dev, txq);
goto out;
} else {
// 回环设备:无队列,直接调用dev_hard_start_xmit
if (dev->flags & IFF_UP) {
dev_hard_start_xmit(skb, dev, txq);
}
// ...
}
}
调用回环设备"驱动",loopback_xmit函数,这里的"驱动"之所以加引号,是因为它没有任何物理硬件交互——纯软件逻辑,核心作用是"将发送的数据包直接转发到接收队列"。
第三步:回环驱动处理——跳过硬件,直接触发接收软中断
loopback_xmit是本机网络IO的"转折点":它不做任何硬件操作,而是将发送的skb(数据包缓存)重新注入到内核的接收队列,并触发软中断,相当于"自己发、自己收"。
loopback_xmit把数据包注入接收队列,触发接收软中断,这是本机网络IO与跨机IO的另一个关键差异,跨机IO需要"物理网卡硬中断→软中断"的触发链,本机IO直接跳过硬中断,由loopback_xmit主动触发软中断。
第四步:接收软中断处理—与跨机IO流程统
软中断触发后,内核会执行NET_RX_SOFTIRQ对应的处理函数net_rx_action,后续流程与跨机网络IO的接收逻辑完全一致:
net_rx_action从input_pkt_queue取出skb;- 调用
__netif_receive_skb将skb送入网络协议栈; - 网络层(
ip_rcv)、传输层(tcp_v4_rcv/udp_rcv)依次处理,最终将数据放入目标socket的接收缓冲区; - 唤醒等待数据的用户进程(如
recv/read阻塞的进程)。
本机网络IO完整流程总结
1. 用户进程(send/recv)
↓
2. 系统调用(如sendto)
↓
3. 传输层(TCP/UDP封装,生成skb)
↓
4. 网络层(ip_queue_xmit)
├─ 查local路由表→命中RTN_LOCAL
├─ 绑定回环设备(lo)
└─ IP封装、Netfilter过滤
↓
5. 网络设备子系统(dev_queue_xmit)
├─ 回环设备无队列→直接调用dev_hard_start_xmit
└─ 物理网卡需经过队列调度(对比差异)
↓
6. 回环驱动(loopback_xmit)
├─ 剥离skb与原socket关联
├─ 注入接收队列(input_pkt_queue)
└─ 触发NET_RX_SOFTIRQ软中断(无硬中断,对比差异)
↓
7. 软中断处理(net_rx_action)
├─ 从接收队列取出skb
└─ 送入协议栈(ip_rcv→tcp_v4_rcv)
↓
8. 传输层(将数据放入socket接收缓冲区)
↓
9. 唤醒用户进程(如epoll_wait/poll返回)
↓
10. 用户进程读取数据(recv/read)
本机和跨机网络IO对比
本机网络 IO 并非 “零开销”。即使通过 lo 回环设备,数据仍需经历完整的协议栈处理,相比的跨主机IO对于发包来说,省略了:
- 网络层的IP路由寻址,链路数据包传输
- 不需要经过发送队列调度,物理网卡和DMA的数据拷贝
- 也不需要发包软中断触发,以及数据包处理的完缓冲区清理的硬中断触发
但是内核态整体的系统调用开销基本都在,还是需要用户态和内核态的切换,以及协议栈的处理。需要三次数据拷贝(用户态到内核态,内核态ACK浅拷贝,以及大包切片)
对于收包来说:
- 不需要硬中断处理,直接触发软中断
- 不需要物理网卡的DMA交互、RingBuffer队列
但协议栈开销并未减少:TCP/UDP协议处理、内核态Socket缓冲区拷贝、Netfilter过滤等步骤一个不少。
本机网络IO相比于跨主机网络IO有很大的性能优势,但是不能滥用,上面这些环节会带来显著开销,每次 send/recv 都会触发用户态与内核态切换,系统调用开销同时伴随着数据拷贝,协议栈处理中TCP 的序列号校验、滑动窗口管理,IP的大数据包切片等逻辑都是内核态CPU开销。同时高频本地通信会导致 NET_RX_SOFTIRQ 持续占用 CPU。
对于本机网络IO的性能优化,常用的解决方案绕过 Linux 内核协议栈:
DPDK(用户态旁路):通过 UIO/VFIO 驱动将网卡映射到用户空间,应用程序通过轮询模式驱动(PMD)直接操作网卡,数据包直达用户态,处理过程完全无中断、无系统调用。DP(eBPF 内核态旁路):数据包从网卡进入后,在分配 SKB(内核数据包结构)之前,立即被 XDP 程序处理。程序可以决定是丢弃、转发、重定向还是将数据包送上内核协议栈。类比抓包工具,在协议栈之前某些系统调用埋点,捕获流量。这也是为什么抓包工具可以捕获被防火墙拦截的流量。因为 Netfilter/iptables过滤 在协议栈解析的时候。 K8s 中 Istio Pod 中的边车(Envoy 代理)也是类似的思路,在协议栈之前利用eBPF拦截流量,进行安全策略检查、负载均衡等。
博文部分内容参考
© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)
https://blog.csdn.net/wll1228/article/details/121311707
《深入理解Linux网络: 修炼底层内功,掌握高性能原理 (张彦飞)》
© 2018-至今 , 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)
- 点赞
- 收藏
- 关注作者
评论(0)