UDP协议接受数据流程

当数据达到网络层时,根据IP协议头中的protocol数据域协议码在全局哈希表inet_protos[MAX_INET_PROTOS]中寻找传输层的收包函数,前面介绍UDP基本数据结构时说了了UDP和网络层的接口数据结构,AF_INET协议族初始化函数inet_init中调用inet_add_protoc函数把UDP协议的实例struct net_protocol udp_protocol注册到哈希表inet_protos中。UDP接受网络层的函数是udp_rcv,udp_rcv是__udp4_lib_rcv的封装。

static const struct net_protocol udp_protocol = {
	.handler =	udp_rcv,			//接受IP层数据包函数
	.err_handler =	udp_err,			//icmp错误处理函数
	.gso_send_check = udp4_ufo_send_check,
	.gso_segment = udp4_ufo_fragment,
	.no_policy =	1,
	.netns_ok =	1,
};

注册UDP协议到inet_protos全局哈希表中:

//注册udp处理函数
	if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
		printk(KERN_CRIT "inet_init: Cannot add UDP protocol\n");

1、__udp4_lib_rcv函数

1..1、数据包正确性检查

首先要检查udp协议头的正确性,调用pskb_may_pull函数,然后是检查数据包长度,如果数据包不合法就直接扔掉。

1.2、根据数据包目的地址决定如何发送

根据路由判断数据包的接受地址是组地址或者是广播地址,则调用__udp_lib_mcast_deliver函数完成接受,如果不是组发送地址、广播地址就调用__udp4_lib_lookup_skb根据目的端口在UDP哈希表中寻找打卡的套接字,如果找到了就调用udp_queue_rcv_skb发送给套接字的接受缓冲区,udp_queue_rcv_skb返回值大于0时需要告诉应用程序重新提交输入数据包。

1.3、没有打开的UDP套接字

根据目的端口在UDP套接字哈希表中没有找到打开的套接字就要对数据包在进行校验和计算,如果校验和检查不正确就直接扔掉数据包,也不给对方返回错误信息,因为UDP协议是不可靠的协议,如果校验和正确则更新UDP_MIB_NOPORTS错误统计信息,向数据包发送端返回icmp错误信息,告知端口不可达。

__udp4_lib_rcv代码如下:

int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable,
		   int proto)
{
	//指向udp套接字数据结构体指针
	struct sock *sk;
	//局部变量指向udp协议头
	struct udphdr *uh;
	//udp数据长度
	unsigned short ulen;
	struct rtable *rt = skb_rtable(skb);
	__be32 saddr, daddr;
	struct net *net = dev_net(skb->dev);

	/*
	 *  Validate the packet.
	 */
	 //udp协议头检查
	if (!pskb_may_pull(skb, sizeof(struct udphdr)))
		goto drop;		/* No space for header. */
	//获取udp头
	uh   = udp_hdr(skb);
	//获取数据包长度
	ulen = ntohs(uh->len);
	//源地址
	saddr = ip_hdr(skb)->saddr;
	//目的地址
	daddr = ip_hdr(skb)->daddr;
	//检查长度是否正确
	if (ulen > skb->len)
		goto short_packet;
	//协议号是udp
	if (proto == IPPROTO_UDP) {
		/* UDP validates ulen. */
		//校验和检查
		if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen))
			goto short_packet;
		uh = udp_hdr(skb);
	}

	if (udp4_csum_init(skb, uh, proto))
		goto csum_error;

	//如果路由标志是广播地址或者组播地址
	if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
		return __udp4_lib_mcast_deliver(net, skb, uh,
				saddr, daddr, udptable);
	//寻找打开的套接字
	sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
	//找到了打开的套接字
	if (sk != NULL) {
		//将数据包传给套接字缓冲区
		int ret = udp_queue_rcv_skb(sk, skb);
		//释放套接字
		sock_put(sk);

		/* a return value > 0 means to resubmit the input, but
		 * it wants the return to be -protocol, or 0
		 */
		 //返回值大于0需要告诉调用程序重新提交输入数据包
		if (ret > 0)
			return -ret;
		return 0;
	}

	if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
		goto drop;
	nf_reset(skb);

	/* No socket. Drop packet silently, if checksum is wrong */
	//完成数据包校验和检查
	//校验错误直接丢包,也不给对端返回错误信息
	if (udp_lib_checksum_complete(skb))
		goto csum_error;

	//更新UDP_MIB_NOPORTS统计信息
	UDP_INC_STATS_BH(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
	//返回icmp错误信息,告知目的端口不可达
	icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

	/*
	 * Hmm.  We got an UDP packet to a port to which we
	 * don't wanna listen.  Ignore it.
	 */
	kfree_skb(skb);
	return 0;

short_packet:
	LIMIT_NETDEBUG(KERN_DEBUG "UDP%s: short packet: From %pI4:%u %d/%d to %pI4:%u\n",
		       proto == IPPROTO_UDPLITE ? "-Lite" : "",
		       &saddr,
		       ntohs(uh->source),
		       ulen,
		       skb->len,
		       &daddr,
		       ntohs(uh->dest));
	goto drop;

csum_error:
	/*
	 * RFC1122: OK.  Discards the bad packet silently (as far as
	 * the network is concerned, anyway) as per 4.1.3.4 (MUST).
	 */
	LIMIT_NETDEBUG(KERN_DEBUG "UDP%s: bad checksum. From %pI4:%u to %pI4:%u ulen %d\n",
		       proto == IPPROTO_UDPLITE ? "-Lite" : "",
		       &saddr,
		       ntohs(uh->source),
		       &daddr,
		       ntohs(uh->dest),
		       ulen);
drop:
	UDP_INC_STATS_BH(net, UDP_MIB_INERRORS, proto == IPPROTO_UDPLITE);
	kfree_skb(skb);
	return 0;
}

2、将数据包加入套接字接受队列处理函数

__udp4_lib_lookup_skb函数以端口在UDP哈希表中查找打开的套接字,如果有就调用udp_queue_rcv_skb函数把数据包加入到套接字接受队列中。udp_queue_rcv_skb函数处理分三部分,

a、首先查看套接字队列是否满了,如果满了就丢掉数据包。

b、判断该套接字是否是IPSec协议头封装的套接字,如果是就调用up->encap_rcv处理ipsec协议数据包。

c、判断是否有应用程序等待接受数据包,如果有就将数据包放入套接字接受缓冲区,如果没有就把套接字加入backlog队列。

udp_rcv_skb函数源码:

int udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
	struct udp_sock *up = udp_sk(sk);
	int rc;
	int is_udplite = IS_UDPLITE(sk);

	/*
	 *	Charge it to the socket, dropping if the queue is full.
	 */
	 //检查队列是否满了
	if (!xfrm4_policy_check(sk, XFRM_POLICY_IN, skb))
		goto drop;
	nf_reset(skb);

	if (up->encap_type) {
		
		//数据包是封装数据包
		/* if we're overly short, let UDP handle it */
		if (skb->len > sizeof(struct udphdr) &&
		    up->encap_rcv != NULL) {
			int ret;
			//由ipsec协议处理函数处理封装数据包
			ret = (*up->encap_rcv)(sk, skb);
			if (ret <= 0) {
				UDP_INC_STATS_BH(sock_net(sk),
						 UDP_MIB_INDATAGRAMS,
						 is_udplite);
				return -ret;
			}
		}

		/* FALLTHROUGH -- it's a UDP Packet */
	}

	/*
	 * 	UDP-Lite specific tests, ignored on UDP sockets
	 */
	if ((is_udplite & UDPLITE_RECV_CC)  &&  UDP_SKB_CB(skb)->partial_cov) {

		
		if (up->pcrlen == 0) {          /* full coverage was set  */
			LIMIT_NETDEBUG(KERN_WARNING "UDPLITE: partial coverage "
				"%d while full coverage %d requested\n",
				UDP_SKB_CB(skb)->cscov, skb->len);
			goto drop;
		}
		
		if (UDP_SKB_CB(skb)->cscov  <  up->pcrlen) {
			LIMIT_NETDEBUG(KERN_WARNING
				"UDPLITE: coverage %d too small, need min %d\n",
				UDP_SKB_CB(skb)->cscov, up->pcrlen);
			goto drop;
		}
	}

	if (sk->sk_filter) {
		if (udp_lib_checksum_complete(skb))
			goto drop;
	}

	if (sk_rcvqueues_full(sk, skb))
		goto drop;

	rc = 0;

	bh_lock_sock(sk);
	//套接字上有用户进程在等待接受数据包
	//就将数据包放入套接字接受缓冲区
	if (!sock_owned_by_user(sk))
		rc = __udp_queue_rcv_skb(sk, skb);
	//套接字上没有用户进程等待接受数据
	//就将数据包放入backlog队列
	else if (sk_add_backlog(sk, skb)) {
		bh_unlock_sock(sk);
		goto drop;
	}
	bh_unlock_sock(sk);

	return rc;

drop:
	UDP_INC_STATS_BH(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
	atomic_inc(&sk->sk_drops);
	kfree_skb(skb);
	return -1;
}

3、upd协议接受广播与组播发送数据包

如果目的地址是广播地址或者组地址那么一个主机上可以能有多个目的端口等待接受数据包,组接受函数是__udp4_lib_mcast_deliver,将数据包分发给所有监听的套接字,处理流程是先找到第一个有效的套接字,如果该套接字的端口和数据包UDP协议头中目标端口匹配,然后遍历UDP协议的哈希链表查找每一个监听的套接字,并把克隆好的数据包放入监听的套接字接受缓冲区队列中,如果udp_queue_rcv_skb函数返回值大于0,在源码中提示重新处理数据包而不是扔掉数据包。

__udp4_lib_mcast_deliver函数源码:

static int __udp4_lib_mcast_deliver(struct net *net, struct sk_buff *skb,
				    struct udphdr  *uh,
				    __be32 saddr, __be32 daddr,
				    struct udp_table *udptable)
{
	struct sock *sk, *stack[256 / sizeof(struct sock *)];
	struct udp_hslot *hslot = udp_hashslot(udptable, net, ntohs(uh->dest));
	int dif;
	unsigned int i, count = 0;

	spin_lock(&hslot->lock);
	//在udp哈希表中找到第一个匹配的目的套接字
	sk = sk_nulls_head(&hslot->head);
	dif = skb->dev->ifindex;
	sk = udp_v4_mcast_next(net, sk, uh->dest, daddr, uh->source, saddr, dif);
	while (sk) {
		//复制套接字到缓冲区队列
		stack[count++] = sk;
		//遍历udp哈希链表
		sk = udp_v4_mcast_next(net, sk_nulls_next(sk), uh->dest,
				       daddr, uh->source, saddr, dif);
		if (unlikely(count == ARRAY_SIZE(stack))) {
			if (!sk)
				break;
			//有打开的套接字
			flush_stack(stack, count, skb, ~0);
			count = 0;
		}
	}
	/*
	 * before releasing chain lock, we must take a reference on sockets
	 */
	for (i = 0; i < count; i++)
		sock_hold(stack[i]);

	spin_unlock(&hslot->lock);

	/*
	 * do the slow work with no lock held
	 */
	if (count) {
		flush_stack(stack, count, skb, count - 1);

		for (i = 0; i < count; i++)
			sock_put(stack[i]);
	} else {
		kfree_skb(skb);
	}
	return 0;
}

4、UDP的哈希表

一个应用层程序在打开SOCK_DGRAM套接字,这类套接字可以在各种地址和端口的组合套接字上,__udp4_lib_rcv函数的数据包可能需要传送给多个监听的套接字,为了提高网络性能,linux使用UDP哈希表来快速查找监听中的套接字。

4.1 UDP哈希链表数据结构

UDP哈希表有两个,一个是目的端口哈希表,另一个是目的端口和目的IP哈希表

struct udp_table:

/**
 *	struct udp_table - UDP table
 *
 *	@hash:	hash table, sockets are hashed on (local port)
 *	@hash2:	hash table, sockets are hashed on (local port, local address)
 *	@mask:	number of slots in hash tables, minus 1
 *	@log:	log2(number of slots in hash table)
 */
struct udp_table {
	struct udp_hslot	*hash;		//目的端口哈希表
	struct udp_hslot	*hash2;		//目的端口、目的ip哈希表
	unsigned int		mask;		//哈希表槽数
	unsigned int		log;
};

struct udp_hslot:

/**
 *	struct udp_hslot - UDP hash slot
 *
 *	@head:	head of list of sockets
 *	@count:	number of sockets in 'head' list
 *	@lock:	spinlock protecting changes to head/count
 */
struct udp_hslot {
	struct hlist_nulls_head	head;		//套接字链表
	int			count;				//套接字数量
	spinlock_t		lock;			//锁
} __attribute__((aligned(2 * sizeof(long))));

哈希表匹配函数是__udp4_lib_lookup_skb,首先根据目的端口匹配,如果匹配到了就把数据包放入套接字接受缓冲区,如果没有匹配到再调用compute_core匹配目的地址、目的端口、网络设备接口等多个条件。

__udp4_lib_lookup_skb:

static struct sock *__udp4_lib_lookup(struct net *net, __be32 saddr,
		__be16 sport, __be32 daddr, __be16 dport,
		int dif, struct udp_table *udptable)
{
	struct sock *sk, *result;
	struct hlist_nulls_node *node;
	//获取目的端口号
	unsigned short hnum = ntohs(dport);
	//根据目的端口查找
	unsigned int hash2, slot2, slot = udp_hashfn(net, hnum, udptable->mask);
	struct udp_hslot *hslot2, *hslot = &udptable->hash[slot];
	int score, badness;

	rcu_read_lock();
	if (hslot->count > 10) {
		//目的地址哈希值
		hash2 = udp4_portaddr_hash(net, daddr, hnum);
		slot2 = hash2 & udptable->mask;
		hslot2 = &udptable->hash2[slot2];
		if (hslot->count < hslot2->count)
			goto begin;

		//源地址哈希值
		result = udp4_lib_lookup2(net, saddr, sport,
					  daddr, hnum, dif,
					  hslot2, slot2);
		if (!result) {
			hash2 = udp4_portaddr_hash(net, htonl(INADDR_ANY), hnum);
			slot2 = hash2 & udptable->mask;
			hslot2 = &udptable->hash2[slot2];
			if (hslot->count < hslot2->count)
				goto begin;

			result = udp4_lib_lookup2(net, saddr, sport,
						  htonl(INADDR_ANY), hnum, dif,
						  hslot2, slot2);
		}
		rcu_read_unlock();
		return result;
	}
begin:
	result = NULL;
	badness = -1;
	sk_nulls_for_each_rcu(sk, node, &hslot->head) {
		score = compute_score(sk, net, saddr, hnum, sport,
				      daddr, dport, dif);
		if (score > badness) {
			result = sk;
			badness = score;
		}
	}
	/*
	 * if the nulls value we got at the end of this lookup is
	 * not the expected one, we must restart lookup.
	 * We probably met an item that was moved to another chain.
	 */
	 //哈希槽中的hash值和搜索的hash值不匹配则重新搜索
	if (get_nulls_value(node) != slot)
		goto begin;

	if (result) {
		//找到可能的套接字引用数加1
		if (unlikely(!atomic_inc_not_zero(&result->sk_refcnt)))
			result = NULL;
		else if (unlikely(compute_score(result, net, saddr, hnum, sport,
				  daddr, dport, dif) < badness)) {
			sock_put(result);
			goto begin;
		}
	}
	rcu_read_unlock();
	return result;
}

5、UDP协议在套接字层接受处理

当应用层程序调用read函数读取数据时,套接字层调用的是struct proto数据结构中的rcvmsg函数指针指向的udp_rcvmsg函数,udp_rcvmsg函数完成套接字层的数据接受。

5.1、udp_rcvmsg函数输入参数

struct kiocb *iocb:应用层I/O控制缓冲区。

struct sock *sk:执行接受数据包的套接字结构。

struct msghdr *msg:保存数据包一些控制信息和目的地址。

size_t len:数据包长度。

int noblock:应用层层序阻塞标志。

int flag:套接字接受队列中的数据包信息标志。

int *addr_len:应用层存放发送方地址长度。

int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int noblock, int flags, int *addr_len)
{

5.2、函数处理流程

首先设置用户地址空间存放发送端地址长度的参数,查看套接字的错误信息队列中是否有错误信息需要处理,如果错误信息队列中有数据就调用ip_rcv_error函数来处理错误信息。

int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int noblock, int flags, int *addr_len)
{
	struct inet_sock *inet = inet_sk(sk);
	struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;
	struct sk_buff *skb;
	unsigned int ulen;
	int peeked;
	int err;
	int is_udplite = IS_UDPLITE(sk);
	bool slow;

	/*
	 *	Check any passed addresses
	 */
	 //获取发送端源地址长度
	if (addr_len)
		*addr_len = sizeof(*sin);

	//套接字接受队列是否有错误信息要处理
	if (flags & MSG_ERRQUEUE)
		return ip_recv_error(sk, msg, len);

...

}

然后调用接受函数__skb_rcv_datagram函数从套接字缓冲区队列中读取下一个数据包的缓冲区,如果该缓冲区的首地址存放在局部变量sk中,如果skb指针为空说明套接字接受缓冲区没有等待读入的数据,函数结束处理。

...

	skb = __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0),
				  &peeked, &err);
	if (!skb)
		goto out;
	//从数据包中扔掉UDP协议头
	ulen = skb->len - sizeof(struct udphdr);
	
	if (len > ulen)
		len = ulen;
	else if (len < ulen)
		msg->msg_flags |= MSG_TRUNC;

...

查看用户程序是否需要读取比当前数据包负载中更多的数据,在网络数据包的处理过程中一个重要原则是避免对数据包进行多次处理,提高网络数据吞吐量。在接受网络数据包过程中如果要对数据包全校验和检验,则应在复制数据的同时完成校验和处理。

如果只是对数据包进行部分校验和检验,则应该在数据复制前完成校验和检验。

处理过程:

(1)、对数据包进行部分校验和检验,如果部分校验和出错,则应该扔掉数据包。

(2)、如果不需要对数据包进行校验和,则直接调用skb_copy_datagram_iovec函数把skb中的数据从内核地址空间复制到用户地址空间

(3)、如果需要对数据包进行校验和检验,则调用skb_copy_and_csum_datagram_iovec函数将数据包从内核地址空间复制到

用户地址空间同时完成检验和验证计数。

...

if (len < ulen || UDP_SKB_CB(skb)->partial_cov) {
		//只做部分校验和
		if (udp_lib_checksum_complete(skb))
			goto csum_copy_err;
	}

	//不做校验和直接拷贝数据包
	if (skb_csum_unnecessary(skb))
		err = skb_copy_datagram_iovec(skb, sizeof(struct udphdr),
					      msg->msg_iov, len);
	else {
		//做校验和,在复制数据时完成校验和计算
		err = skb_copy_and_csum_datagram_iovec(skb,
						       sizeof(struct udphdr),
						       msg->msg_iov);

		if (err == -EINVAL)
			goto csum_copy_err;
	}

...

标记数据包接受的时间戳,如果应用程序提供有效的缓冲区sin来接受数据包发送端源IP和端口号,就要把数据包数据包的源地址和端口复制到sin指定的用户缓冲区。

...

//标记数据包的时间戳
	sock_recv_ts_and_drops(msg, sk, skb);

	/* Copy the address. */
	//如果有缓冲区sin复制发送端IP和端口
	if (sin) {
		sin->sin_family = AF_INET;
		sin->sin_port = udp_hdr(skb)->source;
		sin->sin_addr.s_addr = ip_hdr(skb)->saddr;
		memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
	}

...

接下来查看控制信息标志cmsg_flags查看是否设置了IP套接字选项,如果设置了则调用ip_cmsg_rev函数完成对IP选项值的提取。

...

//有ip选项则提取ip选项
	if (inet->cmsg_flags)
		ip_cmsg_recv(msg, skb);

	err = len;
	if (flags & MSG_TRUNC)
		err = ulen;

...

错误处理有三个:

(1)、如果从内核地址空间复制数据包到用户地址空间不成功,则释放socket buffer和用户进程持有的套接字。

(2)、如果套接字接受队列为空直接返回。

(3)、当校验和出错扔掉数据包,更新错误统计信息,释放用户进程持有的套接字。

udp_msgrcv函数源码:

int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int noblock, int flags, int *addr_len)
{
	struct inet_sock *inet = inet_sk(sk);
	struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;
	struct sk_buff *skb;
	unsigned int ulen;
	int peeked;
	int err;
	int is_udplite = IS_UDPLITE(sk);
	bool slow;

	/*
	 *	Check any passed addresses
	 */
	 //获取发送端源地址长度
	if (addr_len)
		*addr_len = sizeof(*sin);

	//套接字接受队列是否有错误信息要处理
	if (flags & MSG_ERRQUEUE)
		return ip_recv_error(sk, msg, len);

try_again:
	skb = __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0),
				  &peeked, &err);
	if (!skb)
		goto out;
	//从数据包中扔掉UDP协议头
	ulen = skb->len - sizeof(struct udphdr);
	
	if (len > ulen)
		len = ulen;
	else if (len < ulen)
		msg->msg_flags |= MSG_TRUNC;

	/*
	 * If checksum is needed at all, try to do it while copying the
	 * data.  If the data is truncated, or if we only want a partial
	 * coverage checksum (UDP-Lite), do it before the copy.
	 */

	if (len < ulen || UDP_SKB_CB(skb)->partial_cov) {
		//只做部分校验和
		if (udp_lib_checksum_complete(skb))
			goto csum_copy_err;
	}

	//不做校验和直接拷贝数据包
	if (skb_csum_unnecessary(skb))
		err = skb_copy_datagram_iovec(skb, sizeof(struct udphdr),
					      msg->msg_iov, len);
	else {
		//做校验和,在复制数据时完成校验和计算
		err = skb_copy_and_csum_datagram_iovec(skb,
						       sizeof(struct udphdr),
						       msg->msg_iov);

		if (err == -EINVAL)
			goto csum_copy_err;
	}

	if (err)
		goto out_free;

	if (!peeked)
		UDP_INC_STATS_USER(sock_net(sk),
				UDP_MIB_INDATAGRAMS, is_udplite);

	//标记数据包的时间戳
	sock_recv_ts_and_drops(msg, sk, skb);

	/* Copy the address. */
	//如果有缓冲区sin复制发送端IP和端口
	if (sin) {
		sin->sin_family = AF_INET;
		sin->sin_port = udp_hdr(skb)->source;
		sin->sin_addr.s_addr = ip_hdr(skb)->saddr;
		memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
	}
	//有ip选项则提取ip选项
	if (inet->cmsg_flags)
		ip_cmsg_recv(msg, skb);

	err = len;
	if (flags & MSG_TRUNC)
		err = ulen;
//从内核空间复制数据包到应用层空间不成功就是否socket buffer
out_free:
	skb_free_datagram_locked(sk, skb);
	//队列为空直接返回
out:
	return err;

//校验和出错扔掉数据包,更新错误统计信息
csum_copy_err:
	slow = lock_sock_fast(sk);
	if (!skb_kill_datagram(sk, skb, flags))
		UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
	unlock_sock_fast(sk, slow);

	if (noblock)
		return -EAGAIN;
	goto try_again;
}

猜你喜欢

转载自blog.csdn.net/City_of_skey/article/details/84404890