一、前情回顾
上一节《socket 地址绑定 》中提到,应用程序传递过来的端口在内核中需要检查端口是否可用:
if (sk->sk_prot->get_port(sk, snum)) { inet->saddr = inet->rcv_saddr = 0; err = -EADDRINUSE; goto out_release_sock; }
按照前面的例子来分析,这里是调用了 tcp_prot 结构变量中的 get_prot 函数指针,该函数位于 net/ipv4/Inet_connection_sock.c 中;这个函数比较长,也是我们今天要分析的重点;
二、端口的管理
1 、端口管理数据结构
Linux 内核将所有 socket 使用时的端口通过一个哈希表来管理,该哈希表存放在全局变量 tcp_hashinfo 中,通过 tcp_prot 变量的 h 成员引用,该成员是一个联合类型;对于 tcp 套接字类型,其引用存放在 h. hashinfo 成员中;下面是 tcp_hashinfo 的结构体类型:
struct inet_hashinfo { struct inet_ehash_bucket *ehash; rwlock_t *ehash_locks; unsigned int ehash_size; unsigned int ehash_locks_mask; struct inet_bind_hashbucket *bhash;//管理端口的哈希表 unsigned int bhash_size;//端口哈希表的大小 struct hlist_head listening_hash[INET_LHTABLE_SIZE]; rwlock_t lhash_lock ____cacheline_aligned; atomic_t lhash_users; wait_queue_head_t lhash_wait; struct kmem_cache *bind_bucket_cachep;//哈希表结构高速缓存 }
端口管理相关的,目前可以只关注加注释的这三个成员,其中 bhash 为已经哈希表结构, bhash_size 为哈希表的大小;所有哈希表中的节点内存都是在 bind_bucket_cachep 高速缓存中分配;
下面看一下 inet_bind_hashbucket 结构体:
struct inet_bind_hashbucket { spinlock_t lock; struct hlist_head chain; }; struct hlist_head { struct hlist_node *first; }; struct hlist_node { struct hlist_node *next, **pprev; };
inet_bind_hashbucket 是哈希桶结构, lock 成员是用于操作时对桶进行加锁, chain 成员是相同哈希值的节点的链表;示意图如下:
2 、默认端口的分配
当应用程序没有指定端口时(如 socket 客户端连接到服务端时,会由内核从可用端口中分配一个给该 socket );
看看下面的代码 ( 参见 net/ipv4/Inet_connection_sock.c: inet_csk_get_port() 函数 ) :
if (!snum) { int remaining, rover, low, high; inet_get_local_port_range(&low, &high); remaining = (high - low) + 1; rover = net_random() % remaining + low; do { head = &hashinfo->bhash[inet_bhashfn(rover, hashinfo->bhash_size)]; spin_lock(&head->lock); inet_bind_bucket_for_each(tb, node, &head->chain) if (tb->ib_net == net && tb->port == rover) goto next; break; next: spin_unlock(&head->lock); if (++rover > high) rover = low; } while (--remaining > 0); ret = 1; if (remaining <= 0) goto fail; snum = rover; }
这里,随机端口的范围是 32768~61000 ;上面代码的逻辑如下:
1) 从 [32768, 61000] 中随机取一个端口 rover ;
2) 计算该端口的 hash 值,然后从全局变量 tcp_hashinfo 的哈希表 bhash 中取出相同哈希值的链表 head ;
3) 遍历链表 head ,检查每个节点的网络设备是否和当前网络设置相同,同时检查节点的端口是否和 rover 相同;
4) 如果相同,表明端口被占用,继续下一个端口;如果和链表 head 中的节点都不相同,则跳出循环,继续后面的逻辑;
inet_bind_bucket_foreach 宏利用《 创建 socket 》一文中提到的 container_of 宏来实现 的,大家可以自己看看;
3 、端口重用
当应用程序指定端口时,参考下面的源代码:
else { head = &hashinfo->bhash[inet_bhashfn(snum, hashinfo->bhash_size)]; spin_lock(&head->lock); inet_bind_bucket_for_each(tb, node, &head->chain) if (tb->ib_net == net && tb->port == snum) goto tb_found; }
此时同样会检查该端口有没有被占用;如果被占用,会检查端口重用(跳转到 tb_found ):
tb_found: if (!hlist_empty(&tb->owners)) { 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; } }
1) 端口节点结构
struct inet_bind_bucket { struct net *ib_net;//端口所对应的网络设置 unsigned short port;//端口号 signed short fastreuse;//是否可重用 struct hlist_node node;//作为bhash中chain链表的节点 struct hlist_head owners;//绑定在该端口上的socket链表 };
前面提到的哈希桶结构中的 chain 链表中的每个节点,其宿主结构体是 inet_bind_bucket ,该结构体通过成员 node 链入链表;
2) 检查端口是否可重用
这里涉及到两个属性,一个是 socket 的 sk_reuse ,另一个是 inet_bind_bucket 的 fastreuse ;
sk_reuse 可以通过 setsockopt() 库函数进行设置,其值为 0 或 1 ,当为 1 时,表示当一个 socket 进入 TCP_TIME_WAIT 状态 ( 连接关闭已经完成 ) 后,它所占用的端口马上能够被重用,这在调试服务器时比较有用,重启程序不用进行等待;而 fastreuse 代表该端口是否允许被重用:
l 当该端口第一次被使用时( owners 为空),如果 sk_reuse 为 1 且 socket 状态不为 TCP_LISTEN ,则设置 fastreuse 为 1 ,否则设置为 0 ;
l 当该端口同时被其他 socket 使用时( owners 不为空),如果当前端口能被重用,但是当前 socket 的 sk_reuse 为 0 或其状态为 TCP_LISTEN ,则将 fastreuse 设置为 0 ,标记为不能重用;
3) 当不能重用时,再次检查冲突
此时会调用 inet_csk(sk)->icsk_af_ops->bind_conflict(sk, tb) 再次检查端口是否冲突;回想《 创建 socket 》一文中提到,创建 socket 成功后,要使用相应的协议来初始化 socket ,对于 tcp 协议来说,其初始化方法是 net/ipv4/Tcp_ipv4.c:tcp_v4_init_sock() ,其中就做了如下一步的设置:
icsk->icsk_af_ops = &ipv4_specific; struct inet_connection_sock_af_ops ipv4_specific = { .queue_xmit = ip_queue_xmit, .send_check = tcp_v4_send_check, .rebuild_header = inet_sk_rebuild_header, .conn_request = tcp_v4_conn_request, .syn_recv_sock = tcp_v4_syn_recv_sock, .remember_stamp = tcp_v4_remember_stamp, .net_header_len = sizeof(struct iphdr), .setsockopt = ip_setsockopt, .getsockopt = ip_getsockopt, .addr2sockaddr = inet_csk_addr2sockaddr, .sockaddr_len = sizeof(struct sockaddr_in), .bind_conflict = inet_csk_bind_conflict, #ifdef CONFIG_COMPAT .compat_setsockopt = compat_ip_setsockopt, .compat_getsockopt = compat_ip_getsockopt, #endif };
下面看看这里再次检查冲突的代码:
int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb) { const __be32 sk_rcv_saddr = inet_rcv_saddr(sk); struct sock *sk2; struct hlist_node *node; int reuse = sk->sk_reuse; sk_for_each_bound(sk2, node, &tb->owners) { if (sk != sk2 && !inet_v6_ipv6only(sk2) && (!sk->sk_bound_dev_if || !sk2->sk_bound_dev_if || sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) { if (!reuse || !sk2->sk_reuse || sk2->sk_state == TCP_LISTEN) { const __be32 sk2_rcv_saddr = inet_rcv_saddr(sk2); if (!sk2_rcv_saddr || !sk_rcv_saddr || sk2_rcv_saddr == sk_rcv_saddr) break; } } } return node != NULL; }
上面函数的逻辑是:从 owners 中遍历绑定在该端口上的 socket ,如果某 socket 跟当前的 socket 不是同一个,并且是绑定在同一个网络设备接口上的,并且它们两个之中至少有一个的 sk_reuse 表示自己的端口不能被重用或该 socket 已经是 TCP_LISTEN 状态了,并且它们两个之中至少有一个没有指定接收 IP 地址,或者两个都指定接收地址,但是接收地址是相同的,则冲突产生,否则不冲突。
也就是说,不使用同一个接收地址的 socket 可以共用端口号,绑定在不同的网络设备接口上的 socket 可以共用端口号,或者两个 socket 都表示自己可以被重用,并且还不在 TCP_LISTEN 状态,则可以重用端口号。
4 、新建 inet_bind_bucket
当在 bhash 中没有找到指定的端口时,需要创建新的桶节点,然后挂入 bhash 中:
tb_not_found: ret = 1; if (!tb && (tb = inet_bind_bucket_create(hashinfo->bind_bucket_cachep, net, head, snum)) == NULL) goto fail_unlock; 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: if (!inet_csk(sk)->icsk_bind_hash) inet_bind_hash(sk, tb, snum);
有兴趣的可以自己看看这段代码的实现,这里就不再展开了。