TCP层bind系统调用的实现分析
说明:该文章中部分代码未能完全理解透彻,可能对您造成误解,请慎读;
并建议您先阅读本博另外一篇文章:<Linux TCP套接字选项 之 SO_REUSEADDR && SO_REUSEPORT>
另:该文章将会持续更新改进;
TCP的接口绑定通过函数inet_csk_get_port函数执行,其中包含了未设置端口号自动分配的情况,设置了地址重用标记(SO_REUSEADDR)和设置了端口重用(SO_REUSEPORT)选项的处理;检查成功则控制块节点加入到绑定接口hash中对应端口的链表中;
1 /* Obtain a reference to a local port for the given sock, 2 * if snum is zero it means select any available local port. 3 * We try to allocate an odd port (and leave even ports for connect()) 4 */ 5 /* 绑定端口bind */ 6 /* 下面的重用地址表示SO_REUSEADDR,重用端口表示SO_REUSEPORT */ 7 int inet_csk_get_port(struct sock *sk, unsigned short snum) 8 { 9 bool reuse = sk->sk_reuse && sk->sk_state != TCP_LISTEN; 10 struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo; 11 int ret = 1, port = snum; 12 struct inet_bind_hashbucket *head; 13 struct net *net = sock_net(sk); 14 struct inet_bind_bucket *tb = NULL; 15 kuid_t uid = sock_i_uid(sk); 16 17 /* 未设定端口,自动绑定 */ 18 if (!port) { 19 /* 返回端口所在的桶节点 */ 20 head = inet_csk_find_open_port(sk, &tb, &port); 21 /* 未找到桶节点 */ 22 if (!head) 23 return ret; 24 /* 没有相同节点,创建节点 */ 25 if (!tb) 26 goto tb_not_found; 27 28 /* 有相同节点,成功 */ 29 goto success; 30 } 31 32 /* 设置了端口,找到端口hash桶节点 */ 33 head = &hinfo->bhash[inet_bhashfn(net, port, 34 hinfo->bhash_size)]; 35 spin_lock_bh(&head->lock); 36 37 /* 遍历该桶节点下面的所有端口 */ 38 inet_bind_bucket_for_each(tb, &head->chain) 39 /* 找到相同端口 */ 40 if (net_eq(ib_net(tb), net) && tb->port == port) 41 goto tb_found; 42 tb_not_found: 43 /* 创建绑定节点 */ 44 tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, 45 net, head, port); 46 if (!tb) 47 goto fail_unlock; 48 tb_found: 49 /* 节点上有控制块 */ 50 if (!hlist_empty(&tb->owners)) { 51 /* 强制绑定,成功 */ 52 if (sk->sk_reuse == SK_FORCE_REUSE) 53 goto success; 54 55 /* 快速的比对 */ 56 /* 已绑定的开启重用&&新请求绑定的开启了地址重用|| 端口重用检查通过,成功 */ 57 /* 先检查地址重用,未通过则检查端口重用 */ 58 if ((tb->fastreuse > 0 && reuse) || 59 sk_reuseport_match(tb, sk)) 60 goto success; 61 62 /* 地址重用和端口重用fast检查都失败 */ 63 64 /* 走更精确的比对 */ 65 /* 注意,在指定端口的情况下,不需要进行严格检查,并且可以重用端口 */ 66 if (inet_csk_bind_conflict(sk, tb, true, true)) 67 goto fail_unlock; 68 } 69 success: 70 /* 该节点有控制块共用 */ 71 if (!hlist_empty(&tb->owners)) { 72 /* 设置地址重用标志 */ 73 tb->fastreuse = reuse; 74 75 /* 如果开启端口重用 */ 76 if (sk->sk_reuseport) { 77 tb->fastreuseport = FASTREUSEPORT_ANY; 78 tb->fastuid = uid; 79 tb->fast_rcv_saddr = sk->sk_rcv_saddr; 80 tb->fast_ipv6_only = ipv6_only_sock(sk); 81 #if IS_ENABLED(CONFIG_IPV6) 82 tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr; 83 #endif 84 } 85 /* 未开启端口重用 */ 86 else { 87 tb->fastreuseport = 0; 88 } 89 } 90 /* 该端口节点还没有其他控制块 */ 91 else { 92 /* 未开启地址重用,置0 */ 93 if (!reuse) 94 tb->fastreuse = 0; 95 96 /* 开启了端口重用 */ 97 if (sk->sk_reuseport) { 98 /* We didn't match or we don't have fastreuseport set on 99 * the tb, but we have sk_reuseport set on this socket 100 * and we know that there are no bind conflicts with 101 * this socket in this tb, so reset our tb's reuseport 102 * settings so that any subsequent sockets that match 103 * our current socket will be put on the fast path. 104 * 105 * If we reset we need to set FASTREUSEPORT_STRICT so we 106 * do extra checking for all subsequent sk_reuseport 107 * socks. 108 */ 109 if (!sk_reuseport_match(tb, sk)) { 110 tb->fastreuseport = FASTREUSEPORT_STRICT; 111 tb->fastuid = uid; 112 tb->fast_rcv_saddr = sk->sk_rcv_saddr; 113 tb->fast_ipv6_only = ipv6_only_sock(sk); 114 #if IS_ENABLED(CONFIG_IPV6) 115 tb->fast_v6_rcv_saddr = sk->sk_v6_rcv_saddr; 116 #endif 117 } 118 } 119 /* 未开启端口重用 */ 120 else { 121 tb->fastreuseport = 0; 122 } 123 } 124 125 /* 添加到绑定hash */ 126 if (!inet_csk(sk)->icsk_bind_hash) 127 inet_bind_hash(sk, tb, port); 128 WARN_ON(inet_csk(sk)->icsk_bind_hash != tb); 129 ret = 0; 130 131 fail_unlock: 132 spin_unlock_bh(&head->lock); 133 return ret; 134 }
对于未设置端口号的绑定,系统会在端口号范围内查找一个没有冲突的端口号;
1 /* 2 * Find an open port number for the socket. Returns with the 3 * inet_bind_hashbucket lock held. 4 */ 5 static struct inet_bind_hashbucket * 6 inet_csk_find_open_port(struct sock *sk, struct inet_bind_bucket **tb_ret, int *port_ret) 7 { 8 struct inet_hashinfo *hinfo = sk->sk_prot->h.hashinfo; 9 int port = 0; 10 struct inet_bind_hashbucket *head; 11 struct net *net = sock_net(sk); 12 int i, low, high, attempt_half; 13 struct inet_bind_bucket *tb; 14 u32 remaining, offset; 15 16 /* 地址重用则attempt_half设置为1 */ 17 attempt_half = (sk->sk_reuse == SK_CAN_REUSE) ? 1 : 0; 18 other_half_scan: 19 /* 获取端口范围区间 */ 20 inet_get_local_port_range(net, &low, &high); 21 high++; /* [32768, 60999] -> [32768, 61000[ */ 22 23 /* 端口范围很小,attempt_half设置为0 */ 24 if (high - low < 4) 25 attempt_half = 0; 26 /* attempt_half不为0 */ 27 if (attempt_half) { 28 29 /* 找到一半的位置 */ 30 int half = low + (((high - low) >> 2) << 1); 31 32 /* 第一次尝试低一半 */ 33 if (attempt_half == 1) 34 high = half; 35 /* 否则尝试高一半 */ 36 else 37 low = half; 38 } 39 40 /* 地址数 */ 41 remaining = high - low; 42 43 /* 地址数消除低位 */ 44 if (likely(remaining > 1)) 45 remaining &= ~1U; 46 47 /* 随机一个偏移 */ 48 offset = prandom_u32() % remaining; 49 /* __inet_hash_connect() favors ports having @low parity 50 * We do the opposite to not pollute connect() users. 51 */ 52 /* 偏移低位置1 ,方便下面分半遍历端口*/ 53 offset |= 1U; 54 55 other_parity_scan: 56 57 /* 取一个端口 */ 58 port = low + offset; 59 60 /* 遍历查找合适端口,先遍历一半端口 */ 61 for (i = 0; i < remaining; i += 2, port += 2) { 62 if (unlikely(port >= high)) 63 port -= remaining; 64 65 /* 如果端口配置为保留端口,继续下一个端口 */ 66 if (inet_is_local_reserved_port(net, port)) 67 continue; 68 69 /* 找到该端口对应的绑定端口列表 */ 70 head = &hinfo->bhash[inet_bhashfn(net, port, 71 hinfo->bhash_size)]; 72 spin_lock_bh(&head->lock); 73 /* 遍历该链表 */ 74 inet_bind_bucket_for_each(tb, &head->chain) 75 /* 同一个命名空间&& 端口相同 */ 76 if (net_eq(ib_net(tb), net) && tb->port == port) { 77 /* 绑定无冲突,成功 */ 78 /* 注意,随机端口,需要进行严谨的检查,并且不能使用端口重用 */ 79 if (!inet_csk_bind_conflict(sk, tb, false, false)) 80 goto success; 81 82 /* 有冲突,下一个端口 */ 83 goto next_port; 84 } 85 /* 没有命名空间和端口相同,成功 */ 86 tb = NULL; 87 goto success; 88 next_port: 89 spin_unlock_bh(&head->lock); 90 cond_resched(); 91 } 92 93 /* 遍历另外一半端口 */ 94 offset--; 95 if (!(offset & 1)) 96 goto other_parity_scan; 97 98 /* 端口均不能用,则尝试高位端口的一半 */ 99 if (attempt_half == 1) { 100 /* OK we now try the upper half of the range */ 101 attempt_half = 2; 102 goto other_half_scan; 103 } 104 return NULL; 105 106 /* 成功返回端口和节点(有端口相同时不为NULL) */ 107 success: 108 *port_ret = port; 109 *tb_ret = tb; 110 return head; 111 }
inet_csk_bind_conflict用来判断端口是否冲突;
1 static int inet_csk_bind_conflict(const struct sock *sk, 2 const struct inet_bind_bucket *tb, 3 bool relax, bool reuseport_ok) 4 { 5 struct sock *sk2; 6 /* 地址重用 */ 7 bool reuse = sk->sk_reuse; 8 /* 端口重用 */ 9 bool reuseport = !!sk->sk_reuseport && reuseport_ok; 10 /* 用户id */ 11 kuid_t uid = sock_i_uid((struct sock *)sk); 12 13 /* 14 * Unlike other sk lookup places we do not check 15 * for sk_net here, since _all_ the socks listed 16 * in tb->owners list belong to the same net - the 17 * one this bucket belongs to. 18 */ 19 /* 遍历所有绑定控制块 */ 20 sk_for_each_bound(sk2, &tb->owners) { 21 if (sk != sk2 && /* 控制块不同 */ 22 /* 输出报文的网络接口号为0或者相等 */ 23 (!sk->sk_bound_dev_if || 24 !sk2->sk_bound_dev_if || 25 sk->sk_bound_dev_if == sk2->sk_bound_dev_if)) { 26 27 /* 或逻辑好多,看着晕啊,看看什么情况下不需要判断吧 */ 28 29 /* 30 未启用地址重用 && 未启用端口重用:检查冲突; 31 启用了地址重用 && 未启用端口重用:状态是LISTEN才检查冲突; 32 未启用地址重用 && 启用了端口重用:状态不是TIME_WAIT并且不是同一有效用户ID时,检查冲突; 33 也就是说,假若是TIME_WAIT,则不需要检查; 34 假如不是TIME_WAIT,但是有效用户ID相同,也不需要检查; 35 36 启用了地址重用 && 启用了端口重用:状态是LISTEN时,可能需要检查,需要继续判断端口重用, 37 这时候只当有效用户ID不相同的时候,才需要检查; 38 就是说,可以相同用户ID的进程可以同时LISTEN多个相同的地址+端口; 39 */ 40 41 if ((!reuse || !sk2->sk_reuse || 42 sk2->sk_state == TCP_LISTEN) && 43 (!reuseport || !sk2->sk_reuseport || 44 rcu_access_pointer(sk->sk_reuseport_cb) || 45 (sk2->sk_state != TCP_TIME_WAIT && 46 !uid_eq(uid, sock_i_uid(sk2))))) { 47 48 /* 地址相同,冲突 */ 49 if (inet_rcv_saddr_equal(sk, sk2, true)) 50 break; 51 } 52 53 /* 上面不需要判断的走到这里的情况 */ 54 /* 情况1.新旧绑定都设置了地址重用,状态不是LISTEN ,不满足本条,继续下面2*/ 55 /* 情况2.新旧绑定都设置了端口重用,状态是TIME_WAIT或者用户ID相等 */ 56 57 58 /* 上面1情况如果不放宽检查,则检查 */ 59 if (!relax && reuse && sk2->sk_reuse && 60 sk2->sk_state != TCP_LISTEN) { 61 62 /* 地址相同,冲突 */ 63 if (inet_rcv_saddr_equal(sk, sk2, true)) 64 break; 65 } 66 } 67 } 68 return sk2 != NULL; 69 }