SO_REUSEPORT
选项在Linux 3.9被引入内核,在这之前也有一个很像的选项SO_REUSEADDR
。如果你不太清楚这两者的区别和联系,建议搜索 How do SO_REUSEADDR and SO_REUSEPORT differ?。
如果不想读,那么下面这一节算是为懒人准备的。
SO_REUSEADDR 与 SO_REUSEPORT 是什么?TCP/UDP用
五元组
唯一标识一个连接。任何时候,两条连接的五元组都不能完全相同,否则当收到一个报文时,协议栈没办法判断它是属于哪个连接的。
五元组{
, , , , } 五元组里,
protocol
在创建socket时确定,和
在bind()
时确定,和
在connect()
时确定。当然,
bind()
和connect()
在一些时候并不需要显式使用,不过这不在本文的讨论范围里。那么,如果对socket设置了
SO_REUSEADDR
和SO_REUSEPORT
选项,它们什么时候起作用呢?答案是
bind()
,也就在确定和
时。不同操作系统内核对待
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可以绑定完全相同的“。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
下面看看具体是怎么做的: 内核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 =
3.9
版本内核增加了对SO_REUSEPORT
的支持,listener
可以绑定到相同的“了。这个时候,当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报文的“)计算出一条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报文,那么内核会遍历
listening_hash[0]
,为上面的7个socket进行打分,而由于B监听的是精确的地址,所以B的得分会比A高,内核最终选择出一个SocketB进行后续处理。4.5
从上面的例子可以看出,当收到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; } }
以上就是良许教程网为各位朋友分享的Linux 内核中 reuseport 的演进。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你!