1、基本原理
关于UDP相关协议和原理,这里就不啰嗦了~。
简单说,UDP是无连接的协议,相关流程比TCP相对简单,但效率更高。
2、基本流程
Linux内核中协议栈中,UDP报文发送的主要流程如下(仅关注L4):
send(或sendto)系统调用-->
sock_sendmsg()-->
__sock_sendmsg()-->
__sock_sendmsg_nosec()-->
sock->ops->sendmsg()-->
udp_sendmsg()
udp_sendmsg()主要流程如下:
1)前期处理。包括,对数据长度合法性判断、pending数据的判断、目的地址的处理和获取、控制信息的处理、组播处理、connected信息处理、MSG_CONFIRM标志的处理等。
2)调用ip_append_data()接口将其添加到传输控制块(sock)的发送队列中(利用发送队列中的现有skb,或者新创建skb,详细原理和流程请参见ip_append_data()接口的分析)。
3)判断是否有cork标记(MSG_MORE),如果没有,则说明需要立即发送,则调用udp_push_pending_frames()接口发送报文,实际是将包提交至IP层;如果设置了cork,则说明需要阻塞等待直到数据达到MTU大小,则完成本次的udp_sendmsg()处理。
3、代码分析
UDP报文发送在L4中的主要接口为udp_sendmsg()函数,本文重点分析此函数:
udp_sendmsg():
-
/*
-
* 用户态sendto/send(先调用connect建立连接)调用接口对应的UDP包发送的内核入口
-
* @iocb: 异步控制块(尚未使用?)
-
* @sk: sock描述符(传输控制块)
-
* @msg: 描述发送数据的msghdr的指针
-
* @len: 待发送数据的长度
-
*/
-
int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
-
size_t len)
-
{
-
// 将sock结构转换为inet_sock结构
-
struct inet_sock *inet = inet_sk(sk);
-
// 将sock接口转换为udp_sock结构(UDP的传输控制块)
-
struct udp_sock *up = udp_sk(sk);
-
struct flowi4 fl4_stack;
-
struct flowi4 *fl4;
-
int ulen = len;
-
struct ipcm_cookie ipc;
-
struct rtable *rt = NULL;
-
int free = 0;
-
int connected = 0;
-
__be32 daddr, faddr, saddr;
-
__be16 dport;
-
u8 tos;
-
int err, is_udplite = IS_UDPLITE(sk);
-
/*设置是否需要cork(阻塞),通常由MSG_MORE标记控制*/
-
int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
-
/*用户从用户态拷贝实际数据的接口*/
-
int (*getfrag)(void *, char *, int, int, int, struct sk_buff *);
-
struct sk_buff *skb;
-
struct ip_options_data opt_copy;
-
/*数据长度不能超过64k*/
-
if (len > 0xFFFF)
-
return -EMSGSIZE;
-
-
/*
-
* Check the flags.
-
*/
-
-
if (msg->msg_flags & MSG_OOB) /* Mirror BSD error message compatibility */
-
return -EOPNOTSUPP;
-
-
ipc.opt = NULL;
-
ipc.tx_flags = 0;
-
/*
-
* 根据is_udplite标志来指定"从用户态复制数据到UDP分片"的函数,UDP和轻量级UDP的实现共用了一套函数,
-
* 只是在计算校验和上有点区别。轻量级UDP可以在发送前(而不是在复制数据到分片中时)对数据前部
-
* 指定数目的字节或全部数据执行校验和。而UDP如果是由软件执行校验和(当网卡硬件支持udp checksum offload
-
* 并开启相关功能后,校验和由硬件执行),则在复制数据到分片中时对数据包中的全部数据执行校验和。
-
* 所以,轻量级UDP和UDP使用不同的"getfrag"函数。
-
*/
-
getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag;
-
-
fl4 = &inet->cork.fl.u.ip4;
-
/*
-
* 当前的sock还有pending的帧(skb)没有发送到IP层,在设置了CORK标记的场景下才会设置这个标记
-
*
-
*/
-
if (up->pending) {
-
/*
-
* There are pending frames.
-
* The socket lock must be held while it's corked.
-
*/
-
lock_sock(sk);
-
/*
-
* 为什么再判断一次?为了提升效率。
-
* 因为大部分情况下pending标记是没有的,这样的话就不会进入到这里,就可以省掉一个lock_sock(比较复杂、耗时),仅当设置了pending后,
-
* 才加锁并再检查一次,这样就能在大部分情况下不用锁,少数情况下加锁,这种方法是内核中常用的提升效率的策略。
-
*/
-
if (likely(up->pending)) {
-
if (unlikely(up->pending != AF_INET)) {/*pending既不是0,又不是AF_INET,那就是有问题了*/
-
release_sock(sk);
-
return -EINVAL;
-
}
-
/*利用当前sock发送队列中的原有的skb发送数据,将新数据附加到相应skb中的数据区即可*/
-
goto do_append_data;
-
}
-
release_sock(sk);
-
}
-
/*UDP数据报长度累积,加上UDP头长度*/
-
ulen += sizeof(struct udphdr);
-
-
/*
-
* Get and verify the address.
-
*/
-
/*判断msg中是否带了目的地址信息,如果有的话,则通常是由sendto调用进入*/
-
if (msg->msg_name) {
-
/*从msg中提取目的地址,并判断合法性*/
-
struct sockaddr_in *usin = (struct sockaddr_in *)msg->msg_name;
-
/*判断长度*/
-
if (msg->msg_namelen < sizeof(*usin))
-
return -EINVAL;
-
/*判断地址族,必须为AF_INET*/
-
if (usin->sin_family != AF_INET) {
-
if (usin->sin_family != AF_UNSPEC)
-
return -EAFNOSUPPORT;
-
}
-
/*提取目的地址和端口信息*/
-
daddr = usin->sin_addr.s_addr;
-
dport = usin->sin_port;
-
if (dport == 0)
-
return -EINVAL;
-
} else {
-
/*
-
* 如果msg中不带目的地址信息,则通常是用send调用进入,在send之前调用了
-
* connect建立连接,建立连接后相应状态为TCP_ESTABLISHED
-
*/
-
if (sk->sk_state != TCP_ESTABLISHED)
-
/*如果既不是sendto,又不是connect+send的方式进入的话,那就有问题了*/
-
return -EDESTADDRREQ;
-
daddr = inet->inet_daddr;
-
dport = inet->inet_dport;
-
/* Open fast path for connected socket.
-
Route will not be used, if at least one option is set.
-
*/
-
/*对于已连接的UDP套接口,设置connected标志,在后续查找路由时用,可以根据此做快速处理*/
-
connected = 1;
-
}
-
ipc.addr = inet->inet_saddr;
-
-
ipc.oif = sk->sk_bound_dev_if;
-
-
sock_tx_timestamp(sk, &ipc.tx_flags);
-
/*处理待发送的控制信息,如果msg->msg_controllen不为0,则说明有控制信息需要处理*/
-
if (msg->msg_controllen) {
-
/*处理控制信息,比如对IP选项的处理*/
-
err = ip_cmsg_send(sock_net(sk), msg, &ipc);
-
if (err)
-
return err;
-
/*如果ipc中存在IP选项,则设置free标记,表示需要在处理完成后释放。因为此时的ipc->opt肯定是在ip_cmsg_send中分配的*/
-
if (ipc.opt)
-
free = 1;
-
connected = 0;
-
}
-
/*如果发送数据中的控制信息中没有IP选项信息,则从inet_sock结构中获取*/
-
if (!ipc.opt) {
-
struct ip_options_rcu *inet_opt;
-
/*这里的rcu锁很关键,老版本中不用就触发了bug*/
-
rcu_read_lock();
-
inet_opt = rcu_dereference(inet->inet_opt);
-
if (inet_opt) {
-
memcpy(&opt_copy, inet_opt,
-
sizeof(*inet_opt) + inet_opt->opt.optlen);
-
ipc.opt = &opt_copy.opt;
-
}
-
rcu_read_unlock();
-
}
-
/*由于控制信息需要保存目的地址,因此将源地址保存*/
-
saddr = ipc.addr;
-
ipc.addr = faddr = daddr;
-
/*
-
* 如果存在宽松或严格源站选路的IP选项,则不能根据目的地址选路,而需要将IP选项中
-
* 下一站地址作为目的地址来选路,因此从IP选项中提取下一站地址,供后续选路时作为
-
* 目的地址使用。另外,因为需要重新选路,所以清除connected标记
-
*/
-
if (ipc.opt && ipc.opt->opt.srr) {
-
if (!daddr)
-
return -EINVAL;
-
faddr = ipc.opt->opt.faddr;
-
connected = 0;
-
}
-
tos = RT_TOS(inet->tos);
-
/*
-
* 如果设置了SOCK_LOCALROUTE或者发送时设置了MSG_DONTROUTE标记,再或者IP选项中存在严格源站选路
-
* 选项,则说明目的地址或下一跳必然位于本地子网中。此时需要设置tos中的RTO_ONLINK标记,表示
-
* 后续查找路由时与目的地直连。
-
*/
-
if (sock_flag(sk, SOCK_LOCALROUTE) ||
-
(msg->msg_flags & MSG_DONTROUTE) ||
-
(ipc.opt && ipc.opt->opt.is_strictroute)) {
-
tos |= RTO_ONLINK;
-
connected = 0;
-
}
-
/*广播相关处理*/
-
if (ipv4_is_multicast(daddr)) {
-
if (!ipc.oif)
-
ipc.oif = inet->mc_index;
-
if (!saddr)
-
saddr = inet->mc_addr;
-
connected = 0;
-
} else if (!ipc.oif)
-
ipc.oif = inet->uc_index;
-
/*选路相关处理,获取相应的路由缓存项*/
-
if (connected)
-
rt = (struct rtable *)sk_dst_check(sk, 0);
-
-
if (rt == NULL) {
-
struct net *net = sock_net(sk);
-
-
fl4 = &fl4_stack;
-
flowi4_init_output(fl4, ipc.oif, sk->sk_mark, tos,
-
RT_SCOPE_UNIVERSE, sk->sk_protocol,
-
inet_sk_flowi_flags(sk)|FLOWI_FLAG_CAN_SLEEP,
-
faddr, saddr, dport, inet->inet_sport);
-
-
security_sk_classify_flow(sk, flowi4_to_flowi(fl4));
-
rt = ip_route_output_flow(net, fl4, sk);
-
if (IS_ERR(rt)) {
-
err = PTR_ERR(rt);
-
rt = NULL;
-
if (err == -ENETUNREACH)
-
IP_INC_STATS_BH(net, IPSTATS_MIB_OUTNOROUTES);
-
goto out;
-
}
-
-
err = -EACCES;
-
if ((rt->rt_flags & RTCF_BROADCAST) &&
-
!sock_flag(sk, SOCK_BROADCAST))
-
goto out;
-
if (connected)
-
sk_dst_set(sk, dst_clone(&rt->dst));
-
}
-
/*如果设置了MSG_CONFIRM标记,表明应用层确认网关有效并可达,则直接跳转到do_confirm对目的路由缓存项进行确认*/
-
if (msg->msg_flags&MSG_CONFIRM)
-
goto do_confirm;
-
back_from_confirm:
-
/*
-
* 从获取到的路由中获取源地址和目的地址。在发送UDP数据时其实可以不指定目的地址,而在通过在发送控制信
-
* 息中加入严格或宽松源站选路选项,所以如此此时还没有获取目的地址,则需从路由缓存项中获取
-
*/
-
saddr = fl4->saddr;
-
if (!ipc.addr)
-
daddr = ipc.addr = fl4->daddr;
-
-
/* Lockless fast path for the non-corking case. */
-
/*快速路径(大部分情况下都没有使用CORK,此时不需要lock_sock,能避免锁带来的开销,提升效率)。不需要cork时,直接新建skb,并直接发送*/
-
if (!corkreq) {
-
/*
-
* 将sock相应的skb队列中的所有skb合并成一个数据报文(skb),实际使用skb_shinfo->frag_list将所有skb连接起来。
-
* 为什么要这样?这里将所有skb都合并后,可能导致这个包的size大于mtu,那到IP层的时候还会进行进一步分片?
-
* 原因是:udp是面向数据报文的,报文必须完整,属于同一个报文的数据必须要放到同一个skb中,否则对端无法知道这是同一个报文。
-
* 那IP层怎么处理呢?就不考虑同一个报文的问题?IP层分片会携带相关头信息,对端会根据这些信息进行重组,重组后对传输层来说就是一个报文。
-
* 分片其实就是IP层应该负责的。此时IP分片实际就是将原来skb->frag_list中的skb摘出来,不会做其它的操作,效率很高。
-
*/
-
skb = ip_make_skb(sk, fl4, getfrag, msg->msg_iov, ulen,
-
sizeof(struct udphdr), &ipc, &rt,
-
msg->msg_flags);
-
err = PTR_ERR(skb);
-
if (!IS_ERR_OR_NULL(skb))
-
/*直接发送数据*/
-
err = udp_send_skb(skb, fl4);
-
goto out;
-
}
-
/*要往skb中添加数据了并访问sock中的相关数据了,此时需要加锁了,因为同一个socket可能被多个进程在多个CPU上同时访问*/
-
lock_sock(sk);
-
if (unlikely(up->pending)) {
-
/* The socket is already corked while preparing it. */
-
/* ... which is an evident application bug. --ANK */
-
/*
-
* 运行到这里说明,设置了cork,而在进入此函数的开始时已经检测过pending了,pending为空时才会走到这里,
-
* 而这里再次检查时,pending又有了,此时说明有多个进程在同时使用socket,其它核上的流程进行了相关修改,此时认定为
-
* 应用程序的bug。而实际上如果对访问pending时进行加锁保护,也不会有问题,但是这个pending不太好加锁,因为外面也在
-
* 访问比如udp_sendpage中,此时很容易死锁。所以,无奈之下,这里进行了这样的判断,认定为是app bug。
-
*/
-
release_sock(sk);
-
-
LIMIT_NETDEBUG(KERN_DEBUG pr_fmt("cork app bug 2\n"));
-
err = -EINVAL;
-
goto out;
-
}
-
/*
-
* Now cork the socket to pend data.
-
*/
-
/*缓存目的地址、目的端口、源地址和源端口信息,便于在发送处理时方便获取信息。*/
-
fl4 = &inet->cork.fl.u.ip4;
-
fl4->daddr = daddr;
-
fl4->saddr = saddr;
-
fl4->fl4_dport = dport;
-
fl4->fl4_sport = inet->inet_sport;
-
/*设置AF_INET标记,表明正在处理UDP数据包*/
-
up->pending = AF_INET;
-
/*
-
* 运行到这里,有两种情况:1.最初进入函数时pending=AF_INET,即有udp数据包正在处理中;2.设置了cork,
-
* 则表明需要阻塞,使用原有的skb一起发送数据.
-
*/
-
do_append_data:
-
/*增加包长*/
-
up->len += ulen;
-
/*
-
* 调用IP层接口函数ip_append_data,进入IP层处理,主要工作为:
-
* 将数据拷贝到适合的skb(利用发送队列中现有的或新创建)中,可能有两种情况: 1. 放入skb的线性
-
* 区(skb->data)中,或者放入skb_shared_info的分片(frag)中,同时还需要考虑MTU对skb数据进行分割。
-
*/
-
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)
-
/*出错则清空所有pending的数据帧,并清空pending标记*/
-
udp_flush_pending_frames(sk);
-
/*未设置cork(如果设置了cork,则需要等待组成64k大小的UDP数据报后再发送),则直接发送数据到IP层*/
-
else if (!corkreq)
-
err = udp_push_pending_frames(sk);
-
else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
-
/*如果发送队列为空,则说明没有数据正在处理了,则复位pending标记*/
-
up->pending = 0;
-
/*Fixme:为什么此时把sock释放了,如果数据还没发完咋办?因为发送队列传递到下一层了?*/
-
release_sock(sk);
-
-
out:
-
/*发送完成,不再需要路由信息,因此递减对路由的引用计数*/
-
ip_rt_put(rt);
-
/*之前设置的free标记,用于是否IP选项*/
-
if (free)
-
kfree(ipc.opt);
-
/*如果发送成功,则返回发送数据的字节数*/
-
if (!err)
-
return len;
-
/*
-
* ENOBUFS = no kernel mem, SOCK_NOSPACE = no sndbuf space. Reporting
-
* ENOBUFS might not be good (it's not tunable per se), but otherwise
-
* we don't have a good statistic (IpOutDiscards but it can be too many
-
* things). We could add another new stat but at least for now that
-
* seems like overkill.
-
*/
-
/*如果发送失败,则返回相应的错误码。*/
-
if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {
-
UDP_INC_STATS_USER(sock_net(sk),
-
UDP_MIB_SNDBUFERRORS, is_udplite);
-
}
-
return err;
-
/*发送时设置了MSG_CONFIRM标记时,跳转到这里*/
-
do_confirm:
-
/*应用层确认网关可达,因此直接对目的路由缓存项进行确认。*/
-
dst_confirm(&rt->dst);
-
/*
-
* MSG_PROBE标志用于发现路径,并非真正发送数据。因此此处检查此标记,如果设置,
-
* 这直接跳转到out,退出;如果未设置,且有需要发送的数据,则跳回到back_from_confirm
-
* 处理相关数据。
-
*/
-
if (!(msg->msg_flags&MSG_PROBE) || len)
-
goto back_from_confirm;
-
err = 0;
-
goto out;
-
}
udp_sendmsg()-->udp_push_pending_frames():
-
/*
-
* Push out all pending data as one UDP datagram. Socket is locked.
-
*/
-
/*
-
* 将待发送的数据打包成一个UDP数据报发送。
-
*/
-
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;
-
/*
-
* 将sock相应的skb队列中的所有skb合并成一个数据报文(skb),实际使用skb_shinfo->frag_list将所有skb连接起来。
-
*/
-
skb = ip_finish_skb(sk, fl4);
-
if (!skb)
-
goto out;
-
/*将组合后的skb发送出去,交由IP层处理*/
-
err = udp_send_skb(skb, fl4);
-
-
out:
-
/*发送完成后清除pending标记,并向上层返回发送的字节数或者错误*/
-
up->len = 0;
-
up->pending = 0;
-
return err;
-
}
udp_push_pending_frames()-->ip_finish_skb()-->__ip_make_skb():
-
/*
-
* Combined all pending IP fragments on the socket as one IP datagram
-
* and push them out.
-
*/
-
/*
-
* 将sock相应的skb队列中的所有skb合并成一个数据报文(skb),实际使用skb_shinfo->frag_list将所有skb连接起来。
-
*/
-
-
struct sk_buff *__ip_make_skb(struct sock *sk,
-
struct flowi4 *fl4,
-
struct sk_buff_head *queue,
-
struct inet_cork *cork)
-
{
-
struct sk_buff *skb, *tmp_skb;
-
struct sk_buff **tail_skb;
-
struct inet_sock *inet = inet_sk(sk);
-
struct net *net = sock_net(sk);
-
struct ip_options *opt = NULL;
-
struct rtable *rt = (struct rtable *)cork->dst;
-
struct iphdr *iph;
-
__be16 df = 0;
-
__u8 ttl;
-
-
if ((skb = __skb_dequeue(queue)) == NULL)
-
goto out;
-
/*使用skb_shinfo->frag_list,将所有的skb合并成一个*/
-
tail_skb = &(skb_shinfo(skb)->frag_list);
-
-
/* move skb->data to ip header from ext header */
-
/*将skb->data指向IP头部,__skb_pull将skb->data指向向后启动offset距离,同时skb->len减小offset*/
-
if (skb->data < skb_network_header(skb))
-
__skb_pull(skb, skb_network_offset(skb));
-
/*从skb队列中逐个取出skb,并进行合并*/
-
while ((tmp_skb = __skb_dequeue(queue)) != NULL) {
-
__skb_pull(tmp_skb, skb_network_header_len(skb));
-
/*用当前skb的skb_shinfo(skb)->frag_list指向新取出的skb,使其链入同一个skb中*/
-
*tail_skb = tmp_skb;
-
/*移向下一个,实现逐个串联*/
-
tail_skb = &(tmp_skb->next);
-
/*skb->len(包括线性和非线性区数据的总长度)增加*/
-
skb->len += tmp_skb->len;
-
/*skb->data_len(仅包括非线性区的数据长度,包括frags和frag_list中的数据)增加len,因为后面的skb都链入frag_list列表了*/
-
skb->data_len += tmp_skb->len;
-
/*skb->truesize(包括数据和skb结构自身的总长度)增加*/
-
skb->truesize += tmp_skb->truesize;
-
/*删除已被合并的skb*/
-
tmp_skb->destructor = NULL;
-
tmp_skb->sk = NULL;
-
}
-
-
/* Unless user demanded real pmtu discovery (IP_PMTUDISC_DO), we allow
-
* to fragment the frame generated here. No matter, what transforms
-
* how transforms change size of the packet, it will come out.
-
*/
-
if (inet->pmtudisc < IP_PMTUDISC_DO)
-
skb->local_df = 1;
-
-
/* DF bit is set when we want to see DF on outgoing frames.
-
* If local_df is set too, we still allow to fragment this frame
-
* locally. */
-
if (inet->pmtudisc >= IP_PMTUDISC_DO ||
-
(skb->len <= dst_mtu(&rt->dst) &&
-
ip_dont_fragment(sk, &rt->dst)))
-
df = htons(IP_DF);
-
-
if (cork->flags & IPCORK_OPT)
-
opt = cork->opt;
-
-
if (rt->rt_type == RTN_MULTICAST)
-
ttl = inet->mc_ttl;
-
else
-
ttl = ip_select_ttl(inet, &rt->dst);
-
-
iph = ip_hdr(skb);
-
iph->version = 4;
-
iph->ihl = 5;
-
iph->tos = inet->tos;
-
iph->frag_off = df;
-
iph->ttl = ttl;
-
iph->protocol = sk->sk_protocol;
-
ip_copy_addrs(iph, fl4);
-
ip_select_ident(iph, &rt->dst, sk);
-
-
if (opt) {
-
iph->ihl += opt->optlen>>2;
-
ip_options_build(skb, opt, cork->addr, rt, 0);
-
}
-
-
skb->priority = sk->sk_priority;
-
skb->mark = sk->sk_mark;
-
/*
-
* Steal rt from cork.dst to avoid a pair of atomic_inc/atomic_dec
-
* on dst refcount
-
*/
-
cork->dst = NULL;
-
skb_dst_set(skb, &rt->dst);
-
-
if (iph->protocol == IPPROTO_ICMP)
-
icmp_out_count(net, ((struct icmphdr *)
-
skb_transport_header(skb))->type);
-
-
ip_cork_release(cork);
-
out:
-
return skb;
-
}
阅读(1086) | 评论(0) | 转发(0) |