UDP之系统调用bind()实现

本文探讨了UDP套接字的端口绑定,包括AF_INET协议族的`inet_bind()`接口,UDP端口分配流程,以及`udp_v4_get_port()`中的端口可用性判断。详细解析了`udp_lib_lport_inuse()`函数的双重功能,并介绍了端口冲突判断的条件。此外,还简要介绍了端口自动绑定`inet_autobind()`的实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


UDP套接字的端口号绑定发生在两种场景:

  1. 应用程序显示的调用bind(2)实现端口绑定。通常服务器端编程时会这么做,让服务端绑定都一个约定好的端口上;
  2. UDP套接字创建后的connect(2)/sendto(2)/sendmsg(2)调用流程中,kernel会在发送自动为该套接字绑定一个可用端口;

在内核实现时,无论是哪种情况,它们最终都是使用UDP协议的同一个接口进行端口分配的。

这篇笔记就来分析下UDP套接字的端口绑定相关代码的实现,主要是bind()系统调用的实现,最后也会简单的看下端口自动绑定相关的实现。

注意:bind(2)可以绑定IP地址和端口号,这里我们只关心端口的绑定过程

AF_INET协议族绑定接口: inet_bind()

实际上,系统调用bind()的真正入口在net/socket.c中,但是由于通用入口处处理的是文件描述符到struct socket的映射,然后就调用各个协议族提供的绑定接口,该流程不是我们关心的内容,所以我们从AF_INET协议族的绑定入口函数开始分析。

// 与系统调用bind(2)的参数含义相同
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;

	// 如果L4协议提供了自己的bind()回调,则直接使用L4自己的回调完成绑定。
	// AF_INET协议族中只有RAW套接字实现了该接口
	/* If the socket has its own bind function then use it. (RAW) */
	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;

	// 识别应用程序指定的IP地址类型
	chk_addr_ret = inet_addr_type(&init_net, 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)
	 */
	err = -EADDRNOTAVAIL;
	if (!sysctl_ip_nonlocal_bind && !inet->freebind && 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中
	snum = ntohs(addr->sin_port);
	err = -EACCES;
	// 如果应用程序指定了想要绑定的端口(不为0),并且该端口小于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;
	// 如果传输控制块的状态不是CLOSE或者该传输控制块已经绑定过了(绑定后的源端口信息会被保存
	// 到inet->num中,见下文),则不允许重复绑定
	if (sk->sk_state != TCP_CLOSE || inet->num)
		goto out_release_sock;

	// 将应用程序指定要绑定的IP地址保存到传输控制块中。关于这两个地址的区别,待研究
	inet->rcv_saddr = inet->saddr = addr->sin_addr.s_addr;
	if (chk_addr_ret == RTN_MULTICAST || chk_addr_ret == RTN_BROADCAST)
		inet->saddr = 0;  /* Use device */

	// 调用传输层协议提供的get_port()回调完成具体的端口绑定,
	// TCP为inet_csk_get_port(),UDP为udp_v4_get_port()
	if (sk->sk_prot->get_port(sk, snum)) {
		inet->saddr = inet->rcv_saddr = 0;
		err = -EADDRINUSE;
		goto out_release_sock;
	}

	// 端口绑定成功,设置地址和端口绑定标记到传输控制块中
	if (inet->rcv_saddr)
		sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
	if (snum)
		sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
	// 绑定端口的网络字节序表示保存到inet->sport中,inet->num保存的是主机字节序的已绑定端口值
	inet->sport = htons(inet->num);
	inet->daddr = 0;
	inet->dport = 0;
	// 复位socket的路由结果
	sk_dst_reset(sk);
	err = 0;
out_release_sock:
	release_sock(sk);
out:
	return err;
}

注意:inet_bind()实际上是AF_INET协议族提供的通用bind()处理过程,TCP和UDP是共用该函数的,它们只有在get_port()回调实现上有区别。

UDP端口分配

UDP传输控制块组织结构

UDP将所有绑定过的传输控制块组织到哈希表中。相关数据结构如下。

#define UDP_HTABLE_SIZE		128

// 哈希表桶节点
struct udp_hslot {
	struct hlist_nulls_head	head;
	spinlock_t		lock;
} __attribute__((aligned(2 * sizeof(long))));

struct udp_table {
	struct udp_hslot hash[UDP_HTABLE_SIZE];
}

哈希函数以本端端口为参数对哈希桶取余。

static inline int udp_hashfn(struct net *net, const unsigned num)
{
	return (num + net_hash_mix(net)) & (UDP_HTABLE_SIZE - 1);
}

上述数据结构之间的关系如下:
在这里插入图片描述
注意:实际中,每个冲突链都是双向循环链表,图中没有体现出循环链表。

udp_v4_get_port()

int udp_v4_get_port(struct sock *sk, unsigned short snum)
{
	// 回调函数ipv4_rcv_saddr_equel()用于判定两个地址是否相同,见下文
	return udp_lib_get_port(sk, snum, ipv4_rcv_saddr_equal);
}

/**
 *  udp_lib_get_port  -  UDP/-Lite port lookup for IPv4 and IPv6
 *
 *  @sk:          socket struct in question
 *  @snum:        port number to look up
 *  @saddr_comp:  AF-dependent comparison of bound local IP addresses
 */
int udp_lib_get_port(struct sock *sk, unsigned short snum,
	int (*saddr_comp)(const struct sock *sk1, const struct sock *sk2))
{
	struct udp_hslot *hslot;
	// UDP所有已绑定端口的套接字的传输控制块由全局的udp_table哈希表管理
	struct udp_table *udptable = sk->sk_prot->h.udp_table;
	int    error = 1;
	struct net *net = sock_net(sk);

	// 调用者没有指定绑定哪个具体端口,这时需要自动选择一个没有被使用的端口
	if (!snum) {
		int low, high, remaining;
		unsigned rand;
		unsigned short first, last;
		// 定义一个可以容纳65536个bit的数组
		DECLARE_BITMAP(bitmap, PORTS_PER_CHAIN);
		// 获取可用于自动分配的端口范围区间。这两个系统参数由sysctl_local_ports.range[0]和
		// sysctl_local_ports.range[1]指定,初始值为[32768, 61000],可以通过
		// "/proc/sys/net/ipv4/ip_local_port_range"修改这两个参数。注意:这两个参数TCP和UDP是共用的。
		inet_get_local_port_range(&low, &high);
		remaining = (high - low) + 1;

		// 随机选取一个遍历起点
		rand = net_random();
		first = (((u64)rand * remaining) >> 32) + low;
		/*
		 * force rand to be an odd multiple of UDP_HTABLE_SIZE
		 */
		rand = (rand | 1) * UDP_HTABLE_SIZE;
		// 遍历一边哈希桶就可以找到可用的端口
		for (last = first + UDP_HTABLE_SIZE; first != last; first++) {
			// hslot指向哈希表的一个冲突链
			hslot = &udptable->hash[udp_hashfn(net, first)];
			bitmap_zero(bitmap, PORTS_PER_CHAIN);
			spin_lock_bh(&hslot->lock);
			// 检查该冲突链上有哪些端口号已经被占用了,在相应的bitmap中设置为1
			udp_lib_lport_inuse(net, snum, hslot, bitmap, sk, saddr_comp);

			snum = first;
			/*
			 * Iterate on all possible values of snum for this hash.
			 * Using steps of an odd multiple of UDP_HTABLE_SIZE
			 * give us randomization and full range coverage.
			 */
			do {
				// 端口号满足要求,那么就找到了
				if (low <= snum && snum <= high && !test_bit(snum / UDP_HTABLE_SIZE, bitmap))
					goto found;
				snum += rand;
			} while (snum != first);
			spin_unlock_bh(&hslot->lock);
		}
		goto fail;
	} else {
		// 调用者指定了要绑定哪个端口,需要判断该端口是否可用
		hslot = &udptable->hash[udp_hashfn(net, snum)];
		spin_lock_bh(&hslot->lock);
		// 检查该端口是否可用
		if (udp_lib_lport_inuse(net, snum, hslot, NULL, sk, saddr_comp))
			goto fail_unlock;
	}
found:
	// 找到了可用端口,将其记录到传输控制块中
	inet_sk(sk)->num = snum;
	sk->sk_hash = snum;
	if (sk_unhashed(sk)) {
		// 将该传输控制块加入到udp_table哈希表中
		sk_nulls_add_node_rcu(sk, &hslot->head);
		// 为了统计
		sock_prot_inuse_add(sock_net(sk), sk->sk_prot, 1);
	}
	error = 0;
fail_unlock:
	spin_unlock_bh(&hslot->lock);
fail:
	return error;
}

端口可用性判断: udp_lib_lport_inuse()

该函数的功能并非如其名字所表达的单纯。大的流程上,它实际上实现了两个功能,如下图:

在这里插入图片描述

说明:

  1. 如果是标记bitmap,udp_lib_lport_inuse()会将冲突链中不能使用的端口号标记到bitmap中;
  2. 如果是判断指定端口是否冲突,那么会通过返回值告诉调用者,返回0表示不冲突,返回1表示冲突;

两个功能中对端口号是否冲突的判断逻辑是一致的。因为地址复用选项的存在,使得该判断逻辑较为复杂。

// 判断两socket绑定得本地地址是否相同,判断条件是严苛的,只要有一方的地址未指定,也认为是相同的
static int ipv4_rcv_saddr_equal(const struct sock *sk1, const struct sock *sk2)
{
	struct inet_sock *inet1 = inet_sk(sk1), *inet2 = inet_sk(sk2);

    // inet->rcv_saddr就是在bind(2)调用中用户态传入得地址
	return 	(!ipv6_only_sock(sk2)  &&
		  (!inet1->rcv_saddr || !inet2->rcv_saddr || inet1->rcv_saddr == inet2->rcv_saddr));
}

static int udp_lib_lport_inuse(struct net *net, __u16 num, const struct udp_hslot *hslot,
	unsigned long *bitmap, struct sock *sk,
	int (*saddr_comp)(const struct sock *sk1, const struct sock *sk2))
{
	struct sock *sk2;
	struct hlist_nulls_node *node;

	sk_nulls_for_each(sk2, node, &hslot->head)
		if (net_eq(sock_net(sk2), net) && // cond1: 同属一个网络命名空间
			sk2 != sk && // cond2: 不是同一个传输控制块
		    (bitmap || sk2->sk_hash == num)	&& cond3: 端口号相同
		    (!sk2->sk_reuse || !sk->sk_reuse) && cond4: 至少有一方没有打开端口可复用
		    // cond5: 至少有一方没有绑定设备,或者双方绑定到了同一个设备
		    (!sk2->sk_bound_dev_if || !sk->sk_bound_dev_if || sk2->sk_bound_dev_if == sk->sk_bound_dev_if) &&
		    (*saddr_comp)(sk, sk2)) // cond6: L3层地址相同
		{
			if (bitmap)
				__set_bit(sk2->sk_hash / UDP_HTABLE_SIZE, bitmap);
			else
				return 1;
		}
	return 0;
}

解释上述逻辑之前,先介绍两个socket选项:

  • SO_REUSEADDR选项:设置它表示该socket的本地地址是否可以和其它socket复用,对应的变量就是sk->sk_resue;
  • SO_BINDTODEVICE选项:设置它表示将socket绑定到某个网络设备上,对应的变量就是sk->sk_bound_dev_if;

两个端口出现冲突,必须同时满足上述6个条件。下面重点解释下cond4~cond6:

  • cond4: 两个socket至少有一方没有打开地址复用设置。这说明要想复用端口,前提是涉及的两个socket必须都要设置才行;
  • cond5:绑定的网络设备必须是不同的。因为L3层地址是配置在网络设备上的,该条件在一定程度上可以避免L3地址重复,但是最终还是得靠cond6来区分;
  • cond6:两个socket得L3绑定地址是相同的。

端口自动绑定: inet_autobind()

UDP套接字在connect(2)、sendmsg(2)、sendto(2)等系统调用中,都会尝试将未绑定的socket进行绑定,这时叫做“自动绑定”,就是让内核为其自动选择一个未使用的源端口号。

static int inet_autobind(struct sock *sk)
{
	struct inet_sock *inet;
	/* We may need to bind the socket. */
	lock_sock(sk);
	inet = inet_sk(sk);
	if (!inet->num) {
		// 调用的依然是get_port()回调
		if (sk->sk_prot->get_port(sk, 0)) {
			release_sock(sk);
			return -EAGAIN;
		}
		inet->sport = htons(inet->num);
	}
	release_sock(sk);
	return 0;
}

注:端口的自动绑定流程属于AF_INET协议族的通用处理,TCP和UDP都符合这个流程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值