当应用程序调用send()等一系列系统调用向UDP套接字写数据时,最终都会调用到传输层的udp_sendmsg(),这篇笔记重点分析下UDP协议是如何将用户态数据封装成skb递交给IP层的。
要点说明
在分析代码之前,有必要对一些UDP的写操作过程中的一些关键点进行说明,否则会看的晕头转向。
MSG_MORE标记
UDP数据报不像TCP,它是有边界的,即发送端的一个UDP数据报会完整的被接收端以一个UDP数据报的方式接收。
然而,并非一次写操作对应一个UDP数据报,应用程序可以通过MSG_MORE标记或者UDP_CORK选项将多次写操作的数据合并成一个UDP数据报发送。具体操作流程如下:
- 在调用sendmsg()时,flag参数中设置MSG_MORE标记,表示还有更多数据要发送。应用期望内核收到设置了该标记的数据时先不要将本次递交的数据发送给IP层,而是将其缓存,并且将后面连续的设定了该标记的数据合并成同一个UDP报文(一个IP报文,但是可能是多个IP片段)。直到没有设定该标记的发送时,将数据报发送给IP层;
- 类似的,在使能和关闭UDP_CORK选项期间发送的所有数据也要组合成一个UDP报文发送给IP。
注意:应用程序在使用这种方式的时候必须要注意多次组合的数据最好不要超过MTU,否则IP层就不得不将这些要组合的数据分成多个IP数据包发送出去,这样会造成性能的下降。
socket操作
用户态调用socket()创建UDP套接字后,有两种方式可以发送数据:
- 直接调用sendto()或者sendmsg(),在这些函数的参数中指定目的地址;
- 先调用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)