# Linux REUSEADDR 地址复用原理

# 问题引入

看如下代码,当我们注释掉 setsockopt(serverFD, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); 时将会抛出异常:

error code: 98 
Address already in use
1
2

解除注释后,将恢复正常。为何?我们先来看错误号 98 在内核中的定义:

#define EADDRINUSE  98  /* Address already in use */
1

本文将详细解释 REUSEADDR 标志位的作用以及原理。


int errno; // 保存错误号int serverListen(char *serverIP, int serverPort){
    int serverFD;
    int opt = 1;
    struct sockaddr_in addr;
    serverFD = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // 创建server socket 指定 TCP 协议
    // setsockopt(serverFD, SOL_SOCKET, SO_REUSEADDR, &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); // 开启监听,0 表示由内核自己决定 backlog 队列大小
    return serverFD;
};int main(int argc, char *argv[]) {
    // 先开启一个server socket 指定 IP 地址和端口 并接收连接。随后 服务端主动关闭 连接 同时关闭 server socket,随后在server socket 等待 TIME_WAIT 的时候,再次创建 server socket 并监听
    int serverFD = serverListen("127.0.0.1", 8080);
    int clientFD = accept(serverFD,NULL,NULL);
    close(clientFD);
    close(serverFD);
    serverListen("127.0.0.1", 8080); 
    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

# bind 端口占用检测

该函数我们之前看过,当绑定端口时调用 get_port 函数来检测绑定的端口是否被使用。源码如下。

int inet_bind(struct socket *sock, struct sockaddr *uaddr, int addr_len){
    ...
    if (sk->sk_prot->get_port(sk, snum)) { // 端口验证失败
        inet->saddr = inet->rcv_saddr = 0;
        err = -EADDRINUSE; // 很明显,直接返回 地址被占用错误号,和我们预期相符
        goto out_release_sock;
    }
    ...
}
1
2
3
4
5
6
7
8
9

# tcp_v4_get_port 函数

tcp_v4_get_port 函数在一开始创建 tcp 时就被设置为 struct proto tcp_prot 的 回调函数。该函数用于检测端口是否可以使用。在该函数中分为三种情况:

  1. 遍历设置的端口range(通常为:32768 ----- 60999),找不到可以用到的端口,那么直接退出
  2. 端口号 完全是新的,未被使用过的,这种情况最简单 直接 分配 tcp_bind_bucket 结构,并将 tcp_bind_bucket 的 owner 设置为 当前 sock,然后插入到hash表中
  3. 端口号 不是新的,存在 tcp_bind_bucket 结构,那么根据是否设置 REUSEADDR 标志位来选择是否可以复用该端口,这里有个强制性条件:!hlist_empty(&tb->owners) ,只有当 TCP 状态 不为 TCP_CLOSE 时才决定是否复用
struct proto tcp_prot = {
    ...
    .get_port   =   tcp_v4_get_port, 
};static struct inet_protosw inetsw_array[] =
{
        {
                .type =       SOCK_STREAM,
                .protocol =   IPPROTO_TCP,
                .prot =       &tcp_prot,
                ...
        }
}static int tcp_v4_get_port(struct sock *sk, unsigned short snum)
{
    struct tcp_bind_hashbucket *head;
    struct hlist_node *node;
    struct tcp_bind_bucket *tb;
    int ret;local_bh_disable();
    if (!snum) { // 未指定端口,那么从指定的端口范围中找到一个未使用的端口(我们可以使用:cat /proc/sys/net/ipv4/ip_local_port_range 命令来看,笔者机器输出:32768 60999)
        int low = sysctl_local_port_range[0]; // 最低端口
        int high = sysctl_local_port_range[1];// 最高端口
        int remaining = (high - low) + 1; // 总端口数
        int rover;
        spin_lock(&tcp_portalloc_lock);
        rover = tcp_port_rover; // 初始查找下标(初始为: int tcp_port_rover = 1024 - 1)
        do {  // 从最低下限开始找,直到找到一个没有被占用的端口
            rover++;
            if (rover < low || rover > high)
                rover = low;
            head = &tcp_bhash[tcp_bhashfn(rover)];
            spin_lock(&head->lock);
            tb_for_each(tb, node, &head->chain)
                if (tb->port == rover)
                    goto next;
            break;
        next:
            spin_unlock(&head->lock);
        } while (--remaining > 0); // 遍历一遍,仍未找到未占用端口,直接退出循环
        tcp_port_rover = rover; // 保留当前查找的下标,在下一次查找时,从该处开始向前查找(优化重复查找已占用端口)
        spin_unlock(&tcp_portalloc_lock);
        ret = 1;
        if (remaining <= 0) // 未找到可用端口
            goto fail;
        snum = rover; // 若remaining大于0,表示未遍历所有端口号,此时找到的 rover 作为当前绑定的端口
    } else { // 指定端口,通过hash表找到链地址的头部,遍历头部看看链表中是否存在指定端口号对应的 tcp_bind_bucket 结构,若找到,跳转到 tb_found 执行
        head = &tcp_bhash[tcp_bhashfn(snum)];
        spin_lock(&head->lock);
        tb_for_each(tb, node, &head->chain)
            if (tb->port == snum)
                goto tb_found;
    }
    // 执行到这里,说明找到合适端口
    tb = NULL;
    goto tb_not_found;
tb_found: // 找到占用端口的  tcp_bind_bucket 结构
    if (!hlist_empty(&tb->owners)) { // 当前tb 存在所属 sock 结构(TCP状态未转为 TCP_CLOSE ,TIME_WAIT 阶段为该情况,详情看下面 TCP 关闭解释)
        if (sk->sk_reuse > 1)  // 设置了 REUSEADDR 地址复用标志,那么端口检测成功
            goto success;
        if (tb->fastreuse > 0 && // tcp_bind_bucket 设置快速复用 且 sk_reuse 为 1 且 当前 TCP 状态不为监听状态,那么可以复用
            sk->sk_reuse && sk->sk_state != TCP_LISTEN) {
            goto success;
        } else { // 否则 绑定冲突
            ret = 1;
            if (tcp_bind_conflict(sk, tb))
                goto fail_unlock;
        }
    }
tb_not_found: // 不存在 tcp_bind_bucket 结构 ,也即当前端口号未被使用,那么创建一个新的 tcp_bind_bucket 结构
    ret = 1;
    if (!tb && (tb = tcp_bucket_create(head, snum)) == NULL)
        goto fail_unlock;
    if (hlist_empty(&tb->owners)) { // tb 所属 sock 不存在(也即上述的 tb_not_found 分支判断失败 或者当前 tb 为刚创建),那么根据当前 TCP 状态是否处于监听状态 来设置是否支持 快速复用端口
        if (sk->sk_reuse && sk->sk_state != TCP_LISTEN)
            tb->fastreuse = 1;
        else
            tb->fastreuse = 0;
    } else if (tb->fastreuse &&
           (!sk->sk_reuse || sk->sk_state == TCP_LISTEN))
        tb->fastreuse = 0;
success:
    if (!tcp_sk(sk)->bind_hash) // 成功获取端口号,那么将当前 tcp_bind_bucket 结构的 owner 设置为当前sock
        tcp_bind_hash(sk, tb, snum);
    BUG_TRAP(tcp_sk(sk)->bind_hash == tb);
    ret = 0;
​
fail_unlock:
    spin_unlock(&head->lock);
fail:
    local_bh_enable();
    return ret;
}// 设置端口号并将当前sk 设置为 owners
void tcp_bind_hash(struct sock *sk, struct tcp_bind_bucket *tb, unsigned short snum){
    inet_sk(sk)->num = snum;
    sk_add_bind_node(sk, &tb->owners);
    tcp_sk(sk)->bind_hash = tb;
}
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

# 何时将 tcp_bind_bucket 结构的 owner 删除?

看如下源码,当tcp关闭时,转换状态为 TCP_CLOSE 时删除,当我们进入 TIME_WAIT 时 将会删除。源码如下。

void tcp_close(struct sock *sk, long timeout){
    ...
    if (sk->sk_state == TCP_FIN_WAIT2) {
        ...
        tcp_set_state(sk, TCP_CLOSE); // TCP 状态 转变为 关闭状态
        ...
    }
    ...
}// 改变 TCP 状态
static __inline__ void tcp_set_state(struct sock *sk, int state){
    int oldstate = sk->sk_state;
    switch (state) {
    case TCP_ESTABLISHED:
        if (oldstate != TCP_ESTABLISHED)
            TCP_INC_STATS(TcpCurrEstab);
        break;case TCP_CLOSE: // 最终状态为 关闭状态
        if (oldstate == TCP_CLOSE_WAIT || oldstate == TCP_ESTABLISHED)
            TCP_INC_STATS(TcpEstabResets);
​
        sk->sk_prot->unhash(sk);
        if (tcp_sk(sk)->bind_hash && // 存在绑定信息
            !(sk->sk_userlocks & SOCK_BINDPORT_LOCK))
            tcp_put_port(sk); // 将端口释放
            
    default:
        if (oldstate==TCP_ESTABLISHED)
            TCP_DEC_STATS(TcpCurrEstab);
    }
​
    sk->sk_state = state;}inline void tcp_put_port(struct sock *sk)
{
    ...
    __tcp_put_port(sk);
    ...
}static void __tcp_put_port(struct sock *sk)
{
    struct inet_opt *inet = inet_sk(sk);
    struct tcp_bind_hashbucket *head = &tcp_bhash[tcp_bhashfn(inet->num)];
    struct tcp_bind_bucket *tb;spin_lock(&head->lock);
    tb = tcp_sk(sk)->bind_hash;
    __sk_del_bind_node(sk); // 将端口的 owner 去除当前 sock
    tcp_sk(sk)->bind_hash = NULL;
    inet->num = 0; // 重置端口号
    tcp_bucket_destroy(tb);
    spin_unlock(&head->lock);
}// 将当前 sock 从 tcp_bind_bucket 的 owner 中去除
static __inline__ void __sk_del_bind_node(struct sock *sk)
{
    __hlist_del(&sk->sk_bind_node);
}
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

# 合适设置 sk->sk_reuse?

看如下代码为 setsockopt 系统调用,过程很明了。源码如下。

asmlinkage long sys_setsockopt(int fd, int level, int optname, char __user *optval, int optlen)
{
    int err;
    struct socket *sock;
    if (optlen < 0)
        return -EINVAL;
    if ((sock = sockfd_lookup(fd, &err))!=NULL) // 根据fd 获取 socket (之前讲过这里不再赘述)
    {
        err = security_socket_setsockopt(sock,level,optname);
        if (err) {
            sockfd_put(sock);
            return err;
        }
        if (level == SOL_SOCKET) // 指定设置等级为 socket 对象级别
            err=sock_setsockopt(sock,level,optname,optval,optlen);
        else // 否则设置将下沉为实际 sock 对象级别
            err=sock->ops->setsockopt(sock, level, optname, optval, optlen);
        sockfd_put(sock);
    }
    return err;
}int sock_setsockopt(struct socket *sock, int level, int optname, char __user *optval, int optlen)
{
    struct sock *sk=sock->sk;
    ...
    int val; // 保存用户空间的选项值
    int valbool;    
    if(optlen<sizeof(int)) // 长度必须大于等于整形值大小
        return(-EINVAL);
    if (get_user(val, (int __user *)optval)) // 将用户空间的值复制到内核
        return -EFAULT;
    valbool = val?1:0; // 将值转为布尔值
    ...
    switch(optname) {
          case SO_REUSEADDR: // 指定选项设置复用地址
            sk->sk_reuse = valbool;  
            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
41