TCP之系统调用bind()
这篇笔记记录了TCP协议对bind()系统调用的实现。
1. 概述 应用程序可以通过bind()系统调用将套接字和本地地址绑定,这里的地址包括L3的IP地址和L4的端口,应用程序可以只指定其中一个,另外一个由内核自动选择。
这里我们不关注IP地址的绑定过程,因为这没什么好看的,就是校验地址合法性,然后保存到内核相关数据结构中即可;这里重点看L4端口的绑定过程。
2. 端口信息的管理 一般来讲,一个端口不能同时分配给两个套接字使用(这里不考虑端口复用的情形,这是另外一个复杂的话题),这样可能会破坏五元组的唯一性,导致数据收发的混乱。所以TCP有必要将那些已分配的端口维护起来,这样在绑定过程中,才能快速的识别是否能够绑定成功。
2.1 端口信息 内核定义了struct inet_bind_bucket来表示一个已经被绑定了的端口,每一个已绑定端口对应一个该结构,该结构定义如下:
struct inet_bind_bucket {
struct net*ib_net;
//端口号,主机字节序
unsigned shortport;
//端口复用相关
signed shortfastreuse;
//用于将inet_bind_bucket结构组织成哈希列表
struct hlist_node node;
//端口被分配给了哪个套接字。由于端口可能被多个套接字复用,所以这里使用哈希链表
//该链表的元素为struct tcp_sock
struct hlist_head owners;
};
可见,数据结构的定义相对来说是直接的,包含了端口号和对应的TCB(传输控制块)。
2.2 已绑定端口信息哈希表 整个TCP层面使用哈希表为来组织已绑定端口信息,即上面的struct inet_bind_bucket。哈希表是一个全局结构,其占用内存在TCP协议初始化函数tcp_init()执行过程中分配。
path: net/ipv4/tcp_ipv4.cstruct inet_hashinfo __cacheline_aligned tcp_hashinfo;
//inet_hashinfo是TCP层面的多个哈希表的集合,下面只列出了和端口管理相关的字段
struct inet_hashinfo {
...
//指向已绑定端口哈希表,哈希表占用内存在tcp_init()中分配
struct inet_bind_hashbucket *bhash;
//bhash哈希表的桶大小,必要时会扩大哈希表的容量以提升效率
unsigned intbhash_size;
//保护对该结构成员的互斥访问
rwlock_tlhash_lock ____cacheline_aligned;
//对该结构的引用计数
atomic_tlhash_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_tlock;
struct hlist_head chain;
};
最后,已分配端口信息的哈希表组织结构如下图所示:
文章图片
3. bind()系统调用实现 我们直接略过文件系统和通用套接字层的处理,从协议族的处理开始看起。
3.1 inet_bind() inet_bind()是AF_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;
//如果传输层提供了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.2 inet_csk_get_port() TCP协议的端口绑定过程由函数inet_csk_get_port()完成,在看该函数实现之前,先要理清楚该函数要完成的工作:
- 如果应用程序没有指明要绑定的端口,那么首先分配一个可用的端口;
- 拿到了可用端口后,如果是尚未分配的,那么需要创建对应的端口信息,即struct inet_bind_bucket,并初始化其中的各个字段;如果是已经分配了的端口,那么发生了端口复用,更新已有的struct inet_bind_bucket字段即可;
- 工作完成后,将struct inet_bind_bucket结构加入到TCP的bhash中。
//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. 如果能够找到该端口号,说明该端口号已经被其它套接字绑定过了,这时需要跳转到
//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;
}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.2.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));
}
4. 小结 【TCP之系统调用bind()】这里并没有介绍判断端口复用的细节,因为要想理解这段代码,还有许多概念需要理解,所以这里暂时不深究,等需要时再来仔细研究。
推荐阅读
- CVE-2020-16898|CVE-2020-16898 TCP/IP远程代码执行漏洞
- PMSJ寻平面设计师之现代(Hyundai)
- 太平之莲
- 闲杂“细雨”
- 七年之痒之后
- 深入理解Go之generate
- 由浅入深理解AOP
- 期刊|期刊 | 国内核心期刊之(北大核心)
- 生活随笔|好天气下的意外之喜
- 感恩之旅第75天