UDP之数据报发送过程

本文深入剖析UDP协议如何将用户数据封装为数据报并传递给IP层。重点讨论MSG_MORE标记的作用,以及udp_sendmsg()、udp_push_pending_frames()在数据报发送过程中的关键步骤,强调了数据量不超过MTU的重要性,同时概述了socket操作在UDP数据发送中的不同方式。
摘要由CSDN通过智能技术生成

当应用程序调用send()等一系列系统调用向UDP套接字写数据时,最终都会调用到传输层的udp_sendmsg(),这篇笔记重点分析下UDP协议是如何将用户态数据封装成skb递交给IP层的。

要点说明

在分析代码之前,有必要对一些UDP的写操作过程中的一些关键点进行说明,否则会看的晕头转向。

MSG_MORE标记

UDP数据报不像TCP,它是有边界的,即发送端的一个UDP数据报会完整的被接收端以一个UDP数据报的方式接收。

然而,并非一次写操作对应一个UDP数据报,应用程序可以通过MSG_MORE标记或者UDP_CORK选项将多次写操作的数据合并成一个UDP数据报发送。具体操作流程如下:

  1. 在调用sendmsg()时,flag参数中设置MSG_MORE标记,表示还有更多数据要发送。应用期望内核收到设置了该标记的数据时先不要将本次递交的数据发送给IP层,而是将其缓存,并且将后面连续的设定了该标记的数据合并成同一个UDP报文(一个IP报文,但是可能是多个IP片段)。直到没有设定该标记的发送时,将数据报发送给IP层;
  2. 类似的,在使能和关闭UDP_CORK选项期间发送的所有数据也要组合成一个UDP报文发送给IP。

注意:应用程序在使用这种方式的时候必须要注意多次组合的数据最好不要超过MTU,否则IP层就不得不将这些要组合的数据分成多个IP数据包发送出去,这样会造成性能的下降。

socket操作

用户态调用socket()创建UDP套接字后,有两种方式可以发送数据:

  1. 直接调用sendto()或者sendmsg(),在这些函数的参数中指定目的地址;
  2. 先调用connect()将UDP套接字和一个目的地址绑定,这时除了上面这两个接口以外,还可以调用write()、send()等没有目的地址参数的接口发送数据。当然,UDP是一个无连接的协议,这里的connect()仅仅是本机内部行为,不会有任何数据报发送出去的。

UDP数据报发送: udp_sendmsg()

@iocb: 为异步IO预留扩展,暂不关注
@sk:传输控制块
@msg:包含了用户空间要发送的数据
@len:要发送的数据长度
int udp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg, size_t len)
{
	struct inet_sock *inet = inet_sk(sk);
	struct udp_sock *up = udp_sk(sk);
	// 本次发送要发送的数据长度,包括UDP首部
	int ulen = len;
	// UDP的一些控制信息,如IP选项,UDP将这些信息传递给ip_append_data()
	struct ipcm_cookie ipc;
	struct rtable *rt = NULL;
	int free = 0;
	int connected = 0;
	__be32 daddr, faddr, saddr;
	__be16 dport;
	u8  tos;
	// UDPlite协议暂不关注,下面只看UDP协议的流程处理
	int err, is_udplite = IS_UDPLITE(sk);
	// corkreq表示本次发送是否要按照上面MSG_MORE部分介绍的方式仅仅组织报文,而不发送;
	// up->corkflag标识了是否设置了UDP_CORK选项
	int corkreq = up->corkflag || msg->msg_flags&MSG_MORE;
	int (*getfrag)(void *, char *, int, int, int, struct sk_buff *);

	// UDP首部的长度字段只有16bit,所以一个UDP数据报的数据部分长度不能超过65535字节
	if (len > 0xFFFF)
		return -EMSGSIZE;

	/*
	 *	Check the flags.
	 */
	// UDP不支持带外数据,所以不能设置MSG_OOB
	if (msg->msg_flags & MSG_OOB)	/* Mirror BSD error message compatibility */
		return -EOPNOTSUPP;

	ipc.opt = NULL;
	// pending标记和前面说的MSG_MORE标记有关。当设置MSG_MORE标记的数据到达时,UDP会将待
	// 发送的数据暂存到发送队列中,这些数据就处于pending状态,等标记取消时,会将数据发送
	// 给IP,然后清空发送队列,这时退出pending状态。
	if (up->pending) {
		/*
		 * There are pending frames.
		 * The socket lock must be held while it's corked.
		 */
		lock_sock(sk)
		 // 再判断一次是因为了lock_sock()可能会导致进程休眠。内核中有许多地方使用这样的方式编程。
		 // 因为大部分情况下pending标记是没有的,这样的话就不会进入到这里,这种编程方式就可以省掉
		 // 一个lock_sock(比较复杂、耗时)调用,仅当设置了pending后,才加锁并再检查一次,这样就
		 // 能在大部分情况下不用锁,这种方法是内核中常用的提升效率的编程技巧之一。
		if (likely(up->pending)) {
			// pengding的值只能是0或者AF_INET
			if (unlikely(up->pending != AF_INET)) {
				release_sock(sk);
				return -EINVAL;
			}
			// 因为已经有挂起的数据,所以可以不用再次进行地址、路由的选择,直接跳转到do_append_data
			// 处追加数据即可。因为如果有pending标记,这些工作在处理第一次发送操作时已经完成了
			goto do_append_data;
		}
		release_sock(sk);
	}
	// 能到这里,无论是否是MSG_MORE方式的发送,一定是一个UDP报文的第一次发送,所以发送的长度加上UDP首部8个字节
	ulen += sizeof(struct udphdr);

	// 下面这段逻辑是确定目的端IP地址和端口号

	if (msg->msg_name) {
		// msg_name不为空,表示用户空间程序本次调用指定了目的地址信息,这种
		// 情况下校验指定的地址参数并设置地址族、目的地址和目的端口
		
		// 目的地址必须是IPv4地址格式:struct sockaddr_in
		struct sockaddr_in * usin = (struct sockaddr_in*)msg->msg_name;
		if (msg->msg_namelen < sizeof(*usin))
			return -EINVAL;
		// 地址族必须是AF_INET或者AF_UNSPEC
		if (usin->sin_family != AF_INET) {
			if (usin->sin_family != AF_UNSPEC)
				return -EAFNOSUPPORT;
		}
		// 目的IP和目的端口
		daddr = usin->sin_addr.s_addr;
		dport = usin->sin_port;
		// 目的端口不能为0
		if (dport == 0)
			return -EINVAL;
	} else {
		// 用户空间程序本次调用没有指定目的地址,那么需要检查之前是否已经connect()成功,
		// 否则就是一次失败的调用,因为内核不知道应该将数据发送给谁
		
		// 如果之前connect()成功,那么UDP传输控制块的状态一定是TCP_ESTABLISHED
		if (sk->sk_state != TCP_ESTABLISHED)
			return -EDESTADDRREQ;
		// 之前connect()成功过程中会将目的端地址信息保存在inet_sock结构中
		daddr = inet->daddr;
		dport = inet->dport;
		/* Open fast path for connected socket. Route will not be used, if at least one option is set. */
		// 已连接标记置1
		connected = 1;
	}

	ipc.addr = inet->saddr;
	ipc.oif = sk->sk_bound_dev_if;
	// 如果发送数据时指定了控制信息(sendmsg()系统调用),用的极少,暂不关注
	if (msg->msg_controllen) {
		err = ip_cmsg_send(msg, &ipc);
		if (err)
			return err;
		if (ipc.opt)
			free = 1;
		connected = 0;
	}
	if (!ipc.opt)
		ipc.opt = inet->opt;

	saddr = ipc.addr;
	ipc.addr = faddr = daddr;
	// 源路由选项相关处理,先忽略
	if (ipc.opt && ipc.opt->srr) {
		if (!daddr)
			return -EINVAL;
		faddr = ipc.opt->faddr;
		connected = 0;
	}
	// tos默认来自inet->tos,后者又可以通过socket选项进行设置
	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->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;
	}

	// 对于已经连接的情况,之前一定已经查询过路由了,这里需要检查该路由是否依然有效
	if (connected)
		rt = (struct rtable*)sk_dst_check(sk, 0);
	if (rt == NULL) {
		// 没有路由信息,这里路由表查询条件有:输出设备接口、源和目的IP、TOS、源和目的端口
		struct flowi fl = {
        	.oif = ipc.oif,
			.nl_u = {
            	.ip4_u = {
                	.daddr = faddr,
					.saddr = saddr,
					.tos = tos
                }
            },
			.proto = sk->sk_protocol,
			.uli_u = {
            	.ports = {
                	.sport = inet->sport,
					.dport = dport
                }
           }
       };
		security_sk_classify_flow(sk, &fl);
		// 调用发送报文的路由表查询接口进行路由查询
		err = ip_route_output_flow(&init_net, &rt, &fl, sk, 1);
		// 查询失败、发送失败
		if (err) {
			if (err == -ENETUNREACH)
				IP_INC_STATS_BH(IPSTATS_MIB_OUTNOROUTES);
			goto out;
		}
		err = -EACCES;
		// 路由结果为广播但是该socket不允许广播,发送失败
		if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST))
			goto out;
		// 如果是已连接套接字,那么将路由信息设置到套接字,下次检查即可,不用重复查询,见上面,
		// 这里可以解释为何一个未连接的udp套接字,可以每次通过指定不同的目的地址进行报文发送,
		// 因为未连接情况下,每次都会重新查路由,但是不会将路由查询结果设置到TCB中
		if (connected)
			sk_dst_set(sk, dst_clone(&rt->u.dst));
	}

	// MSG_CONFIRM表示该报文要求接收端的数据链路层进行确认,用的很少,忽略
	if (msg->msg_flags&MSG_CONFIRM)
		goto do_confirm;
back_from_confirm:
	// 最后确定要使用的源和目的
	saddr = rt->rt_src;
	if (!ipc.addr)
		daddr = ipc.addr = rt->rt_dst;

	lock_sock(sk);
	// 这种情况不应该出现
	if (unlikely(up->pending)) {
		/* The socket is already corked while preparing it. */
		/* ... which is an evident application bug. --ANK */
		release_sock(sk);

		LIMIT_NETDEBUG(KERN_DEBUG "udp cork app bug 2\n");
		err = -EINVAL;
		goto out;
	}
	/*
	 *	Now cork the socket to pend data.
	 */
	// 将一些重要信息暂存到inet->cork中,以备可能存在的后续发送过程使用
	inet->cork.fl.fl4_dst = daddr;
	inet->cork.fl.fl_ip_dport = dport;
	inet->cork.fl.fl4_src = saddr;
	inet->cork.fl.fl_ip_sport = inet->sport;
	// 下面就要将待发送数据放入发送队列了,先设置pending标记
	up->pending = AF_INET;

do_append_data:
	// up->len变量记录了当前该传输控制块上已经pending的字节数,这里将ulen累加到该变量上
	up->len += ulen;
	// 根据是否为UDPlite选用不同的拷贝函数,这两个协议公用一套函数,但是因为校验和计算方法
	// 有差别,而且可能需要在拷贝过程中顺便计算校验和(这样可以避免再次遍历数据),所以这里需要区分
	getfrag  =  is_udplite ?  udplite_getfrag : ip_generic_getfrag;
	// ip_append_data()很重要,而且足够复杂,它属于IP层提供给上层协议使用的一个发送接口,目前
	// 主要由UDP和raw套接字使用,该函数后面会单独分析,这里只需要知道如下几点:
	// 1. 该函数将要发送的数据按照MTU大小分割成若干个方便IP处理的片段,每个片段一个skb;并且这些
	//    skb会放入到套接字的发送缓冲区中;
	// 2. 该函数只是组织数据包,并不执行发送动作,如果需要发送,需要由调用者主动调用ip_push_frames()
	// 3. 该函数处理成功返回0,失败返回错误码
	err = ip_append_data(sk, 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);
	// 数据包处理没有问题,并且没有启用MSG_MORE特性,那么直接将发送队列中的数据发送给IP。
	// udp_push_pending_frames()会将up->pending标记清空,这点非常重要;
	// 对于大多数应用都是走了该分支,即一次写操作对应一个UDP数据包,从发送行为上讲,这种UDP
	// 套接字相当于没有发送缓冲区
	else if (!corkreq)
		err = udp_push_pending_frames(sk);
	// 这种情况不大可能发生,除非应用程序指定要发送的数据长度为0
	else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
		up->pending = 0;
	release_sock(sk);

out:
	// 释放对路由缓存的引用
	ip_rt_put(rt);
	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(UDP_MIB_SNDBUFERRORS, is_udplite);
	}
	return err;

do_confirm:
	// 确认处理,用的很少,忽略
	dst_confirm(&rt->u.dst);
	if (!(msg->msg_flags&MSG_PROBE) || len)
		goto back_from_confirm;
	err = 0;
	goto out;
}

从上面的分析来看,udp_sendmsg()函数的实现还是相当直接的,关键流程如下:

  1. 确定路由;
  2. 通过ip_append_data()将待发送数据组织成skb排到发送队列中;
  3. 如果非pending状态,通过udp_push_pending_frames()直接将发送队列中的所有数据发送给IP层;

发送数据报给IP层:udp_push_pending_frames()

如注释所述,该函数会将当前所有pending的所有skb作为一个UDP数据报发送出去,它实际上是ip_push_pending_frames()的包装,该函数主要干了两件事:

  1. 填充UDP报文首部信息,包括校验和的计算;
  2. 调用ip_push_frames()将所有skb发送给IP层继续处理;
/*
 * 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 flowi *fl = &inet->cork.fl;
	struct sk_buff *skb;
	struct udphdr *uh;
	int err = 0;
	int is_udplite = IS_UDPLITE(sk);
	__wsum csum = 0;

	/* Grab the skbuff where UDP header space exists. */
	// 获取发送队列中第一个SKB的指针,注意是获取,并不会将该skb从发送队列上摘除,
	// 这里获取是为了填充UDP首部,真正的出队列操作由ip_push_pending_frames()执行。
	// 发送队列中此时可能有多个skb,每个skb携带的数据为一个MTU大小,这是由前面的
	// ip_append_data()处理好的,目的是方便IP层的后续处理
	if ((skb = skb_peek(&sk->sk_write_queue)) == NULL)
		goto out;

	// 组装UDP首部各个字段
	uh = udp_hdr(skb);
	uh->source = fl->fl_ip_sport;
	uh->dest = fl->fl_ip_dport;
	uh->len = htons(up->len);
	uh->check = 0;

	// 计算数据报的校验和
	if (is_udplite)  /*     UDP-Lite      */
		csum  = udplite_csum_outgoing(sk, skb);
	else if (sk->sk_no_check == UDP_CSUM_NOXMIT) {   /* UDP csum disabled */
		skb->ip_summed = CHECKSUM_NONE;
		goto send;
	} else if (skb->ip_summed == CHECKSUM_PARTIAL) { /* UDP hardware csum */
		udp4_hwcsum_outgoing(sk, skb, fl->fl4_src,fl->fl4_dst, up->len);
		goto send;
	} else	/*   `normal' UDP    */
		csum = udp_csum_outgoing(sk, skb);

	// 添加伪首部校验和计算
	uh->check = csum_tcpudp_magic(fl->fl4_src, fl->fl4_dst, up->len, sk->sk_protocol, csum);
	if (uh->check == 0)
		uh->check = CSUM_MANGLED_0;

send:
	// 调用ip_push_pending_frames()函数将数据报发送出去。这些数据报虽然可能是由多个片段组成,
	// 而且每个片段都达到了MTU大小,但是它们共用一个ipid,表明它们属于同一个IP报文,只是分段了而已
	err = ip_push_pending_frames(sk);
out:
	// 无论成功与否,发送队列中不再有数据,所以清空len和pending标记
	up->len = 0;
	up->pending = 0;
	if (!err)
		UDP_INC_STATS_USER(UDP_MIB_OUTDATAGRAMS, is_udplite);
	return err;
}

清除发送队列:udp_flush_pending_frames()

如前面udp_sendmsg()函数分析,如果ip_append_data()调用失败,那么会调用该函数将发送队列中的所有skb全部丢弃,并且清除pending标记。

/*
 * Throw away all pending data and cancel the corking. Socket is locked.
 */
static void udp_flush_pending_frames(struct sock *sk)
{
	struct udp_sock *up = udp_sk(sk);

	if (up->pending) {
		up->len = 0;
		up->pending = 0;
		// 调用IP层的flush接口
		ip_flush_pending_frames(sk);
	}
}

小结

从上面的代码分析过程中可以看出,UDP的发送过程还是相当直接的,它几乎不缓存应用写入的数据,直接将这些数据组装成UDP数据报,然后丢给IP处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值