溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Linux 內(nèi)核中reuseport 的演進示例分析

發(fā)布時間:2022-01-21 10:21:08 來源:億速云 閱讀:113 作者:kk 欄目:開發(fā)技術

今天給大家介紹一下Linux 內(nèi)核中reuseport 的演進示例分析。文章的內(nèi)容小編覺得不錯,現(xiàn)在給大家分享一下,覺得有需要的朋友可以了解一下,希望對大家有所幫助,下面跟著小編的思路一起來閱讀吧。

SO_REUSEPORT選項在Linux 3.9被引入內(nèi)核,在這之前也有一個很像的選項SO_REUSEADDR。

如果你不太清楚這兩者的區(qū)別和聯(lián)系,建議搜索 How do SO_REUSEADDR and SO_REUSEPORT differ?。

如果不想讀,那么下面這一節(jié)算是為懶人準備的。

SO_REUSEADDR 與 SO_REUSEPORT 是什么?

TCP/UDP用五元組唯一標識一個連接。

任何時候,兩條連接的五元組都不能完全相同,否則當收到一個報文時,協(xié)議棧沒辦法判斷它是屬于哪個連接的。

五元組{}

五元組里,protocol在創(chuàng)建socket時確定,bind()時確定,connect()時確定。

當然,bind()connect()在一些時候并不需要顯式使用,不過這不在本文的討論范圍里。

那么,如果對socket設置了SO_REUSEADDRSO_REUSEPORT選項,它們什么時候起作用呢?

答案是bind(),也就在確定時。

不同操作系統(tǒng)內(nèi)核對待SO_REUSEADDRSO_REUSEPORT的行為有少許差異,但它們都源自BSD

因此,接下來就以BSD的實現(xiàn)為標準進行說明。

SO_REUSEADDR

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

如果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的實現(xiàn)中SO_REUSEADDR可以讓一個使用通配地址(0.0.0.0),一個使用指定地址(192.168.1.0)的socket同時綁定成功

SO_REUSEADDR還有一種應用情景:在TCP中存在一個TIME_WAIT狀態(tài),它是指主動關閉的一端最后停留的階段。

假設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內(nèi)核有一些不一樣的地方,具體表現(xiàn)為

  • 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

下面看看具體是怎么做的:  內(nèi)核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()時,內(nèi)核使用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狀態(tài)時,此次bind()可以成功。

3.9 =

3.9版本內(nèi)核增加了對SO_REUSEPORT的支持,listener可以綁定到相同的“了。

這個時候,當Server收到Client發(fā)送的SYN報文時,會選擇其中一個socket進行響應。

具體到實現(xiàn),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會首先根據(jù)本地端口(SYN報文的“)計算出一條hash沖突鏈,然后遍歷該鏈表上的所有Socket,根據(jù)四元組匹配程度進行打分;

如果使能了reuseport,那么可能有多個Socket都將拿到最高分,此時內(nèi)核將隨機選擇一個進行后續(xù)處理。

/* 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]; // 根據(jù)本地端口找到hash沖突鏈    /* code omitted */    result = NULL;    hiscore = 0;    sk_nulls_for_each_rcu(sk, node, &ilb->head) {        score = compute_score(sk, net, hnum, daddr, dif); // 根據(jù)匹配程度進行打分        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;}

舉個栗子,假設內(nèi)核有4條listening socket的hash沖突鏈,然后用戶建立了4個Server:A、B、C、D,監(jiān)聽的地址和端口如下圖所示,A和B使能了SO_REUSEPORT。

沖突鏈是以端口為Key的,因此A、B、D會掛到同一條沖突鏈上。

如果此時收到對端一個SYN報文,那么內(nèi)核會遍歷listening_hash[0],為上面的7個socket進行打分,而由于B監(jiān)聽的是精確的地址,所以B的得分會比A高,內(nèi)核最終選擇出一個SocketB進行后續(xù)處理。

Linux 內(nèi)核中reuseport 的演進示例分析
4.5

從上面的例子可以看出,當收到SYN報文時,內(nèi)核一定會遍歷一條完整hash沖突鏈,為每一個socket進行打分,這稍微有些多余。

因此,在4.5版本中,內(nèi)核引入了reuseport groups,它將綁定到同一個IP和Port,并且設置了SO_REUSEPORT選項的socket組織到一個group內(nèi)部。

Linux 內(nèi)核中reuseport 的演進示例分析
--- 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時,內(nèi)核將不用再遍歷整個沖突鏈,而是在找到一個合格的socket時,如果它設置了SO_REUSEPORT,就直接找到它所屬的reuseport group,從中選擇一個進行后續(xù)處理。

@@ -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系統(tǒng)

Linux是一種免費使用和自由傳播的類UNIX操作系統(tǒng),是一個基于POSIX的多用戶、多任務、支持多線程和多CPU的操作系統(tǒng),使用Linux能運行主要的Unix工具軟件、應用程序和網(wǎng)絡協(xié)議。

以上就是Linux 內(nèi)核中reuseport 的演進示例分析的全部內(nèi)容了,更多與Linux 內(nèi)核中reuseport 的演進示例分析相關的內(nèi)容可以搜索億速云之前的文章或者瀏覽下面的文章進行學習哈!相信小編會給大家增添更多知識,希望大家能夠支持一下億速云!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI