# Linux listen 原理
在绑定操作将绑定数据保存到 inet_sock 中后,我们下一步就是开启服务端 socket 的监听机制,我们可以调用 listen 函数完成该操作,当该函数成功返回后,客户端便可以开启连接,TCP 三次握手后的连接将会放入 backlog 参数指定大小的接收队列中。本节我们将详细描述 listen 函数的执行原理。
int serverFD = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in addr; // 端口绑定信息
addr.sin_family = AF_INET; // 指定协议簇
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
addr.sin_port = htons(8080); // 将端口信息转为网络字节序(大端序)
bind(serverFD, (sockaddr*)&addr, sizeof(addr)); // 开始绑定
listen(serverFD,100); // 绑定后开启监听,同时指定接收队列长度为 100
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
sys_listen 函数
// 最大 backlog 队列大小
int sysctl_somaxconn = SOMAXCONN;
#define SOMAXCONN 128
asmlinkage long sys_listen(int fd, int backlog)
{
struct socket *sock;
int err;
if ((sock = sockfd_lookup(fd, &err)) != NULL) { // 通过 fd 找到 file,然后找到 inode,然后获取 socket_alloc 结构中的 socket
if ((unsigned) backlog > sysctl_somaxconn) // accept 队列最大 128
backlog = sysctl_somaxconn;
...
err=sock->ops->listen(sock, backlog); // 调用listen 函数
sockfd_put(sock);
}
return err;
}
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
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
inet_listen 函数
该函数我们之前在 struct proto_ops inet_stream_ops 中看到到过,注册为socket 的 ops 监听函数,可以看到该函数判断TCP状态后,调用 tcp_listen_start 启动TCP 同时修改状态为 TCPF_LISTEN ,然后保存 accept 队列大小。
int inet_listen(struct socket *sock, int backlog)
{
struct sock *sk = sock->sk;
unsigned char old_state;
int err;
lock_sock(sk);
err = -EINVAL;
// 检测状态
if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
goto out;
old_state = sk->sk_state;
if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
goto out;
if (old_state != TCP_LISTEN) { // 由于我们之气那的状态为:TCP_CLOSE ,还未到达 listen状态,那么第哦啊用 tcp_listen_start 函数启动TCP同时修改状态
err = tcp_listen_start(sk);
if (err)
goto out;
}
// 保存 accept 队列大小
sk->sk_max_ack_backlog = backlog;
err = 0;
out:
release_sock(sk);
return err;
}
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
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
tcp_listen_start 函数
该函数用于将TCP 状态修改为TCP_LISTEN,前面我们看到 socket 结构将包含在 tcp_sock结构中,其中的 inet_opt结构,我们在绑定端口时已经初始化,而这里的 开始监听 将会初始化 tcp_opt 结构,同时将监听结构 struct tcp_listen_opt 地址保存在 其中,最后再次检测端口是否被占用,若没有,那么直接返回,否则释放内存返回错误码。
// 表示TCP协议的 sock 元信息
struct tcp_sock {
struct sock sk; // sock 信息
struct inet_opt inet; // ip 层信息
struct tcp_opt tcp; // tcp 层信息
};
int tcp_listen_start(struct sock *sk)
{
struct inet_opt *inet = inet_sk(sk);
struct tcp_opt *tp = tcp_sk(sk); // 根据sk的地址获取到 tcp_sock 中的 tcp_opt 地址(参考 struct tcp_sock)
struct tcp_listen_opt *lopt;
// 初始化 socket 和 tcp_opt 成员变量
sk->sk_max_ack_backlog = 0;
sk->sk_ack_backlog = 0;
tp->accept_queue = tp->accept_queue_tail = NULL; // 接收队列
tp->syn_wait_lock = RW_LOCK_UNLOCKED;
tcp_delack_init(tp); // 将 tcp_opt 结构中的 ack 结构 初始化为0(通过 memset)
lopt = kmalloc(sizeof(struct tcp_listen_opt), GFP_KERNEL); // 创建 tcp 监听信息 结构
if (!lopt)
return -ENOMEM;
memset(lopt, 0, sizeof(struct tcp_listen_opt)); // 内部内存初始化为0
...
tp->listen_opt = lopt; // 监听信息 保存在 tcp_opt 结构中
...
sk->sk_state = TCP_LISTEN; // 将TCP 状态变为 监听状态
if (!sk->sk_prot->get_port(sk, inet->num)) { // 再次检查 当前绑定的端口 是否已经被占用,若没有,那么直接返回
inet->sport = htons(inet->num);
sk_dst_reset(sk);
sk->sk_prot->hash(sk);
return 0;
}
// 否则还原TCP状态,同时释放锁,随后将分配的内存释放,并返回 EADDRINUSE 错误码
sk->sk_state = TCP_CLOSE;
write_lock_bh(&tp->syn_wait_lock);
tp->listen_opt = NULL;
write_unlock_bh(&tp->syn_wait_lock);
kfree(lopt);
return -EADDRINUSE;
}
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
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
小结
C语言服务端网络编程顺序:
- 创建 socket:根据协议簇和协议创建对应的 socket 结构,同时对socket 结构的函数指针赋值
- 绑定端口:检测端口是否被占用,同时初始化 inet_opt 结构信息(保存用户空间传递的信息:绑定地址、绑定端口)
- 开启监听:初始化 tcp_opt 结构信息,同时将 sk_state 修改为 TCP_LISTEN
- 接收链接:下一节分析