设备接口层接收数据包(二)


设备接口层接收数据包(一)中详细分析了驱动程序和内核框架相互配合,通过NAPI模式或者非NAPI模式接收数据包的过程,但是忽略了接收过程中对数据包的处理以及是如何将数据包递交给高层协议的,这篇笔记就来介绍这部分内容。

数据包的创建

从前面的笔记介绍中可以很容易想到:非NAPI模式,skb的创建应该在中断处理函数中完成;NAPI模式,应该在poll()回调中完成。不过无论skb在哪里创建,驱动程序应该完成的事情是一致的,下面以3c527.c网卡的代码为例,看下驱动程序应该对skb完成哪些初始化。

static void mc32_rx_ring(struct net_device *dev)
{
...
	// length为整个以太网帧的长度,包括头部,这里在skb的线性区分配length+2大小(为什么要+2呢?)
	skb=dev_alloc_skb(length+2);
	if(skb==NULL) {
		goto dropped;
	}
	// 开头空2个字节
	skb_reserve(skb,2);
	// 将帧内容拷贝到skb的线性区域,然后设置正确的tail指针
	memcpy(skb_put(skb, length), lp->rx_ring[rx_ring_tail].skb->data, length);
	// 因为3c527收发的是以太网(或者IEEE 802系列)帧,所以这里调用eth_type_trans()尝试按照以太网
	// 帧的格式来解析,找到应该把该帧交给哪个高层协议继续处理,protocol的赋值非常重要
	skb->protocol = eth_type_trans(skb,dev);
	dev->last_rx = jiffies;
	// 非NAPI模式继续向上递交数据包
	netif_rx(skb);
...
}

skb被分配后,字段都是取默认值0,从上面可以看的出来,非常关键的步骤就是对protocol进行设置,该字段指示了上层协议的类型。

协议字段解析: eth_type_trans()

不同的上层协议可能使用的底层协议并不相同,甚至同一种L3协议,可以基于不同的L2协议传输,比如IP协议可以在Ethernet,也可以在令牌环网络中传输。这里我们不讨论其它的L2网络,只关注下Ethernet是如何决定该字段的。

#define ETH_HLEN	14		/* Total octets in header.	 */

// 以太网帧首部定义
struct ethhdr {
	// 6字节的源MAC地址和目的MAC地址
	unsigned char	h_dest[ETH_ALEN];	/* destination eth addr	*/
	unsigned char	h_source[ETH_ALEN];	/* source ether addr	*/
	// 协议字段(也可能是长度字段)
	__be16		h_proto;		/* packet type ID field	*/
} __attribute__((packed));

/**
 * eth_type_trans - determine the packet's protocol ID.
 * @skb: received socket data
 * @dev: receiving network device
 *
 * The rule here is that we
 * assume 802.3 if the type field is short enough to be a length.
 * This is normal practice and works for any 'now in use' protocol.
 */
__be16 eth_type_trans(struct sk_buff *skb, struct net_device *dev)
{
	struct ethhdr *eth;
	unsigned char *rawp;
	
	// 输入数据包中,该字段表示数据包是被谁收到的
	skb->dev = dev;
	// 让skb->mac_header指向skb->data位置,即指向帧的开头
	skb_reset_mac_header(skb);
	// skb->data指针前移,剥掉以太网帧首部的14个字节,这样skb->data将指向上层协议报文的开头
	skb_pull(skb, ETH_HLEN);
	eth = eth_hdr(skb);

	// 根据mac地址类型确定输入数据包的类型,即skb->pkt_type字段
	if (is_multicast_ether_addr(eth->h_dest)) {
		if (!compare_ether_addr(eth->h_dest, dev->broadcast))
			skb->pkt_type = PACKET_BROADCAST;
		else
			skb->pkt_type = PACKET_MULTICAST;
	}
	/*
	 *      This ALLMULTI check should be redundant by 1.4
	 *      so don't forget to remove it.
	 *
	 *      Seems, you forgot to remove it. All silly devices
	 *      seems to set IFF_PROMISC.
	 */
	else if (1 /*dev->flags&IFF_PROMISC */ ) {
		if (unlikely(compare_ether_addr(eth->h_dest, dev->dev_addr)))
			skb->pkt_type = PACKET_OTHERHOST;
	}

	// 如果帧首部的协议字段值超过1536,那么h_proto字段的含义就是上层协议号,这是标准的以太网帧
	if (ntohs(eth->h_proto) >= 1536)
		return eth->h_proto;

	// 如果协议字段小于1536,那么h_proto代表的就是报文的长度,从rawp开始继续解析报头
	rawp = skb->data;
	/*
	 *      This is a magic hack to spot IPX packets. Older Novell breaks
	 *      the protocol design and runs IPX over 802.3 without an 802.2 LLC
	 *      layer. We look for FFFF which isn't a used 802.2 SSAP/DSAP. This
	 *      won't work for fault tolerant netware but does for the rest.
	 */
	// 如果帧数据的前两个字节为0xFFFF,那么这是一个802.3以太网数据帧
	if (*(unsigned short *)rawp == 0xFFFF)
		return htons(ETH_P_802_3);

	/*
	 *      Real 802.2 LLC
	 */
	// 其余情况说明这是一个802.2以太网数据帧
	return htons(ETH_P_802_2);
}

为什么eth_type_trans()要这么实现,那是因为Ethernet和IEEE 802系列协议的发展历史决定的,这部分内容可以参考《深入理解Linux网络技术内幕》的13.8节。

eth_type_trans()函数还有一个重要任务是从L2的角度确定输入数据包的类型,将确认结果记录到skb->pakcet_type,该字段高层协议会进行判断,进而决定如何处理该数据包。

内核从L2角度总共定义了如下几种数据包类型:

/* Packet types */
#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 */
/* These ones are invisible by user level */
#define PACKET_LOOPBACK		5		/* MC/BRD frame looped back */
#define PACKET_FASTROUTE	6		/* Fastrouted frame	*/

可以看到,如果不对packet_type赋值,那么默认的0表示PACKET_HOST,表示是给本机的。

小结

到此驱动程序对skb的处理就结束了,它干了如下几件事:

  1. 分配一个新的skb并且将帧拷贝到skb的数据区;
  2. 对skb的dev、protocol、pkt_type字段初始化;
  3. 让skb的mac_header执行帧的开头,data指向高层协议数据包的开头。

向高层协议递交skb: netif_receive_skb()

网络接收软中断处理流程中的最后一步是调用netif_receive_skb()将数据包递交给高层协议。NAPI模式下,该调用应该由驱动的poll()回调完成;非NAPI方式下,由公共的process_backlog()完成。

/**
 *	netif_receive_skb - process receive buffer from network
 *	@skb: buffer to process
 *
 *	netif_receive_skb() is the main receive data processing function.
 *	It always succeeds. The buffer may be dropped during processing
 *	for congestion control or by the protocol layers.
 *
 *	This function may only be called from softirq context and interrupts
 *	should be enabled.
 *
 *	Return values (usually ignored):
 *	NET_RX_SUCCESS: no congestion
 *	NET_RX_DROP: packet was dropped
 */
int netif_receive_skb(struct sk_buff *skb)
{
	struct packet_type *ptype, *pt_prev;
	struct net_device *orig_dev;
	struct net_device *null_or_orig;
	int ret = NET_RX_DROP;
	__be16 type;

    // VLAN特性,忽略
	if (skb->vlan_tci && vlan_hwaccel_do_receive(skb))
		return NET_RX_SUCCESS;
	// NetPoll特性,忽略
	if (netpoll_receive_skb(skb))
		return NET_RX_DROP;
    // 如果还没有设置接收时间戳则设置,正确的地方应该是驱动程序进行设置
	if (!skb->tstamp.tv64)
		net_timestamp(skb);
	// 设置输入网络设备的索引
	if (!skb->iif)
		skb->iif = skb->dev->ifindex;
    // 网络设备绑定,属于高级话题,忽略
	null_or_orig = NULL;
	orig_dev = skb->dev;
	if (orig_dev->master) {
		if (skb_bond_should_drop(skb))
			null_or_orig = orig_dev; /* deliver only exact match */
		else
			skb->dev = orig_dev->master;
	}

	__get_cpu_var(netdev_rx_stat).total++;

    // 让skb中网络层首部和传输层首部都指向skb->data字段
	skb_reset_network_header(skb);
	skb_reset_transport_header(skb);
	// 设置MAC层帧头部的长度
	skb->mac_len = skb->network_header - skb->mac_header;

	pt_prev = NULL;

	rcu_read_lock();

#ifdef CONFIG_NET_CLS_ACT
	if (skb->tc_verd & TC_NCLS) {
		skb->tc_verd = CLR_TC_NCLS(skb->tc_verd);
		goto ncls;
	}
#endif
	// 遍历ptype_all链表,传递一份数据到注册在ptype_all链表上的协议,这是在网络
	// 设备接口层提供的一种勾包方式,AF_PACKET套接字的原理就是基于该链表
	list_for_each_entry_rcu(ptype, &ptype_all, list) {
		if (ptype->dev == null_or_orig || ptype->dev == skb->dev ||
		    ptype->dev == orig_dev) {
			if (pt_prev)
				ret = deliver_skb(skb, pt_prev, orig_dev);
			pt_prev = ptype;
		}
	}

#ifdef CONFIG_NET_CLS_ACT
	skb = handle_ing(skb, &pt_prev, &ret, orig_dev);
	if (!skb)
		goto out;
ncls:
#endif
	// 网桥和Vlan处理,属于高级话题,忽略
	skb = handle_bridge(skb, &pt_prev, &ret, orig_dev);
	if (!skb)
		goto out;
	skb = handle_macvlan(skb, &pt_prev, &ret, orig_dev);
	if (!skb)
		goto out;

	// 遍历ptype_base哈希表中type所映射的冲突链,调用其提供的func()方法传递给上层协议处理
	// 这里之所以看起来这么复杂,完全是为了少调用一次skb_free(),网上有许多关于
	// 该话题的讨论,可以参考:https://blog.csdn.net/plt2007plt/article/details/8876034
	type = skb->protocol;
	list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
		if (ptype->type == type &&
		    (ptype->dev == null_or_orig || ptype->dev == skb->dev || ptype->dev == orig_dev)) {
			if (pt_prev)
				ret = deliver_skb(skb, pt_prev, orig_dev);
			pt_prev = ptype;
		}
	}
	if (pt_prev) {
		ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
	} else {
		kfree_skb(skb);
		/* Jamal, now you will not able to escape explaining
		 * me how you were going to use this. :-)
		 */
		ret = NET_RX_DROP;
	}
out:
	rcu_read_unlock();
	return ret;
}

上面截取的代码段有如下三个关键点:

  1. 遍历ptype_all链表,如果符合条件(未指定dev,或者dev匹配),则将该数据包传递给对应的回调函数func();
  2. 遍历ptype_base的某个冲突链(根据skb->protocol哈希),如果type和dev匹配,则将数据包传递给对应的回调函数func();
  3. 只要符合匹配条件,同一个数据包可能会被递交给多个上层协议。

上面在递交给高层协议处理的过程中使用的是deliver_skb(),它递交的实际上是skb的副本:

static inline int deliver_skb(struct sk_buff *skb, struct packet_type *pt_prev, struct net_device *orig_dev)
{
	// 增加了skb的引用计数,高层协议在处理完毕后应该负责将引用计数减1
	atomic_inc(&skb->users);
	return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
}

高层协议处理函数的管理

前面看到的回调函数func(),我们可以称之为协议处理函数。从代码中我们看到有两张表:ptype_all和ptype_base,这两个表中保存的数据结构实际上是struct packet_type,协议处理函数就是该结构的一个成员。

说白了,设备接口层在将数据包传递给上层协议时,需要知道系统中当前有哪些协议工作在层三,并且它们都可以处理什么类型的帧,这些信息设备接口层自己肯定不知道,需要在内核初始化或者协议模块初始化时,由那些能够处理这些帧的高层协议模块向设备接口层注册,注册时提供的就是类型为struct packet_type的对象。有了该信息,设备接口层就可以通过上面的netif_receive_skb()函数向上分发数据包了。

struct packet_type

struct packet_type {
	// 协议代号(网络字节序),如netif_receive_skb()中代码所示,对于ptype_base链表中的协议,
	// type字段要和层二报文中的协议字段一致,层二才会将数据报文递交给该上层协议
	__be16			type;	/* This is really htons(ether_type). */
	// 如果为NULL,那么表明该层三协议处理来自所有设备的数据包(当然type要匹配);
	// 如果不为NULL,则表明只处理来自特定设备的数据包
	struct net_device	*dev;	/* NULL is wildcarded here	     */
	// 协议处理函数
	int	(*func) (struct sk_buff *, struct net_device *, struct packet_type *, struct net_device *);
	// GSO/GRO特性相关,忽略
	struct sk_buff* (*gso_segment)(struct sk_buff *skb, int features);
	int			(*gso_send_check)(struct sk_buff *skb);
	struct sk_buff	**(*gro_receive)(struct sk_buff **head, struct sk_buff *skb);
	int			(*gro_complete)(struct sk_buff *skb);
	// 该私有数据结构由层三协议自己定义使用
	void			*af_packet_priv;
	// 用于将struct packet_type结构组织成链表
	struct list_head	list;
};

比如,IP协议定义的该结构内容如下,如果设备接口层收到的数据包类型是ETH_P_IP,那么就会递交给IP协议处理,处理函数为ip_rcv()。

static struct packet_type ip_packet_type = {
	.type = __constant_htons(ETH_P_IP),
	.func = ip_rcv,
	.gso_send_check = inet_gso_send_check,
	.gso_segment = inet_gso_segment,
};

内核在include/linu/if_ether.h中定义了很多的L3协议类型,虽然在if_ether.h中定义,但是这些协议类型不仅仅可以包含在以太网帧中。

ptype_all和ptype_base链表

ptype_all是一个全局的双向链表,注册到其中的struct packet_type指定的数据包类型为ETH_P_ALL,表示接收任意类型的帧。

ptype_base是一个全局的散列表,注册到其中的struct packet_type只能接收那些指定类型的帧。

这两个全局表的定义及组织结构如下所示:

// 这两个数据结构用RCU + 自旋锁保护
static DEFINE_SPINLOCK(ptype_lock);
static struct list_head ptype_base[PTYPE_HASH_SIZE] __read_mostly;
static struct list_head ptype_all __read_mostly;	/* Taps */

协议处理函数的注册和注销

在各个层三协议初始化过程中,会向设备接口层注册struct packet_type,并且在卸载的时候进行去注册,这两个过程分别是通过调用dev_add_pack()和dev_remove_pack()完成的。

注册: dev_add_pack()

/**
 *	dev_add_pack - add packet handler
 *	@pt: packet type declaration
 *
 *	Add a protocol handler to the networking stack. The passed &packet_type
 *	is linked into kernel lists and may not be freed until it has been
 *	removed from the kernel lists.
 *
 *	This call does not sleep therefore it can not
 *	guarantee all CPU's that are in middle of receiving packets
 *	will see the new packet type (until the next received packet).
 */

void dev_add_pack(struct packet_type *pt)
{
	int hash;

	// 写操作之间使用自旋锁保护
	spin_lock_bh(&ptype_lock);
	// 如果type为ETH_P_ALL则加入到ptype_all链表中
	if (pt->type == htons(ETH_P_ALL))
		list_add_rcu(&pt->list, &ptype_all);
	else {
		// 否则加入到ptype_base哈希表中,根据type字段进行哈希,并且哈希表的大小为16
		hash = ntohs(pt->type) & PTYPE_HASH_MASK;
		list_add_rcu(&pt->list, &ptype_base[hash]);
	}
	spin_unlock_bh(&ptype_lock);
}

注销: dev_remove_pack()

/**
 *	__dev_remove_pack	 - remove packet handler
 *	@pt: packet type declaration
 *
 *	Remove a protocol handler that was previously added to the kernel
 *	protocol handlers by dev_add_pack(). The passed &packet_type is removed
 *	from the kernel lists and can be freed or reused once this function
 *	returns.
 *
 *      The packet type might still be in use by receivers
 *	and must not be freed until after all the CPU's have gone
 *	through a quiescent state.
 */
void __dev_remove_pack(struct packet_type *pt)
{
	struct list_head *head;
	struct packet_type *pt1;

	spin_lock_bh(&ptype_lock);

	if (pt->type == htons(ETH_P_ALL))
		head = &ptype_all;
	else
		head = &ptype_base[ntohs(pt->type) & PTYPE_HASH_MASK];

	// 遍历链表,匹配地址相同的struct packet_type
	list_for_each_entry(pt1, head, list) {
		if (pt == pt1) {
			list_del_rcu(&pt->list);
			goto out;
		}
	}
	printk(KERN_WARNING "dev_remove_pack: %p not found.\n", pt);
out:
	spin_unlock_bh(&ptype_lock);
}

/**
 *	dev_remove_pack	 - remove packet handler
 *	@pt: packet type declaration
 *
 *	Remove a protocol handler that was previously added to the kernel
 *	protocol handlers by dev_add_pack(). The passed &packet_type is removed
 *	from the kernel lists and can be freed or reused once this function
 *	returns.
 *
 *	This call sleeps to guarantee that no CPU is looking at the packet
 *	type after return.
 */
void dev_remove_pack(struct packet_type *pt)
{
	__dev_remove_pack(pt);
	// 移除后要同步等待其它CPU也已经感知到了该结构的删除
	synchronize_net();
}
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值