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 }
原文地址:https://www.cnblogs.com/wanpengcoder/p/11750510.html