# Linux REUSEADDR 地址复用原理
# 问题引入
看如下代码,当我们注释掉 setsockopt(serverFD, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); 时将会抛出异常:
error code: 98
Address already in use
1
2
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
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
2
3
4
5
6
7
8
9
# tcp_v4_get_port 函数
tcp_v4_get_port 函数在一开始创建 tcp 时就被设置为 struct proto tcp_prot 的 回调函数。该函数用于检测端口是否可以使用。在该函数中分为三种情况:
- 遍历设置的端口range(通常为:32768 ----- 60999),找不到可以用到的端口,那么直接退出
- 端口号 完全是新的,未被使用过的,这种情况最简单 直接 分配 tcp_bind_bucket 结构,并将 tcp_bind_bucket 的 owner 设置为 当前 sock,然后插入到hash表中
- 端口号 不是新的,存在 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
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
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
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