图解 Linux 内核网络栈
基于linux版本v6.12.31
网络模型
提到网络,我们可能会想到TCP/IP网络模型
和OSI网络模型
:前者是目前事实的协议模型,后者是试图成为互联网世界的标准框架的概念模型;
两者都是分层
的模型,区别主要在应用层
和物理层
,OSI网络模型
进行了细分,分别对应应用层/表示层/会话层
和数据链路层/物理层
,中间的传输层
和网络层
是一致的;
网络数据都是自上而下
,进行层层封装
的,每一层都由上一层数据
加本层协议的头数据
组成,如下图所示:

数据格式
本文我们主要是对Linux内核网络栈
进行了解,其主要处理数据链路层
、网络层
、传输层
这三层的网络协议数据,所以我们主要分析下这三层网络协议的数据格式。

后续文中的
L2层
对应数据链路层
,L3层
对应网络层
,L4层
对应传输层
。
接下来,我们自上而下逐层进行分析:
L4层(传输层)
在传输层
,我们听过较多的就是TCP/UDP
协议了,TCP
的数据单元称作数据段segment
,UDP
的数据单元称作数据报datagram
。
由于UDP
协议比较简单,本文我们关注UDP
协议:

UDP
数据报文分为两部分:头数据、来自模型上层的数据;
头数据分为四部分:
- 源端口号
- 目标端口号
- 总长度:头部长度+数据长度
- 检验和:检验数据准确性
L3层(网络层)
在网络层
,被广泛使用的就是IPv4
协议了,其数据单元称作数据包packet
:

IP
数据包文分为两部分:头数据、来自模型上层的数据,这里就是UDP
协议的数据了;
头数据组成比较多,我们关注如下部分:
- 源IP地址
- 目标IP地址
- 版本:有v4和v6,本文只讨论v4
- 总长度:头部长度+数据长度
- 协议:上层协议是什么
L2层(数据链路层)
在数据链路层
,本文我们关注介质访问控制协议(MAC)
,其数据单元称作数据帧frame
:

数据帧由如下部分组成:
- 目的mac地址
- 源mac地址
- 类型
- Preamble 用于标记帧开始
- FCS 用于检查数据是否正确
目前我们对数据格式有了大概了解,接下来用一个UDP发送/接收的例子,来分析数据在Linux内核网络栈中的流转:
UDP 例子:发送/接收

接收端 server.c
c
// server.c
// gcc server.c -o server
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <strings.h>
#include <sys/socket.h>
#include <sys/types.h>
int main() {
char *ip = "127.0.0.1";
int port = 1234;
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = inet_addr(ip);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
bind(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
char buffer[1024];
bzero(buffer, 1024);
struct sockaddr_in client_addr;
int addr_size = sizeof(client_addr);
recvfrom(sockfd, buffer, 1024, 0, (struct sockaddr *)&client_addr,
&addr_size);
printf("[+]Data recv: %s\n", buffer);
return 0;
}
发送端 client.c
c
// client.c
// gcc client.c -o client
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
int main(void) {
char *ip = "127.0.0.1";
int port = 1234;
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = inet_addr(ip);
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
char buffer[1024];
bzero(buffer, 1024);
strcpy(buffer, "Hello, World!");
sendto(sockfd, buffer, 1024, 0, (struct sockaddr *)&server_addr, sizeof(server_addr));
printf("[+]Data send: %s\n", buffer);
return 0;
}
编译运行
编译并运行两个程序,查看两个程序的输出:接收端收到了发送端发送的Hello, World!
。
bash
gcc server.c -o server
gcc client.c -o client
# 两个窗口分别运行,查看输出:
./server
./client
# 输出如下:
# client 窗口:[+]Data send: Hello, World!
# server 窗口:[+]Data recv: Hello, World!
本文不对socket做详细介绍,可理解为Linux对网络操作的抽象,不管用户使用什么协议,只需要通过socket操作
我们重点关注sendto
,recvfrom
调用
Linux 网络栈分析

0. 重要结构体 sk_buff
在开始分析Linux网络栈之前,我们先了解一下一个重要的结构体sk_buff,其保存着接收或发送数据的元数据,贯穿Linux网络栈。
c
// include/linux/skbuff.h
struct sk_buff {
// ...
// 双链表
struct sk_buff *next;
struct sk_buff *prev;
struct net_device *dev; // 设备
unsigned int len, // 总长度
data_len; // 数据部分长度
sk_buff_data_t tail;
sk_buff_data_t end;
unsigned char *head,
*data; // 数据
__u16 transport_header; // L4 头指针,相对 head 的偏移量
__u16 network_header; // L3 头指针,相对 head 的偏移量
__u16 mac_header; // L2 头指针,相对 head 的偏移量
// ...
};
sk_buff结构体很大,我们省略一些字段,关注目前我们需要的字段:
sk_buff
通过双链表组织数据,通过next
和prev
字段关联前后链表节点;sk_buff.head
指向存储数据的区域,这部分区域又分为两部分:sk_buff.data
指向的实际数据(协议头、载荷)、共享信息,具体看下一节的操作图解;- 网络栈逐层传递
sk_buff
时,只会添加对应层的协议头部,不会复制数据,具体也看下一节图解;
操作skb_buff.head
指向的数据区域
通过skb_reserve
, skb_put
, sk_push
等函数操作skb_buff.head
指向的数据区域

c
// 1. 通过 alloc_skb 分配 sk_buff
// include/linux/skbuff.h
// alloc_skb -> __alloc_skb
// net/core/skbuff.c
// __alloc_skb -> __build_skb_around -> __finalize_skb_around
static inline void __finalize_skb_around(struct sk_buff *skb, void *data,
unsigned int size)
{
// ...
skb->head = data;
skb->data = data;
// ...
}
// 2. 通过 skb_reserve 分配头部空间
// include/linux/skbuff.h
static inline void skb_reserve(struct sk_buff *skb, int len)
{
skb->data += len;
skb->tail += len;
}
// 3. 通过 skb_put 分配数据空间
// net/core/skbuff.c
void *skb_put(struct sk_buff *skb, unsigned int len)
{
// ...
skb->tail += len;
skb->len += len;
// ...
}
// 4. 通过 skb_push 往数据空间前追加数据(使用头部空间)
// 通常用于追加协议头部数据
// net/core/skbuff.c
void *skb_push(struct sk_buff *skb, unsigned int len)
{
skb->data -= len; // NOTE: 指针上移,使用头部空间
skb->len += len;
// ...
}
// 5. skb_pull 和 skb_push 效果相反
// 通常用于将 data 指针指向某个协议的数据(不包含协议头)
// include/linux/skbuff.h
// skb_pull -> __skb_pull
static inline void *__skb_pull(struct sk_buff *skb, unsigned int len)
{
skb->len -= len;
return skb->data += len;
}
添加头部

这里用L2层的以太网协议头做个例子,L3层和L4层的操作类似,都是对这几个字段进行操作:
c
// net/core/skbuff.c
int skb_eth_push(struct sk_buff *skb, const unsigned char *dst,
const unsigned char *src)
{
struct ethhdr *eth;
// ...
skb_push(skb, sizeof(*eth)); // NOTE: 追加头大小的数据空间
skb_reset_mac_header(skb); // 设置头,相对head的偏移量
skb_reset_mac_len(skb); // 设置头长度
eth = eth_hdr(skb); // 获取协议头
ether_addr_copy(eth->h_dest, dst); // 设置目的mac地址,函数的第2个参数
ether_addr_copy(eth->h_source, src); // 设置源mac地址,函数的第3个参数
eth->h_proto = skb->protocol; // 设置协议
// ..
}
static inline void skb_reset_mac_header(struct sk_buff *skb)
{
// NOTE: 保存着相对head的偏移量
skb->mac_header = skb->data - skb->head;
}
1. 发送
发送数据按如下步骤执行:
- 用户态执行
系统调用sendto
- 内核态执行相关系统调用,
内核网络栈处理数据
,放至网卡处Ringbuffer - 网卡将数据发送出去,发送成功后,
请求中断
- CPU执行
中断处理逻辑
,清除Ringbuffer
函数调用大概如下,稍微过一下即可:
c
// udp sendto
// 1. 系统调用层
// net/socket.c
sendto -> __sys_sendto -> __sock_sendmsg -> sock_sendmsg_nosec -> inet_sendmsg
// net/ipv4/af_inet.c
inet_sendmsg -> udp_sendmsg
// 2. 传输层
// net/ipv4/udp.c
udp_sendmsg -> udp_send_skb -> ip_send_skb
// 3. 网络层
// net/ipv4/ip_output.c
ip_send_skb -> ip_local_out -> __ip_local_out -> dst_output(路由)
// 3.1 路由(网络层数据包转发)
// include/net/dst.h
dst_output -> output -> ip_output
// net/ipv4/ip_output.c
ip_output -> ip_finish_output -> __ip_finish_output -> ip_finish_output2 -> neigh_output
// 4. 链路层(邻居协议,比如ARP协议,找到mac地址)
// 邻居定义:同一以局域网且在同一子网下,或L3层一跳可达
// include/net/neighbour.h
neigh_output -> neigh_hh_output -> dev_queue_xmit(进入网络设备子系统)
// 5. 网络设备层:管理网络设备
// include/linux/netdevice.h
dev_queue_xmit -> __dev_queue_xmit
// net/core/dev.c
__dev_queue_xmit -> __dev_xmit_skb -> sch_direct_xmit -> dev_hard_start_xmit -> xmit_one -> netdev_start_xmit -> __netdev_start_xmit
// include/linux/netdevice.h
__netdev_start_xmit -> ndo_start_xmit
// 5. 驱动层:以igb为例
// drivers/net/ethernet/intel/igb/igb_main.c
igb_xmit_frame(注册到ndo_start_xmit) -> igb_xmit_frame_ring -> igb_tx_map(到设备)
// DMA
igb_tx_map -> dma_map_single
// NOTE: 这样子就把数据放到网卡了,网卡会通过**中断**告知数据发送成功
// 1. 硬中断
// drivers/net/ethernet/intel/igb/igb_main.c
igb_msix_ring -> napi_schedule
// include/linux/netdevice.h
napi_schedule -> __napi_schedule
// net/core/dev.c
__napi_schedule -> ____napi_schedule -> raise_softirq_irqoff
// 2. 软中断:清理资源
// net/core/dev.c
__napi_poll(n->poll,注册的igb_poll)
// drivers/net/ethernet/intel/igb/igb_main.c
igb_poll -> igb_clean_tx_irq
我们主要分析 是如何处理sk_buff
里各层协议的协议头:
- 传输层 入口:udp_sendmsg
c
// net/ipv4/udp.c
int udp_sendmsg(struct sock *sk, struct msghdr *msg, size_t len)
{
// ...
struct sk_buff *skb;
// ...
// NOTE: 注意这个 getfrag 函数,是从用户层复制数据的
getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag;
// ...
// NOTE: 这里会在sk_buff内分配好 IP/UDP 协议的头部,后续直接拿就可以赋值
// sizeof(struct udphdr) 表示 udp 协议头的长度,后续的transhdrlen字段就是这个值
skb = ip_make_skb(sk, fl4, getfrag, msg, ulen,
sizeof(struct udphdr), &ipc, &rt,
&cork, msg->msg_flags);
// ...
// 发送 skb
err = udp_send_skb(skb, fl4, &cork);
// ...
}
udp_sendmsg
这个函数会
- 调用
ip_make_skb
函数分配好sk_buff
(包含L4, L3层的协议头)且复制用户的数据
到sk_buff
- 调用
udp_send_skb
给udp协议头
赋值且通过ip_send_skb
把skb发往下一层
c
// 1.分配 sk_buff,复制用户数据
// net/ipv4/ip_output.c
struct sk_buff *ip_make_skb(struct sock *sk,
struct flowi4 *fl4,
int getfrag(void *from, char *to, int offset,
int len, int odd, struct sk_buff *skb),
void *from, int length, int transhdrlen,
struct ipcm_cookie *ipc, struct rtable **rtp,
struct inet_cork *cork, unsigned int flags)
{
// 管理 sk_buff 双链表
struct sk_buff_head queue;
__skb_queue_head_init(&queue);
// ...
// NOTE: 具体分配sk_buffer的地方
err = __ip_append_data(sk, fl4, &queue, cork,
¤t->task_frag, getfrag,
from, length, transhdrlen, flags);
// 给IP协议头赋值
return __ip_make_skb(sk, fl4, &queue, cork);
}
// 具体分配sk_buffer
static int __ip_append_data(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork,
struct page_frag *pfrag,
int getfrag(void *from, char *to, int offset,
int len, int odd, struct sk_buff *skb),
void *from, int length, int transhdrlen,
unsigned int flags)
{
// 拿到队列尾部的skb
skb = skb_peek_tail(queue);
// 分配 sk_buff
if (!skb)
goto alloc_new_skb;
//...
alloc_new_skb:
//...
// 分配新 sk_buff
skb = sock_alloc_send_skb(sk, alloclen,
(flags & MSG_DONTWAIT), &err);
//...
// ip 协议头长度
fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);
//...
// sk_buff 放入数据的地方,设置了网络层头、传输层头
// L3层链路层的协议头,在后续处理
data = skb_put(skb, fraglen + exthdrlen - pagedlen);
skb_set_network_header(skb, exthdrlen);
skb->transport_header = (skb->network_header +
fragheaderlen);
// ...
// 从 from 复制copy数量的数据到 data+transhdrlen
// transhdrlen 是在调用ip_make_skb是计算的udp协议头大小
// 也就是说把用户数据复制到udp数据报的数据部分
if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {
// ...
// NOTE: 将 sk_buff 放入队列
__skb_queue_tail(queue, skb);
// ...
}
// 设置ip协议头
struct sk_buff *__ip_make_skb(struct sock *sk,
struct flowi4 *fl4,
struct sk_buff_head *queue,
struct inet_cork *cork)
{
// ...
skb = __skb_dequeue(queue); // NOTE: 从队列中获取一个skb
//...
// NOTE: 设置 IP协议头:版本、源IP地址、目标IP地址
iph = ip_hdr(skb);
iph->version = 4;
//...
ip_copy_addrs(iph, fl4); // NOTE: 设置ip地址
//...
}
// 2. 给协议头复制,发往下一层
// net/ipv4/udp.c
// NOTE: 这里获取了skb 的 UDP 协议头,进行赋值
static int udp_send_skb(struct sk_buff *skb, struct flowi4 *fl4,
struct inet_cork *cork)
{
struct udphdr *uh;
uh = udp_hdr(skb);
// NOTE 设置udp协议头:源端口、目标端口
uh->source = inet->inet_sport;
uh->dest = fl4->fl4_dport;
// 往下一层发送 skb
err = ip_send_skb(sock_net(sk), skb);
}
- 网络层入口:ip_send_skb
网络层对sk_buff
基本没有什么操作,都在__ip_make_skb
函数里做完了。
- 链路层入口:neigh_output
c
// include/net/neighbour.h
static inline int neigh_output(struct neighbour *n, struct sk_buff *skb,
bool skip_cache)
{
// ...
return neigh_hh_output(hh, skb); // NOTE: 使用缓存的硬件地址发送,也就是mac地址,hh里包含协议头
// ...
}
static inline int neigh_hh_output(const struct hh_cache *hh, struct sk_buff *skb)
{
// ...
hh_len = READ_ONCE(hh->hh_len); // NOTE: 获取头长度
memcpy(skb->data - HH_DATA_MOD, hh->hh_data,
HH_DATA_MOD); // NOTE: skb->data 指针上移,放置 hh->hh_data 协议头
// ...
return dev_queue_xmit(skb); // NOTE: 发往网络设备子系统处理
}
2. 接收
接收数据按如下步骤执行:
- 网卡收到数据,
请求中断
- CPU执行
中断处理逻辑
,从Ringbuffer中拿出数据 内核网络栈处理数据
- 用户态的
recvfrom
调用获得数据
函数调用大概如下,稍微过一下即可:
c
1. 软中断
// net/core/dev.c
__napi_poll(n->poll,网卡初始化时注册的igb_poll)
// drivers/net/ethernet/intel/igb/igb_main.c
igb_poll -> igb_clean_rx_irq -> napi_gro_receive(skb 结构体已包含可用的 L2 数据)
2. gro: Generic Receive Offloading
// net/core/gro.c
napi_gro_receive -> napi_skb_finish -> gro_normal_one
// include/net/gro.h
gro_normal_one -> gro_normal_list -> netif_receive_skb_list_internal
// net/core/dev.c
netif_receive_skb_list_internal -> __netif_receive_skb_list -> __netif_receive_skb_list_core -> __netif_receive_skb_core
-> deliver_ptype_list_skb -> deliver_skb(pt_prev->func指向ip_rcv,通过dev_add_pack注册ip_packet_type)
// NOTE: 再进行Linux网络栈处理
// 1. IP 网络层
// net/ipv4/ip_input.c
ip_rcv -> ip_rcv_finish -> dst_input
// 1.1 路由
// include/net/dst.h
dst_input -> ip_local_deliver
// net/ipv4/ip_input.c
ip_local_deliver -> ip_local_deliver_finish -> ip_protocol_deliver_rcu -> udp_rcv
// 2. 传输层
// net/ipv4/udp.c
udp_rcv -> __udp4_lib_rcv -> udp_unicast_rcv_skb -> udp_queue_rcv_skb -> udp_queue_rcv_one_skb -> __udp_queue_rcv_skb
-> __udp_enqueue_schedule_skb -k __skb_queue_tail -> sk_data_ready/sock_def_readable
// 3. 用户/系统调用层
// net/core/sock.c
sock_def_readable -> sk_wake_async_rcu
// include/net/sock.h
sk_wake_async_rcu -> sock_wake_async
从sk_buff
中拿出各层协议头,可以通过对应协议_hdr
方法获取
c
// include/linux/ip.h
static inline struct iphdr *ip_hdr(const struct sk_buff *skb)
{
return (struct iphdr *)skb_network_header(skb);
}
// include/linux/udp.h
static inline struct udphdr *udp_hdr(const struct sk_buff *skb)
{
return (struct udphdr *)skb_transport_header(skb);
}
// include/linux/skbuff.h
static inline unsigned char *skb_mac_header(const struct sk_buff *skb)
{
DEBUG_NET_WARN_ON_ONCE(!skb_mac_header_was_set(skb));
return skb->head + skb->mac_header;
}
c
// net/ipv4/udp.c
int __udp_enqueue_schedule_skb(struct sock *sk, struct sk_buff *skb)
{
// socket的接收队列
struct sk_buff_head *list = &sk->sk_receive_queue;
// ...
// 放入队尾
__skb_queue_tail(list, skb);
// ...
// 通知数据准备好了
if (!sock_flag(sk, SOCK_DEAD))
INDIRECT_CALL_1(sk->sk_data_ready, sock_def_readable, sk);
// ...
}
Linux 关于网络的下一些工具
bash
# apt install net-tools iproute2
# L4
## 检查端口是不是open,UDP 加上 -u
nc -zv IP PORT
# L3
## 获取当前设备网络接口地址
ip -br addr
## 发送 ICMP 协议包,看地址是否可达
ping IP
# 路由表:决定包转发到哪里
route
ip -br route
# L2
# 当前网络接口配置、状态等
ip -br link
## arp表:ip 和 mac 地址之间的映射
arp -a
# 查看网络设备驱动
lspci -v | grep -A8 Ethernet