Linux 内核之旅(八):内核数据包接收

基础知识汇总

Posted by pandaychen on March 2, 2025

0x00 前言

笔者最近在研究基于ebpf的网络协议栈可观测及tracing,本文对协议栈的数据处理基础做了若干总结

本文代码基于 v4.11.6 版本

0x01 网卡的报文接收过程

一些背景知识:

  • 网卡驱动是加载到内核中的模块,负责衔接网卡和内核的网络模块,驱动在加载的时候将自己注册进网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据

recv-arch

本小节使用以太网的物理网卡结合一个UDP packet的接收过程为例子描述下内核收包过程,如下:

一、阶段1:数据包从网卡到内存

下图展示了数据包(packet)如何进入内存,并被内核的网络模块开始处理

                   +-----+
                   |     |                            Memroy
+--------+   1     |     |  2  DMA     +--------+--------+--------+--------+
| Packet |-------->| NIC |------------>| Packet | Packet | Packet | ...... |
+--------+         |     |             +--------+--------+--------+--------+
                   |     |<--------+
                   +-----+         |
                      |            +---------------+
                      |                            |
                    3 | Raise IRQ                  | Disable IRQ
                      |                          5 |
                      |                            |
                      ↓                            |
                   +-----+                   +------------+
                   |     |  Run IRQ handler  |            |
                   | CPU |------------------>| NIC Driver |
                   |     |       4           |            |
                   +-----+                   +------------+
                                                   | 
                                                6  | Raise soft IRQ
                                                   |
                                                   ↓

0、系统初始化时,网卡驱动程序会向内核申请一块内存(RingBuffer),用于存储未来到达的网络数据包;网卡驱动程序将申请的RingBuffer地址告诉网卡

1、数据包由外部网络进入物理网卡,如果目的地址非该网卡,且该网卡未开启混杂(promiscuous)模式,该包会被网卡丢弃

2、网卡将数据包通过DMA(Direct Memory Access)的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化(网卡会通过DMA将数据拷贝到RingBuffer中,此过程不需要cpu参与)

3、网卡通过硬件中断(IRQ)通知CPU,告诉CPU有数据来了,CPU必须最高优先级处理,否则数据待会存不下了

4、CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数(调用对应的网卡驱动硬中断处理程序)

5、网卡驱动被调用后,网卡驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断;然后启动对应的软中断函数

6、启动软中断,这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理

(软中断函数开始从RingBuffer中进行循环取包,并且封装为sk_buff,然后投递给网络协议栈进行处理;协议栈处理完成后数据就进入用户态的对应进程,进程就可以操作数据了)

二、阶段2:内核的网络模块

软中断会触发内核网络模块中的软中断处理函数,继续上面的流程

                                                     +-----+
                                             17      |     |
                                        +----------->| NIC |
                                        |            |     |
                                        |Enable IRQ  +-----+
                                        |
                                        |
                                  +------------+                                      Memroy
                                  |            |        Read           +--------+--------+--------+--------+
                 +--------------->| NIC Driver |<--------------------- | Packet | Packet | Packet | ...... |
                 |                |            |          9            +--------+--------+--------+--------+
                 |                +------------+
                 |                      |    |        skb
            Poll | 8      Raise softIRQ | 6  +-----------------+
                 |                      |             10       |
                 |                      ↓                      ↓
         +---------------+  Call  +-----------+        +------------------+        +--------------------+  12  +---------------------+
         | net_rx_action |<-------| ksoftirqd |        | napi_gro_receive |------->| enqueue_to_backlog |----->| CPU input_pkt_queue |
         +---------------+   7    +-----------+        +------------------+   11   +--------------------+      +---------------------+
                                                               |                                                      | 13
                                                            14 |        + - - - - - - - - - - - - - - - - - - - - - - +
                                                               ↓        ↓
                                                    +--------------------------+    15      +------------------------+
                                                    | __netif_receive_skb_core |----------->| packet taps(AF_PACKET) |
                                                    +--------------------------+            +------------------------+
                                                               |
                                                               | 16
                                                               ↓
                                                      +-----------------+
                                                      | protocol layers |
                                                      +-----------------+

7、内核中的ksoftirqd进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第6步中是网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数(ksoftirqd线程开始调用驱动的poll函数收包)

8、net_rx_action会调用网卡驱动里的poll函数(对于igb网卡驱动来说,此函数就是igb_poll)来一个个的处理数据包(poll函数将收到的包送到协议栈注册的ip_rcv函数中)

9、在poll函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道

10、网卡驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数

11、napi_gro_receive会处理GRO(Generic Receive Offloading)相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS(Receive Packet Steering),如果开启了,将会调用enqueue_to_backlog

12、在enqueue_to_backlog函数中,会将数据包放入CPU的softnet_data结构体的input_pkt_queue中,然后返回,如果input_pkt_queue满了的话,该数据包将会被丢弃,queue的大小可以通过net.core.netdev_max_backlog来配置(备注:enqueue_to_backlog函数也会被netif_rx函数调用,而netif_rx正是lo设备发送数据包时调用的函数)

13、CPU会接着在自己的软中断上下文中处理自己input_pkt_queue里的网络数据(调用__netif_receive_skb_core

14、如果没开启RPSnapi_gro_receive会直接调用__netif_receive_skb_core

15、看是不是有AF_PACKET类型的socket(raw socket),如果有的话,拷贝一份数据给它(tcpdump即抓取来自这里的packet)

16、调用协议栈相应的函数,将数据包交给协议栈处理

17、待内存中的所有数据包被处理完成后(即poll函数执行完成),会再次启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU

三、阶段3:协议栈网络层(IP)

接着看下数据包来到协议栈的处理过程,重要的内核函数如下:

          |
          |
          ↓         promiscuous mode &&
      +--------+    PACKET_OTHERHOST (set by driver)   +-----------------+
      | ip_rcv |-------------------------------------->| drop this packet|
      +--------+                                       +-----------------+
          |
          |
          ↓
+---------------------+
| NF_INET_PRE_ROUTING |
+---------------------+
          |
          |
          ↓
      +---------+
      |         | enabled ip forword  +------------+        +----------------+
      | routing |-------------------->| ip_forward |------->| NF_INET_FORWARD |
      |         |                     +------------+        +----------------+
      +---------+                                                   |
          |                                                         |
          | destination IP is local                                 ↓
          ↓                                                 +---------------+
 +------------------+                                       | dst_output_sk |
 | ip_local_deliver |                                       +---------------+
 +------------------+
          |
          |
          ↓
 +------------------+
 | NF_INET_LOCAL_IN |
 +------------------+
          |
          |
          ↓
    +-----------+
    | UDP layer |
    +-----------+
  • ip_rcv:此函数是IP模块的入口函数,该函数第一件事就是将垃圾数据包(目的mac地址不是当前网卡,但由于网卡设置了混杂模式而被接收进来)直接丢掉,然后调用注册在NF_INET_PRE_ROUTING上的函数
  • NF_INET_PRE_ROUTING: netfilter注册在协议栈中的钩子,可以通过iptables来注入一些数据包处理函数,用来修改或者丢弃数据包,如果数据包没被丢弃,将继续往下走
  • routing: 进行路由,如果是目的IP不是本地IP,且没有开启ip forward功能,那么数据包将被丢弃;如果开启了ip forward功能,那将进入ip_forward函数(转发模式常用)
  • ip_forwardip_forward会先调用netfilter注册的NF_INET_FORWARD相关函数,如果数据包没有被丢弃,那么将继续往后调用dst_output_sk函数
  • dst_output_sk: 该函数会调用IP层的相应函数将该数据包发送出去(参考协议栈数据包发送流程的后半部分)
  • ip_local_deliver:如果上面routing的时候发现目的IP是本地IP,那么将会调用该函数。该函数会先调用NF_INET_LOCAL_IN相关的钩子程序,如果通过,数据包将会向下发送到UDP层

四、阶段4:协议栈传输层(UDP)

          |
          |
          ↓
      +---------+            +-----------------------+
      | udp_rcv |----------->| __udp4_lib_lookup_skb |
      +---------+            +-----------------------+
          |
          |
          ↓
 +--------------------+      +-----------+
 | sock_queue_rcv_skb |----->| sk_filter |
 +--------------------+      +-----------+
          |
          |
          ↓
 +------------------+
 | __skb_queue_tail |
 +------------------+
          |
          |
          ↓
  +---------------+
  | sk_data_ready |
  +---------------+

  • udp_rcv: 此函数是UDP模块的入口函数,它里面会调用其它的函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,该函数会根据目的IP和端口找对应的socket,如果没有找到相应的socket,那么该数据包将会被丢弃,否则继续(关联hook点kprobe:udp_rcv
  • sock_queue_rcv_skb: 该函数会检查这个socket的receive buffer是不是满了,如果满了的话,丢弃该数据包,然后就是调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃(在Linux里面,每个socket上都可以像tcpdump里面一样定义filter,不满足条件的数据包将会被丢弃)
  • __skb_queue_tail: 将数据包放入socket接收队列的末尾
  • sk_data_ready: 通知socket数据包已经准备好;调用完sk_data_ready之后,一个数据包处理完成,等待应用层程序来读取,上面所有函数的执行过程都在软中断的上下文中

五、阶段5:socket应用程序

应用层一般有两种方式接收数据:

  • recvfrom函数阻塞等待数据到来,这种情况下当socket收到通知后,recvfrom就会被唤醒,然后读取接收队列的数据
  • 通过epoll/select监听相应的socket,当收到通知后,再调用recvfrom函数去读取接收队列的数据

至此,一个UDP包就经由网卡成功送到了应用层程序

网卡收包的若干细节

0x02 核心数据结构

  • struct socket:传输层使用的数据结构,用于声明、定义套接字
  • struct sock:网络层会调用struct sock结构体,该结构体包含struct sock_common结构体
  • struct sk_buff:内核中使用的套接字缓冲区结构体,套接字结构体用于表示一个网络连接对应的本地接口的网络信息,而sk_buff结构则是该网络连接对应的数据包的存储

struct sk_buff结构: 套接字缓冲区

对于ebpf应用开发者而言,最关注的结构莫过于sk_buff了。sk_buff用来管理和控制接收OR发送数据包的信息,各层协议都依赖于sk_buff而存在。内核中sk_buff结构体在各层协议之间传输不是用拷贝sk_buff结构体,而是通过增加协议头和移动指针来操作的。如果是从L4传输到L2,则是通过往sk_buff结构体中增加该层协议头来操作;如果是从L4到L2,则是通过移动sk_buff结构体中的data指针来实现,不会删除各层协议头

struct sk_buff {
	union {
		struct {
			/* These two members must be first. 构成sk_buff链表*/
			struct sk_buff		*next;
			struct sk_buff		*prev;
			union {
				struct net_device	*dev;	//网络设备对应的结构体,很重要但是不是本文重点,所以不做展开
				/* Some protocols might use this space to store information,
				 * while device pointer would be NULL.
				 * UDP receive path is one user.
				 */
				unsigned long		dev_scratch;   // 对于某些不适用net_device的协议需要采用该字段存储信息,如UDP的接收路径
			};
		};
		struct rb_node		rbnode; /* used in netem, ip4 defrag, and tcp stack 将sk_buff以红黑树组织,在TCP中有用到*/
		struct list_head	list;   // sk_buff链表头指针(5.10内核后)
	};
	union {
		struct sock		*sk;       // 指向网络层套接字结构体
		int			ip_defrag_offset;   //用来处理IPv4报文分片
	};
	union {
		ktime_t		tstamp;    // 时间戳
		u64		skb_mstamp_ns; /* earliest departure time */
	};
	/* 存储私有信息
	 * This is the control buffer. It is free to use for every
	 * layer. Please put your private variables there. If you
	 * want to keep them across layers you have to do a skb_clone()
	 * first. This is owned by whoever has the skb queued ATM.
	 */
	char			cb[48] __aligned(8);
	union {
		struct {
			unsigned long	_skb_refdst;				   // 目标entry
			void		(*destructor)(struct sk_buff *skb);	// 析构函数
		};
		struct list_head	tcp_tsorted_anchor;			    // TCP发送队列(tp->tsorted_sent_queue)
	};
....
	unsigned int		len,	// 实际长度
				data_len;	    // 数据长度
	__u16			mac_len,    // mac层长度
				hdr_len;        // 可写头部长度
	/* Following fields are _not_ copied in __copy_skb_header()
	 * Note that queue_mapping is here mostly to fill a hole.
	 */
	__u16			queue_mapping;   // 多队列设备的队列映射
......
	/* fields enclosed in headers_start/headers_end are copied
	 * using a single memcpy() in __copy_skb_header()
	 */
	/* private: */
	__u32			headers_start[0];	
	/* public: */
......
	__u8			__pkt_type_offset[0];
	__u8			pkt_type:3;
	__u8			ignore_df:1;
	__u8			nf_trace:1;
	__u8			ip_summed:2;
	__u8			ooo_okay:1;
	__u8			l4_hash:1;
	__u8			sw_hash:1;
	__u8			wifi_acked_valid:1;
	__u8			wifi_acked:1;
	__u8			no_fcs:1;
	/* Indicates the inner headers are valid in the skbuff. */
	__u8			encapsulation:1;
	__u8			encap_hdr_csum:1;
	__u8			csum_valid:1;
......
	__u8			__pkt_vlan_present_offset[0];
	__u8			vlan_present:1;
	__u8			csum_complete_sw:1;
	__u8			csum_level:2;
	__u8			csum_not_inet:1;
	__u8			dst_pending_confirm:1;
......
	__u8			ipvs_property:1;
	__u8			inner_protocol_type:1;
	__u8			remcsum_offload:1;
......
	union {
		__wsum		csum;
		struct {
			__u16	csum_start;
			__u16	csum_offset;
		};
	};
	__u32			priority;
	int			skb_iif;		// 接收到该数据包的网络接口的编号
	__u32			hash;
	__be16			vlan_proto;
	__u16			vlan_tci;
......
	union {
		__u32		mark;
		__u32		reserved_tailroom;
	};
	union {
		__be16		inner_protocol;
		__u8		inner_ipproto;
	};
	__u16			inner_transport_header;
	__u16			inner_network_header;
	__u16			inner_mac_header;
	__be16			protocol;
	__u16			transport_header;	// 传输层头部
	__u16			network_header;		// 网络层头部
	__u16			mac_header;			// mac层头部
	/* private: */
	__u32			headers_end[0];
	/* public: */
	/* These elements must be at the end, see alloc_skb() for details.  */
	sk_buff_data_t		tail;
	sk_buff_data_t		end;
	unsigned char		*head, *data;
	unsigned int		truesize;
	refcount_t		users;
......
};

struct sk_buff成员&&管理

sk_buff的成员主要关注如下三类:

  • 组织布局(Layout)
  • 通用数据成员(General)
  • 管理sk_buff结构体的函数(Management functions)

1、sk_buff的管理组织方式

通常sk_buff使用双链表sk_buff_head结构进行管理(并且sk_buff需要能在O(1)时间内获得双链表的头节点),布局如下图所示

//有些情况下sk_buff不是用双链表而是用红黑树组织的,那么有效的域是rbnode
//5.10内核中,list域是一个list_head结构体,而非sk_buff_head
struct sk_buff_head {
	/* These two members must be first. */
	struct sk_buff	*next;  //sk_buff中的next、prev指向相邻的sk_buff
	struct sk_buff	*prev;

	__u32		qlen;   //链表中元素的数量
	spinlock_t	lock;   //并发访问时保护
};

layout1

2、sk_buff的线性空间管理 && 创建

/* These elements must be at the end, see alloc_skb() for details.  */
	sk_buff_data_t		tail;
	sk_buff_data_t		end;
	unsigned char		*head,*data;

从下图中可以发现,headend指向的位置始终不变,数据的变化、协议头的添加都是依靠taildata的移动来实现的;此外,初始化时,headdatatail都指向开始位置

layout2

sk_buff线性数据区的创建过程如下,sk_buff结构数据区初始化成功后,此时 head 指针、data 指针、tail 指针都是指向同一个地方(head 指针和 end 指针指向的位置一直都不变,而对于数据的变化和协议信息的添加都是通过 data 指针和 tail 指针的改变来表现的

3、重要成员

  • struct sock *sk:指向拥有该sk_buff的套接字(即skb 关联的socket),当这个包是socket 发出或接收时,这里指向对应的socket,而如果是转发包,这里是NULL;在可观测场景下,当socket已经建立时,可以用来解析获取传输层的关键信息
  • unsigned int truesize:表示skb使用的大小,包括skb结构体以及它所指向的数据
  • unsigned int len:所有数据的长度之和,包括分片的数据以及协议头
  • unsigned int data_len:分片的数据长度
  • __u16 mac_len:链路层帧头长度
  • __u16 hdr_len:被copy的skb中可写的头部长度

4、General成员,即skb的通用成员,与协议类型或内核特性无关,这里也列举几个

struct net_device *dev成员,用来表示从哪个设备收到报文,或将把报文发到哪个设备

//include/linux/skbuff.h
			union {
				struct net_device	*dev;
				/* Some protocols might use this space to store information,
				 * while device pointer would be NULL.
				 * UDP receive path is one user.
				 */
				unsigned long		dev_scratch;
			};

char cb[48]成员,是skb能被各层共用的精髓,48即为TCP的控制块tcp_sbk_cb数据结构的size

//include/linux/skbuff.h
	/*
	 * This is the control buffer. It is free to use for every
	 * layer. Please put your private variables there. If you
	 * want to keep them across layers you have to do a skb_clone()
	 * first. This is owned by whoever has the skb queued ATM.
	 */
	char			cb[48] __aligned(8);

tcp_sbk_cb结构如下,可以通过TCP_SKB_CB这个宏获取cb字段(被转换为struct tcp_skb_cb *类型)

//include/net/tcp.h
#define TCP_SKB_CB(__skb)	((struct tcp_skb_cb *)&((__skb)->cb[0]))
//include/net/tcp.h
struct tcp_skb_cb {
	__u32		seq;		/* Starting sequence number	*/
	__u32		end_seq;	/* SEQ + FIN + SYN + datalen	*/

	/* ... */
	__u8		tcp_flags;	/* TCP header flags. (tcp[13])	*/

	/* ... */
};

pkt_type这个字段用于表示数据包类型,此类型是由目标MAC地址决定的

//include/linux/skbuff.h
	__u8			pkt_type:3;


#define PACKET_HOST		0		/* To us		*/
#define PACKET_BROADCAST	1		/* To all		*/
#define PACKET_MULTICAST	2		/* To group		*/
#define PACKET_OTHERHOST	3		/* To someone else 	*/
#define PACKET_OUTGOING		4		/* Outgoing of any type */
#define PACKET_LOOPBACK		5		/* MC/BRD frame looped back */
#define PACKET_USER		6		/* To user space	*/
#define PACKET_KERNEL		7		/* To kernel space	*/

__be16 protocol这个字段标识了L2上层的协议类型,典型的协议类型如下:

//include/uapi/linux/if_ether.h
/*
 *	These are the defined Ethernet Protocol ID's.
 */


#define ETH_P_IP	0x0800		/* Internet Protocol packet	*/
#define ETH_P_X25	0x0805		/* CCITT X.25			*/
#define ETH_P_ARP	0x0806		/* Address Resolution packet	*/

#define ETH_P_IPV6	0x86DD		/* IPv6 over bluebook		*/

transport_headernetwork_headermac_header这三个字段标识了各层头部相对于head的偏移量,在ebpf对sk_buff结构的解析中也非常常用

//include/linux/skbuff.h
	__u16			transport_header;
	__u16			network_header;
	__u16			mac_header;

5、管理函数(Management functions)

5.1、分配和释放内存,通过alloc_skb获取一个struct sk_buff加长度为size(经过对齐)的数据缓冲区,其中skb->end指向的是一个skb_shared_info结构体,headdatatail以及end初始化指向如图所示,end留出了padding(tailroom)保证使读取为主的skb_shared_info结构与前面的数据不在一个缓存行

alloc_skb

//net/core/skbuff.c
static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority){
    //....
	skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
	/* ... */
	size = SKB_DATA_ALIGN(size);
	size += SKB_DATA_ALIGN(sizeof(struct skb_shared_info));
	data = kmalloc_reserve(size, gfp_mask, node, &pfmemalloc);
	/* kmalloc(size) might give us more room than requested.
	 * Put skb_shared_info exactly at the end of allocated zone,
	 * to allow max possible filling before reallocation.
	 */
	size = SKB_WITH_OVERHEAD(ksize(data));
	/* ... */
	skb->head = data;
	skb->data = data;
	skb_reset_tail_pointer(skb);
	skb->end = skb->tail + size;
    //...
}

5.2、sk_buff数据缓冲区指针操作

sk_buff_oper

  • skb_put
  • skb_push
  • skb_pull:常用于协议栈接收报文时,从外到内剥离协议头(以太网头-IP 头-TCP 头)的操作
  • skb_reserve

5.3、接收数据包的处理过程,伪代码描述如下:

// 驱动构造 skb(DMA 数据已写入)
struct sk_buff *skb = build_skb(dma_buffer, buffer_size);
skb_put(skb, packet_len);  // 设置数据长度

// 协议栈处理(以太网层)
__be16 proto = eth_type_trans(skb, dev);
skb_pull(skb, ETH_HLEN);    // 剥离以太网头

// 协议栈处理(IP 层)
struct iphdr *iph = ip_hdr(skb);
skb_pull(skb, iph->ihl * 4); // 剥离 IP 头

// 协议栈处理(TCP 层)
struct tcphdr *th = tcp_hdr(skb);
skb_pull(skb, th->doff * 4); // 剥离 TCP 头

// 应用层获取负载数据
char *payload = skb->data;
int payload_len = skb->len;

5.4、发送数据包的处理过程,下图展示了发送数据包时skb缓冲区被填满的过程,伪代码描述如下:

unsigned int total_header_len = ETH_HLEN + IP4_HLEN + TCP_HLEN;
unsigned int payload_len = 1000;
// 分配skb,总空间为 headers + payload,初始 data 和 tail 指向缓冲区起始位置
struct sk_buff *skb = alloc_skb(total_header_len + payload_len, GFP_KERNEL);
// 预留所有协议头的空间,预留头部空间,data 和 tail 后移
skb_reserve(skb, total_header_len);
// 添加负载数据,填充负载数据,tail 后移,扩展数据区
skb_put(skb, payload_len);
memcpy(skb->data, payload, payload_len);
// 添加TCP头,添加协议头(从内到外),data 前移,覆盖预留空间
tcp_header = skb_push(skb, TCP_HLEN);
// 添加IP头
ip_header = skb_push(skb, IP4_HLEN);
// 添加以太网头
eth_header = skb_push(skb, ETH_HLEN);
// 发送
dev_queue_xmit(skb);

sk_buff_send_packet

struct socket结构

每个struct socket结构都有一个struct sock结构成员,sock是对socket的扩充,socket->sk指向对应的sock结构,sock->socket 指向对应的socket结构

struct socket {
	socket_state		state;  //链接状态
	short			type;       //套接字类型,如SOCK_STREAM等
	unsigned long		flags;
	struct socket_wq	*wq;
	struct file		*file;      //套接字对应的文件指针,毕竟Linux一切皆文件
	struct sock		*sk;        //网络层的套接字
	const struct proto_ops	*ops;
};

struct sock结构

struct sock {
	struct sock_common	__sk_common;	   // 网络层套接字通用结构体
......
	socket_lock_t		sk_lock;	       // 套接字同步锁
	atomic_t		sk_drops;	           // IP/UDP包丢包统计
	int			sk_rcvlowat;        	   // SO_RCVLOWAT标记位
......
	struct sk_buff_head	sk_receive_queue;	// 收到的数据包队列
......
	int			sk_rcvbuf;				  // 接收缓存大小
......
	union {
		struct socket_wq __rcu	*sk_wq;	    // 等待队列
		struct socket_wq	*sk_wq_raw;
	};
......
	int			sk_sndbuf;			       // 发送缓存大小
	/* ===== cache line for TX ===== */
	int			sk_wmem_queued;			   // 传输队列大小
	refcount_t		sk_wmem_alloc;		    // 已确认的传输字节数
	unsigned long		sk_tsq_flags;	    // TCP Small Queue标记位
	union {
		struct sk_buff	*sk_send_head;		// 发送队列对首
		struct rb_root	tcp_rtx_queue;		 
	};
	struct sk_buff_head	sk_write_queue;		 // 发送队列
......
	u32			sk_pacing_status; /* see enum sk_pacing 发包速率控制状态*/ 
	long			sk_sndtimeo;		    // SO_SNDTIMEO 标记位
	struct timer_list	sk_timer;			// 套接字清空计时器
	__u32			sk_priority;		    // SO_PRIORITY 标记位
......
	unsigned long		sk_pacing_rate; /* bytes per second 发包速率*/
	unsigned long		sk_max_pacing_rate;  // 最大发包速率
	struct page_frag	sk_frag;		    // 缓存页帧
......
	struct proto		*sk_prot_creator;
	rwlock_t		sk_callback_lock;
	int			sk_err,					  // 上次错误
				sk_err_soft;			  // “软”错误:不会导致失败的错误
	u32			sk_ack_backlog;			   // ack队列长度
	u32			sk_max_ack_backlog;		   // 最大ack队列长度
	kuid_t			sk_uid;				  // user id
	struct pid		*sk_peer_pid;		   // 套接字对应的peer的id
......
	long			sk_rcvtimeo;		  // 接收超时
	ktime_t			sk_stamp;			  // 时间戳
......
	struct socket		*sk_socket;		   // Identd协议报告IO信号
	void			*sk_user_data;		  // RPC层私有信息
......
	struct sock_cgroup_data	sk_cgrp_data;   // cgroup数据
	struct mem_cgroup	*sk_memcg;		   // 内存cgroup关联
	void			(*sk_state_change)(struct sock *sk);	// 状态变化回调函数
	void			(*sk_data_ready)(struct sock *sk);		// 数据处理回调函数
	void			(*sk_write_space)(struct sock *sk);		// 写空间可用回调函数
	void			(*sk_error_report)(struct sock *sk);    // 错误报告回调函数
	int			(*sk_backlog_rcv)(struct sock *sk, struct sk_buff *skb);	// 处理存储区回调函数
......
	void                    (*sk_destruct)(struct sock *sk);	// 析构回调函数
	struct sock_reuseport __rcu	*sk_reuseport_cb;			   // group容器重用回调函数
......
};

struct sock_common结构

struct sock_common是套接口在网络层的最小表示,即最基本的网络层套接字信息

struct sock_common {
	/* skc_daddr and skc_rcv_saddr must be grouped on a 8 bytes aligned
	 * address on 64bit arches : cf INET_MATCH()
	 */
	union {
		__addrpair	skc_addrpair;
		struct {
			__be32	skc_daddr;		// 外部/目的IPV4地址
			__be32	skc_rcv_saddr;	// 本地绑定IPV4地址
		};
	};
	union  {
		unsigned int	skc_hash;	// 根据协议查找表获取的哈希值
		__u16		skc_u16hashes[2]; // 2个16位哈希值,UDP专用
	};
	/* skc_dport && skc_num must be grouped as well */
	union {
		__portpair	skc_portpair;	// 
		struct {
			__be16	skc_dport;	    // inet_dport占位符
			__u16	skc_num;	    // inet_num占位符
		};
	};
	unsigned short		skc_family;	      // 网络地址family
	volatile unsigned char	skc_state;    // 连接状态
	unsigned char		skc_reuse:4;      // SO_REUSEADDR 标记位
	unsigned char		skc_reuseport:1;  // SO_REUSEPORT 标记位
	unsigned char		skc_ipv6only:1;   // IPV6标记位
	unsigned char		skc_net_refcnt:1; // 该套接字网络名字空间内引用数
	int			skc_bound_dev_if;		 // 绑定设备索引
	union {
		struct hlist_node	skc_bind_node;     // 不同协议查找表组成的绑定哈希表
		struct hlist_node	skc_portaddr_node; // UDP/UDP-Lite protocol二级哈希表
	};
	struct proto		*skc_prot;			  // 协议回调函数,根据协议不同而不同
......
	union {									
		struct hlist_node	skc_node;		    // 不同协议查找表组成的主哈希表
		struct hlist_nulls_node skc_nulls_node;  // UDP/UDP-Lite protocol主哈希表
	};
	unsigned short		skc_tx_queue_mapping;    // 该连接的传输队列
	unsigned short		skc_rx_queue_mapping;    // 该连接的接受队列
......
	union {
		int		skc_incoming_cpu; // 多核下处理该套接字数据包的CPU编号
		u32		skc_rcv_wnd;	  // 接收窗口大小
		u32		skc_tw_rcv_nxt; /* struct tcp_timewait_sock  */
	};
	refcount_t		skc_refcnt;   // 套接字引用计数
......
};

小结

网络通信中通过网卡获取到的数据包至少包括了物理层,链路层和网络层的内容,因此套接字结构体仅仅从网络层开始,即通常只定义了传输层的套接字socket和网络层的套接字socksocket 是用于负责对上给用户提供接口,并且和文件系统关联。而 sock负责向下对接内核网络协议栈

从传输层到链路层,它是存放数据的通用结构,为了保持高效率,数据在传递过程中尽量不发生额外的拷贝。因此,从高层到低层的时候,会不断地在数据前加头,因此每一层的协议都会调用skb_reserve,为自己的报头预留空间。至于从低层到高层,去掉低层报头的方式就是移动一下指针,指向高层头,非常简洁

0x03 可观测:内核收包的主要过程

准备工作

Linux驱动,内核协议栈等等模块在具备接收网卡数据包之前,需要完整如下的初始化工作,这部分内容可以参考图解Linux网络包接收过程

  1. Linux系统启动,创建ksoftirqd内核线程,用来处理软中断
  2. 网络子系统初始化
  3. 协议栈注册,针对协议栈支持的各类协议如arp/icmp/ip/udp/tcp等,每一个协议都会将自己的处理函数注册
  4. 网卡驱动初始化,初始化DMA以及向内核注册收包函数地址(NAPI的poll函数)
  5. 启动网卡,分配RX/TX队列,注册中断对应的处理函数
  6. 当上面工作都完成之后,就可以打开硬中断,等待数据包的到来

其中协议栈注册主要完成了各层协议处理函数的注册,如内核实现网络层的ip协议,传输层的tcp/udp协议等,这些协议对应的实现函数分别是ip_rcv()/tcp_v4_rcv()/udp_rcv(),内核调用inet_init后开始网络协议栈注册。通过inet_init将上述函数注册到了inet_protosptype_base数据结构中

inet_init

//file: net/ipv4/af_inet.c
//udp_protocol结构体中的handler是udp_rcv,tcp_protocol结构体中的handler是tcp_v4_rcv
//通过inet_add_protocol被初始化到数据结构中
static struct packet_type ip_packet_type __read_mostly = {

    .type = cpu_to_be16(ETH_P_IP),
    .func = ip_rcv,};static const struct net_protocol udp_protocol = {
    .handler =  udp_rcv,
    .err_handler =  udp_err,
    .no_policy =    1,
    .netns_ok = 1,};static const struct net_protocol tcp_protocol = {
    .early_demux    =   tcp_v4_early_demux,
    .handler    =   tcp_v4_rcv,
    .err_handler    =   tcp_v4_err,
    .no_policy  =   1,
    .netns_ok   =   1,

};

static int __init inet_init(void){
    ......

	//inet_add_protocol:注册icmp/tcp/udp等协议钩子
    if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
        pr_crit("%s: Cannot add ICMP protocol\n", __func__);
    if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
        pr_crit("%s: Cannot add UDP protocol\n", __func__);
    if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
        pr_crit("%s: Cannot add TCP protocol\n", __func__);
    ......

	//注册ip_packet_type
	//dev_add_pack(&ip_packet_type)
	//ip_packet_type结构体中的type是协议名,func是ip_rcv函数
	//在dev_add_pack中会被注册到ptype_base哈希表中
    dev_add_pack(&ip_packet_type);
}

/*
inet_add_protocol函数将tcp和udp对应的处理函数都注册到了inet_protos数组
*/
int inet_add_protocol(const struct net_protocol *prot, unsigned char protocol){
    if (!prot->netns_ok) {
        pr_err("Protocol %u is not namespace aware, cannot register.\n",
            protocol);
        return -EINVAL;
    }

    return !cmpxchg((const struct net_protocol **)&inet_protos[protocol],
            NULL, prot) ? 0 : -1;
}

//file: net/core/dev.c
void dev_add_pack(struct packet_type *pt){
    struct list_head *head = ptype_head(pt);
    ......
}

static inline struct list_head *ptype_head(const struct packet_type *pt){
    if (pt->type == htons(ETH_P_ALL))
        return &ptype_all;
    else
        return &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];
}

小结下,上述逻辑中inet_protos记录了udp,tcp处理函数的地址,ptype_base存储了ip_rcv()函数的处理地址,软中断中会通过ptype_base找到ip_rcv函数地址,进而将ip包正确地送到ip_rcv()函数中执行,进而在ip_rcv中将会通过inet_protos结构定位到tcp或者udp的处理函数,再而把包转发给udp_rcv()tcp_v4_rcv()函数。另外,在ip_rcvtcp_v4_rcvudp_rcv等函数中可以了解更详细的处理细节,比如ip_rcv中会处理netfilter和iptables过滤规则, netfilter 或 iptables 规则,这些规则都是在软中断的上下文中执行的,会加大网络延迟(规则复杂且数目较多)

接收数据的主要流程(核心)

1、硬中断处理

2、 ksoftirqd内核线程处理软中断

3、网络协议栈处理

4、IP协议层处理

5、UDP协议层处理

0x04 应用层处理

上一章节描述了整个Linux内核对数据包的接收和处理过程,最后把数据包放到socket的接收队列中,那么应用层如何接受数据呢?以UDP应用常用的recvfrom函数(glibc库函数)为例进行分析

系统调用:recvfrom

对于系统库函数recvfrom,该函数在执行后会将用户进行陷入到内核态,进入到Linux实现的系统调用sys_recvfrom,回想下前文介绍的socket相关结构

recvfrom_socket

如上图,struct socket结构中的const struct proto_ops成员对应的是协议的方法集合(每个协议都会实现不同的方法集)

//file: net/ipv4/af_inet.c
//IPv4 Internet协议族来说,每种协议都有对应的处理方法
const struct proto_ops inet_stream_ops = {
    ......
    .recvmsg       = inet_recvmsg,	//
    .mmap          = sock_no_mmap,    
}

//udp是通过inet_dgram_ops来定义的,其中注册了inet_recvmsg方法
const struct proto_ops inet_dgram_ops = {
    ......
    .sendmsg       = inet_sendmsg,
    .recvmsg       = inet_recvmsg,  //UDP的处理方法
}

struct sock *sk结构的sk_prot成员定义了二级处理函数。对于UDP协议来说,会被设置成UDP协议实现的方法集udp_prot

//file: net/ipv4/udp.c
struct proto udp_prot = {
    .name          = "UDP",
    .owner         = THIS_MODULE,
    .close         = udp_lib_close,
    .connect       = ip4_datagram_connect,
    ......
    .sendmsg       = udp_sendmsg,
    .recvmsg       = udp_recvmsg,
    .sendpage      = udp_sendpage,    
}

inet_recvmsg的实现最后会调用sk->sk_prot->recvmsg进行后续处理,对于UDP协议的socket来说(sk_prot就是struct proto udp_prot),该实现就是udp_recvmsg

//file: net/ipv4/af_inet.c
int inet_recvmsg(struct socket *sock, struct msghdr *msg, size_t size,
		 int flags)
{
	struct sock *sk = sock->sk;
	int addr_len = 0;
	int err;

	sock_rps_record_flow(sk);

	err = sk->sk_prot->recvmsg(sk, msg, size, flags & MSG_DONTWAIT,
				   flags & ~MSG_DONTWAIT, &addr_len);
	if (err >= 0)
		msg->msg_namelen = addr_len;
	return err;
}
EXPORT_SYMBOL(inet_recvmsg);

sys_recvfrom的调用链

sys_recvfrom

udp_recvmsg的核心逻辑

udp_recvmsg函数包含下面的主要工作:

  1. 取数据包:__skb_recv_datagram()函数,从sk_receive_queue上取一个skb
  2. 拷贝数据(内核空间copy到用户空间):skb_copy_datagram_iovec()skb_copy_and_csum_datagram_iovec()
  3. 必要时计算校验和:skb_copy_and_csum_datagram_iovec()

从内核udp_recvmsg实现,寻找收包的调用链,最终会调用__skb_recv_datagram函数

udp_recvmsg
	--->__skb_recv_udp
		---> __skb_recv_datagram
			--->__skb_try_recv_datagram

__skb_recv_datagram的实现如下,从__skb_try_recv_datagram函数代码可知收包过程就是访问sk->sk_receive_queue;如果没有数据,且用户允许等待,则将调用__skb_wait_for_more_packets()执行等待操作(会让用户进程进入睡眠状态)

struct sk_buff *__skb_recv_datagram(struct sock *sk, unsigned int flags,
				    void (*destructor)(struct sock *sk,
						       struct sk_buff *skb),
				    int *peeked, int *off, int *err)
{
	struct sk_buff *skb, *last;
	long timeo;

	timeo = sock_rcvtimeo(sk, flags & MSG_DONTWAIT);

	do {
		//
		skb = __skb_try_recv_datagram(sk, flags, destructor, peeked,
					      off, err, &last);
		if (skb)
			return skb;

		if (*err != -EAGAIN)
			break;
	} while (timeo &&
		!__skb_wait_for_more_packets(sk, err, &timeo, last));

	return NULL;
}
EXPORT_SYMBOL(__skb_recv_datagram);

一图以蔽之

recv

0x05 主要hook点

0x06 参考