你的浏览器版本过低,可能导致网站不能正常访问!
为了你能正常使用网站功能,请使用这些浏览器。

SO_REUSEADDR 与 SO_REUSEPORT 是什么?

[复制链接]
gaosmile 发布时间:2021-1-18 11:16
SO_REUSEPORT 选项在Linux 3.9被引入内核,在这之前也有一个很像的选项SO_REUSEADDR。
如果你不太清楚这两者的区别和联系,建议搜索 How do SO_REUSEADDR and SO_REUSEPORT differ?。
如果不想读,那么下面这一节算是为懒人准备的。
微信图片_20210118111339.jpg
SO_REUSEADDR 与 SO_REUSEPORT 是什么?+ @7 B9 @, D* x) i- Y# F4 B; S
TCP/UDP用五元组唯一标识一个连接。
任何时候,两条连接的五元组都不能完全相同,否则当收到一个报文时,协议栈没办法判断它是属于哪个连接的。
  1. ! A, X' p) W$ u% P6 M4 D" D
  2. 五元组
    - h% d6 S+ r' L- N. ^2 J5 z
  3. {<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
复制代码

# S$ M$ @  v: m) V0 ^' D
五元组里,protocol在创建socket时确定,<src addr>和<src port>在bind()时确定,<dest addr>和<dest port>在connect()时确定。
当然,bind()和connect()在一些时候并不需要显式使用,不过这不在本文的讨论范围里。
那么,如果对socket设置了SO_REUSEADDR和SO_REUSEPORT选项,它们什么时候起作用呢?
答案是bind(),也就在确定<src addr>和<src port>时。
不同操作系统内核对待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       Result9 L" d6 h5 a8 @8 w- D" k% c
---------------------------------------------------------------------, ]" e& D" U4 U( X' n
  ON/OFF       192.168.0.1:21   192.168.0.1:21    Error (EADDRINUSE)
1 j" v, X  z  W6 _+ ^( `  ON/OFF       192.168.0.1:21      10.0.0.1:21    OK
# P8 [% O* A6 l  ON/OFF          10.0.0.1:21   192.168.0.1:21    OK
; j. w" E. {% {! s$ }6 o; }/ K   OFF             0.0.0.0:21   192.168.1.0:21    Error (EADDRINUSE)# b  v# t5 F; [- f7 n: e
   OFF         192.168.1.0:21       0.0.0.0:21    Error (EADDRINUSE)
/ A3 K" C. ^, I  [" E8 f, T, n   ON              0.0.0.0:21   192.168.1.0:21    OK
9 P/ B# Y5 f. f9 [+ e4 B9 _   ON          192.168.1.0:21       0.0.0.0:21    OK; g- K! S9 R3 r, R, P+ o  }
  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可以绑定完全相同的<IPort>。
SO_REUSEPORT       socketA        socketB       Result
1 \9 ^- i1 S9 V; g* S4 Y---------------------------------------------------------------------
+ y/ M1 m8 V% S2 F; O+ x    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更加严格

    % x+ p  R( ^2 {
SO_REUSEADDR       socketA        socketB       Result
+ t& P( ~( e( e6 y, T* X---------------------------------------------------------------------3 _  l' L. P+ X
    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更加宽松。
    5 |" [9 J& x3 y, i7 a7 S1 ~* s
SO_REUSEADDR      socketA            socketB           Result9 b$ r2 z$ i+ K. l. K9 M
---------------------------------------------------------------------
, @' c- g0 U5 U+ J: @0 U$ |    ON        192.168.0.2:55555   192.168.0.2:55555      OK

, l' ^6 `+ p  D
Linux中reuseport的演进

( Q' f, G* ?' i& s8 [/ m. Q9 s
Linux < 3.9

3 X6 X: |. x7 N  f  l
下面看看具体是怎么做的:   内核socket使用skc_reuse字段表示是否设置了SO_REUSEADDR

4 f2 k) _( W7 H
struct sock_common {& y* Y. V; \" c0 ^( t
     /* omitted */" F: l9 m$ J, o8 `) @5 Z
    unsigned char        skc_reuse;
8 X6 E, X5 z/ c- U    /* omitted */
1 ]2 q8 D, T4 V9 e$ U( @9 F}  e& ~4 ~+ B/ o0 p- |) Q  W

* x% K4 C/ x/ n, Oint sock_setsockopt(struct socket *sock, int level, int optname,...$ Y, G3 |* k% [5 h3 L. @0 Y' J
{: S( N% p# x- e1 W% Y
    ......
9 K4 @4 X* m4 T& o; b- ~    case SO_REUSEADDR:( h0 W+ G' f1 M( \0 F
     sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE);
$ s6 ~8 B; Z9 i# I4 a- Z8 j     break;$ M& }* k" V8 b; I; k! s
}
inet_bind_bucket表示一个绑定的端口。
struct inet_bind_bucket {- E( J( K& X# Z2 G0 w& V( O' n
    /* omitted */
1 b  }9 w1 d5 |) _# c    unsigned short        port;4 R: l( I8 \" @% }
    signed short        fastreuse;
  h" O- P& O& N% ~    int            num_owners;
% n4 T1 p/ ?: Z0 ]3 N    struct hlist_node    node;+ B8 k, W! D9 |2 Z5 l
    struct hlist_head    owners;. U1 |+ |% p* R8 u
};
上面结构中的fastreuse表示该端口是否支持共享,所有共享该端口的socket挂到owner成员上。在用户使用bind()时,内核使用TCP:inet_csk_get_port(),UDP:udp_v4_get_port()来绑定端口。
/* inet_connection_Sock.c: inet_csk_get_port() */: k6 x3 d; K8 D! K
tb_found:
& l4 Z# s  n% Q" ^& I, b9 w    if (!hlist_empty(&tb->owners)) {
: b4 L4 s  X4 R% a/ @        ......
; s9 h7 T" n0 h3 Q' c        if (tb->fastreuse > 0 &&
  g; ~/ W- w! O) t( D$ q6 B: [            sk->sk_reuse && sk->sk_state != TCP_LISTEN &&
3 E. {" _$ d" m8 |6 U( E            smallest_size == -1) {
& h3 u9 P& j/ \7 a            goto success;
所以,当该端口支持共享,且socket也设置了SO_REUSEADDR并且不为LISTEN状态时,此次bind()可以成功。
3.9 =< Linux < 4.5
3.9版本内核增加了对SO_REUSEPORT的支持,listener可以绑定到相同的<IPort>了。
这个时候,当Server收到Client发送的SYN报文时,会选择其中一个socket进行响应。
具体到实现,3.9版本扩展了sock_common,将原来记录skc_reuse进行了拆分.
struct sock_common {
0 J0 c% B6 Q9 I) [/ i     unsigned short        skc_family;
' f+ Z/ y. v! a6 C4 l% K     volatile unsigned char    skc_state;
' _3 T# P' z) C! ]-    unsigned char        skc_reuse;
! Z8 S# Y/ T" X5 d, X: T+    unsigned char        skc_reuse:4;! R% [$ B9 O3 Y" g! @& P" n- m
+    unsigned char        skc_reuseport:4;
+ Y" {# o+ v# k4 p5 R/ ^* B: P0 V* X) r7 D: Q+ A
" {; h/ e2 A$ k: `/ y5 T5 f# D+ G
@@ int sock_setsockopt(struct socket *sock, int level, int optname,9 w! W% x/ R  C* K  V8 q8 c
     case SO_REUSEADDR:% ?5 n6 b; I+ h& ^- ^5 y; B
         sk->sk_reuse = (valbool ? SK_CAN_REUSE : SK_NO_REUSE);7 x5 @! h7 X; V/ O
         break;! |0 v6 ?3 [* @0 w# f
+    case SO_REUSEPORT:- K! L- R) g+ g9 y: R' J& }# z
+        sk->sk_reuseport = valbool;
& l" h# g% J1 m7 K' |7 e+        break;  w2 a# W; @+ w- x+ \* W
然后对inet_bind_bucket也相应进行了扩展
struct inet_bind_bucket {9 c! f+ f* S. n
     /* omitted */
, m# @" N, F% E8 Q, r     unsigned short        port;
0 g6 h9 w2 M/ h7 k-    signed short        fastreuse;! N6 r% @' }0 J- x) I
+    signed char        fastreuse;' F+ X8 q. ~* ?/ Q' ~8 D; @
+    signed char        fastreuseport;
4 o! A& Y6 e% q  o( U+    kuid_t            fastuid;
而在绑定端口时,增加了一个队reuseport的通过条件
/* inet_connection_sock.c: inet_csk_get_port() */
/ F" |* W& e. w7 o* [tb_found:
1 H. a6 u3 d5 M5 {  ?) S, n' M         if (sk->sk_reuse == SK_FORCE_REUSE)* g# F3 r! ^0 W( r
             goto success;
! D' T/ t% H/ G-        if (tb->fastreuse > 0 &&# n  m* o5 @, {4 S- G3 A. c
-            sk->sk_reuse && sk->sk_state != TCP_LISTEN &&
) g; h" S- D. R6 Y4 F9 v  w2 P/ }+        if (((tb->fastreuse > 0 &&
6 B# a4 K1 v; N2 K8 l$ r+              sk->sk_reuse && sk->sk_state != TCP_LISTEN) ||
3 ?: O, I: h( F; ?0 J1 c+             (tb->fastreuseport > 0 &&
% V6 e6 K. M) H  ~; j* ~+              sk->sk_reuseport && uid_eq(tb->fastuid, uid)))
% B! P2 B: D/ J5 p. \8 ]: ~! Y             && smallest_size == -1) {
8 z, R! g5 [0 `+ n2 r  A               goto success;
而当Client的SYN报文到达时,Server会首先根据本地端口(SYN报文的<dport>)计算出一条hash冲突链,然后遍历该链表上的所有Socket,根据四元组匹配程度进行打分;
如果使能了reuseport,那么可能有多个Socket都将拿到最高分,此时内核将随机选择一个进行后续处理。
/* inet_hashtables.c  */
# v0 q6 s0 a' t! J% ]+ x) w5 Jstruct sock *__inet_lookup_listener(struct......)
" }, Y5 u, I2 u- A- H: ]% C{
, y/ v4 D) Y; A    struct sock *sk, *result;9 q# y/ x# h( w
    unsigned int hash = inet_lhashfn(net, hnum);
7 L) Q5 M( T) n/ ?" Q8 r  {    struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash]; // 根据本地端口找到hash冲突链4 b: P" C: f1 G* R( o9 V0 e
    /* code omitted */
1 m. h) a' W5 R( I& Z    result = NULL;* l1 W4 v4 t. A) \1 f* G; c
    hiscore = 0;7 q' @& Q. |. l; @8 C/ X- v
    sk_nulls_for_each_rcu(sk, node, &ilb->head) {& C9 ~! F: w4 P, d+ |
        score = compute_score(sk, net, hnum, daddr, dif); // 根据匹配程度进行打分) z3 o6 W: W4 r
        if (score > hiscore) {4 p- f% ~+ w  k5 [
            result = sk;
9 P9 U8 f9 d3 k  L            hiscore = score;
- ^' e0 W( ?7 H+ t& j1 t) `& y' E5 ~* M            reuseport = sk->sk_reuseport;0 H/ o- |4 |* s  O  X
            if (reuseport) {
0 z6 i1 S/ }$ X  ?3 m! r                phash = inet_ehashfn(net, daddr, hnum,: f' ~. w7 @! q( w; v6 E
                             saddr, sport);
7 o3 G: T1 p, E4 s3 h                matches = 1;                             // 如果是reuseport 则累计多少个socket满足
( |: U$ s3 K6 K8 {6 y% {            }  A6 `3 c* [3 `5 ^, h
        } else if (score == hiscore && reuseport) {
0 }- @) [4 R% ?2 {. Z# V6 B            matches++;+ [$ u* V' U# g
            if (reciprocal_scale(phash, matches) == 0). ?5 [8 u$ i/ I
                result = sk;
0 A# S8 A) |4 G/ O2 S; E            phash = next_pseudo_random32(phash);
6 i) h. m$ P  b+ x- {. s        }6 y- n: o" z: G8 w; r
    }
. @" F' [- ^, F5 Y) Y- `    /*, ~1 E! ]2 ~( y: Y
     * if the nulls value we got at the end of this lookup is
+ x; \3 c0 V$ ~" ]0 h% r     * not the expected one, we must restart lookup.
8 ?" Z, m' [3 l9 ?     * We probably met an item that was moved to another chain.
) O- ^! e5 ~" G5 @     */
0 L, N3 X* X2 ~' G8 D2 v$ {; Q    return result;3 o& `% J' B' e
}
举个栗子,假设内核有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进行后续处理。
微信图片_20210118111343.png
4.5 < Linux
从上面的例子可以看出,当收到SYN报文时,内核一定会遍历一条完整hash冲突链,为每一个socket进行打分,这稍微有些多余。
因此,在4.5版本中,内核引入了reuseport groups,它将绑定到同一个IP和Port,并且设置了SO_REUSEPORT选项的socket组织到一个group内部。
微信图片_20210118111346.png
--- a/include/net/sock.h
. T7 B2 J# B$ n. S* ~+++ b/include/net/sock.h
# C3 y5 v; X8 U; G@@ -318,6 +318,7 @@ struct cg_proto;! r  o" j; x# C) P1 _1 t. x
   *    @sk_error_report: callback to indicate errors (e.g. %MSG_ERRQUEUE)7 b$ r& x) x$ R' F6 I2 U3 D( T3 S/ k* O
   *    @sk_backlog_rcv: callback to process the backlog
7 W* Y% k: X, L   *    @sk_destruct: called at sock freeing time, i.e. when all refcnt == 03 u: l. P& ?; h/ X
+  *    @sk_reuseport_cb: reuseport group container; z5 S% r; _0 n' U% {. J* r$ T
  */, [. h' v0 _$ q/ f7 `+ `, D9 t$ i- V1 e1 A
struct sock {5 \6 R5 a: t0 i; g' d8 U
     /*% Y" i4 k: c) \
@@ -453,6 +454,7 @@ struct sock {
7 j- R! A8 s+ K* J; e3 ~     int            (*sk_backlog_rcv)(struct sock *sk,& @( Y( j( E( V' g% K1 [
                           struct sk_buff *skb);, ]6 R1 N+ e6 O7 `* Z
     void                    (*sk_destruct)(struct sock *sk);% W8 H+ T* v$ ], ?
+    struct sock_reuseport __rcu    *sk_reuseport_cb;; H3 c9 q  J) j% L5 q# t
};
这个特性在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,* C" f, v* O: t$ K
     unsigned int hash = inet_lhashfn(net, hnum);. n( m+ _+ e* a6 x
     struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash];
# U% }3 w# Z; g. h  l     int score, hiscore, matches = 0, reuseport = 0;; n( p& A- |! N9 {! P
+    bool select_ok = true;
. d% H5 Y, \8 o6 K     u32 phash = 0;
1 o% Z( R: |6 V$ L6 w1 @  y - l& z$ D  R+ S. y
     rcu_read_lock();
- T3 J2 l3 z: n@@ -230,6 +233,15 @@ begin:
3 Q( R# k# _4 k% `2 Q( O9 V             if (reuseport) {/ D6 \% {, t1 m4 n5 J
                 phash = inet_ehashfn(net, daddr, hnum,
: \. X+ }: |. I                              saddr, sport);! X2 ]4 k2 q+ j, o& y
+                if (select_ok) {
. L+ c% ^* O; o* }7 E' F- Z# {+                    struct sock *sk2;5 S& T: V9 }( b6 B+ A$ O8 W
+                    sk2 = reuseport_select_sock(sk, phash,
' F" K% U7 N) ?& N% E* N2 I+                                    skb, doff);" f/ k( i+ W0 \+ n& F
+                    if (sk2) {
) P1 F8 H$ r5 r; P2 N4 Y+                        result = sk2;
# L8 C, s2 ^0 N& J; v9 W- b7 V) ]+                        goto found;
( h" n8 }4 Z- ^2 z: v7 L+                    }
/ B& a, [: C6 E  c% Q" g- f5 m+                }2 H+ {$ ~: X$ ?& p# I% J9 n2 ~6 q
                 matches = 1;
4 K( o7 k- d; w' @# H  x% O4 @             }( D" @% L) ^: _/ S% d, r3 ?
         }
0 _' O6 `& }+ U( d- |3 G
赞 收藏 评论0 发布时间:2021-1-18 11:16

举报

0个回答

所属标签

关于
我们是谁
投资者关系
意法半导体可持续发展举措
创新与技术
意法半导体官网
联系我们
联系ST分支机构
寻找销售人员和分销渠道
社区
媒体中心
活动与培训
隐私策略
隐私策略
Cookies管理
行使您的权利
官方最新发布
人形机器人运动控制、感知与智能配电
半导体创新技术与应用方向
EE架构与软件定义汽车
12V/48V 汽车智能配电(SPD)
区域控制单元(ZCU)与分区架构
关注我们
st-img 微信公众号
st-img 手机版