温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Linux 内核中reuseport 的演进示例分析

发布时间:2022-01-21 10:21:08 来源:亿速云 阅读:122 作者:kk 栏目:开发技术

今天给大家介绍一下Linux 内核中reuseport 的演进示例分析。文章的内容小编觉得不错,现在给大家分享一下,觉得有需要的朋友可以了解一下,希望对大家有所帮助,下面跟着小编的思路一起来阅读吧。

SO_REUSEPORT选项在Linux 3.9被引入内核,在这之前也有一个很像的选项SO_REUSEADDR

如果你不太清楚这两者的区别和联系,建议搜索 How do SO_REUSEADDR and SO_REUSEPORT differ?。

如果不想读,那么下面这一节算是为懒人准备的。

SO_REUSEADDR 与 SO_REUSEPORT 是什么?

TCP/UDP用五元组唯一标识一个连接。

任何时候,两条连接的五元组都不能完全相同,否则当收到一个报文时,协议栈没办法判断它是属于哪个连接的。

五元组{}

五元组里,protocol在创建socket时确定,bind()时确定,connect()时确定。

当然,bind()connect()在一些时候并不需要显式使用,不过这不在本文的讨论范围里。

那么,如果对socket设置了SO_REUSEADDRSO_REUSEPORT选项,它们什么时候起作用呢?

答案是bind(),也就在确定时。

不同操作系统内核对待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可以绑定完全相同的“。

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

下面看看具体是怎么做的:  内核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 =

3.9版本内核增加了对SO_REUSEPORT的支持,listener可以绑定到相同的“了。

这个时候,当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报文的“)计算出一条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报文,那么内核会遍历listening_hash[0],为上面的7个socket进行打分,而由于B监听的是精确的地址,所以B的得分会比A高,内核最终选择出一个SocketB进行后续处理。

Linux 内核中reuseport 的演进示例分析
4.5

从上面的例子可以看出,当收到SYN报文时,内核一定会遍历一条完整hash冲突链,为每一个socket进行打分,这稍微有些多余。

因此,在4.5版本中,内核引入了reuseport groups,它将绑定到同一个IP和Port,并且设置了SO_REUSEPORT选项的socket组织到一个group内部。

Linux 内核中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时,内核将不用再遍历整个冲突链,而是在找到一个合格的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;             }         }

什么是Linux系统

Linux是一种免费使用和自由传播的类UNIX操作系统,是一个基于POSIX的多用户、多任务、支持多线程和多CPU的操作系统,使用Linux能运行主要的Unix工具软件、应用程序和网络协议。

以上就是Linux 内核中reuseport 的演进示例分析的全部内容了,更多与Linux 内核中reuseport 的演进示例分析相关的内容可以搜索亿速云之前的文章或者浏览下面的文章进行学习哈!相信小编会给大家增添更多知识,希望大家能够支持一下亿速云!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI