Linux内核中reuseport的演进

SO_REUSEPORT选项在Linux 3.9被引入内核,在这以前也有一个很像的选项SO_REUSEADDR。若是你不太清楚这二者的区别和联系,建议阅读How do SO_REUSEADDR and SO_REUSEPORT differ?
若是不想读,那么下面这一节算是为懒人准备的。node

SO_REUSEADDR 与 SO_REUSEPORT 是什么?

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_REUSEADDRSO_REUSEPORT选项,它们何时起做用呢? 答案是bind(),也就在肯定<src addr><src port>时。bash

不一样操做系统内核对待SO_REUSEADDRSO_REUSEPORT的行为有少量差别,但它们都源自BSD。所以,接下来就以BSD的实现为标准进行说明。dom

SO_REUSEADDR

假设我如今须要bind()socketA绑定到A:X,将socketB绑定到B:Y(不考虑X=0或者Y=0,由于0表示让内核自动分配端口,必定不会冲突)。socket

若是X!=Y,那么不管AB的关系如何,两个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_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;
             }
         }
相关文章
相关标签/搜索