tcp/ip 协议栈Linux内核源码分析14 udp套接字接收流程一

内核版本:3.4.39

前面两篇文章分析了UDP套接字从应用层发送数据到内核层的处理流程,这里继续分析相反的流程,看看数据是怎么从内核送到应用层的。

与发送类似,内核也提供了多个接收数据的系统调用接口,接口定义如下:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                     struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

与send类似,recv一般也是面向连接的套接字。原因在于,对于非面向连接的套接字来说,若使用recv接收数据,通过该接口将不能获得发送端的地址,也就是说不知道这个数据是谁发过来的。所以,如果使用者不关心发送端信息,或者该信息可以从数据中获得,那么recv接口同样也可以用于非面向连接的套接字。再来看看recvfrom,它会通过额外的参数src_addr和addrlen,来获得发送方的地址,其中需要注意的是addrlen,它既是输入值又是输出值。最后是recvmsg,它与sendmsg一样,把接收到的数据和地址都保存在了msg中。其中msg.msg_name和msg.msg_len用于保存接收端地址,而msg.msg_iov用于保存接收到的数据。

先看下recv系统调用的内核源码:

asmlinkage long sys_recv(int fd, void __user *ubuf, size_t size,
			 unsigned flags)
{
	return sys_recvfrom(fd, ubuf, size, flags, NULL, NULL);
}

代码很简单,recv完全是通过调用sys_recvfrom来实现的,仅仅是将sys_recvfrom的最后两个参数设置为0而已。

那么接下来就进入recvfrom的源码:

SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t, size,
		unsigned, flags, struct sockaddr __user *, addr,
		int __user *, addr_len)
{
	struct socket *sock;
	struct iovec iov;
	struct msghdr msg;
	struct sockaddr_storage address;
	int err, err2;
	int fput_needed;

	/* 限制读取字节长度的最大值为整数的最大值INT_MAX */
	if (size > INT_MAX)
		size = INT_MAX;
	/* 从文件描述符得到套接字结构 */
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;
		
	/* 控制信息清零 */
	msg.msg_control = NULL;
	msg.msg_controllen = 0;
	/* 设置消息的数据段信息 */
	msg.msg_iovlen = 1;
	msg.msg_iov = &iov;
	iov.iov_len = size;
	iov.iov_base = ubuf;
	/* 设置消息的存储地址信息 */
	msg.msg_name = (struct sockaddr *)&address;
	msg.msg_namelen = sizeof(address);
	/* 如果套接字设置了O_NONBLOCK标志,即非阻塞标志,则设置MSG_DONTWAIT标志,表示此次接收消息,无须等待 */
	if (sock->file->f_flags & O_NONBLOCK)
		flags |= MSG_DONTWAIT;
	/* 调用sock_recvmsg接收数据 */	
	err = sock_recvmsg(sock, &msg, size, flags);

	if (err >= 0 && addr != NULL) {
		/* 将地址信息复制到用户空间 */
		err2 = move_addr_to_user(&address,
					 msg.msg_namelen, addr, addr_len);
		if (err2 < 0)
			err = err2;
	}

	fput_light(sock->file, fput_needed);
out:
	return err;
}

后面的调用流程则为sock_recvmsg→__sock_recvmsg→__sock_recvmsg_nosec。

下面跟踪第三个接收数据包的系统调用recvmsg,代码如下:

SYSCALL_DEFINE3(recvmsg, int, fd, struct msghdr __user *, msg,
		unsigned int, flags)
{
	int fput_needed, err;
	struct msghdr msg_sys;
	/* 从文件描述符fd获得套接字 */	
	struct socket *sock = sockfd_lookup_light(fd, &err, &fput_needed);

	if (!sock)
		goto out;
	/* __sys_recvmsg用于实现接收数据 */
	err = __sys_recvmsg(sock, msg, &msg_sys, flags, 0);
	/* 释放fd引用(如果需要的话),这也是fput_light与fput的区别 */
	fput_light(sock->file, fput_needed);
out:
	return err;
}

 下面进入__sys_recvmsg,代码如下:

static int __sys_recvmsg(struct socket *sock, struct msghdr __user *msg,
			 struct msghdr *msg_sys, unsigned flags, int nosec)
{
	struct compat_msghdr __user *msg_compat =
	    (struct compat_msghdr __user *)msg;
	struct iovec iovstack[UIO_FASTIOV];
	struct iovec *iov = iovstack;
	unsigned long cmsg_ptr;
	int err, iov_size, total_len, len;

	/* kernel mode address */
	struct sockaddr_storage addr;

	/* user mode address pointers */
	struct sockaddr __user *uaddr;
	int __user *uaddr_len;

	/* 将消息头从用户空间复制到内核空间 */
	if (MSG_CMSG_COMPAT & flags) {
		if (get_compat_msghdr(msg_sys, msg_compat))
			return -EFAULT;
	} else if (copy_from_user(msg_sys, msg, sizeof(struct msghdr)))
		return -EFAULT;

	err = -EMSGSIZE;
	/* 检查数据段的个数 */
	if (msg_sys->msg_iovlen > UIO_MAXIOV)
		goto out;

	/* Check whether to allocate the iovec area */
	err = -ENOMEM;
	 /*
    为了避免频繁申请内存,内核在栈上申请了UIO_FASTIOV大小的iovec数组以供iov使用。
当数据段个数超过UIO_FASTIOV时,就需要动态申请内存。*/
	iov_size = msg_sys->msg_iovlen * sizeof(struct iovec);
	if (msg_sys->msg_iovlen > UIO_FASTIOV) {
		iov = sock_kmalloc(sock->sk, iov_size, GFP_KERNEL);
		if (!iov)
			goto out;
	}

	/*
	 *      Save the user-mode address (verify_iovec will change the
	 *      kernel msghdr to use the kernel address space)
	 */
	/* 验证用户传递的数据段参数和地址参数 */
	uaddr = (__force void __user *)msg_sys->msg_name;
	uaddr_len = COMPAT_NAMELEN(msg);
	if (MSG_CMSG_COMPAT & flags) {
		err = verify_compat_iovec(msg_sys, iov, &addr, VERIFY_WRITE);
	} else
		err = verify_iovec(msg_sys, iov, &addr, VERIFY_WRITE);
	if (err < 0)
		goto out_freeiov;
	total_len = err;

	cmsg_ptr = (unsigned long)msg_sys->msg_control;
	/* 确保消息标志中只有内核支持的两个标志 */
	msg_sys->msg_flags = flags & (MSG_CMSG_CLOEXEC|MSG_CMSG_COMPAT);
	/* 如果套接字为非阻塞,则设置标志位为不等待(非阻塞) */
	if (sock->file->f_flags & O_NONBLOCK)
		flags |= MSG_DONTWAIT;
	/* 根据安全检查标志,调用不同的接收函数,但最终都会调用到sock_recvmsg */
	err = (nosec ? sock_recvmsg_nosec : sock_recvmsg)(sock, msg_sys,
							  total_len, flags);
	if (err < 0)
		goto out_freeiov;
	len = err;

	/* 将发送端的地址复制到用户空间 */
	if (uaddr != NULL) {
		err = move_addr_to_user(&addr,
					msg_sys->msg_namelen, uaddr,
					uaddr_len);
		if (err < 0)
			goto out_freeiov;
	}
	err = __put_user((msg_sys->msg_flags & ~MSG_CMSG_COMPAT),
			 COMPAT_FLAGS(msg));
	if (err)
		goto out_freeiov;
	if (MSG_CMSG_COMPAT & flags)
		err = __put_user((unsigned long)msg_sys->msg_control - cmsg_ptr,
				 &msg_compat->msg_controllen);
	else
		err = __put_user((unsigned long)msg_sys->msg_control - cmsg_ptr,
				 &msg->msg_controllen);
	if (err)
		goto out_freeiov;
	err = len;

out_freeiov:
	if (iov != iovstack)
		sock_kfree_s(sock->sk, iov, iov_size);
out:
	return err;
}

由上面的代码可以看出,内核提供的三个接收数据包的系统调用,最终确实如我们所期望的,都会走到一个共同的函数__sock_recvmsg_nose里。下面来看一下这个函数,代码如下:

static inline int __sock_recvmsg_nosec(struct kiocb *iocb, struct socket *sock,
				       struct msghdr *msg, size_t size, int flags)
{
	struct sock_iocb *si = kiocb_to_siocb(iocb);

	sock_update_classid(sock->sk);
	/* 设置套接字异步IO信息 */
	si->sock = sock;
	si->scm = NULL;
	si->msg = msg;
	si->size = size;
	si->flags = flags;
	/* 根据不同的套接字类型,调用不同的数据接收函数 */
	return sock->ops->recvmsg(iocb, sock, msg, size, flags);
}

 根据上面的代码,后面的接收流程就要依赖于具体的协议实现了。

我们来分析一下相对简单的UDP协议的数据包接收流程,代码如下:

int udp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int noblock, int flags, int *addr_len)
{
	struct inet_sock *inet = inet_sk(sk);
	/* 让sin指向msg_name,用于保存发送端地址 */
	struct sockaddr_in *sin = (struct sockaddr_in *)msg->msg_name;
	struct sk_buff *skb;
	unsigned int ulen, copied;
	int peeked, off = 0;
	int err;
	int is_udplite = IS_UDPLITE(sk);
	bool slow;

	/*
	 *	Check any passed addresses
	 */
	/* 若addr_len不为NULL,即用户传递了地址长度参数。进入了具体的协议层,已经可以明确地址的长度信息了。 */ 
	if (addr_len)
		*addr_len = sizeof(*sin);
		
	/* 用户设置了MSG_ERRQUEUE标志,用于接收错误消息。因为这个应用并不广泛,因此在此忽略这种情况,
	不进入该函数。 */
	if (flags & MSG_ERRQUEUE)
		return ip_recv_error(sk, msg, len);

try_again:
	/* 接收了一个数据报文 */
	skb = __skb_recv_datagram(sk, flags | (noblock ? MSG_DONTWAIT : 0),
				  &peeked, &off, &err);
	/* 若没有收到报文,则直接退出 */
	if (!skb)
		goto out;
		
	/* 得到UDP的数据长度 */
	ulen = skb->len - sizeof(struct udphdr);
	/* 要复制的长度被初始化为用户指定的长度 */
	copied = len;
	/* 若复制长度大于UDP的数据长度,则调整复制长度为数据长度。若复制长度小于数据长度,
	则设置标志MSG_TRUNC,表示数据发生了截断。 */
	if (copied > ulen)
		copied = ulen;
	else if (copied < ulen)
		msg->msg_flags |= MSG_TRUNC;

	/*
	 * If checksum is needed at all, try to do it while copying the
	 * data.  If the data is truncated, or if we only want a partial
	 * coverage checksum (UDP-Lite), do it before the copy.
	 */
	/*
		如果发生了数据截断,或者我们只需要部分覆盖的校验和,那么就在复制前进行校验。
	*/
	if (copied < ulen || UDP_SKB_CB(skb)->partial_cov) {
		/* 进行UDP校验和校验 */
		if (udp_lib_checksum_complete(skb))
			goto csum_copy_err;
	}
	/* 判断是否需要进行校验和校验 */
	if (skb_csum_unnecessary(skb))
		/*  若不需要进行校验,则直接复制数据包内容到msg_iov中 */
		err = skb_copy_datagram_iovec(skb, sizeof(struct udphdr),
					      msg->msg_iov, copied);
	else {
		/* 复制数据包内容的同时,进行校验和校验 */
		err = skb_copy_and_csum_datagram_iovec(skb,
						       sizeof(struct udphdr),
						       msg->msg_iov);

		if (err == -EINVAL)
			goto csum_copy_err;
	}

	/* 复制错误检查 */
	if (err)
		goto out_free;

	/* 如果不是peek动作,则增加相应的统计计数 */
	if (!peeked)
		UDP_INC_STATS_USER(sock_net(sk),
				UDP_MIB_INDATAGRAMS, is_udplite);
	/* 更新套接字的最新的接收数据包时间戳及丢包消息 */
	sock_recv_ts_and_drops(msg, sk, skb);

	/* Copy the address. */
	/* 如果用户指定了保存对端地址的参数,则从数据包中复制地址和端口信息 */
	if (sin) {
		sin->sin_family = AF_INET;
		sin->sin_port = udp_hdr(skb)->source;
		sin->sin_addr.s_addr = ip_hdr(skb)->saddr;
		memset(sin->sin_zero, 0, sizeof(sin->sin_zero));
	}
	/* 设置了接收控制消息 */
	if (inet->cmsg_flags)
		/* 接收控制消息如TTL、TOS等 */
		ip_cmsg_recv(msg, skb);
	/* 设置了已复制的字节长度 */
	err = copied;
	if (flags & MSG_TRUNC)
		err = ulen;

out_free:
	/* 释放接收到的这个数据包 */
	skb_free_datagram_locked(sk, skb);
out:
	/* 返回读取的字节数 */
	return err;
	/* 错误处理 */
csum_copy_err:
	slow = lock_sock_fast(sk);
	if (!skb_kill_datagram(sk, skb, flags))
		UDP_INC_STATS_USER(sock_net(sk), UDP_MIB_INERRORS, is_udplite);
	unlock_sock_fast(sk, slow);

	if (noblock)
		return -EAGAIN;

	/* starting over for a new packet */
	msg->msg_flags &= ~MSG_TRUNC;
	goto try_again;
}

从上面的代码中,我们可以得到一个大部分书中都不会涉及的信息。先想一想,在读取一个UDP数据包时,如果传递给接口的缓存空间小于UDP数据包的实际大小时,结果会是什么样的呢?对于TCP来说,这个问题比较简单,因为其是流协议,没有数据报文边界,所以这次未读取的数据,会在下一次读取时被复制。但是UDP是基于数据包的,从上面的内核源码可以看到,当缓存小于UDP报文的实际大小时,内核会将报文截断,只复制缓存大小的数据,同时设置上MSG_TRUNC截断标志。这种情况,是很难从书本上了解到的,只有通过阅读源码才能理解其中的奥妙。

再进入__skb_recv_datagram,来查看UDP是如何接收报文的,代码如下:

struct sk_buff *__skb_recv_datagram(struct sock *sk, unsigned flags,
				    int *peeked, int *off, int *err)
{
	struct sk_buff *skb;
	long timeo;
	/*
	 * Caller is allowed not to check sk->sk_err before skb_recv_datagram()
	 */
	 /* 检查套接字是否出错 */
	int error = sock_error(sk);

	if (error)
		goto no_packet;
	/* 得到超时时间,如果设置了MSG_DONTWAIT,则超时为0。 */
	timeo = sock_rcvtimeo(sk, flags & MSG_DONTWAIT);

	do {
		/* Again only user level code calls this function, so nothing
		 * interrupt level will suddenly eat the receive_queue.
		 *
		 * Look at current nfs client by the way...
		 * However, this function was correct in any case. 8)
		 */
		unsigned long cpu_flags;
		struct sk_buff_head *queue = &sk->sk_receive_queue;

		spin_lock_irqsave(&queue->lock, cpu_flags);
		 /* 得到接收队列的第一个数据包 */
		skb_queue_walk(queue, skb) {
			*peeked = skb->peeked;
			/* 如果只是查看动作,则要增加数据包的引用计数,并不用把数据包从队列中移除。 */
			if (flags & MSG_PEEK) {
				if (*off >= skb->len && skb->len) {
					*off -= skb->len;
					continue;
				}
				skb->peeked = 1;
				atomic_inc(&skb->users);
			} else
				/* 将数据包从接收队列中删除 */
				__skb_unlink(skb, queue);

			/* 得到了数据包,直接返回 */
			spin_unlock_irqrestore(&queue->lock, cpu_flags);
			return skb;
		}
		spin_unlock_irqrestore(&queue->lock, cpu_flags);

		/* User doesn't want to wait */
		/* 若已经没有了剩余的超时时间,则跳转到no_packet并返回NULL */
		error = -EAGAIN;
		if (!timeo)
			goto no_packet;
				 /* 使task在套接字上等待 */
	} while (!wait_for_packet(sk, err, &timeo));

	return NULL;

no_packet:
	*err = error;
	return NULL;
}
EXPORT_SYMBOL(__skb_recv_datagram);

如果当前的UDP套接字没有数据包,则会进入wait_for_packet进行等待,代码如下:

static int wait_for_packet(struct sock *sk, int *err, long *timeo_p)
{
	int error;
	/* 定义等待队列和回调的唤醒函数 */
	DEFINE_WAIT_FUNC(wait, receiver_wake_function);
	/* 初始化等待队列,需要注意的是TASK_INTERRUPTIBLE。这表明进程在睡眠等待时,是可以被中断的。 */
	prepare_to_wait_exclusive(sk_sleep(sk), &wait, TASK_INTERRUPTIBLE);

	/* Socket errors? */
	/* 检查套接字是否出错,如被RESET。如有错误,则直接退出。 */
	error = sock_error(sk);
	if (error)
		goto out_err;

	/* 若接收队列不为空,则可以直接退出 */
	if (!skb_queue_empty(&sk->sk_receive_queue))
		goto out;

	/* Socket shut down? */
	/* 检查套接字是否已经做了接收半关闭 */
	if (sk->sk_shutdown & RCV_SHUTDOWN)
		goto out_noerr;

	/* Sequenced packets can come disconnected.
	 * If so we report the problem
	 */
	 /* 如果套接字是基于连接的,并且不是处于已连接状态或监听状态,则报错退出 */ 
	error = -ENOTCONN;
	if (connection_based(sk) &&
	    !(sk->sk_state == TCP_ESTABLISHED || sk->sk_state == TCP_LISTEN))
		goto out_err;

	/* 是否有未处理的信号 */
	/* handle signals */
	if (signal_pending(current))
		goto interrupted;

	/* 将当前进程调度出去,直到超时,即进程已经休眠了设定的超时时间。但是由于某些原因,
	 进程被提前唤醒,所以需要保存返回的时间*timeo_p,表示还剩下多少时间。 */
	error = 0;
	*timeo_p = schedule_timeout(*timeo_p);
out:
	finish_wait(sk_sleep(sk), &wait);
	return error;
interrupted:
	error = sock_intr_errno(*timeo_p);
out_err:
	*err = error;
	goto out;
out_noerr:
	*err = 0;
	error = 1;
	goto out;
}

 至此,UDP数据包的接收流程已经跟踪完毕,还剩下一个问题,内核接收到的数据包如何保存到套接字接收队列里。

这个放到下一篇文章里。

 

参考文档:

1. 《Linux环境编程:从应用到内核》

2.  浅析Linux网络子系统(一) 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值