# Linux REUSEPORT 端口复用原理

看如下代码,分别编写了两个 server 服务端代码,均指定了 SO_REUSEPORT 标志位,两个 server 绑定了同一个端口,当我们使用 本地地址 和 其他地址 访问时,内核将会根据 访问地址来选择 server 来服务客户端(生成 client socket 放入 accept 队列),那么如果是两个相同的 外部地址呢?负载均衡 ------- 内核级别的。

// server 1
int serverListen(char *serverIP, int serverPort){
  int serverFD;
  int opt = 1;
  struct sockaddr_in addr;
  serverFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  setsockopt(serverFD, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); // 这里设置了复用端口标志位
  bzero(&addr, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = inet_addr(serverIP);
  addr.sin_port = htons(serverPort);
  
  int err = bind(serverFD, (struct sockaddr *)&addr, sizeof(addr));
  if (err) {
    printf("error code: %d \n",errno);
    perror(NULL);
    exit(-1);
  }
  listen(serverFD, 0);
  return serverFD;
};int main(int argc, char *argv[]) {
  int serverFD = serverListen("127.0.0.1", 8080); // 监听 本地回环 地址
  int clientFD = accept(serverFD,NULL,NULL);
  printf("%s\n","127.0.0.1");
  close(clientFD);
  close(serverFD);
  return 1;
}// server 1
int serverListen(char *serverIP, int serverPort){
  int serverFD;
  int opt = 1;
  struct sockaddr_in addr;
  serverFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  setsockopt(serverFD, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt)); // 这里设置了复用端口标志位
  bzero(&addr, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = inet_addr(serverIP);
  addr.sin_port = htons(serverPort);
  
  int err = bind(serverFD, (struct sockaddr *)&addr, sizeof(addr));
  if (err) {
    printf("error code: %d \n",errno);
    perror(NULL);
    exit(-1);
  }
  listen(serverFD, 0);
  return serverFD;
};int main(int argc, char *argv[]) {
  int serverFD = serverListen("0.0.0.0", 8080); // 监听 所有 地址
  int clientFD = accept(serverFD,NULL,NULL);
  printf("%s\n","0.0.0.0");
  close(clientFD);
  close(serverFD);
  return 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

# SO_REUSEPORT 设置原理

源码如下,很明显,直接设置 sk->sk_reuseport 为1。

SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname, char __user *, optval, int, optlen)
{
    int err, fput_needed;
    struct socket *sock;if (optlen < 0)
        return -EINVAL;
    sock = sockfd_lookup_light(fd, &err, &fput_needed); // 根据 fd 找到 socket 结构
    if (sock != NULL) {
        if (level == SOL_SOCKET) // 在socket级别设置socket属性
            err =
            sock_setsockopt(sock, level, optname, optval,
                            optlen);
        else // 否则在 linux  sock 层面设置
            err =
            sock->ops->setsockopt(sock, level, optname, optval,
                                  optlen);
        out_put:
        fput_light(sock->file, fput_needed);
    }
    return err;
}int sock_setsockopt(struct socket *sock, int level, int optname,char __user *optval, unsigned int optlen){
    struct sock *sk = sock->sk;
    int val;
    int valbool;
    if (get_user(val, (int __user *)optval)) // 将用户态数据复制到内核中
        return -EFAULT;
    valbool = val ? 1 : 0;
    ...
    switch (optname) {
       ...
       case SO_REUSEPORT:
           sk->sk_reuseport = valbool; // 设置 socket 的 sk_reuseport 为 1
           break;
       ...
    }
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

# TCP 层处理端口复用

我们从源码中看到,当 skb 传递到 TCP 层时,将会根据 四元组中 目的IP 和 目的端口 找到服务于 当前 skb 的server sock,至于流程,注释打的很详细了。我们这里通过源码来解释为何在例子中:localhost 访问 和 外部IP 访问 所唤醒的进程不一样?从得分情况看:当精确匹配 IP 时 得分加 4 ,所以 localhost 域名解析后 为 127.0.0.1 与绑定 ip相同,所以 将有监听该地址的 server sock 来服务。

int tcp_v4_rcv(struct sk_buff *skb){
    struct sock *sk;
    ...
    sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest); // 找到 当前 skb 所表示的 sock (监听状态的 server sock 或者已经建立链接的 sock,这里我们看 监听状态的 server sock 即可) 我们需要研究的REUSEPORT 的负载均衡 将会在该方法实现
    if (!sock_owned_by_user(sk)) { // sk 未被上锁,那么进入该分支(看这里即可)
        if (!tcp_prequeue(sk, skb)) // 我们不开启延迟放入预备队列,所以返回0,取反为 1 进入分支
            ret = tcp_v4_do_rcv(sk, skb); 
    } else // sk 被上锁,先放入backlog 队列,等待释放后处理
        sk_add_backlog(sk, skb);
    ...
}
​
​
static inline struct sock *__inet_lookup_skb(struct inet_hashinfo *hashinfo,
                         struct sk_buff *skb,
                         const __be16 sport,
                         const __be16 dport)
{
    struct sock *sk = skb_steal_sock(skb); // 复用当前 skb 未释放的 sock,若获取成功,那么直接返回(当然 我们不看它)
    const struct iphdr *iph = ip_hdr(skb);
    if (sk)
        return sk;
    else
        return __inet_lookup(dev_net(skb_dst(skb)->dev), hashinfo,
                     iph->saddr, sport,
                     iph->daddr, dport, inet_iif(skb));
}static inline struct sock *__inet_lookup(struct net *net,
                     struct inet_hashinfo *hashinfo,
                     const __be32 saddr, const __be16 sport,
                     const __be32 daddr, const __be16 dport,
                     const int dif)
{
    u16 hnum = ntohs(dport); // 当前 skb tcp 头部的目标端口,也即当前我们需要连接的 server fd 的端口(咳咳,连接的四元组,在混沌学堂中,一定要注意听哈)
    struct sock *sk = __inet_lookup_established(net, hashinfo,
                saddr, sport, daddr, hnum, dif); // 已经建立连接return sk ? : __inet_lookup_listener(net, hashinfo, saddr, sport,
                         daddr, hnum, dif); // 看这里,注意:我们有多个server fd监听同一个端口,所以将会在这里面负载均衡
}struct sock *__inet_lookup_listener(struct net *net,
                                    struct inet_hashinfo *hashinfo,
                                    const __be32 saddr, __be16 sport,
                                    const __be32 daddr, const unsigned short hnum,
                                    const int dif){
    struct sock *sk, *result;
    struct hlist_nulls_node *node;
    // 根据设备和目的端口号计算hash值,找到hash entry 所在下标,也即server fd 所在链表(采用链地址法构建的hash表)
    unsigned int hash = inet_lhashfn(net, hnum);
    struct inet_listen_hashbucket *ilb = &hashinfo->listening_hash[hash]; 
    int score, hiscore, matches = 0, reuseport = 0;
    u32 phash = 0;
    rcu_read_lock();
begin:
    result = NULL;
    hiscore = 0; // 初始化最高分
    sk_nulls_for_each_rcu(sk, node, &ilb->head) { // 遍历链表
        score = compute_score(sk, net, hnum, daddr, dif); // 计算当前 server sock 对 skb IP 和 TCP 头部的目标地址和端口的得分
        if (score > hiscore) { // 当前得分大于之前的 计算得分
            result = sk; // 保存当前sk 作为服务 当前 server sock
            hiscore = score;
            reuseport = sk->sk_reuseport; 
            if (reuseport) {  // 指定复用端口,那么需要保存hash值,以免下次TCP SYN 连接时为同一个 server sock
                phash = inet_ehashfn(net, daddr, hnum,
                             saddr, sport);
                matches = 1; // 匹配成功
            }
        } else if (score == hiscore && reuseport) { // 相同得分,同时指定复用端口,那么根据 hash值与 matches 匹配值 的高 32为0,来决定是否使用当前 相同得分的 server sock
            matches++;
            if (((u64)phash * matches) >> 32 == 0)
                result = sk;
            phash = next_pseudo_random32(phash);
        }
    }
    ...
}// 计算 目标IP 和 目标端口 计算当前 监听的 server sk 的得分
static inline int compute_score(struct sock *sk, struct net *net,
                const unsigned short hnum, const __be32 daddr,
                const int dif)
{
    int score = -1;
    struct inet_sock *inet = inet_sk(sk);
    if (net_eq(sock_net(sk), net) && // 开启namespace  那么判断是否处于 统一个 ns 的net设备,若没有开启 为 1
        inet->inet_num == hnum && // 目的端口与当前 server sock 监听的端口 必须相同
            !ipv6_only_sock(sk)) { 
        __be32 rcv_saddr = inet->inet_rcv_saddr;
        score = sk->sk_family == PF_INET ? 2 : 1; // PF_INET协议簇 得分为 2  其他为 1
        if (rcv_saddr) { // 指定了监听IP地址,那么精确匹配 IP 地址,若匹配成功 得分加 4
            if (rcv_saddr != daddr) // 监听IP不匹配直接返回 -1 
                return -1;
            score += 4;
        }
        if (sk->sk_bound_dev_if) { // 若绑定了指定设备索引,那么比较当前 skb 进入的 设备索引号 dif, 匹配失败返回 -1 ,否则加分 4
            if (sk->sk_bound_dev_if != dif)
                return -1;
            score += 4;
        }
    }
    return score;
}// 随便看下就行,以下代码不重要
int tcp_v4_do_rcv(struct sock *sk, struct sk_buff *skb){ 
    if (sk->sk_state == TCP_ESTABLISHED) { // sock 已经处于链接状态(server fd 处于监听状态,我们不走该分支)
        ...
    }
    ...
    if (sk->sk_state == TCP_LISTEN) { // server fd ,此时进入该分支,建立连接对端的socket
        struct sock *nsk = tcp_v4_hnd_req(sk, skb); // 调用 syn_recv_sock 回调函数 创建 对等 sock
        if (!nsk)
            goto discard;
        ...
    } else
        sock_rps_save_rxhash(sk, skb);
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120

# 绑定时 对于 REUSEPORT 的处理?

请读者根据上一篇对于 REUSEADDR 的解释和本文的解释,自行研究吧,真的很简单,培养下自我思考的能力吧。我写出来那终究是我的,不懂得可以在混沌学堂群里,给 StartSky 提问,他会给你画图!