公开学习文档

公开学习文档


地址和端口复用

<h2>概述</h2> <p>实测:</p> <table> <thead> <tr> <th>方式 TCP</th> <th>多进程监听 127.0.0.1:8888</th> <th>多进程监听 *:8888</th> <th>多进程监听 ip:8888</th> </tr> </thead> <tbody> <tr> <td>SO_REUSEPORT</td> <td>OK</td> <td>OK</td> <td>OK</td> </tr> <tr> <td>SO_REUSEADDR</td> <td>FAILED</td> <td>FAILED</td> <td>FAILED</td> </tr> </tbody> </table> <p>SO_REUSEPORT:随便绑定,IP 相同或不相同都可以 SO_REUSEADDR:不允许处于 <code>LISTEN</code> 状态的地址重复使用。(对于 TCP 监听而言:IP 必须不同,并且如果监听了 0.0.0.0,则只能监听一个)</p> <p>摘自网络的总结: <code>SO_REUSEPORT</code> 是 <code>SO_REUSEADDR</code> 的超集,两个参数的目的都是为了重复使用本地地址,但 <code>SO_REUSEADDR</code> 不允许处于 <code>LISTEN</code> 状态的地址重复使用,而 <code>SO_REUSEPORT</code> 允许,并且 <code>SO_REUSEPORT</code> 会将进来的 TCP 连接负载均衡到各个 LISTEN sokcet 上。</p> <h2>设置 sock 选项</h2> <pre><code class="language-c">// file: net/core/sock.c int sock_setsockopt(struct socket *sock, int level, int optname, char __user *optval, unsigned int optlen) { // ... switch (optname) { case SO_REUSEADDR: sk-&amp;gt;sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE); // 1 或 0 break; case SO_REUSEPORT: sk-&amp;gt;sk_reuseport = valbool; break; } } // file: include/net/sock.h /* * SK_CAN_REUSE and SK_NO_REUSE on a socket mean that the socket is OK * or not whether his port will be reused by someone else. SK_FORCE_REUSE * on a socket means that the socket will reuse everybody else's port * without looking at the other's sk_reuse value. */ #define SK_NO_REUSE 0 #define SK_CAN_REUSE 1 #define SK_FORCE_REUSE 2 // file: include/net/inet_hashtables.h #define FASTREUSEPORT_ANY 1 #define FASTREUSEPORT_STRICT 2</code></pre> <h2>bind和listen</h2> <p>在调用 <code>bind</code> 时,内核会调用 <code>inet_csk_get_port</code> 分配端口:</p> <pre><code class="language-c">// file: net/ipv4/inet_connection_sock.c /* Obtain a reference to a local port for the given sock, * if snum is zero it means select any available local port. * We try to allocate an odd port (and leave even ports for connect()) */ int inet_csk_get_port(struct sock *sk, unsigned short snum) { bool reuse = sk-&amp;gt;sk_reuse &amp;amp;&amp;amp; sk-&amp;gt;sk_state != TCP_LISTEN; // listen 状态不能重用地址(注意并非不能重用端口) struct inet_hashinfo *hinfo = sk-&amp;gt;sk_prot-&amp;gt;h.hashinfo; int ret = 1, port = snum; struct inet_bind_hashbucket *head; struct net *net = sock_net(sk); struct inet_bind_bucket *tb = NULL; if (!port) { // 未指定端口的情况,不是这种场景 head = inet_csk_find_open_port(sk, &amp;amp;tb, &amp;amp;port); if (!head) return ret; if (!tb) goto tb_not_found; goto success; } head = &amp;amp;hinfo-&amp;gt;bhash[inet_bhashfn(net, port, hinfo-&amp;gt;bhash_size)]; // bhash spin_lock_bh(&amp;amp;head-&amp;gt;lock); inet_bind_bucket_for_each(tb, &amp;amp;head-&amp;gt;chain) if (net_eq(ib_net(tb), net) &amp;amp;&amp;amp; tb-&amp;gt;port == port) // 遍历 bhash,找到此端口的绑定信息 tb goto tb_found; tb_not_found: tb = inet_bind_bucket_create(hinfo-&amp;gt;bind_bucket_cachep, net, head, port); // 第一次 bind 时没有 tb 节点,则创建之 if (!tb) goto fail_unlock; tb_found: if (!hlist_empty(&amp;amp;tb-&amp;gt;owners)) { // tb 上已经记录了绑定者。针对非首次的绑定场景 if (sk-&amp;gt;sk_reuse == SK_FORCE_REUSE) goto success; if ((tb-&amp;gt;fastreuse &amp;gt; 0 &amp;amp;&amp;amp; reuse) || // 在 listen 时也会调用此函数,此时 reuse = 0,这个条件就不成立 sk_reuseport_match(tb, sk)) goto success; if (inet_csk_bind_conflict(sk, tb, true, true)) goto fail_unlock; } success: inet_csk_update_fastreuse(tb, sk); // 更新重用信息 if (!inet_csk(sk)-&amp;gt;icsk_bind_hash) inet_bind_hash(sk, tb, port); // 将 sk 添加到 tb-&amp;gt;owners 上,并设置 inet-&amp;gt;inet_num = port 和 icsk_bind_hash = tb WARN_ON(inet_csk(sk)-&amp;gt;icsk_bind_hash != tb); ret = 0; fail_unlock: spin_unlock_bh(&amp;amp;head-&amp;gt;lock); return ret; } EXPORT_SYMBOL_GPL(inet_csk_get_port);</code></pre> <p>需要特别注意:<code>inet_csk_get_port</code> 在 listen 时也会调用,如下:</p> <pre><code class="language-c">// file: net/ipv4/inet_connection_sock.c int inet_csk_listen_start(struct sock *sk, int backlog) { struct inet_connection_sock *icsk = inet_csk(sk); struct inet_sock *inet = inet_sk(sk); int err; err = inet_ulp_can_listen(sk); if (unlikely(err)) return err; reqsk_queue_alloc(&amp;amp;icsk-&amp;gt;icsk_accept_queue); sk-&amp;gt;sk_max_ack_backlog = backlog; sk-&amp;gt;sk_ack_backlog = 0; inet_csk_delack_init(sk); /* There is race window here: we announce ourselves listening, * but this transition is still not validated by get_port(). * It is OK, because this socket enters to hash table only * after validation is complete. */ err = -EADDRINUSE; inet_sk_state_store(sk, TCP_LISTEN); // 更新 sk 状态为 TCP_LISTEN if (!sk-&amp;gt;sk_prot-&amp;gt;get_port(sk, inet-&amp;gt;inet_num)) { // 即是 inet_csk_get_port。所以在一般监听的场景下,这函数会调用 2 次 inet-&amp;gt;inet_sport = htons(inet-&amp;gt;inet_num); sk_dst_reset(sk); err = sk-&amp;gt;sk_prot-&amp;gt;hash(sk); // 即 inet_hash,里面会有 sk-&amp;gt;sk_reuseport 相关处理 if (likely(!err)) return 0; } inet_sk_set_state(sk, TCP_CLOSE); return err; } EXPORT_SYMBOL_GPL(inet_csk_listen_start);</code></pre> <p>再看下如何更新重用信息:</p> <pre><code class="language-c">// file: net/ipv4/inet_connection_sock.c void inet_csk_update_fastreuse(struct inet_bind_bucket *tb, struct sock *sk) { kuid_t uid = sock_i_uid(sk); bool reuse = sk-&amp;gt;sk_reuse &amp;amp;&amp;amp; sk-&amp;gt;sk_state != TCP_LISTEN; if (hlist_empty(&amp;amp;tb-&amp;gt;owners)) { // 首次绑定场景,具体来说是首次 bind 时。在 listen 调用时 owners 就不为空了 tb-&amp;gt;fastreuse = reuse; // 在首次 bind 时 sk_state = TCP_CLOSE,所以 fastreuse = 1 if (sk-&amp;gt;sk_reuseport) { tb-&amp;gt;fastreuseport = FASTREUSEPORT_ANY; // 跟随 sk-&amp;gt;sk_reuseport tb-&amp;gt;fastuid = uid; tb-&amp;gt;fast_rcv_saddr = sk-&amp;gt;sk_rcv_saddr; tb-&amp;gt;fast_ipv6_only = ipv6_only_sock(sk); tb-&amp;gt;fast_sk_family = sk-&amp;gt;sk_family; #if IS_ENABLED(CONFIG_IPV6) tb-&amp;gt;fast_v6_rcv_saddr = sk-&amp;gt;sk_v6_rcv_saddr; #endif } else { tb-&amp;gt;fastreuseport = 0; // 跟随 sk-&amp;gt;sk_reuseport } } else { // 首次 listen(或二次 bind 时) if (!reuse) tb-&amp;gt;fastreuse = 0; // listen 时 reuse = 0,所以这里会更新为 0 if (sk-&amp;gt;sk_reuseport) { /* We didn't match or we don't have fastreuseport set on * the tb, but we have sk_reuseport set on this socket * and we know that there are no bind conflicts with * this socket in this tb, so reset our tb's reuseport * settings so that any subsequent sockets that match * our current socket will be put on the fast path. * * If we reset we need to set FASTREUSEPORT_STRICT so we * do extra checking for all subsequent sk_reuseport * socks. */ if (!sk_reuseport_match(tb, sk)) { tb-&amp;gt;fastreuseport = FASTREUSEPORT_STRICT; // 首次绑定时有些特殊。但整体上也是跟随 sk-&amp;gt;sk_reuseport tb-&amp;gt;fastuid = uid; tb-&amp;gt;fast_rcv_saddr = sk-&amp;gt;sk_rcv_saddr; tb-&amp;gt;fast_ipv6_only = ipv6_only_sock(sk); tb-&amp;gt;fast_sk_family = sk-&amp;gt;sk_family; #if IS_ENABLED(CONFIG_IPV6) tb-&amp;gt;fast_v6_rcv_saddr = sk-&amp;gt;sk_v6_rcv_saddr; #endif } } else { tb-&amp;gt;fastreuseport = 0; // 跟随 sk-&amp;gt;sk_reuseport } } }</code></pre> <p>对于 <code>SO_REUSEADDR</code> 来说,首次 bind 时没有端口绑定信息,所以会创建一个 tb 节点。由于此时 sk 状态为 TCP_CLOSE,所以会设置 tb-&gt;fastreuse = 1。 后面调用 listen 时,再次调用 <code>inet_csk_get_port</code>。这里会找到 tb 并检查 tb-&gt;fastreuse 和 sk-&gt;sk_reuse。但由于此时 sk 状态为 TCP_LISTEN,所以总的 reuse = 0,因此在执行 <code>inet_csk_update_fastreuse</code> 会更新 tb-&gt;fastreuse = 0。 这时再起另一个进程实例,在 bind 时判断 <code>tb-&amp;gt;fastreuse &amp;gt; 0 &amp;amp;&amp;amp; reuse</code> 不成立,会进入 <code>inet_csk_bind_conflict</code> 调用从而报错。</p> <p>实测:</p> <p>修改代码:</p> <p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=5cccb45b527903a51ca8fbcf67cdb75e&amp;amp;file=file.png" alt="" /></p> <p>内核日志:</p> <p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=5d1e013bf8d08ee09e3c148036b9b251&amp;amp;file=file.png" alt="" /></p> <p>日志中第一段是首次执行的结果;第二段是起新进程实例的结果。</p> <h2>再分析 SO_REUSEADDR 经典作用</h2> <p>分析 <code>SO_REUSEADDR</code> 经典作用,即服务器重启的场景。</p> <p>在服务器重启执行 bind 时,由于还有 TIME_WAITE 的 sk 在,所以可以找到 tb 并且 owners 不为空。由于没有设置 <code>SO_REUSEADDR</code> 所以 tb-&gt;fastreuse = 0,因此会进入 <code>inet_csk_bind_conflict</code> 检查冲突。由于没有设置 reuse 和 reuseport,所以会检测到有冲突(本质上就是检测到 sk-&gt;sk_rcv_saddr 和 sk2-&gt;sk_rcv_saddr 相同),因此会出错。</p> <pre><code class="language-c">// file: net/ipv4/inet_connection_sock.c static int inet_csk_bind_conflict(const struct sock *sk, const struct inet_bind_bucket *tb, bool relax, bool reuseport_ok) { struct sock *sk2; bool reuse = sk-&amp;gt;sk_reuse; bool reuseport = !!sk-&amp;gt;sk_reuseport &amp;amp;&amp;amp; reuseport_ok; kuid_t uid = sock_i_uid((struct sock *)sk); /* * Unlike other sk lookup places we do not check * for sk_net here, since _all_ the socks listed * in tb-&amp;gt;owners list belong to the same net - the * one this bucket belongs to. */ sk_for_each_bound(sk2, &amp;amp;tb-&amp;gt;owners) { if (sk != sk2 &amp;amp;&amp;amp; (!sk-&amp;gt;sk_bound_dev_if || !sk2-&amp;gt;sk_bound_dev_if || sk-&amp;gt;sk_bound_dev_if == sk2-&amp;gt;sk_bound_dev_if)) { if ((!reuse || !sk2-&amp;gt;sk_reuse || sk2-&amp;gt;sk_state == TCP_LISTEN) &amp;amp;&amp;amp; // 未设置 reuse (!reuseport || !sk2-&amp;gt;sk_reuseport || // 未设置 reuseport rcu_access_pointer(sk-&amp;gt;sk_reuseport_cb) || (sk2-&amp;gt;sk_state != TCP_TIME_WAIT &amp;amp;&amp;amp; !uid_eq(uid, sock_i_uid(sk2))))) { if (inet_rcv_saddr_equal(sk, sk2, true)) // 注意 true 是指 0.0.0.0 和任意地址都认为是相同的 break; } if (!relax &amp;amp;&amp;amp; reuse &amp;amp;&amp;amp; sk2-&amp;gt;sk_reuse &amp;amp;&amp;amp; sk2-&amp;gt;sk_state != TCP_LISTEN) { if (inet_rcv_saddr_equal(sk, sk2, true)) break; } } } return sk2 != NULL; }</code></pre> <h2>再说说 SO_REUSEPORT</h2> <p>在 bind 时,创建 tb。由于 sk_reuseport = 1,所以设置 tb-&gt;fastreuseport = 1。 在 listen 时,找到 tb,并调用 <code>sk_reuseport_match</code> 判断是否可以端口复用,结果为 1。然后调用 <code>inet_csk_update_fastreuse</code> 更新重用信息时,对于 tb-&gt;fastreuseport 是跟随 sk_reuseport 的。所以 tb-&gt;fastreuseport = 1。</p> <p>这时再起另一个进程实例,在 bind 时调用 <code>sk_reuseport_match</code> 结果为 1,再进入 <code>inet_csk_update_fastreuse</code> 依然是 tb-&gt;fastreuseport = 1。</p> <pre><code class="language-c">// file: net/ipv4/inet_connection_sock.c static inline int sk_reuseport_match(struct inet_bind_bucket *tb, struct sock *sk) { kuid_t uid = sock_i_uid(sk); if (tb-&amp;gt;fastreuseport &amp;lt;= 0) return 0; if (!sk-&amp;gt;sk_reuseport) return 0; if (rcu_access_pointer(sk-&amp;gt;sk_reuseport_cb)) // 从代码来看,这是在 listen 分配端口后才会设置的。据说用于查找所有监听相同端口的 sk return 0; if (!uid_eq(tb-&amp;gt;fastuid, uid)) return 0; /* We only need to check the rcv_saddr if this tb was once marked * without fastreuseport and then was reset, as we can only know that * the fast_*rcv_saddr doesn't have any conflicts with the socks on the * owners list. */ if (tb-&amp;gt;fastreuseport == FASTREUSEPORT_ANY) // 一般是这个 return 1; #if IS_ENABLED(CONFIG_IPV6) if (tb-&amp;gt;fast_sk_family == AF_INET6) return ipv6_rcv_saddr_equal(&amp;amp;tb-&amp;gt;fast_v6_rcv_saddr, inet6_rcv_saddr(sk), tb-&amp;gt;fast_rcv_saddr, sk-&amp;gt;sk_rcv_saddr, tb-&amp;gt;fast_ipv6_only, ipv6_only_sock(sk), true, false); #endif return ipv4_rcv_saddr_equal(tb-&amp;gt;fast_rcv_saddr, sk-&amp;gt;sk_rcv_saddr, ipv6_only_sock(sk), true, false); }</code></pre> <h2>参考文档</h2> <p>&gt; 飞哥端口复用:<a href="https://mp.weixin.qq.com/s/wkxDLWmXInWq4fSPxNdDuQ?version=4.1.27.8111&amp;platform=win&amp;nwr_flag=1#wechat_redirect">https://mp.weixin.qq.com/s/wkxDLWmXInWq4fSPxNdDuQ?version=4.1.27.8111&amp;platform=win&amp;nwr_flag=1#wechat_redirect</a></p> <p>&gt; <a href="https://segmentfault.com/a/1190000020008130">https://segmentfault.com/a/1190000020008130</a></p> <p>&gt; <a href="https://cloud.tencent.com/developer/article/1484223">https://cloud.tencent.com/developer/article/1484223</a></p>

页面列表

ITEM_HTML