# 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

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

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

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

小结

C语言服务端网络编程顺序:

  1. 创建 socket:根据协议簇和协议创建对应的 socket 结构,同时对socket 结构的函数指针赋值
  2. 绑定端口:检测端口是否被占用,同时初始化 inet_opt 结构信息(保存用户空间传递的信息:绑定地址、绑定端口)
  3. 开启监听:初始化 tcp_opt 结构信息,同时将 sk_state 修改为 TCP_LISTEN
  4. 接收链接:下一节分析