图解 Linux 内核网络栈

图解 Linux 内核网络栈

基于linux版本v6.12.31

网络模型

提到网络,我们可能会想到TCP/IP网络模型OSI网络模型:前者是目前事实的协议模型,后者是试图成为互联网世界的标准框架的概念模型;

两者都是分层的模型,区别主要在应用层物理层OSI网络模型进行了细分,分别对应应用层/表示层/会话层数据链路层/物理层,中间的传输层网络层是一致的;

网络数据都是自上而下,进行层层封装的,每一层都由上一层数据本层协议的头数据组成,如下图所示:

数据格式

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

后续文中的L2层对应数据链路层L3层对应网络层L4层对应传输层

接下来,我们自上而下逐层进行分析:

L4层(传输层)

传输层,我们听过较多的就是TCP/UDP协议了,TCP的数据单元称作数据段segmentUDP的数据单元称作数据报datagram

由于UDP协议比较简单,本文我们关注UDP协议:

UDP数据报文分为两部分:头数据、来自模型上层的数据;

头数据分为四部分:

  1. 源端口号
  2. 目标端口号
  3. 总长度:头部长度+数据长度
  4. 检验和:检验数据准确性

L3层(网络层)

网络层,被广泛使用的就是IPv4协议了,其数据单元称作数据包packet

IP数据包文分为两部分:头数据、来自模型上层的数据,这里就是UDP协议的数据了;

头数据组成比较多,我们关注如下部分:

  1. 源IP地址
  2. 目标IP地址
  3. 版本:有v4和v6,本文只讨论v4
  4. 总长度:头部长度+数据长度
  5. 协议:上层协议是什么

L2层(数据链路层)

数据链路层,本文我们关注介质访问控制协议(MAC),其数据单元称作数据帧frame

数据帧由如下部分组成:

  1. 目的mac地址
  2. 源mac地址
  3. 类型
  4. Preamble 用于标记帧开始
  5. 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结构体很大,我们省略一些字段,关注目前我们需要的字段:

  1. sk_buff通过双链表组织数据,通过nextprev字段关联前后链表节点;
  2. sk_buff.head指向存储数据的区域,这部分区域又分为两部分:sk_buff.data指向的实际数据(协议头、载荷)、共享信息,具体看下一节的操作图解;
  3. 网络栈逐层传递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. 发送

发送数据按如下步骤执行:

  1. 用户态执行系统调用sendto
  2. 内核态执行相关系统调用,内核网络栈处理数据,放至网卡处Ringbuffer
  3. 网卡将数据发送出去,发送成功后,请求中断
  4. 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里各层协议的协议头:

  1. 传输层 入口: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这个函数会

  1. 调用ip_make_skb函数分配好sk_buff(包含L4, L3层的协议头)且复制用户的数据sk_buff
  2. 调用udp_send_skbudp协议头赋值且通过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,
			       &current->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);
}
  1. 网络层入口:ip_send_skb

网络层对sk_buff基本没有什么操作,都在__ip_make_skb函数里做完了。

  1. 链路层入口: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. 接收

接收数据按如下步骤执行:

  1. 网卡收到数据,请求中断
  2. CPU执行中断处理逻辑,从Ringbuffer中拿出数据
  3. 内核网络栈处理数据
  4. 用户态的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
相关推荐
听风lighting34 分钟前
1. C++ WebServer项目分享
linux·c语言·c++·设计模式·嵌入式·webserver
chengf2231 小时前
WSL 安装使用和常用命令
linux
MALLYUN1 小时前
ssh 服务和 rsync 数据同步
linux·服务器·ssh
we199898981 小时前
Ubuntu最新版本(Ubuntu22.04LTS)安装nfs服务器
linux·服务器·ubuntu
、我是男生。1 小时前
Linux、Ubuntu、虚拟机三者的关系和角色
linux·运维·ubuntu
우 유2 小时前
Linux从入门到入门
linux·运维·服务器
Sally璐璐3 小时前
CentOS查日志
linux·运维·centos
m0_719817114 小时前
Linux运维新人自用笔记(用虚拟机Ubuntu部署lamp环境,搭建WordPress博客)
linux·学习
“αβ”4 小时前
Linux-多线程安全
linux·运维·服务器·ssh·github·线程·进程
我最厉害。,。5 小时前
Webshell篇&魔改哥斯拉&打乱特征指纹&新增后门混淆&过云查杀&过流量识别
linux