Chinaunix首页 | 论坛 | 博客
  • 博客访问: 555435
  • 博文数量: 117
  • 博客积分: 10
  • 博客等级: 民兵
  • 技术积分: 359
  • 用 户 组: 普通用户
  • 注册时间: 2011-03-13 21:58
个人简介

爱上香烟

文章分类

全部博文(117)

文章存档

2018年(3)

2017年(8)

2016年(65)

2015年(41)

我的朋友

分类: C/C++

2015-06-04 15:16:00

转:http://blog.csdn.net/minghe_uestc/article/details/7824768

开场白

传输层常见的两大协议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;

 

[html] view plaincopy
  1.   
  2. struct socket {  
  3.     socket_state        state;   
  4.   
  5.     kmemcheck_bitfield_begin(type);  
  6.     short           type;   
  7.     kmemcheck_bitfield_end(type);  
  8.   
  9.     unsigned long       flags;   
  10.   
  11.     struct socket_wq __rcu  *wq;  
  12.   
  13.     struct file     *file;   
  14.     struct sock     *sk;   
  15.     const struct proto_ops  *ops;   
  16. };  
注: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是和后面的参数是连续的内存的。

 

[html] view plaincopy
  1. struct udp_sock {  
  2.       
  3.     struct inet_sock inet;  
  4. ……………………………………  
  5. };  

 


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

 

[html] view plaincopy
  1. struct inet_sock {  
  2.       
  3.       
  4.     struct sock     sk;  
  5. #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)  
  6.     struct ipv6_pinfo   *pinet6;   
  7. #endif  
  8. ……………………………………  
  9. };  


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

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

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

 

[html] view plaincopy
  1. struct inet_sock *inet = inet_sk(sk);  
  2. struct udp_sock *up = udp_sk(sk);  

 

 

[html] view plaincopy
  1. static inline struct udp_sock *udp_sk(const struct sock *sk)  
  2. {  
  3.     return (struct udp_sock *)sk;  
  4. }  
[html] view plaincopy
  1. static inline struct inet_sock *inet_sk(const struct sock *sk)  
  2. {  
  3.     return (struct inet_sock *)sk;  
  4. }  

 

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

3、struct udp_sock结构

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

 

[html] view plaincopy
  1. struct udp_sock {  
  2.       
  3.     struct inet_sock inet;  
  4. #define udp_port_hash       inet.sk.__sk_common.skc_u16hashes[0]  
  5. #define udp_portaddr_hash   inet.sk.__sk_common.skc_u16hashes[1]  
  6. #define udp_portaddr_node   inet.sk.__sk_common.skc_portaddr_node  
  7.     int      pending;     
  8.     unsigned int     corkflag;    
  9.     __u16        encap_type;      
  10.       
  11.     __u16        len;         
  12.       
  13.     __u16        pcslen;  
  14.     __u16        pcrlen;  
  15.   
  16. #define UDPLITE_BIT      0x1          
  17. #define UDPLITE_SEND_CC  0x2          
  18. #define UDPLITE_RECV_CC  0x4          
  19.     __u8         pcflag;          
  20.     __u8         unused[3];  
  21.       
  22.     int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);  
  23. };  
这里关注2个参数就好了,一个是struct inet_sock inet另一个是int pending这个参数。这个参数代表发送状态(见《Linux内核源码剖析-TCP/IP实现》表33-1):

 


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

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


4 、struct inet_sock结构

 

[html] view plaincopy
  1. struct inet_sock {  
  2.       
  3.       
  4.     struct sock     sk;  
  5. #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)  
  6.     struct ipv6_pinfo   *pinet6;   
  7. #endif  
  8. …………………………  
  9.     struct ip_mc_socklist __rcu *mc_list;  
  10.     struct inet_cork_full   cork;   
  11. };  

 

这个结构关注两个参数,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个参数,

 

[html] view plaincopy
  1. struct sock {  
  2. ……………………  
  3.     struct sk_buff_head sk_receive_queue;   
  4. ………………  
  5.     struct sk_buff_head sk_write_queue;   
  6. ………………  
  7. };  

 

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中相对应的代码

 

[html] view plaincopy
  1. if (connected)  
  2.     rt = (struct rtable *)sk_dst_check(sk, 0);  
  3.   
  4. if (rt == NULL) {  
  5. …………………  
  6.     rt = ip_route_output_flow(net, fl4, sk);  
  7. …………………  
  8.     if (connected)  
  9.         sk_dst_set(sk, dst_clone(&rt->dst));  
  10. }  
对于调用过connect函数的套接口,每次发送报文还是需要检查其路由缓存项的,如果不对就会返回一个null值。所以即使通过connect函数链接的套接字还是可能会进入路由子系统查找的。

 

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

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

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

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

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

代码如下

 

[html] view plaincopy
  1. back_from_confirm:  
  2.   
  3.     saddr = fl4->saddr;  
  4.     if (!ipc.addr)  
  5.         daddr = ipc.addr = fl4->daddr;  
  6.   
  7.       
  8.       
  9.     if (!corkreq) {  
  10.         skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen,  
  11.                   sizeof(struct udphdr), &ipc, &rt,  
  12.                   msg->msg_flags);  
  13.         err = PTR_ERR(skb);  
  14.         if (skb && !IS_ERR(skb))  
  15.             err = udp_send_skb(skb, fl4);  
  16.         goto out;  
  17.     }  
  18.   
  19. ………………  
  20.   
  21. do_append_data:  
  22.     up->len += ulen;  
  23.     err = ip_append_data(sk, fl4, getfrag, msg->msg_iov, ulen,  
  24.                  sizeof(struct udphdr), &ipc, &rt,  
  25.                  corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);  
  26.     if (err)  
  27.         udp_flush_pending_frames(sk);  
  28.     else if (!corkreq)  
  29.         err = udp_push_pending_frames(sk);  
  30.     else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))  
  31.         up->pending = 0;  
  32.     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函数

 

[html] view plaincopy
  1.   
  2. static int udp_push_pending_frames(struct sock *sk)  
  3. {  
  4.     struct udp_sock  *up = udp_sk(sk);  
  5.     struct inet_sock *inet = inet_sk(sk);  
  6.     struct flowi4 *fl4 = &inet->cork.fl.u.ip4;  
  7.     struct sk_buff *skb;  
  8.     int err = 0;  
  9.   
  10.     skb = ip_finish_skb(sk, fl4);  
  11.     if (!skb)  
  12.         goto out;  
  13.   
  14.     err = udp_send_skb(skb, fl4);  
  15.   
  16. out:  
  17.     up->len = 0;  
  18.     up->pending = 0;  
  19.     return err;  
  20. }  
其中:ip_finish_skb对应(C)

 

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

这样一看就简单了。


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


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


四、接收流程

累了,过几天再写

阅读(1736) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~