地址和端口复用
<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-&gt;sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE); // 1 或 0
break;
case SO_REUSEPORT:
sk-&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-&gt;sk_reuse &amp;&amp; sk-&gt;sk_state != TCP_LISTEN; // listen 状态不能重用地址(注意并非不能重用端口)
struct inet_hashinfo *hinfo = sk-&gt;sk_prot-&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;tb, &amp;port);
if (!head)
return ret;
if (!tb)
goto tb_not_found;
goto success;
}
head = &amp;hinfo-&gt;bhash[inet_bhashfn(net, port,
hinfo-&gt;bhash_size)]; // bhash
spin_lock_bh(&amp;head-&gt;lock);
inet_bind_bucket_for_each(tb, &amp;head-&gt;chain)
if (net_eq(ib_net(tb), net) &amp;&amp; tb-&gt;port == port) // 遍历 bhash,找到此端口的绑定信息 tb
goto tb_found;
tb_not_found:
tb = inet_bind_bucket_create(hinfo-&gt;bind_bucket_cachep,
net, head, port); // 第一次 bind 时没有 tb 节点,则创建之
if (!tb)
goto fail_unlock;
tb_found:
if (!hlist_empty(&amp;tb-&gt;owners)) { // tb 上已经记录了绑定者。针对非首次的绑定场景
if (sk-&gt;sk_reuse == SK_FORCE_REUSE)
goto success;
if ((tb-&gt;fastreuse &gt; 0 &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)-&gt;icsk_bind_hash)
inet_bind_hash(sk, tb, port); // 将 sk 添加到 tb-&gt;owners 上,并设置 inet-&gt;inet_num = port 和 icsk_bind_hash = tb
WARN_ON(inet_csk(sk)-&gt;icsk_bind_hash != tb);
ret = 0;
fail_unlock:
spin_unlock_bh(&amp;head-&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;icsk-&gt;icsk_accept_queue);
sk-&gt;sk_max_ack_backlog = backlog;
sk-&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-&gt;sk_prot-&gt;get_port(sk, inet-&gt;inet_num)) { // 即是 inet_csk_get_port。所以在一般监听的场景下,这函数会调用 2 次
inet-&gt;inet_sport = htons(inet-&gt;inet_num);
sk_dst_reset(sk);
err = sk-&gt;sk_prot-&gt;hash(sk); // 即 inet_hash,里面会有 sk-&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-&gt;sk_reuse &amp;&amp; sk-&gt;sk_state != TCP_LISTEN;
if (hlist_empty(&amp;tb-&gt;owners)) { // 首次绑定场景,具体来说是首次 bind 时。在 listen 调用时 owners 就不为空了
tb-&gt;fastreuse = reuse; // 在首次 bind 时 sk_state = TCP_CLOSE,所以 fastreuse = 1
if (sk-&gt;sk_reuseport) {
tb-&gt;fastreuseport = FASTREUSEPORT_ANY; // 跟随 sk-&gt;sk_reuseport
tb-&gt;fastuid = uid;
tb-&gt;fast_rcv_saddr = sk-&gt;sk_rcv_saddr;
tb-&gt;fast_ipv6_only = ipv6_only_sock(sk);
tb-&gt;fast_sk_family = sk-&gt;sk_family;
#if IS_ENABLED(CONFIG_IPV6)
tb-&gt;fast_v6_rcv_saddr = sk-&gt;sk_v6_rcv_saddr;
#endif
} else {
tb-&gt;fastreuseport = 0; // 跟随 sk-&gt;sk_reuseport
}
} else { // 首次 listen(或二次 bind 时)
if (!reuse)
tb-&gt;fastreuse = 0; // listen 时 reuse = 0,所以这里会更新为 0
if (sk-&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-&gt;fastreuseport = FASTREUSEPORT_STRICT; // 首次绑定时有些特殊。但整体上也是跟随 sk-&gt;sk_reuseport
tb-&gt;fastuid = uid;
tb-&gt;fast_rcv_saddr = sk-&gt;sk_rcv_saddr;
tb-&gt;fast_ipv6_only = ipv6_only_sock(sk);
tb-&gt;fast_sk_family = sk-&gt;sk_family;
#if IS_ENABLED(CONFIG_IPV6)
tb-&gt;fast_v6_rcv_saddr = sk-&gt;sk_v6_rcv_saddr;
#endif
}
} else {
tb-&gt;fastreuseport = 0; // 跟随 sk-&gt;sk_reuseport
}
}
}</code></pre>
<p>对于 <code>SO_REUSEADDR</code> 来说,首次 bind 时没有端口绑定信息,所以会创建一个 tb 节点。由于此时 sk 状态为 TCP_CLOSE,所以会设置 tb->fastreuse = 1。
后面调用 listen 时,再次调用 <code>inet_csk_get_port</code>。这里会找到 tb 并检查 tb->fastreuse 和 sk->sk_reuse。但由于此时 sk 状态为 TCP_LISTEN,所以总的 reuse = 0,因此在执行 <code>inet_csk_update_fastreuse</code> 会更新 tb->fastreuse = 0。
这时再起另一个进程实例,在 bind 时判断 <code>tb-&gt;fastreuse &gt; 0 &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;file=file.png" alt="" /></p>
<p>内核日志:</p>
<p><img src="https://www.showdoc.com.cn/server/api/attachment/visitFile?sign=5d1e013bf8d08ee09e3c148036b9b251&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->fastreuse = 0,因此会进入 <code>inet_csk_bind_conflict</code> 检查冲突。由于没有设置 reuse 和 reuseport,所以会检测到有冲突(本质上就是检测到 sk->sk_rcv_saddr 和 sk2->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-&gt;sk_reuse;
bool reuseport = !!sk-&gt;sk_reuseport &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-&gt;owners list belong to the same net - the
* one this bucket belongs to.
*/
sk_for_each_bound(sk2, &amp;tb-&gt;owners) {
if (sk != sk2 &amp;&amp;
(!sk-&gt;sk_bound_dev_if ||
!sk2-&gt;sk_bound_dev_if ||
sk-&gt;sk_bound_dev_if == sk2-&gt;sk_bound_dev_if)) {
if ((!reuse || !sk2-&gt;sk_reuse ||
sk2-&gt;sk_state == TCP_LISTEN) &amp;&amp; // 未设置 reuse
(!reuseport || !sk2-&gt;sk_reuseport || // 未设置 reuseport
rcu_access_pointer(sk-&gt;sk_reuseport_cb) ||
(sk2-&gt;sk_state != TCP_TIME_WAIT &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; reuse &amp;&amp; sk2-&gt;sk_reuse &amp;&amp;
sk2-&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->fastreuseport = 1。
在 listen 时,找到 tb,并调用 <code>sk_reuseport_match</code> 判断是否可以端口复用,结果为 1。然后调用 <code>inet_csk_update_fastreuse</code> 更新重用信息时,对于 tb->fastreuseport 是跟随 sk_reuseport 的。所以 tb->fastreuseport = 1。</p>
<p>这时再起另一个进程实例,在 bind 时调用 <code>sk_reuseport_match</code> 结果为 1,再进入 <code>inet_csk_update_fastreuse</code> 依然是 tb->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-&gt;fastreuseport &lt;= 0)
return 0;
if (!sk-&gt;sk_reuseport)
return 0;
if (rcu_access_pointer(sk-&gt;sk_reuseport_cb)) // 从代码来看,这是在 listen 分配端口后才会设置的。据说用于查找所有监听相同端口的 sk
return 0;
if (!uid_eq(tb-&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-&gt;fastreuseport == FASTREUSEPORT_ANY) // 一般是这个
return 1;
#if IS_ENABLED(CONFIG_IPV6)
if (tb-&gt;fast_sk_family == AF_INET6)
return ipv6_rcv_saddr_equal(&amp;tb-&gt;fast_v6_rcv_saddr,
inet6_rcv_saddr(sk),
tb-&gt;fast_rcv_saddr,
sk-&gt;sk_rcv_saddr,
tb-&gt;fast_ipv6_only,
ipv6_only_sock(sk), true, false);
#endif
return ipv4_rcv_saddr_equal(tb-&gt;fast_rcv_saddr, sk-&gt;sk_rcv_saddr,
ipv6_only_sock(sk), true, false);
}</code></pre>
<h2>参考文档</h2>
<p>> 飞哥端口复用:<a href="https://mp.weixin.qq.com/s/wkxDLWmXInWq4fSPxNdDuQ?version=4.1.27.8111&platform=win&nwr_flag=1#wechat_redirect">https://mp.weixin.qq.com/s/wkxDLWmXInWq4fSPxNdDuQ?version=4.1.27.8111&platform=win&nwr_flag=1#wechat_redirect</a></p>
<p>> <a href="https://segmentfault.com/a/1190000020008130">https://segmentfault.com/a/1190000020008130</a></p>
<p>> <a href="https://cloud.tencent.com/developer/article/1484223">https://cloud.tencent.com/developer/article/1484223</a></p>