基于Linux内核的UDP协议源码分析

版权声明:欢迎转载,但请注明出处https://me.csdn.net/qq_28753373 。谢谢! https://blog.csdn.net/qq_28753373/article/details/85119238

  当一个数据包(package)经过IP层的处理之后,最终调用ip_local_deliever()函数,这个函数会根据这个数据包(packet)的传输层头儿确定其采用的传输协议,如果是UDP协议,将会调用udp_rcv()函数。至此,进入传输层的范围。
UDP协议栈的报头定义如下:

struct udphdr {  
  unsigned short    source;//源端口   
  unsigned short    dest;//目的端口   
  unsigned short    len;//数据包长度   
  unsigned short    check;//检验和   
}; 

  udp_rcv()函数的源代码如下:

/*
 *	All we need to do is get the socket, and then do a checksum. 
 */
 
int udp_rcv(struct sk_buff *skb, struct device *dev, struct options *opt,
	unsigned long daddr, unsigned short len,
	unsigned long saddr, int redo, struct inet_protocol *protocol)
{
  	struct sock *sk;
  	struct udphdr *uh;
	unsigned short ulen;
	int addr_type = IS_MYADDR;
	
	if(!dev || dev->pa_addr!=daddr) //检查这个数据包是不是发送给本地的数据包
		addr_type=ip_chk_addr(daddr); //该函数定义在devinet.c中,用于检查ip地址是否是本地或多播、广播地址
		
	/*
	 *	Get the header.
	 */
  	uh = (struct udphdr *) skb->h.uh; //获得UDP数据报的报头   
  	
  	ip_statistics.IpInDelivers++;

	/*
	 *	Validate the packet and the UDP length.
	 */
	 
	ulen = ntohs(uh->len);
    //参数len表示ip负载长度(IP数据报的数据部分长度)= UDP数据包头+UDP数据包的数据部分+填充部分长度   
    //ulen表示的是UDP数据报首部和负载部分的长度,所以正常情况下len>=ulen  
	if (ulen > len || len < sizeof(*uh) || ulen < sizeof(*uh))  //进行UDP数据包校验 
	{
		printk("UDP: short packet: %d/%d\n", ulen, len);
		udp_statistics.UdpInErrors++;
		kfree_skb(skb, FREE_WRITE);
		return(0);
	}

	if (uh->check && udp_check(uh, len, saddr, daddr)) 
	{
		/* <[email protected]> wants to know, who sent it, to
		   go and stomp on the garbage sender... */
		printk("UDP: bad checksum. From %08lX:%d to %08lX:%d ulen %d\n",
		       ntohl(saddr),ntohs(uh->source),
		       ntohl(daddr),ntohs(uh->dest),
		       ulen);
		udp_statistics.UdpInErrors++;
		kfree_skb(skb, FREE_WRITE);
		return(0);
	}


	len=ulen; //对len赋值为实际的UDP数据报长度

#ifdef CONFIG_IP_MULTICAST
	if (addr_type!=IS_MYADDR)
	{
		/*
		 *	Multicasts and broadcasts go to each listener.
		 */
		struct sock *sknext=NULL; //next指针
		/*get_sock_mcast 获取在对应端口的多播套接字队列 
        *下面函数的参数依次表示:sock结构指针,本地端口,远端地址,远端端口,本地地址 
        */  
		sk=get_sock_mcast(udp_prot.sock_array[ntohs(uh->dest)&(SOCK_ARRAY_SIZE-1)], uh->dest,
				saddr, uh->source, daddr);
		if(sk)
		{		
			do
			{
				struct sk_buff *skb1;

				sknext=get_sock_mcast(sk->next, uh->dest, saddr, uh->source, daddr); //下一个满足条件的套接字
				if(sknext)
					skb1=skb_clone(skb,GFP_ATOMIC);
				else
					skb1=skb;
				if(skb1)
					udp_deliver(sk, uh, skb1, dev,saddr,daddr,len); //对满足条件的套接字调用发送函数发送
				sk=sknext;
			}
			while(sknext!=NULL);
		}
		else
			kfree_skb(skb, FREE_READ);
		return 0;
	}	
#endif
  	sk = get_sock(&udp_prot, uh->dest, saddr, uh->source, daddr);
	if (sk == NULL)  //没有找到本地对应的套接字,则进行出错处理
  	{
  		udp_statistics.UdpNoPorts++;
		if (addr_type == IS_MYADDR) 
		{
			icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0, dev); //回复ICMP出错报文,目的主机不可达
		}
		/*
		 * Hmm.  We got an UDP broadcast to a port to which we
		 * don't wanna listen.  Ignore it.
		 */
		skb->sk = NULL;
		kfree_skb(skb, FREE_WRITE);
		return(0);
  	}

	return udp_deliver(sk,uh,skb,dev, saddr, daddr, len); //调用函数发送套接字
}

  这里首先会获取到这个pakcet的UDP头部信息(582-584),同时获取到UDP的长度(590),接着根据长度判断这个UDP包是否坏了(592-598),如果坏了,直接把这个包对应的内存空间释放(596 kfree_skb(skb, FREE_WRITE);)。如果通过了,就做UDP的checksum(600~611),如果checksum通不过,就把这个包对应的内存空间释放(609).

  从617行开始,根据之前取得得包头信息判断这个包是否要进行mcast,如果不是(这里暂时不讨论mcast的情况),接着走下去。

  从647行开始UDP数据包需要找到对应的“寄生体”,也就是struct sock *结构。sock将是这个数据包在linux内核里的最终归宿(它的内核之旅将在这里终结)。
  一个数据包如何找到它对应的sock结构?这需要两个要点:1、搜索的对象。2、搜索的索引。

sk = get_sock(&udp_prot, uh->dest, saddr, uh->source, daddr);

  这行代码表示了搜索的对象是udp_prot,搜索的索引条件包括目的端口、源地址、源端口、目的地址。该函数定义的位置在文件af_inet.c中,具体代码为:

/* 
 *  Deliver a datagram to broadcast/multicast sockets. 
 */  
   
struct sock *get_sock_mcast(struct sock *sk, //套接字指针   
     unsigned short num,//本地端口   
     unsigned long raddr,//远端地址   
     unsigned short rnum,//远端端口   
     unsigned long laddr)//本地地址    
{  
    struct sock *s;  
    unsigned short hnum;  
  
    hnum = ntohs(num);  
  
    /* 
     * SOCK_ARRAY_SIZE must be a power of two.  This will work better 
     * than a prime unless 3 or more sockets end up using the same 
     * array entry.  This should not be a problem because most 
     * well known sockets don't overlap that much, and for 
     * the other ones, we can just be careful about picking our 
     * socket number when we choose an arbitrary one. 
     */  
      
    s=sk;  
  
    for(; s != NULL; s = s->next)   
    {  
        if (s->num != hnum) //本地端口不符合,跳过   
            continue;  
        if(s->dead && (s->state == TCP_CLOSE))//dead=1表示该sock结构已经处于释放状态   
            continue;  
        if(s->daddr && s->daddr!=raddr)//sock的远端地址不等于条件中的远端地址   
            continue;  
        if (s->dummy_th.dest != rnum && s->dummy_th.dest != 0)  
            continue;  
        if(s->saddr  && s->saddr!=laddr)//sock的本地地址不等于条件的本地地址   
            continue;  
        return(s);  
    }  
    return(NULL);  
}  

  找到sk之后,就可以把对应的数据包挂接到sk上去了(调用udp_deliver(sk,uh,skb,dev, saddr, daddr, len) 664)。若没有找到,说明出错了,对于UDP而言,出错了就把数据包删除,也就是直接把数据包所有的内存都释放掉。(kfree_skb(skb, FREE_WRITE); 660)
  上述过程中调用了udp_deliver函数,下面分析udp_deliver函数:

static int udp_deliver(struct sock *sk,  //sock结构指针
			struct udphdr *uh, //UDP头指针
 			struct sk_buff *skb,  //sk_buff
 			struct device *dev,  //接收的网络设备
			 long saddr,  //本地地址
 			long daddr, //远端地址
 			int len) //数据包的长度
{
    //对skb结构相应字段赋值
	skb->sk = sk;
	skb->dev = dev;
	skb->len = len;

	/*
	 *	These are supposed to be switched. 
	 */
	 
	skb->daddr = saddr;
	skb->saddr = daddr;


	/*
	 *	Charge it to the socket, dropping if the queue is full.
	 */

	skb->len = len - sizeof(*uh);  
	 
	if (sock_queue_rcv_skb(sk,skb)<0)  //调用sock_queu_rcv_skb()函数,将skb挂到sk接构中的接收队列中
	{
		udp_statistics.UdpInErrors++;
		ip_statistics.IpInDiscards++;
		ip_statistics.IpInDelivers--;
		skb->sk = NULL;
		kfree_skb(skb, FREE_WRITE);
		release_sock(sk);
		return(0);
	}
  	udp_statistics.UdpInDatagrams++;
	release_sock(sk);
	return(0);
}

  这个函数的关键部分是: if (sock_queue_rcv_skb(sk,skb)<0) (687)。这行代码调用了sock_queue_rcv_skb函数,这个函数是传输层的关键,其实也就是把skb与struct sock *sk关联起来,把skb放入sk里的skb接收队里。
  下面分析sock_queue_rcv_skb函数:

/*
 *	Queue a received datagram if it will fit. Stream and sequenced protocols
 *	can't normally use this as they need to fit buffers in and play with them.
 */

int sock_queue_rcv_skb(struct sock *sk, struct sk_buff *skb)
{
	unsigned long flags;
	if(sk->rmem_alloc + skb->mem_len >= sk->rcvbuf)
		return -ENOMEM;
	save_flags(flags);
	cli();
	sk->rmem_alloc+=skb->mem_len;
	skb->sk=sk;
	restore_flags(flags);
	skb_queue_tail(&sk->receive_queue,skb);
	if(!sk->dead)
		sk->data_ready(sk,skb->len);
	return 0;
}

  到这里,struct sk_buff *skb就与struct sock *sk关联上了,而struct socket *sock又包含了struct sock *sk,所以,用户可以通过socket系统调用操作对应的数据包了。 其实UDP简单概括起来就是把数据从IP层挂接到sock结构上去。因为简单,所以性能比TCP要好很多,在一些对性能要求很高,同时对质量要求不怎么高的情况下,非常适用,比如视频、聊天信息等。

猜你喜欢

转载自blog.csdn.net/qq_28753373/article/details/85119238