文章目录
UDP套接字的端口号绑定发生在两种场景:
- 应用程序显示的调用bind(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()
该函数的功能并非如其名字所表达的单纯。大的流程上,它实际上实现了两个功能,如下图:
说明:
- 如果是标记bitmap,udp_lib_lport_inuse()会将冲突链中不能使用的端口号标记到bitmap中;
- 如果是判断指定端口是否冲突,那么会通过返回值告诉调用者,返回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都符合这个流程。