Linux內核中reuseport的演進

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_REUSEADDRSO_REUSEPORT選項,它們什麼時候起作用呢? 答案是bind(),也就在確定<src addr><src port>時。

不同操作系統內核對待SO_REUSEADDRSO_REUSEPORT的行爲有少許差異,但它們都源自BSD。因此,接下來就以BSD的實現爲標準進行說明。

SO_REUSEADDR

假設我現在需要bind()socketA綁定到A:X,將socketB綁定到B:Y(不考慮X=0或者Y=0,因爲0表示讓內核自動分配端口,一定不會衝突)。

如果X!=Y,那麼無論AB的關係如何,兩個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進行後續處理。

ul6OFs.md.png

4.5 < Linux

從上面的例子可以看出,當收到SYN報文時,內核一定會遍歷一條完整hash衝突鏈,爲每一個socket進行打分,這稍微有些多餘。因此,在4.5版本中,內核引入了reuseport groups,它將綁定到同一個IP和Port,並且設置了SO_REUSEPORT選項的socket組織到一個group內部。

ul6XYn.md.png

--- 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;
             }
         }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章