linux网络协议栈的实现udp,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

a4c26d1e5885305701be709a3d33442f.png

图 1-1

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

二、传输控制块

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

1、struct

socket结构

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

struct socket {

socket_state state;

kmemcheck_bitfield_begin(type);

short type;

kmemcheck_bitfield_end(type);

unsigned long flags;

struct socket_wq __rcu *wq;

struct 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)

a4c26d1e5885305701be709a3d33442f.png

图2-1

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

就我们先来看看udp_sock的结构体。这里需要关注的是第一个注释,它说struct

inet_sock必须在第一个元素。另外需要注意的是这里在声明变量的时候用的不是指针。意味着inet_sock是和后面的参数是连续的内存的。

struct udp_sock {

struct inet_sock inet;

……………………………………

};

然后让我们来看看struct

inet_sock结构这里还是关注第一个注释内容,struct sock和 struct

ipv6_pinfo必须是前两个成员,同时和上面的一样,他们也不是指针。

struct inet_sock {

struct sock sk;

#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)

struct ipv6_pinfo *pinet6;

#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 {

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;

unsigned int corkflag;

__u16 encap_type;

__u16 len;

__u16 pcslen;

__u16 pcrlen;

#define UDPLITE_BIT 0x1

#define UDPLITE_SEND_CC 0x2

#define UDPLITE_RECV_CC 0x4

__u8 pcflag;

__u8 unused[3];

int (*encap_rcv)(struct sock *sk, struct sk_buff *skb);

};

这里关注2个参数就好了,一个是struct

inet_sock inet另一个是int pending这个参数。这个参数代表发送状态(见《Linux内核源码剖析-TCP/IP实现》表33-1):

a4c26d1e5885305701be709a3d33442f.png

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

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

4 、struct

inet_sock结构

struct inet_sock {

struct sock sk;

#if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)

struct ipv6_pinfo *pinet6;

#endif

…………………………

struct ip_mc_socklist __rcu *mc_list;

struct inet_cork_full cork;

};

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

a4c26d1e5885305701be709a3d33442f.png

图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:

a4c26d1e5885305701be709a3d33442f.png

图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

a4c26d1e5885305701be709a3d33442f.png

图3-3 udp_sendmsg流程图

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

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

a4c26d1e5885305701be709a3d33442f.png

说白了就是,通过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;

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

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

3、udp_push_pending_frames

早先从流程是:填充第四层报头 -->> 计算校验和 -->> 调用

ip_push_pending_frames发送。如下图(见《Understand Linux Kernel

Internel》图21-13):

a4c26d1e5885305701be709a3d33442f.png

图3-4

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

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

Internel》图21-12)

a4c26d1e5885305701be709a3d33442f.png

图3-5

发送skb

可以总结为如下流程:

(A)填充第四层报头

(B)计算校验和

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

(D)发送取下的skb

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

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)

这样一看就简单了。

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

四、接收流程

累了,过几天再写

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值