共计 6443 个字符,预计需要花费 17 分钟才能阅读完成。
SO_REUSEPORT
选项在 Linux 3.9 被引入内核,在这之前也有一个很像的选项 SO_REUSEADDR
。如果你不太清楚这两者的区别和联系,建议阅读 How do SO_REUSEADDR and SO_REUSEPORT differ?。
如果不想读,那么下面这一节算是为懒人准备的。
SO_REUSEADDR 与 SO_REUSEPORT 是什么?
TCP/UDP 用 五元组
唯一标识一个连接。任何时候,两条连接的五元组都不能完全相同,否则当收到一个报文时,协议栈没办法判断它是属于哪个连接的。
五元组
{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
五元组里,protocol
在创建 socket 时确定,<src addr>
和 <src port>
在bind()
时确定,<dest addr>
和 <dest port>
在connect()
时确定。当然,bind()
和 connect()
在一些时候并不需要显式使用,不过这不在本文的讨论范围里。
那么,如果对 socket 设置了 SO_REUSEADDR
和SO_REUSEPORT
选项,它们什么时候起作用呢?答案是 bind()
,也就在确定<src addr>
和<src port>
时。
不同操作系统内核对待 SO_REUSEADDR
和SO_REUSEPORT
的行为有少许差异,但它们都源自 BSD。因此,接下来就以BSD 的实现为标准进行说明。
SO_REUSEADDR
假设我现在需要 bind()
将socketA
绑定到 A:X
,将socketB
绑定到 B:Y
(不考虑X=0
或者 Y=0
,因为0
表示让内核自动分配端口,一定不会冲突)。
如果 X!=Y
,那么无论A
和B
的关系如何,两个 bind()
都会成功。但如果X==Y
,那么结果会是下面这样:
SO_REUSEADDR socketA socketB Result
---------------------------------------------------------------------
ON/OFF 192.168.0.1:21 192.168.0.1:21 Error (EADDRINUSE)
ON/OFF 192.168.0.1:21 10.0.0.1:21 OK
ON/OFF 10.0.0.1:21 192.168.0.1:21 OK
OFF 0.0.0.0:21 192.168.1.0:21 Error (EADDRINUSE)
OFF 192.168.1.0:21 0.0.0.0:21 Error (EADDRINUSE)
ON 0.0.0.0:21 192.168.1.0:21 OK
ON 192.168.1.0:21 0.0.0.0:21 OK
ON/OFF 0.0.0.0:21 0.0.0.0:21 Error (EADDRINUSE)
第一列表示是否设置 SO_REUSEADDR
注
,最后一列表示 后绑定的 socket 是否能绑定成功。
注
:这里设置的对象是指 后绑定的 socket(也就是说不关心前一个是否设置)
可以看出,BSD 的实现中 SO_REUSEADDR
可以让 一个使用通配地址 (0.0.0.0),一个使用指定地址(192.168.1.0) 的 socket 同时绑定成功。
SO_REUSEADDR
还有一种应用情景:在 TCP
中存在一个 TIME_WAIT
状态,它是指主动关闭的一端最后停留的阶段。假设 socketA
绑定到 A:X
,在完成 TCP 通信后主动使用close()
, 进入TIME_WAIT
,此时,如果socketB
也去绑定 A:X
,那么同样会得到EADDRINUSE
错误,但如果 socketB
设置了SO_REUSEADDR
,那么就可以绑定成功。
SO_REUSEPORT
如果理解了 SO_REUSEADDR
,那么SO_REUSEPORT
就很好理解了,它让两个 socket 可以绑定完全相同的<IP:Port>
。
SO_REUSEPORT socketA socketB Result
---------------------------------------------------------------------
ON 192.168.0.1:21 192.168.0.1:21 OK
提醒一下,以上的结果都是 BSD 的结果,Linux 内核有一些不一样的地方,具体表现为
- 3.9 版本支持
SO_REUSEPORT
,作为 Server 的 TCP Socket 一旦绑定到了具体的端口,启动了 LISTEN,即使它之前设置过SO_REUSEADDR
, 也不会生效。这一点 Linux 比 BSD 更加严格
SO_REUSEADDR socketA socketB Result
---------------------------------------------------------------------
ON/OFF 192.168.0.1:21 0.0.0.0:21 Error (EADDRINUSE)
- 3.9 版本之前, 作为 Client 的 Socket,
SO_REUSEADDR
选项具有 BSD 中的SO_REUSEPORT
的效果。这一点 Linux 又比 BSD 更加宽松。
SO_REUSEADDR socketA socketB Result
---------------------------------------------------------------------
ON 192.168.0.2:55555 192.168.0.2:55555 OK
Linux 中 reuseport 的演进
Linux < 3.9
下面看看具体是怎么做的:
内核 socket 使用 skc_reuse
字段表示是否设置了SO_REUSEADDR
struct sock_common {
/* omitted */
unsigned char skc_reuse;
/* omitted */
}
int sock_setsockopt(struct socket *sock, int level, int optname,...
{
......
case SO_REUSEADDR:
sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE);
break;
}
inet_bind_bucket
表示一个绑定的端口。
struct inet_bind_bucket {
/* omitted */
unsigned short port;
signed short fastreuse;
int num_owners;
struct hlist_node node;
struct hlist_head owners;
};
上面结构中的 fastreuse
表示该端口是否支持共享,所有共享该端口的 socket 挂到 owner
成员上。在用户使用 bind()
时,内核使用 TCP:inet_csk_get_port()
,UDP:udp_v4_get_port()
来绑定端口。
/* inet_connection_Sock.c: inet_csk_get_port() */
tb_found:
if (!hlist_empty(&tb->owners)) {
......
if (tb->fastreuse > 0 &&
sk->sk_reuse && sk->sk_state != TCP_LISTEN &&
smallest_size == -1) {goto success;
所以,当该端口支持共享,且 socket 也设置了 SO_REUSEADDR
并且不为 LISTEN
状态时,此次 bind()
可以成功。
3.9 =< Linux < 4.5
3.9
版本内核增加了对 SO_REUSEPORT
的支持,listener
可以绑定到相同的 <IP:Port>
了。这个时候,当 Server 收到 Client 发送的 SYN 报文时,会选择其中一个 socket 进行响应.
[图]
具体到实现,3.9
版本扩展了 sock_common
,将原来记录skc_reuse
进行了拆分.
struct sock_common {
unsigned short skc_family;
volatile unsigned char skc_state;
- unsigned char skc_reuse;
+ unsigned char skc_reuse:4;
+ unsigned char skc_reuseport:4;
@@ int sock_setsockopt(struct socket *sock, int level, int optname,
case SO_REUSEADDR:
sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE);
break;
+ case SO_REUSEPORT:
+ sk->sk_reuseport = valbool;
+ break;
然后对 inet_bind_bucket
也相应进行了扩展
struct inet_bind_bucket {
/* omitted */
unsigned short port;
- signed short fastreuse;
+ signed char fastreuse;
+ signed char fastreuseport;
+ kuid_t fastuid;
而在绑定端口时,增加了一个队 reuseport 的通过条件
/* inet_connection_sock.c: inet_csk_get_port() */
tb_found:
if (sk->sk_reuse == SK_FORCE_REUSE)
goto success;
- if (tb->fastreuse > 0 &&
- sk->sk_reuse && sk->sk_state != TCP_LISTEN &&
+ if (((tb->fastreuse > 0 &&
+ sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
+ (tb->fastreuseport > 0 &&
+ sk->sk_reuseport && uid_eq(tb->fastuid, uid)))
&& smallest_size == -1) {goto success;
而当 Client 的 SYN 报文到达时,Server 会首先根据本地端口 (SYN 报文的<dport>
) 计算出一条 hash 冲突链,然后遍历该链表上的所有 Socket,根据四元组匹配程度进行打分; 如果使能了 reuseport,那么可能有多个 Socket 都将拿到最高分,此时内核将随机选择一个进行后续处理。
/* inet_hashtables.c */
struct sock *__inet_lookup_listener(struct......)
{
struct sock *sk, *result;
unsigned int hash = inet_lhashfn(net, hnum);
struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash]; // 根据本地端口找到 hash 冲突链
/* code omitted */
result = NULL;
hiscore = 0;
sk_nulls_for_each_rcu(sk, node, &ilb->head) {score = compute_score(sk, net, hnum, daddr, dif); // 根据匹配程度进行打分
if (score > hiscore) {
result = sk;
hiscore = score;
reuseport = sk->sk_reuseport;
if (reuseport) {
phash = inet_ehashfn(net, daddr, hnum,
saddr, sport);
matches = 1; // 如果是 reuseport 则累计多少个 socket 满足
}
} else if (score == hiscore && reuseport) {
matches++;
if (reciprocal_scale(phash, matches) == 0)
result = sk;
phash = next_pseudo_random32(phash);
}
}
/*
* if the nulls value we got at the end of this lookup is
* not the expected one, we must restart lookup.
* We probably met an item that was moved to another chain.
*/
return result;
}
举个栗子,假设内核有 4 条 listening socket 的 hash 冲突链,然后用户建立了 4 个 Server:A、B、C、D,监听的地址和端口如下图所示,A 和 B 使能了SO_REUSEPORT
。冲突链是以端口为 Key 的,因此 A、B、D 会挂到同一条冲突链上。如果此时收到对端一个 SYN 报文 <192.168.10.1, 21>, 那么内核会遍历listening_hash[0]
,为上面的 7 个 socket 进行打分,而由于 B 监听的是精确的地址,所以 B 的得分会比 A 高,内核最终选择出一个 SocketB 进行后续处理。
4.5 < Linux
从上面的例子可以看出,当收到 SYN 报文时,内核一定会遍历一条完整 hash 冲突链,为每一个 socket 进行打分,这稍微有些多余。因此,在 4.5 版本中,内核引入了 reuseport groups
,它将绑定到同一个 IP 和 Port,并且设置了SO_REUSEPORT
选项的 socket 组织到一个 group
内部。
--- a/include/net/sock.h
+++ b/include/net/sock.h
@@ -318,6 +318,7 @@ struct cg_proto;
* @sk_error_report: callback to indicate errors (e.g. %MSG_ERRQUEUE)
* @sk_backlog_rcv: callback to process the backlog
* @sk_destruct: called at sock freeing time, i.e. when all refcnt == 0
+ * @sk_reuseport_cb: reuseport group container
*/
struct sock {
/*
@@ -453,6 +454,7 @@ struct sock {int (*sk_backlog_rcv)(struct sock *sk,
struct sk_buff *skb);
void (*sk_destruct)(struct sock *sk);
+ struct sock_reuseport __rcu *sk_reuseport_cb;
};
这个特性在 4.5 版本只支持 UDP, 而在 4.6 版本开始支持 TCP(patch)。这样在查找 listen socket 时,内核将不用再遍历整个冲突链,而是在找到一个合格的 socket 时,如果它设置了SO_REUSEPORT
, 就直接找到它所属的reuseport group
, 从中选择一个进行后续处理.
@@ -215,6 +217,7 @@ struct sock *__inet_lookup_listener(struct net *net,
unsigned int hash = inet_lhashfn(net, hnum);
struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
int score, hiscore, matches = 0, reuseport = 0;
+ bool select_ok = true;
u32 phash = 0;
rcu_read_lock();
@@ -230,6 +233,15 @@ begin:
if (reuseport) {
phash = inet_ehashfn(net, daddr, hnum,
saddr, sport);
+ if (select_ok) {
+ struct sock *sk2;
+ sk2 = reuseport_select_sock(sk, phash,
+ skb, doff);
+ if (sk2) {
+ result = sk2;
+ goto found;
+ }
+ }
matches = 1;
}
}