美文网首页
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