美文网首页
listen 如何实现监听

listen 如何实现监听

作者: 董泽润 | 来源:发表于2018-08-23 12:04 被阅读183次

当绑定完端口后,就可以开启监听了,那么 listen 是如何实现的呢?还做哪些初始化操作呢?


listen 函数声明

man 一下,看看函数声明

int listen(int sockfd, int backlog);

返回值是错误码,参数 sockfd 是文件描述符,backlog 有这么一段解释:The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow. 等待 tcp 建立连接的队列最大长度。

入口

首先肯定是系统调用,找到处理函数 __sys_listen

int __sys_listen(int fd, int backlog)
{
    struct socket *sock;
    int err, fput_needed;
    int somaxconn;

    sock = sockfd_lookup_light(fd, &err, &fput_needed);
    if (sock) {
        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
        if ((unsigned int)backlog > somaxconn)
            backlog = somaxconn;

        err = security_socket_listen(sock, backlog);
        if (!err)
            err = sock->ops->listen(sock, backlog);

        fput_light(sock->file, fput_needed);
    }
    return err;
}

sockfd_lookup_light 不用看,根据 fd 找到 socket. 可以看到 backlog 参数受限于系统参数 somaxconn, 一般机器默认是 128, 肯定是不够的,线上一般大于 1024,如果短连接压力比较大,可以继续调大。security_socket_listen 所有以 security_ 开头的都是新版内核轻量级安全框架,在起动时注册安全检查回调,比如 selinux,忽略。最后调用 inet_stream_ops.inet_listen

inet_listen 源码分析

int inet_listen(struct socket *sock, int backlog)
{
    struct sock *sk = sock->sk;
    unsigned char old_state;
    int err, tcp_fastopen;
    lock_sock(sk);
    err = -EINVAL;
    if (sock->state != SS_UNCONNECTED || sock->type != SOCK_STREAM)
        goto out;

判断 socket 状态,如果不处于 SS_UNCONNECTED, 或是类型不是 SOCK_STREAM 不能监听

    old_state = sk->sk_state;
    if (!((1 << old_state) & (TCPF_CLOSE | TCPF_LISTEN)))
        goto out;

检查 struct sock 状态,只有处于 TCPF_CLOSETCPF_LISTEN 状态的才可以监听。可以参考 tcp 连接状态机。另外 struct sock 这些结构体都是套娃,可以参考前面的分享。

    /* Really, if the socket is already in listen state
     * we can only allow the backlog to be adjusted.
     */
    if (old_state != TCP_LISTEN) {
        /* Enable TFO w/o requiring TCP_FASTOPEN socket option.
         * Note that only TCP sockets (SOCK_STREAM) will reach here.
         * Also fastopen backlog may already been set via the option
         * because the socket was in TCP_LISTEN state previously but
         * was shutdown() rather than close().
         */
        tcp_fastopen = sock_net(sk)->ipv4.sysctl_tcp_fastopen;
        if ((tcp_fastopen & TFO_SERVER_WO_SOCKOPT1) &&
            (tcp_fastopen & TFO_SERVER_ENABLE) &&
            !inet_csk(sk)->icsk_accept_queue.fastopenq.max_qlen) {
            fastopen_queue_tune(sk, backlog);
            tcp_fastopen_init_key_once(sock_net(sk));
        }

        err = inet_csk_listen_start(sk, backlog);
        if (err)
            goto out;
    }

核心就在这里,首先判断是否允许 tcp_fastopen, 初始化相关队列。然后调用核心 inet_csk_listen_start 函数初始化。tcp_fastopen 简称 TFO, 网上有很多介绍,大致就是将 tcp 三次握手建连,变成两次,对于移动网络优化非常明显。nginx 也支持 TFO,暂时忽略不看。

    sk->sk_max_ack_backlog = backlog;
    err = 0;

out:
    release_sock(sk);
    return err;
}

如果己经处于 TCP_LISTEN 状态了,那么再次调用 listen 也可以,但只能改变 backlog 值。

inet_csk_listen_start 源码分析

这块代码也比较短,分段来看

int inet_csk_listen_start(struct sock *sk, int backlog)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct inet_sock *inet = inet_sk(sk);
    int err = -EADDRINUSE;

    reqsk_queue_alloc(&icsk->icsk_accept_queue);

reqsk_queue_alloc 初始化请求建立连接队列,初始化一个 FIFO 队列,还有 TFO 队列


    sk->sk_max_ack_backlog = backlog;
    sk->sk_ack_backlog = 0;
    inet_csk_delack_init(sk);

inet_csk_delack_init 用来初始化延迟发送 ack 队列


    /* There is race window here: we announce ourselves listening,
     * but this transition is still not validated by get_port().
     * It is OK, because this socket enters to hash table only
     * after validation is complete.
     */
    inet_sk_state_store(sk, TCP_LISTEN);

此处真正的改变状态为 TCP_LISTEN, 由于 smp 的原因,不简单的赋值。

    if (!sk->sk_prot->get_port(sk, inet->inet_num)) {
        inet->inet_sport = htons(inet->inet_num);

get_port 查看源码,回调 tcp_prot.inet_csk_get_port 用来分配监听端口,如果己经 bind 过了,那么不做任何操作,否则随机分配一个。一般也不会用随机端口,没有意义。

        sk_dst_reset(sk);
        err = sk->sk_prot->hash(sk);

        if (likely(!err))
            return 0;
    }

    inet_sk_set_state(sk, TCP_CLOSE);
    return err;
}

hash 回调 tcp_prot.inet_hash, 最终调用 __inet_hash,将 socket 注册到全局哈希表里。struct inet_hashinfo 这个结构体是一个全局单例,用来保存各种 socket 连接。

小结

至此,listen 粗略流程分析完毕。

相关文章

网友评论

      本文标题:listen 如何实现监听

      本文链接:https://www.haomeiwen.com/subject/wodpiftx.html