目录
2.2 已绑定端口信息哈希表 inet_bind_hashbucket
1. bind 概述
应用程序可以通过bind()系统调用将套接字和本地地址绑定,这里的地址包括L3的IP地址和L4的端口,应用程序可以只指定其中一个,另外一个由内核自动选择。关于bind系统调用的具体用法参见《socket编程:bind 函数说明》
这里我们不关注IP地址的绑定过程,因为这没什么好看的,就是校验地址合法性,然后保存到内核相关数据结构中即可;这里重点看L4端口的绑定过程。
2. 端口信息的管理
一般来讲,一个端口不能同时分配给两个套接字使用(这里不考虑端口复用的情形,这是另外一个复杂的话题),这样可能会破坏五元组的唯一性,导致数据收发的混乱。所以TCP有必要将那些已分配的端口维护起来,这样在绑定过程中,才能快速的识别是否能够绑定成功。
2.1 端口信息 inet_bind_bucket
内核定义了struct inet_bind_bucket来表示一个已经被绑定了的端口,每一个已绑定端口对应一个该结构,该结构定义如下:
struct inet_bind_bucket {
struct net *ib_net;
//端口号,主机字节序
unsigned short port;
//端口复用相关
signed short fastreuse;
//用于将inet_bind_bucket结构组织成哈希列表
struct hlist_node node;
//端口被分配给了哪个套接字。由于端口可能被多个套接字复用,所以这里使用哈希链表
//该链表的元素为struct tcp_sock
struct hlist_head owners;
};
可见,数据结构的定义相对来说是直接的,包含了端口号和对应的TCB(传输控制块)。
2.2 已绑定端口信息哈希表 inet_bind_hashbucket
整个TCP层面使用哈希表为来组织已绑定端口信息,即上面的struct inet_bind_bucket。哈希表是一个全局结构,其占用内存在TCP协议初始化函数 tcp_init() 执行过程中分配。
path: net/ipv4/tcp_ipv4.c
struct inet_hashinfo __cacheline_aligned tcp_hashinfo;
//inet_hashinfo是TCP层面的多个哈希表的集合,下面只列出了和端口管理相关的字段
struct inet_hashinfo {
...
//指向已绑定端口哈希表,哈希表占用内存在tcp_init()中分配
struct inet_bind_hashbucket *bhash;
//bhash哈希表的桶大小,必要时会扩大哈希表的容量以提升效率
unsigned int bhash_size;
//保护对该结构成员的互斥访问
rwlock_t lhash_lock ____cacheline_aligned;
//对该结构的引用计数
atomic_t lhash_users;
//指向一个用于分配struct inet_bind_bucket的高速缓存,该缓存同样在tcp_init()中创建
struct kmem_cache *bind_bucket_cachep;
};
从上面可以看到,哈希桶中表头元素并不是struct inet_bind_bucket,而是struct inet_bind_hashbucket,该结构定义如下,表头元素定义了一个自旋锁,这样可以降低锁的粒度,提升该哈希表的效率。
struct inet_bind_hashbucket {
spinlock_t lock;
struct hlist_head chain;
};
最后,已分配端口信息的哈希表组织结构如下图所示:
3. bind 内核实现(tcp)
这里我们以tcp协议类型的套接字实现bing的过程,以linux内核 2.6.38为例,内核堆栈如下:
sys_bind
inet_bind
udp_v4_get_port ---udp
inet_csk_get_port --tcp
3.1 sys_bind()
正如接口注释锁描述,sys_bind执行如下动作:
- 根据用户空间传入的fd获取相应的套接字sock
- 拷贝用户空间的地址信息(ip_addr,port)
- 调用 AF_INET 协议族的绑定接口inet_bind
/*
* 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).
*/
asmlinkage long sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
{
struct socket *sock;
struct sockaddr_storage address;
int err, fput_needed;
//1、根据 sock_fd 获取sock
sock = sockfd_lookup_light(fd, &err, &fput_needed);
if (sock) {
//2、拷贝用户用户空间的地址
err = move_addr_to_kernel(umyaddr, addrlen, (struct sockaddr *)&address);
if (err >= 0) {
err = security_socket_bind(sock,
(struct sockaddr *)&address,
addrlen);
//3、调用inet协议族的绑定接口inet_bind
if (!err)
err = sock->ops->bind(sock,
(struct sockaddr *)
&address, addrlen);
}
fput_light(sock->file, fput_needed);
}
return err;
}
3.2 inet_bind()
inet_bind()是AF_INET协议族提供的处理bind()系统调用的接口。功能:1、参数检查;2、调用套接字对应的端口绑定接口,具体如下:
- 判断套接字是否提供bind接口,目前raw类型套接字提供bind接口
- 校验ip地址类型是否满足AF_INET、广播、组播、任意地址类型等
- 校验端口合法性:是否重复绑定、< 1024 是否有权限
- 根据套接字类型绑定地址信息到套接字中,udp、tcp在此处分流
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;
//如果传输层提供了bind()接口,则直接使用传输层的接口完成绑定;
//IPv4协议族中只有RAW套接字实现了该接口
if (sk->sk_prot->bind) {
err = sk->sk_prot->bind(sk, uaddr, addr_len);
goto out;
}
//校验地址信息结构是否是AF_INET协议族的地址结构
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);
//这里涉及较多的新概念,不过其大体意思是判定应用程序是否可以绑定到某些特别的IP地址上面
/* 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;
//系统调用参数指定要绑定的端口,0表示有内核自动绑定一个端口
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;
//如果TCB的状态不是CLOSE或者该TCB已经绑定过了(绑定后的源端口信息会被保存
//到inet->num中,见下文),那么绑定失败,可以看出内核不允许重复调用bind()
if (sk->sk_state != TCP_CLOSE || inet->num)
goto out_release_sock;
//将应用程序指定要绑定的地址保存到TCB中。关于这两个地址的区别,待研究
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 */
//调用传输层协议提供的接口执行具体的端口绑定:
//TCP为inet_csk_get_port();UDP为udp_v4_get_port(),
if (sk->sk_prot->get_port(sk, snum)) {
//返回非0值,绑定失败,返回地址被使用错误
inet->saddr = inet->rcv_saddr = 0;
err = -EADDRINUSE;
goto out_release_sock;
}
//设置地址和端口绑定标记到TCB中
if (inet->rcv_saddr)
sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
if (snum)
sk->sk_userlocks |= SOCK_BINDPORT_LOCK;
//已绑定端口的网络字节序表示保存到inet->sport中
inet->sport = htons(inet->num);
inet->daddr = 0;
inet->dport = 0;
//复位路由信息
sk_dst_reset(sk);
err = 0;
out_release_sock:
release_sock(sk);
out:
return err;
}
注:inet_bind()属于AF_INET协议族层面的绑定处理,所以UDP的绑定也会执行该函数。
3.3 inet_csk_get_port()(核心)
tcp 协议的端口绑定过程由函数 inet_csk_get_port() 完成,在看该函数实现之前,先要理清楚该函数要完成的工作:
- 判断端口是用户指定还是系统随机指定?用户指定需要判断port是否被重复绑定(相同IP地址)
- 如果应用程序没有指明要绑定的端口,那么首先分配一个可用的端口,并将其加入到端口hash表中;
- 拿到了可用端口后,如果是尚未分配的,那么需要创建对应的端口信息 tb,即struct inet_bind_bucket,并初始化其中的各个字段;如果是已经分配了的端口,那么发生了端口复用,更新已有的struct inet_bind_bucket字段即可;
- 创建 tb 完成后,将 tb 加入到TCP的icsk_bind_hash中,再将该TCB加入到端口信息的owner链表中,即建立TCB和端口信息结构之间的映射关系。
//snum就是应用调用bind()时指定的端口号
int inet_csk_get_port(struct sock *sk, unsigned short snum)
{
struct inet_hashinfo *hashinfo = sk->sk_prot->hashinfo;
struct inet_bind_hashbucket *head;
struct hlist_node *node;
struct inet_bind_bucket *tb;
int ret;
struct net *net = sk->sk_net;
local_bh_disable();
if (!snum) {
//应用没有指明要绑定哪个端口,需要由内核自动选择一个
int remaining, rover, low, high;
//获取可用于动态绑定的端口区间[low,high],这是由两个系统参数指定的值
inet_get_local_port_range(&low, &high);
//下面循环的核心目的就是找一个可用的端口号,而且尽可能的保证寻找过程具有一定的随机性;
//这样可以保证动态分配的端口号能够尽可能均匀的分布在bhash中
//初始化remaining为端口区间的长度,下面会尝试在[low,high]之间
//找一个可用端口,所以remaining代表的就是最大循环次数
remaining = (high - low) + 1;
//随机选取一个循环起点
rover = net_random() % remaining + low;
do {
//获取端口号对应哈系表表头,哈希算法就是“端口号%哈希表长度”
head = &hashinfo->bhash[inet_bhashfn(rover, hashinfo->bhash_size)];
spin_lock(&head->lock);
//遍历该哈希表,一旦该列表中有相同的端口号(已经被绑定了)则继续轮询下一个
//端口号(跳到netx标签处),由此可见,动态绑定是永远都不会复用已绑定端口的
inet_bind_bucket_for_each(tb, node, &head->chain)
if (tb->ib_net == net && tb->port == rover)
goto next;
//到这里,说明rover就是一个可用的空闲端口,结束查找过程
break;
next:
spin_unlock(&head->lock);
//轮询到达动态端口区间上界,则从下界开始继续轮询
if (++rover > high)
rover = low;
} while (--remaining > 0);
/* Exhausted local port range during search? It is not
* possible for us to be holding one of the bind hash
* locks if this test triggers, because if 'remaining'
* drops to zero, we broke out of the do/while loop at
* the top level, not from the 'break;' statement.
*/
ret = 1;
//remaining小于等于0,说明上面没有找到空闲端口
if (remaining <= 0)
goto fail;
//将找到的空闲端口号记录到snum中
/* OK, here is the one we will use. HEAD is
* non-NULL and we hold it's mutex.
*/
snum = rover;
} else {
//应用程序指明了要绑定的端口号,直接找到对应的哈希列表
head = &hashinfo->bhash[inet_bhashfn(snum, hashinfo->bhash_size)];
spin_lock(&head->lock);
//遍历该哈希列表:
//1. 如果能够找到该端口号,说明该端口号已经被其它套接字绑定过了(相同ip、port),这时需要跳转到
// tb_found标签处继续判断该端口是否允许复用.
//2. 如果该循环没有找到该端口号,那么说明应用程序指定的端口号还没有被绑定过
inet_bind_bucket_for_each(tb, node, &head->chain)
if (tb->ib_net == net && tb->port == snum)
goto tb_found;
}
//到这里有两种情况:
//1. 动态绑定场景:找到了一个可用的空闲端口号
//2. 应用程序指定了端口号场景:该端口号尚未被任何套接字绑定过
//这两种场景都需要跳转到tb_not_found处创建端口信息结构struct inet_bind_bucket,
//并将其加入到TCP的bhash表中
tb = NULL;
goto tb_not_found;
tb_found:
//到这里说明要绑定的端口已经被其它套接字绑定,这时需要判断端口是否允许被复用。
//这里之所以还要判断owners链表不为空,是为了让该函数提供端口检查的功能:即判
//断是否已经当前套接字是否已经绑定了指定端口,如果绑定了,那么直接返回成功,见
//《TCP之系统调用listen()》中有关该功能的用法
//下面实际上就是判断端口是否可以复用的逻辑,如果判断可以复用,那么绑定成功,否则绑定失败
if (!hlist_empty(&tb->owners)) {
if (sk->sk_reuse > 1)
goto success;
if (tb->fastreuse > 0 &&
sk->sk_reuse && sk->sk_state != TCP_LISTEN) {
goto success;
} else {
ret = 1;
if (inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb))
goto fail_unlock;
}
}
tb_not_found:
ret = 1;
//根据snum创建一个新的端口信息结构并将该结构加入到bhash中
if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep,
net, head, snum)) == NULL)
goto fail_unlock;
//设置struct inet_bind_bucket中的复用标记
if (hlist_empty(&tb->owners)) {
if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
tb->fastreuse = 1;
else
tb->fastreuse = 0;
}
else if (tb->fastreuse && (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
tb->fastreuse = 0;
success:
//使得TCB的icsk_bind_hash成员指向端口信息结构,并将该TCB加入到端口信息的owner链表中,
//即建立TCB和端口信息结构之间的相互关联关系
if (!inet_csk(sk)->icsk_bind_hash)
inet_bind_hash(sk, tb, snum);
BUG_TRAP(inet_csk(sk)->icsk_bind_hash == tb);
ret = 0;
fail_unlock:
spin_unlock(&head->lock);
fail:
local_bh_enable();
return ret;
}
3.3.1 动态端口范围
从上面可以看出,动态指定的端口是取自一个区间的,而且动态指定的端口一定是还没有被任何套接字绑定过的端口。可以通过/proc/sys/net/ipv4/ip_local_port_range设置这两个参数。
/*
* This array holds the first and last local port number.
*/
int sysctl_local_port_range[2] = { 32768, 61000 };
DEFINE_SEQLOCK(sysctl_port_range_lock);
void inet_get_local_port_range(int *low, int *high)
{
unsigned seq;
do {
seq = read_seqbegin(&sysctl_port_range_lock);
//动态可分配的端口区间是由下面的系统参数确认的,代码默认范围为[32768, 61000]
*low = sysctl_local_port_range[0];
*high = sysctl_local_port_range[1];
} while (read_seqretry(&sysctl_port_range_lock, seq));
}
3.3.2 端口和套接字映射 inet_bind_hash
- 将端口号保存到sock->snum。
- 将sock添加到端口信息的owner链表中。
- 将端口信息保存到sock的绑定信息icsk_bind_hash中。
void inet_bind_hash(struct sock *sk, struct inet_bind_bucket *tb,
const unsigned short snum)
{
inet_sk(sk)->num = snum;
//将TCB加入到端口信息接口的owner链表中
sk_add_bind_node(sk, &tb->owners);
inet_csk(sk)->icsk_bind_hash = tb;
}
3.3.3 端口复用
....