SO_REUSEPORT
選項在Linux 3.9被引入內核,在這以前也有一個很像的選項SO_REUSEADDR
。若是你不太清楚這二者的區別和聯繫,建議閱讀How do SO_REUSEADDR and SO_REUSEPORT differ?。
若是不想讀,那麼下面這一節算是爲懶人準備的。node
TCP/UDP用五元組
惟一標識一個鏈接。任什麼時候候,兩條鏈接的五元組都不能徹底相同,不然當收到一個報文時,協議棧沒辦法判斷它是屬於哪一個鏈接的。linux
五元組 {<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
五元組裏,protocol
在建立socket時肯定,<src addr>
和<src port>
在bind()
時肯定,<dest addr>
和<dest port>
在connect()
時肯定。固然,bind()
和connect()
在一些時候並不須要顯式使用,不過這不在本文的討論範圍裏。git
那麼,若是對socket設置了SO_REUSEADDR
和SO_REUSEPORT
選項,它們何時起做用呢? 答案是bind()
,也就在肯定<src addr>
和<src port>
時。bash
不一樣操做系統內核對待SO_REUSEADDR
和SO_REUSEPORT
的行爲有少量差別,但它們都源自BSD。所以,接下來就以BSD的實現爲標準進行說明。dom
假設我如今須要bind()
將socketA
綁定到A:X
,將socketB
綁定到B:Y
(不考慮X=0
或者Y=0
,由於0
表示讓內核自動分配端口,必定不會衝突)。socket
若是X!=Y
,那麼不管A
和B
的關係如何,兩個bind()
都會成功。但若是X==Y
,那麼結果會是下面這樣:ui
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是否能綁定成功。this
注
:這裏設置的對象是指後綁定的socket(也就是說不關心前一個是否設置)spa
能夠看出,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_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內核有一些不同的地方,具體表現爲
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)
SO_REUSEADDR
選項具備BSD中的SO_REUSEPORT
的效果。這一點Linux又比BSD更加寬鬆。SO_REUSEADDR socketA socketB Result --------------------------------------------------------------------- ON 192.168.0.2:55555 192.168.0.2:55555 OK
下面看看具體是怎麼作的:
內核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
版本內核增長了對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進行後續處理。
從上面的例子能夠看出,當收到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; } }