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

内核版本:3.4.39

因为过往的开发工作中既包括内核网络层模块的开发,又包括应用层程序的开发,所以对于网络数据的通信有那么一些了解。但是对于网络通信过程中,内核和应用层之间接口是如何运作的不是很清楚,很多问题无从回答,比如应用层数据如何传递给内核协议栈,网卡硬件收到报文后传递给网络协议栈,协议栈又是如何传递给用户层的?多线程共用同一个UDP套接字发送,数据会错乱吗?那么多套接字,内核如何区分?UDP有发送队列吗等等。本篇文章主要分析UDP套接字发送数据过程中应用层和内核层主要做了哪些工作。

通常我们开发网络通信程序的时候只需要调用gblic封装的库函数就可以了,比如说UDP通信,标准流程大概如下:

1. socket()函数创建套接字
2. bind()绑定本地地址或者连接connect()
3. send()、sendto()、sendmsg()发送

顺着函数调用顺序来分析:

首先是socket()调用,socket()创建一个套接字,成功后返回一个文件描述符。该调用由glibc封装,实际会调用内核的socket函数进行处理,简单的流程图如下:

socket内核实现

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
{
	int retval;
	struct socket *sock;	//通用套接字结构
	int flags;

	.............

	//创建一个套接字
	retval = sock_create(family, type, protocol, &sock);
	if (retval < 0)
		goto out;

	retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
	if (retval < 0)
		goto out_release;

        ........
}

 在成功创建套接字后,该套接字仅仅是一个文件描述符,并没有任何地址与之关联。使用该socket发送数据包时,由于该socket没有任何IP地址,内核会根据策略自动选择一个地址。但是,在某些情况下,我们需要手工指定socket使用哪个IP地址进行发送。这时,就需要使用bind系统调用了。

bind源码入口位于net/socket.c中:

/*
 *	Bind a name to a socket. Nothing much to do here since it's
 *	the protocol's responsibility to handle the local address.
 *
 *	We move the socket address to kernel space before we call
 *	the protocol layer (having also checked the address is ok).
 */

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;

	/* 由文件描述符得到套接字在内核中对应的结构struct socket */
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (sock) {
	
		/* umyaddr是用户空间地址,这里将其复制到内核空间address变量中 */
		err = move_addr_to_kernel(umyaddr, addrlen, &address);
		
		if (err >= 0) {
			/* 对bind动作进行安全性检查 */ 
			err = security_socket_bind(sock,
						   (struct sockaddr *)&address,
						   addrlen);
			if (!err)
				/* 调用对应协议的bind动作 */
				err = sock->ops->bind(sock,
						      (struct sockaddr *)
						      &address, addrlen);
		}
		fput_light(sock->file, fput_needed);
	}
	return err;
}

 在bind的调用中,根据不同的协议调用不同的实现函数(Linux的内核代码中,大量使用了这种面向对象的设计思路)。对于AF_INET协议族来说,无论是面向连接的SOCK_STREAM类型,还是SOCK_DGRAM协议类型,其实现函数均是inet_bind。下面来看一下inet_bind的具体实现:

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len)
{
	struct sockaddr_in *addr = (struct sockaddr_in *)uaddr;
	struct sock *sk = sock->sk;
	struct inet_sock *inet = inet_sk(sk);
	unsigned short snum;
	int chk_addr_ret;
	int err;

	/* If the socket has its own bind function then use it. (RAW) */
	/*
		如果具体协议实现了bind函数,则调用协议的bind函数。
		AF_INET协议族中,只有IPPROTO_ICMP和IPPROTO_IP实现了自己的bind函数,
		IPPROTO_TCP和IPPROTO_UDP都使用AF_INET通用的函数,即
		这个inet_bind。	 
	*/ 
	if (sk->sk_prot->bind) {
		err = sk->sk_prot->bind(sk, uaddr, addr_len);
		goto out;
	}
	err = -EINVAL;
	/* 检查地址长度 */
	if (addr_len < sizeof(struct sockaddr_in))
		goto out;

	/* 本来要求地址的协议族要与sock相同,必须为AF_INET,但是这里有个兼容性问题。
		允许协议族为AF_UNSPEC并且地址为INADDR_ANY的任意地址 
	*/ 
	if (addr->sin_family != AF_INET) {
		/* Compatibility games : accept AF_UNSPEC (mapped to AF_INET)
		 * only if s_addr is INADDR_ANY.
		 */
		err = -EAFNOSUPPORT;
		if (addr->sin_family != AF_UNSPEC ||
		    addr->sin_addr.s_addr != htonl(INADDR_ANY))
			goto out;
	}

	/* 判断地址类型 */
	chk_addr_ret = inet_addr_type(sock_net(sk), addr->sin_addr.s_addr);

	/* Not specified by any standard per-se, however it breaks too
	 * many applications when removed.  It is unfortunate since
	 * allowing applications to make a non-local bind solves
	 * several problems with systems using dynamic addressing.
	 * (ie. your servers still start up even if your ISDN link
	 *  is temporarily down)
	 */
	/*
        sysctl_ip_nonlocal_bind系统控制开关,允许bind非本地IP; inet->freebind为一个
        socket选项,允许该socket bind任意IP;在上面这些变量均不成立时,指定地址又不是任意的
        本地地址INADDR_ANY,地址类型又不是本地地址类型,多播或广播时,则bind失败。
    */        
	err = -EADDRNOTAVAIL;
	if (!sysctl_ip_nonlocal_bind &&
	    !(inet->freebind || inet->transparent) &&
	    addr->sin_addr.s_addr != htonl(INADDR_ANY) &&
	    chk_addr_ret != RTN_LOCAL &&
	    chk_addr_ret != RTN_MULTICAST &&
	    chk_addr_ret != RTN_BROADCAST)
		goto out;

	snum = ntohs(addr->sin_port);
	err = -EACCES;
	/* 如果源端口小于PROT_SOCK(1024),则需要检查用户是否有权限创建知名端口*/
	if (snum && snum < PROT_SOCK && !capable(CAP_NET_BIND_SERVICE))
		goto out;

	/*      We keep a pair of addresses. rcv_saddr is the one
	 *      used by hash lookups, and saddr is used for transmit.
	 *
	 *      In the BSD API these are the same except where it
	 *      would be illegal to use them (multicast/broadcast) in
	 *      which case the sending device address is used.
	 */
	lock_sock(sk);

	/* Check these errors (active socket, double bind). */
	err = -EINVAL;
	/* 确保套接字不会被bind两次 */
	if (sk->sk_state != TCP_CLOSE || inet->inet_num)
		goto out_release_sock;

	/* 使用参数设置套接字的接收和发送地址 */
	inet->inet_rcv_saddr = inet->inet_saddr = addr->sin_addr.s_addr;

	/* 如果参数地址是多播或广播类型,则重置发送源地址为0,表示在发送时,使用的是设备地址 */
	if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
		inet->inet_saddr = 0;  /* Use device */

	/* Make sure we are allowed to bind here. */
	/*  调用协议自定义的操作函数get_port,判断该端口是否可以使用。    虽然这里是一个查询的动作,
		但是却会有修改的动作。    当该端口可以使用时,会让inet_sk(sk)->inet_num = snum;    这样做,是因为
		查询动作已经获得了锁。在确定可以使用该端口时,直接修    改inet_num,这样既可以保证设置端口的
		原子性,同时还可以提高性能    
	*/
	if (sk->sk_prot->get_port(sk, snum)) {
		inet->inet_saddr = inet->inet_rcv_saddr = 0;
		err = -EADDRINUSE;
		goto out_release_sock;
	}

	/* 如果设置了bind地址,则置上相应的标志 */ 
	if (inet->inet_rcv_saddr)
		sk->sk_userlocks |= SOCK_BINDADDR_LOCK;

	/* 如果设置了源端口,则设置相应的标志 */
	if (snum)
		sk->sk_userlocks |= SOCK_BINDPORT_LOCK;

	/* 设置inet_sport,其为网络序 */	
	inet->inet_sport = htons(inet->inet_num);
	/* 重置目的地址和端口 */
	inet->inet_daddr = 0;
	inet->inet_dport = 0;

	/* 重置该套接字的路由信息 */
	sk_dst_reset(sk);
	err = 0;
out_release_sock:
	release_sock(sk);
out:
	return err;
}
EXPORT_SYMBOL(inet_bind);

connect的源码入口位于socket.c,代码如下: 

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *, uservaddr,
		int, addrlen)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err, fput_needed;

	/* 通过套接字文件描述符获得对应的struct socket */
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;

	/* 将用户空间地址复制到内核空间变量address中 */	
	err = move_addr_to_kernel(uservaddr, addrlen, &address);
	if (err < 0)
		goto out_put;

	/* 安全性检查 */
	err =
	    security_socket_connect(sock, (struct sockaddr *)&address, addrlen);
	if (err)
		goto out_put;

	/* 与bind类似,调用与协议族对应的connect操作函数 */
	err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen,
				 sock->file->f_flags);
out_put:
	fput_light(sock->file, fput_needed);
out:
	return err;
}

对于AF_INET协议族来说,面向连接的协议类型是SOCK_STREAM,其连接函数为inet_stream_connect,而非面向连接的协议类型SOCK_DGRAM,其连接函数为inet_dgram_connect。这很合理,因为从connect的功能实现上看,两者的实现效果完全不同。

看下UDP的inet_dgram_connect:

int inet_dgram_connect(struct socket *sock, struct sockaddr * uaddr,
		       int addr_len, int flags)
{
	struct sock *sk = sock->sk;

	/* 长度合法性检查 */
	if (addr_len < sizeof(uaddr->sa_family))
		return -EINVAL;

	/* 如果协议族为AF_UNSPEC,则先执行disconnect */ 
	if (uaddr->sa_family == AF_UNSPEC)
		return sk->sk_prot->disconnect(sk, flags);

	/* 如果该套接字没有指定源端口,并且系统自动绑定端口失败,则返回错误 */
	if (!inet_sk(sk)->inet_num && inet_autobind(sk))
		return -EAGAIN;

	/* 调用具体协议的connect实现函数 */	
	return sk->sk_prot->connect(sk, (struct sockaddr *)uaddr, addr_len);
}
EXPORT_SYMBOL(inet_dgram_connect);

udp_prot是UDP协议中所有自定义操作函数的集合。其connect的实现函数为ip4_datagram_connect/net/ipv4/datagram.c。

int ip4_datagram_connect(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
	struct inet_sock *inet = inet_sk(sk);
	struct sockaddr_in *usin = (struct sockaddr_in *) uaddr;
	struct flowi4 *fl4;
	struct rtable *rt;
	__be32 saddr;
	int oif;
	int err;

	/* 地址的长度性检查 */
	if (addr_len < sizeof(*usin))
		return -EINVAL;

	/* 检查是否为AF_INET协议族 */
	if (usin->sin_family != AF_INET)
		return -EAFNOSUPPORT;

	/* 因为connect会改变目的地址,所有socket中保存的路由缓存已经无用,必须重置。	 */ 
	sk_dst_reset(sk);

	lock_sock(sk);
	/* 得到套接字绑定的发送接口 */
	oif = sk->sk_bound_dev_if;
	saddr = inet->inet_saddr;
	
	/* 在目的地址是多播地址的情况下,    如果该套接字没有绑定网卡,则出口网卡为设置的多播网卡索引;    
		如果该套接字没有绑定源IP,则使用设置的多播源地址;
	*/
	if (ipv4_is_multicast(usin->sin_addr.s_addr)) {
		if (!oif)
			oif = inet->mc_index;
		if (!saddr)
			saddr = inet->mc_addr;
	}
	fl4 = &inet->cork.fl.u.ip4;

	/* 判断设置的目的地址是否存在正确的路由 */
	rt = ip_route_connect(fl4, usin->sin_addr.s_addr, saddr,
			      RT_CONN_FLAGS(sk), oif,
			      sk->sk_protocol,
			      inet->inet_sport, usin->sin_port, sk, true);
	if (IS_ERR(rt)) {
		err = PTR_ERR(rt);
		if (err == -ENETUNREACH)
			IP_INC_STATS_BH(sock_net(sk), IPSTATS_MIB_OUTNOROUTES);
		goto out;
	}
	
	/* 如果路由是广播类型,而套接字不是广播类型,则出错 */ 
	if ((rt->rt_flags & RTCF_BROADCAST) && !sock_flag(sk, SOCK_BROADCAST)) {
		ip_rt_put(rt);
		err = -EACCES;
		goto out;
	}

	/* 如果套接字没有设置发送地址或接收地址,则使用对应路由的源地址*/
	if (!inet->inet_saddr)
		inet->inet_saddr = fl4->saddr;	/* Update source address */
	if (!inet->inet_rcv_saddr) {
		inet->inet_rcv_saddr = fl4->saddr;
		if (sk->sk_prot->rehash)
			sk->sk_prot->rehash(sk);
	}

	/* 设置目的地址和端口 */
	inet->inet_daddr = fl4->daddr;
	inet->inet_dport = usin->sin_port;
	sk->sk_state = TCP_ESTABLISHED;
	inet->inet_id = jiffies;

	/* 重新设置路由信息 */
	sk_dst_set(sk, &rt->dst);
	err = 0;
out:
	release_sock(sk);
	return err;
}
EXPORT_SYMBOL(ip4_datagram_connect);

UDP套接字创建、绑定或者连接后就可以发送数据了。

Linux提供了如下发送接口:

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
            const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

send只能用于处理已连接状态的套接字。而sendto可以在调用时,指定目的地址。这样的话,如果套接字已经是连接状态,那么目的地址dest_addr与地址长度就应该为NULL和0,不然就可能会返回错误。sendmsg则比较特殊,无论是要发送的数据还是目的地址,都保存在msg中。其中msg.msg_name和msg.msg_len用于指明目的地址,而msg.msg_iov则用于保存要发送的数据。这三个系统调用都支持设置指示标志位flags。

send的内核实现代码如下:

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,
		unsigned, flags)
{
	/*
		send可以视为sendto的一种特例,即不设置目的地址的sendto调用。
		所以内核实现也是让send直接调用sendto。
	*/
	return sys_sendto(fd, buff, len, flags, NULL, 0);
}

既然其内核实现是让send直接调用sendto,那么,下面我们就来看一下sendto的内核实现,代码如下:

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,
		unsigned, flags, struct sockaddr __user *, addr,
		int, addr_len)
{
	struct socket *sock;
	struct sockaddr_storage address;
	int err;
	struct msghdr msg;
	struct iovec iov;
	int fput_needed;
	
	/* 长度合法性检查 */
	if (len > INT_MAX)
		len = INT_MAX;

	/* 从文件描述符获得套接字socket的结构 */	
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	if (!sock)
		goto out;

	/* 将数据转换为iovec结构,来调用后面的sendmsg */
	iov.iov_base = buff;
	iov.iov_len = len;
	msg.msg_name = NULL;
	msg.msg_iov = &iov;
	msg.msg_iovlen = 1;
	msg.msg_control = NULL;
	msg.msg_controllen = 0;
	msg.msg_namelen = 0;
	
	/* 如果设置了地址,则设置msg_name */
	if (addr) {
		/* 将地址参数复制到内核变量中 */
		err = move_addr_to_kernel(addr, addr_len, &address);
		if (err < 0)
			goto out_put;
		msg.msg_name = (struct sockaddr *)&address;
		msg.msg_namelen = addr_len;
	}

	/* 如果socket设置了非阻塞,则消息的标志设置为DONTWAIT(其实也是非阻塞的语义)*/
	if (sock->file->f_flags & O_NONBLOCK)
		flags |= MSG_DONTWAIT;
	msg.msg_flags = flags;

	/* 调用sock_sendmsg来发送数据包 */
	err = sock_sendmsg(sock, &msg, len);

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

 这里最终调用sock_sendmsg,我们先看下sendmsg调用,看看最后是不是也是调用sock_sendmsg:

SYSCALL_DEFINE3(sendmsg, int, fd, struct msghdr __user *, msg, unsigned, flags)
{
	int fput_needed, err;
	struct msghdr msg_sys;

	/* 通过文件描述符获得socket套接字结构 */
	struct socket *sock = sockfd_lookup_light(fd, &err, &fput_needed);

	if (!sock)
		goto out;
		
	/* 调用__sys_sendmsg来发送数据包 */
	err = __sys_sendmsg(sock, msg, &msg_sys, flags, NULL);

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

 接下来进入__sys_sendmsg,代码如下:

static int __sys_sendmsg(struct socket *sock, struct msghdr __user *msg,
			 struct msghdr *msg_sys, unsigned flags,
			 struct used_address *used_address)
{
	struct compat_msghdr __user *msg_compat =
	    (struct compat_msghdr __user *)msg;
	struct sockaddr_storage address;
	struct iovec iovstack[UIO_FASTIOV], *iov = iovstack;
	unsigned char ctl[sizeof(struct cmsghdr) + 20]
	    __attribute__ ((aligned(sizeof(__kernel_size_t))));
	/* 20 is size of ipv6_pktinfo */
	unsigned char *ctl_buf = ctl;
	int err, ctl_len, iov_size, total_len;

	err = -EFAULT;
	/* 从用户空间得到用户消息 */
	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;

	/* do not move before msg_sys is valid */
	err = -EMSGSIZE;
	/* 消息数据块个数检查 */
	if (msg_sys->msg_iovlen > UIO_MAXIOV)
		goto out;

	/* Check whether to allocate the iovec area */
	err = -ENOMEM;
	/* 在内核空间申请消息数据长度 */
	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;
	}

	/* This will also move the address data into kernel space */
	 /* 前面只是将消息头,或者说消息的结构体,复制到内核空间,现在是将消息的真正内容,
	 即iov的内容复制到内核空间 */
	if (MSG_CMSG_COMPAT & flags) {
		err = verify_compat_iovec(msg_sys, iov, &address, VERIFY_READ);
	} else
		err = verify_iovec(msg_sys, iov, &address, VERIFY_READ);
	if (err < 0)
		goto out_freeiov;
	total_len = err;

	err = -ENOBUFS;
	/* 与消息数据块类似,复制控制消息块,就不详细描述了 */
	if (msg_sys->msg_controllen > INT_MAX)
		goto out_freeiov;
	ctl_len = msg_sys->msg_controllen;
	if ((MSG_CMSG_COMPAT & flags) && ctl_len) {
		err =
		    cmsghdr_from_user_compat_to_kern(msg_sys, sock->sk, ctl,
						     sizeof(ctl));
		if (err)
			goto out_freeiov;
		ctl_buf = msg_sys->msg_control;
		ctl_len = msg_sys->msg_controllen;
	} else if (ctl_len) {
		if (ctl_len > sizeof(ctl)) {
			ctl_buf = sock_kmalloc(sock->sk, ctl_len, GFP_KERNEL);
			if (ctl_buf == NULL)
				goto out_freeiov;
		}
		err = -EFAULT;
		/*
		 * Careful! Before this, msg_sys->msg_control contains a user pointer.
		 * Afterwards, it will be a kernel pointer. Thus the compiler-assisted
		 * checking falls down on this.
		 */
		if (copy_from_user(ctl_buf,
				   (void __user __force *)msg_sys->msg_control,
				   ctl_len))
			goto out_freectl;

		msg_sys->msg_control = ctl_buf;
	}
	/* 设置消息标志 */
	msg_sys->msg_flags = flags;

	/* 如果套接字是非阻塞的,则设置消息标志MSG_DONTWAIT */
	if (sock->file->f_flags & O_NONBLOCK)
		msg_sys->msg_flags |= MSG_DONTWAIT;
	/*
	 * If this is sendmmsg() and current destination address is same as
	 * previously succeeded address, omit asking LSM's decision.
	 * used_address->name_len is initialized to UINT_MAX so that the first
	 * destination address never matches.
	 */
	/* 如果这次发送的目的地址与上次成功发送的目的地址一致,那就可以省略安全性检查 */ 
	if (used_address && msg_sys->msg_name &&
	    used_address->name_len == msg_sys->msg_namelen &&
	    !memcmp(&used_address->name, msg_sys->msg_name,
		    used_address->name_len)) {
		/* 调用不进行安全性检查的函数 */
		err = sock_sendmsg_nosec(sock, msg_sys, total_len);
		goto out_freectl;
	}

	/* 调用sock_sendmsg,需要安全性检查,最终仍然会调用到sock_send_msg_nosec函数 */
	err = sock_sendmsg(sock, msg_sys, total_len);
	/*
	 * If this is sendmmsg() and sending to current destination address was
	 * successful, remember it.
	 */
	/* 如果本次发送成功,则保存当前的目的地址 */ 
	if (used_address && err >= 0) {
		used_address->name_len = msg_sys->msg_namelen;
		if (msg_sys->msg_name)
			memcpy(&used_address->name, msg_sys->msg_name,
			       used_address->name_len);
	}

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

看完了__sys_sendmsg,我们可以确定,无论是哪个发送数据的系统调用,最终都会调用到sock_sendmsg。

文章有点长,sock_sendmsg放到下篇分析。

 

参考文档:

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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值