Linux内核listen系统调用源码分析

发布时间 2024-01-01 20:59:08作者: 划水的猫

一、环境说明

内核版本:Linux 3.10

内核源码地址:https://elixir.bootlin.com/linux/v3.10/source (包含各个版本内核源码,且网页可全局搜索函数)

二、应用层-listen()函数

/**
 * sockfd:要监听的socket描述字
 * backlog:为相应socket可以排队的最大连接个数
 */
int listen(int sockfd, int backlog);

通过网络栈专用操作函数集的总入口函数(sys_socketcall函数),请求会分发到sys_listen()函数。具体细节可以参考《Linux内核bind系统调用源码分析

三、sys_listen()函数

// file: net/socket.c
SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    struct socket *sock;
    int err, fput_needed;
    int somaxconn;

    sock = sockfd_lookup_light(fd, &err, &fput_needed); //获取fd对应的socket结构
    if (sock) {
        // 获取内核参数(/proc/sys/net/core/somaxconn)
        somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
        if ((unsigned int)backlog > somaxconn)
            backlog = somaxconn; //用户提供的backlog值大于somaxconn,则把backlog值修改为somaxconn

        err = security_socket_listen(sock, backlog); //SElinux相关,跳过
        if (!err)
            err = sock->ops->listen(sock, backlog); 

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

sock->ops->listen对应的方法为inet_listen。具体细节可以参考《Linux内核bind系统调用源码分析

四、inet_listen()函数

// file: net/ipv4/af_inet.c
int inet_listen(struct socket *sock, int backlog)
{
    struct sock *sk = sock->sk;
    unsigned char old_state;
    int err;

    lock_sock(sk);

    err = -EINVAL;
    // 对套接字类型、状态进行检查,类型必须是流式套接字且状态必须是close或者listen状态:
    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;

    /* Really, if the socket is already in listen state
     * we can only allow the backlog to be adjusted.
     */
    if (old_state != TCP_LISTEN) { //如果sk已经是TCP_LISTEN状态,仅会影响sk_max_ack_backlog的值
        ...... //fastopen新特性,忽略
        
        err = inet_csk_listen_start(sk, backlog);
        if (err)
            goto out;
    }
    sk->sk_max_ack_backlog = backlog; //将backlog赋值给sk->sk_max_ack_backlog(设置全连接队列长度)
    err = 0;

out:
    release_sock(sk);
    return err;
}

五、inet_csk_listen_start()函数

// file: net/ipv4/inet_connection_sock.c
int inet_csk_listen_start(struct sock *sk, const int nr_table_entries)
{
    struct inet_sock *inet = inet_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);
    // icsk->icsk_accept_queue 是接收队列,接收队列内核对象的申请和初始化
    int rc = reqsk_queue_alloc(&icsk->icsk_accept_queue, nr_table_entries);

    if (rc != 0)
        return rc;

    sk->sk_max_ack_backlog = 0; //初始化全连接队列最大长度为0
    sk->sk_ack_backlog = 0; //初始化全连接队列当前的连接数为0
    inet_csk_delack_init(sk);

    /* 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.
     */
    sk->sk_state = TCP_LISTEN; //sock状态修改为TCP_LISTEN
    if (!sk->sk_prot->get_port(sk, inet->inet_num)) { //检查端口号,如果没有被占用
        inet->inet_sport = htons(inet->inet_num); //设置源端口

        sk_dst_reset(sk); //清除掉dst cache
        sk->sk_prot->hash(sk); //将当前sock链入全局的listen hash表,这样,当SYN到来的时候就能通过__inet_lookup_listen函数找到这个listen中的sock

        return 0;
    }

    // 端口号冲突时,设置sock状态为TCP_CLOSE,并返回错误
    sk->sk_state = TCP_CLOSE;
    __reqsk_queue_destroy(&icsk->icsk_accept_queue);
    return -EADDRINUSE;
}

六、reqsk_queue_alloc()函数

完成了接收队列 request_sock_queue 内核对象的创建和初始化。其中包括内存申请、半连接队列长度的计算、全连接队列头的初始化。

// file: include/net/request_sock.h
struct request_sock_queue {
    //全连接队列
    struct request_sock    *rskq_accept_head;
    struct request_sock    *rskq_accept_tail;
    
    // 半连接队列
    struct listen_sock    *listen_opt;
    ......
};

对于全连接队列来说,在它上面不需要进行复杂的查找工作,accept 的时候只是先进先出地接受就好了。
所以全连接队列通过 rskq_accept_head 和 rskq_accept_tail 以链表的形式来管理。
和半连接队列相关的数据对象是 listen_opt,它是 listen_sock 类型的。

// file: include/net/request_sock.h
struct listen_sock {
 u8   max_qlen_log;
 u32   nr_table_entries;
 ......
 struct request_sock *syn_table[0];
};

因为服务器端需要在第三次握手时快速地查找出来第一次握手时留存的 request_sock 对象,所以其实是用了一个 hash 表来管理,就是 struct request_sock *syn_table[0]。

// file: net/core/request_sock.c
int reqsk_queue_alloc(struct request_sock_queue *queue, unsigned int nr_table_entries)
{
    size_t lopt_size = sizeof(struct listen_sock);
    struct listen_sock *lopt;

    // 计算半连接队列的长度(涉及到这三个somaxconn、backlog和tcp_max_syn_backlog内核参数)
    nr_table_entries = min_t(u32, nr_table_entries, sysctl_max_syn_backlog);
    nr_table_entries = max_t(u32, nr_table_entries, 8);
    nr_table_entries = roundup_pow_of_two(nr_table_entries + 1);
    
    // 为 listen_sock 对象申请内存,这里包含了半连接队列
    lopt_size += nr_table_entries * sizeof(struct request_sock *);
    if (lopt_size > PAGE_SIZE)
        lopt = vzalloc(lopt_size);
    else
        lopt = kzalloc(lopt_size, GFP_KERNEL);
    if (lopt == NULL)
        return -ENOMEM;

    for (lopt->max_qlen_log = 3;
         (1 << lopt->max_qlen_log) < nr_table_entries;
         lopt->max_qlen_log++);

    get_random_bytes(&lopt->hash_rnd, sizeof(lopt->hash_rnd));
    rwlock_init(&queue->syn_wait_lock);
    
    // 全连接队列头初始化
    queue->rskq_accept_head = NULL;
    
    lopt->nr_table_entries = nr_table_entries;
    write_lock_bh(&queue->syn_wait_lock);
    
    // 半连接队列设置
    queue->listen_opt = lopt;
    write_unlock_bh(&queue->syn_wait_lock);

    return 0;
}

七、总结

listen的主要工作:
1.根据fd得到socket;
2.根据socket得到sock;
3.设置sock状态为TCP_LISTEN
4.申请和初始化接收队列,包括全连接队列和半连接队列
sk->sk_max_ack_backlog = backlog; //全连接队列长度最大值
sk->sk_ack_backlog = 0; //当前全连接数

全连接与半连接以及sock的关系: