LINUX网络协议栈--UDP

开场白

传输层常见的两大协议TCP和UDP,TCP太复杂,涉及到拥塞控制的很多内容,在《Linux内核源码剖析-TCP/IP实现》下册中也花费了大量的笔墨来讲述。

咋们先来看看一个简单的UDP。


定位

每篇文章肯定有一个定位,不可能面面俱到,如果这篇的定位是你需要的,祝你能够学到一些新的知识

(1)UDP数据发送和接收的简要流程

(2)不涉及太多细节。

(3)力求了解UDP在协议栈中的框架以及与其他层之间的衔接


参考资料

(1)《Understand Linux Kernel Internel》

(2)《Linux内核源码剖析-TCP/IP实现》

(3)linux内核源码--我使用的版本是3.2.4

注: 《Understand Linux Kernel Internel》中没有关于UDP和TCP的章节,但是有很多知识还是需要的。


一、大蓝图

下图见《Linux内核源码剖析-TCP/IP实现》图22-1


图 1-1

今天讨论的内容在圈圈5下方(也就是proto_ops)下方。这个图信息很多,不过和今天的内容联系的却不多,贴出的原因是希望能够让大家心里有个数。



二、传输控制块

这边简要介绍下,如果想更全面的理解,最好去看看套接口层(见《Linux内核源码剖析-TCP/IP实现》第22~24章)。这里先默认你已经对套接口层有所理解了。

1、struct socket结构

这里我们只要注意1个参数,struct sock *sk;

/**
 *  struct socket - general BSD socket
 *  @state: socket state (%SS_CONNECTED, etc)
 *  @type: socket type (%SOCK_STREAM, etc)
 *  @flags: socket flags (%SOCK_ASYNC_NOSPACE, etc)
 *  @ops: protocol specific socket operations
 *  @file: File back pointer for gc
 *  @sk: internal networking protocol agnostic socket representation
 *  @wq: wait queue for several uses
 */
struct socket {
	socket_state		state; /*套接字状态,见《Linux内核源码剖析-TCP/IP实现》P607 */

	kmemcheck_bitfield_begin(type);
	short			type; /*套接口类型,如SOCK_STREAM,SOCK_DGRAM等 ,见《Linux内核源码剖析-TCP/IP实现》P608*/
	kmemcheck_bitfield_end(type);

	unsigned long		flags; /* 见《Linux内核源码剖析-TCP/IP实现》P607*/

	struct socket_wq __rcu	*wq;

	struct file		*file; /*关联的file指针 */
	struct sock		*sk; /*传输控制块,需要的信息基本都在这个上面 */
	const struct proto_ops	*ops; /*套接字的操作函数 */
};
注:short type;和const struct proto_ops *ops;这两个参数也很重要,不过涉及的内容是套接口层的内容,和今天的内容关系不是很大。

注2:所有的套接字都是使用该结构,那这里就会有一个当深入看的时候让人觉得奇怪的地方。就是这个 struct sock结构


2、传输控制快的的结构

先讨论有一点会比较让人觉得混乱的地方,UDP的传输控制块是struct udp_sock,但是在1、中却使用的是struct sock 结构。

要说明这点,我们先来看看下面这个图(《Linux内核源码剖析-TCP/IP实现》图25-1


图2-1

这个图有一个隐含的信息--这里的内存空间是连续内存,这意味着什么?

就我们先来看看udp_sock的结构体。这里需要关注的是第一个注释,它说struct inet_sock必须在第一个元素。另外需要注意的是这里在声明变量的时候用的不是指针。意味着inet_sock是和后面的参数是连续的内存的。

struct udp_sock {
	/* inet_sock has to be the first member */
	struct inet_sock inet;
……………………………………
};


然后让我们来看看struct inet_sock结构这里还是关注第一个注释内容,struct sock和 struct ipv6_pinfo必须是前两个成员,同时和上面的一样,他们也不是指针。

struct inet_sock {
    /* 为IP协议拓展的传输控制快,提供了IP协议使用的专有属性*/
	/* sk and pinet6 has to be the first two members of inet_sock */
	struct sock		sk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
	struct ipv6_pinfo	*pinet6; /* IPV6协议使用 */
#endif
……………………………………
};


通过这两个说明就容易懂为什么图中的画法为什么是指连续内存了吧。

那这样做又有什么用处呢?又和struct socket中的声明有什么关系呢?原因如下:

我们会经常在代码中见到这样的语句。

	struct inet_sock *inet = inet_sk(sk);
	struct udp_sock *up = udp_sk(sk);

static inline struct udp_sock *udp_sk(const struct sock *sk)
{
	return (struct udp_sock *)sk;
}
static inline struct inet_sock *inet_sk(const struct sock *sk)
{
	return (struct inet_sock *)sk;
}

明明struct inet_sock和struct sock是不同的结构,为什么他们又能够强制类型转换?这就可以去观察图2-1了,原因就在于他们是同一段连续的缓存,这样sk同时是struct sock、struct inet_sock、struct udp_sk的指针。这样也就说的了

3、struct udp_sock结构

咋们只关注UDP协议,所以看结构就从大到小看,和书上的相反,这样可以看的更清晰一些。

struct udp_sock {
	/* inet_sock has to be the first member */
	struct inet_sock inet;
#define udp_port_hash		inet.sk.__sk_common.skc_u16hashes[0]
#define udp_portaddr_hash	inet.sk.__sk_common.skc_u16hashes[1]
#define udp_portaddr_node	inet.sk.__sk_common.skc_portaddr_node
	int		 pending;	/* Any pending frames ? */
	unsigned int	 corkflag;	/* Cork is required */
  	__u16		 encap_type;	/* Is this an Encapsulation socket? */
	/*
	 * Following member retains the information to create a UDP header
	 * when the socket is uncorked.
	 */
	__u16		 len;		/* total length of pending frames */
	/*
	 * Fields specific to UDP-Lite.
	 */
	__u16		 pcslen;
	__u16		 pcrlen;
/* indicator bits used by pcflag: */
#define UDPLITE_BIT      0x1  		/* set by udplite proto init function */
#define UDPLITE_SEND_CC  0x2  		/* set via udplite setsockopt         */
#define UDPLITE_RECV_CC  0x4		/* set via udplite setsocktopt        */
	__u8		 pcflag;        /* marks socket as UDP-Lite if > 0    */
	__u8		 unused[3];
	/*
	 * For encapsulation sockets.
	 */
	int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);
};
这里关注2个参数就好了,一个是struct inet_sock inet另一个是int pending这个参数。这个参数代表发送状态( 《Linux内核源码剖析-TCP/IP实现》表33-1):


注:如果是IPV6协议,那代表正在处理调用sendmsg的状态标志就是AF6_INET

之所以关注这个标志,主要是因为它对后面理解ip_append_data函数的功能有很大的帮助。


4 、struct inet_sock结构

struct inet_sock {
    /* 为IP协议拓展的传输控制快,提供了IP协议使用的专有属性*/
	/* sk and pinet6 has to be the first two members of inet_sock */
	struct sock		sk;
#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
	struct ipv6_pinfo	*pinet6; /* IPV6协议使用 */
#endif
…………………………
	struct ip_mc_socklist __rcu	*mc_list;
	struct inet_cork_full	cork; /*为UDP协议而设计的,里面存储了UDP所需的信息,支持UDPV4和UDPV6 */
};

这个结构关注两个参数,struct sock和struct inet_cork_full cork。cork参数中存放了UDP发送报文时候需要的一些信息。

注:如果是看IPV6的协议,那另外需要关注struct ipv6_pinfo 结构。

注2:这个源代码是3.2.4中的代码,和书上的不一样。它里面包含的内容更多。

注3:inet_sock是为IP协议设计使用的传输控制块,书上说是IPv4专用有些说的不对。


5、struct sock结构

这个结构很大,关注其中2个参数,

struct sock {
……………………
	struct sk_buff_head	sk_receive_queue; /*接收队列 */
………………
	struct sk_buff_head	sk_write_queue; /*发送队列 */
………………
};

UDP和TCP在组织发送队列的方式上不同。


上面的都是一些预热,现在才能转入正题。

《Linux内核源码剖析-TCP/IP实现》图33-3

图3-1

这个图其实就很完整的画出了发送和接收流程的调用图。

注:发送流程中这个图画的有些容易让人误解,而且信息不全。

注2:首先udp_sendmsg最后是先调用ip_append_data函数对数据进行处理,然后调用udp_push_pending_frames进行发送, 图中画法容易让人一位发送函数有两个。

注3:udp_push_pending_frames之后不是直接就到IP层,而过程却是  udp_push_pending_frames >> udp_send_skb >> ip_send_skb >> ip_local_out >> ip_push_pending_data(这个是3.2.4中的流程)

注4:IPV6的udp协议是调用udp_v6_push_pending_frames >> ip6_push_pending_frames >> ip6_local_out。(这个是3.2.4的流程)


三、发送流程

先看下图,《Linux内核源码剖析-TCP/IP实现》图33-9


图3-2

可以返回去看图1-1,我们平时写程序发送一个udp报文常用sendto函数,这个函数是库函数中提供的,进入内核后会调用sys_send函数,这时候才算进入了内核。

这里最后两行需要对套接口层的结构有所了解才能看懂。

暂时记着UDP协议到最后调用的是udp_sendmsg就可以了。

注:如果是ipv6,入口就是udpv6_sendmsg


1、udp_sendmsg

udp_sendmsg流程图如图3-3:《Linux内核源码剖析-TCP/IP实现》图33-10


图3-3 udp_sendmsg流程图

流程图很复杂,代码也很多,咋们抓重点,以及和后面会存在关系的部分类看,但是需要注意的是,这个图画的也不全对,有些地方也给画错了

(A)注意图中判断条件"通过connect()连接过"这个条件。在编程的时候我们有时候也会对udp套接字使用connect函数进行连接,这个就是这里的由来。它会产生什么效果呢?看下相关的说明吧


说白了就是,通过connect函数进行连接的UDP套接字,它可能已经自带了相应的路由项

注意:这里是“可能自带”,不意味着带着的路由项是正确的!!!

所以图中这里的分支是错的。从代码看就更清晰了:以下是udp_sendmsg中相对应的代码

	if (connected)
		rt = (struct rtable *)sk_dst_check(sk, 0);

	if (rt == NULL) {
……………………
		rt = ip_route_output_flow(net, fl4, sk);
……………………
		if (connected)
			sk_dst_set(sk, dst_clone(&rt->dst));
	}
对于调用过connect函数的套接口,每次发送报文还是需要检查其路由缓存项的,如果不对就会返回一个null值。所以即使通过connect函数链接的套接字还是可能会进入路由子系统查找的。

另外经过路由子系统后,对于调用connect函数的套接字就会更新路由项。

所以图中正确的画法应该是,"通过connect()连接过"之后出来的“否“的线应该连接到”从路由子系统中获取目的路由缓存项“。


(B)图3-3最底下部分内容看着也让人混乱,其实说的是两个流程:

一个是,如果设置了corkreq标志,要经过  ip_append_data进行处理后,然后经过udp_push_pending_frames进行发送。

另一个是:如果没有设置corkreq标志,则直接发送。

代码如下

back_from_confirm:

	saddr = fl4->saddr;
	if (!ipc.addr)
		daddr = ipc.addr = fl4->daddr;

	/* Lockless fast path for the non-corking case. */
    /* 如果不设置cork标志,就直接发送。*/
	if (!corkreq) {
		skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen,
				  sizeof(struct udphdr), &ipc, &rt,
				  msg->msg_flags);
		err = PTR_ERR(skb);
		if (skb && !IS_ERR(skb))
			err = udp_send_skb(skb, fl4);
		goto out;
	}

………………

do_append_data:
	up->len += ulen;
	err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen,
			     sizeof(struct udphdr), &ipc, &rt,
			     corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
	if (err)
		udp_flush_pending_frames(sk);
	else if (!corkreq)
		err = udp_push_pending_frames(sk);
	else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
		up->pending = 0;
	release_sock(sk);


2、ip_append_data

这个函数很复杂,请参考http://blog.csdn.net/wearenoth/article/details/7836920

注:这个函数需要认真看看,这样就可以很好的将UDP和IP层之间的数据结构给衔接起来了。


3、udp_push_pending_frames

早先从流程是:填充第四层报头 -->> 计算校验和 -->> 调用 ip_push_pending_frames发送。如下图(见《Understand Linux Kernel Internel》图21-13):


图3-4

然后在ip_push_pending_frames中实现以下两点。

从发送队列中取下skb,如图3-5所示(见《Understand Linux Kernel Internel》图21-12


图3-5

发送skb


可以总结为如下流程:

(A)填充第四层报头

(B)计算校验和

(C)从发送队列中取下skb

(D)发送取下的skb


在3.2.4内核中,这个过程进行了新的排序,并且不再使用ip_push_pending_frames函数

/*
 * Push out all pending data as one UDP datagram. Socket is locked.
 */
static int udp_push_pending_frames(struct sock *sk)
{
	struct udp_sock  *up = udp_sk(sk);
	struct inet_sock *inet = inet_sk(sk);
	struct flowi4 *fl4 = &inet->cork.fl.u.ip4;
	struct sk_buff *skb;
	int err = 0;

	skb = ip_finish_skb(sk, fl4);
	if (!skb)
		goto out;

	err = udp_send_skb(skb, fl4);

out:
	up->len = 0;
	up->pending = 0;
	return err;
}
其中:ip_finish_skb对应(C)

udp_send_msg对应(A)(B)(D)

这样一看就简单了。


最后报文都会流向dst_output函数,之后的内容可以参考http://blog.csdn.net/wearenoth/article/details/7819925


整体上,UDP协议发送的流程就是这样,其中大量细节都没说明,一是因为有书比我说的好,另外是真写下来内容就太多了。


四、接收流程

累了,过几天再写



































你可能感兴趣的:(linux)