TCP连接的ACCEPT队列

发布于:2023-01-22 ⋅ 阅读:(252) ⋅ 点赞:(0)

PROC文件somaxconn默认为128,意味着单个套接口队列的长度,可最大监听128个连接。net_defaults_init_net函数初始化此值为宏SOMAXCONN(128)。此处的套接口意指完成TCP三次握手,连接已建立起来的套接口,用户层也还未accept此套接口(此处的套接为子套接口,即子套接口队列)。另外,连接未完成的套接口的数量由/proc/sys/net/ipv4/tcp_max_syn_backlog控制。

# cat /proc/sys/net/core/somaxconn
128
 
static int __net_init net_defaults_init_net(struct net *net)
{
    net->core.sysctl_somaxconn = SOMAXCONN;
}
连接监听套接口保存在以request_sock_queue结构的成员rskq_accept_head为首的链表中,rskq_accept_tail成员指向链表尾部。这是一个先入先出(FIFO)类型链表。

struct request_sock_queue {
    struct request_sock *rskq_accept_head;
    struct request_sock *rskq_accept_tail;
}; 
套接口函数listen的系统调用指定最大的连接套接口数量(backlog),由其内容可见,用户指定的参数backlog不能大于sysctl_somaxconn的值,否则使用sysctl_somaxconn的值。如需要增大监听的套接口数量,可预先修改PROC文件somaxconn的值。

SYSCALL_DEFINE2(listen, int, fd, int, backlog)
{
    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);
    }
}
最终,backlog的值赋给了套接口对应的sock结构的sk_max_ack_backlog成员。

int inet_listen(struct socket *sock, int backlog)
{
    if (old_state != TCP_LISTEN) {
        err = inet_csk_listen_start(sk, backlog);
    }
    sk->sk_max_ack_backlog = backlog;
}
 
int inet_csk_listen_start(struct sock *sk, int backlog)
{   
    struct inet_connection_sock *icsk = inet_csk(sk);
    
    reqsk_queue_alloc(&icsk->icsk_accept_queue);
    sk->sk_max_ack_backlog = backlog;
    sk->sk_ack_backlog = 0;
}

另外,sock结构中的sk_ack_backlog用于记录当前的队列长度,在inet_csk_listen_start函数中将其初始化为0,最重要的是初始化ACCEPT队列(icsk_accetp_queue)。

一、加入ACCEPT队列

接收到客户端的第三个握手ACK报文之后,在函数tcp_check_req中,如果一切检查正常的话,使用回调syn_recv_sock处理创建子套接口,之后由函数inet_csk_complete_hashdance将子套接口添加到ACCEPT队列中。

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, struct request_sock *req, bool fastopen)
{
    struct sock *child;
 
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);
    if (!child)
        goto listen_overflow;
 
    sock_rps_save_rxhash(child, skb);
    tcp_synack_rtt_meas(child, req);
    return inet_csk_complete_hashdance(sk, child, req, own_req);
}
如下所示,添加到ACCEPT队列的条件是own_req变量为真,此变量在回调函数syn_recv_sock中赋值,其为真意味着此子套接口已经成功添加到全局的TCP套接口队列中(TCP套接口全局哈希链表为tcp_hashinfo),否则,有一种情况是客户端进行的两次TCP握手,在第二次握手完成之时,第一次握手创建的子接口已添加到全局套接口链表中,导致第二次握手的子套接口添加不成功,own_req为空时需要释放第二次握手创建的子套接口。

struct sock *inet_csk_complete_hashdance(struct sock *sk, struct sock *child, struct request_sock *req, bool own_req)
{
    if (own_req) {
        inet_csk_reqsk_queue_drop(sk, req);
        reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
        if (inet_csk_reqsk_queue_add(sk, req, child))
            return child;
    }
    /* Too bad, another child took ownership of the request, undo. */
    bh_unlock_sock(child);
    sock_put(child);
    return NULL;
}
函数inet_csk_reqsk_queue_add完成具体的添加工作,新添加进来的子套接口加到ACCEPT队列的尾部。由函数可见ACCEPT队列实际上存储的是请求套接口(request_sock),不过每个请求套接口对应一个子套接口(req->sk),两者是对应的。

struct sock *inet_csk_reqsk_queue_add(struct sock *sk, struct request_sock *req, struct sock *child)
{
    struct request_sock_queue *queue = &inet_csk(sk)->icsk_accept_queue;
 
    if (unlikely(sk->sk_state != TCP_LISTEN)) {
 
    } else {
        req->sk = child;
        req->dl_next = NULL;
        if (queue->rskq_accept_head == NULL)
            queue->rskq_accept_head = req;
        else
            queue->rskq_accept_tail->dl_next = req;
        queue->rskq_accept_tail = req;
        sk_acceptq_added(sk);
    }
}

函数sk_acceptq_added递增ACCEPT队列长度。

static inline void sk_acceptq_added(struct sock *sk)
{
    sk->sk_ack_backlog++;
}

二、移出ACCEPT队列 
当用户层调用accept套接口函数时,使用reqsk_queue_remove函数将套接口(已是子套接口)从icsk_accept_queue队列中删除。此处的newsk为新TCP连接的子套接口,返回给用户层。

struct sock *inet_csk_accept(struct sock *sk, int flags, int *err, bool kern)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct request_sock_queue *queue = &icsk->icsk_accept_queue;
    struct request_sock *req;
    struct sock *newsk;
 
    req = reqsk_queue_remove(queue, sk);
    newsk = req->sk;
}
函数reqsk_queue_remove为简单的链表移除单个元素的操作,rskq_accept_head为链表的头,注意ACCEPT队列总是从头部开始移除队列中的子套接口元素,即用户层的accept操作总是取走队列中的第一个子套接口。

static inline struct request_sock *reqsk_queue_remove(struct request_sock_queue *queue, struct sock *parent)
{        
    struct request_sock *req;
 
    req = queue->rskq_accept_head;
    if (req) {
        sk_acceptq_removed(parent);
        queue->rskq_accept_head = req->dl_next;
        if (queue->rskq_accept_head == NULL)
            queue->rskq_accept_tail = NULL;
    } 
}
递减队列长度计数。

static inline void sk_acceptq_removed(struct sock *sk)
{
    sk->sk_ack_backlog--;
}

三、ACCEPT队列满
队列满的判断函数sk_acceptq_is_full如下,

static inline bool sk_acceptq_is_full(const struct sock *sk)
{
    return sk->sk_ack_backlog > sk->sk_max_ack_backlog;
}
队列满的判断在内核中有两处,一处位于TCP三次握手完成,接收到客户端的ACK报文时,见函数tcp_v4_syn_recv_sock中所示,由于此处TCP连接建立完成,需要创建新的子套接口,之后将此子套接口加入到accept队列中,所以先判断accept队列是否已满。满的话不进行后续的子接口创建等操作。

struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb, struct request_sock *req, ...)
{
    if (sk_acceptq_is_full(sk))
        goto exit_overflow;
}
函数tcp_v4_syn_recv_sock由TCP入口函数tcp_v4_rcv调用,要求此时的套接口处于TCP_NEW_SYN_RECV状态,即套接口在等待三次握手的最后一个ACK报文。

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, struct request_sock *req, bool fastopen)
{
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);
}
 
int tcp_v4_rcv(struct sk_buff *skb)
{
    if (sk->sk_state == TCP_NEW_SYN_RECV) {
        struct request_sock *req = inet_reqsk(sk);
        if (!tcp_filter(sk, skb))
            nsk = tcp_check_req(sk, skb, req, false);
    }
}
第二个队列满的判断位于函数tcp_conn_request中。此处是在接收到一个新的TCP连接请求之时,判断ACCEPT队列是否已满,满的话就不再继续处理,假如继续处理,比如回复客户端SYN+ACK报文,之后客户端发送ACK再次到达之后,就到了前面讲述的第一个队列满的判断点,最终也是不能处理。

int tcp_conn_request(struct request_sock_ops *rsk_ops, const struct tcp_request_sock_ops *af_ops, struct sock *sk, struct sk_buff *skb)
{
    if (sk_acceptq_is_full(sk)) {
        NET_INC_STATS(sock_net(sk), LINUX_MIB_LISTENOVERFLOWS);
        goto drop;
    }
}
函数tcp_conn_request在TCP接收状态机处理函数tcp_rcv_state_process中调用,要求套接口在LISTEN状态接收到SYN报文。

int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);
    const struct tcphdr *th = tcp_hdr(skb);
 
    switch (sk->sk_state) {
    case TCP_LISTEN:
        if (th->syn) {
            acceptable = icsk->icsk_af_ops->conn_request(sk, skb) >= 0;
            return 0;
        }
}
针对第一处ACCEPT队列满的处理,PROC文件tcp_abort_on_overflow指定了要进行的操作。其默认值为0,置位acked后直接返回;如果不为0,将重置此连接发送TCP的reset报文到对端。如果确认本机的应用程序不能够更快的处理(accept)新进的连接,可置位tcp_abort_on_overflow,但是发送的reset报文会对客户端造成伤害。如果队列满的原因是由于突发的大量连接所导致,之后其会恢复,不建议置位tcp_abort_on_overflow。

$ cat /proc/sys/net/ipv4/tcp_abort_on_overflow
0
 
struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb, struct request_sock *req, bool fastopen)
{    
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL, req, &own_req);
    if (!child)
        goto listen_overflow;
 
listen_overflow:
    if (!sock_net(sk)->ipv4.sysctl_tcp_abort_on_overflow) {
        inet_rsk(req)->acked = 1;
        return NULL;
    }
embryonic_reset:
    if (!(flg & TCP_FLAG_RST)) {
        req->rsk_ops->send_reset(sk, skb);
    }
    if (!fastopen) {
        inet_csk_reqsk_queue_drop(sk, req);
        __NET_INC_STATS(sock_net(sk), LINUX_MIB_EMBRYONICRSTS);
    }
}

四、补充
以上讨论的是TCP连接建立后的ACCEPT子套接口队列,但是在这之前(还未创建子套接口)的请求套接口保存在哪里呢?参见文章 https://blog.csdn.net/sinat_20184565/article/details/87709636 讨论的是tcp_max_syn_backlog控制的还未完全建立连接的TCP请求套接口队列的最大长度。但是内核中并没有此队列,所有的请求套接口保存到全局的TCP套接口链表中,使用ACCEPT队列icsk_accept_queue(结构为request_sock_queue)的成员qlen表示此队列的长度。

struct request_sock_queue {
    atomic_t        qlen;
}
在接收到SYN报文后,使用函数inet_csk_reqsk_queue_hash_add处理请求套接口队列,最终由函数reqsk_queue_added增加队列计数。

int tcp_conn_request(struct request_sock_ops *rsk_ops, const struct tcp_request_sock_ops *af_ops, struct sock *sk, struct sk_buff *skb)
{
    if (fastopen_sk) {
 
    } else {
        if (!want_cookie)
            inet_csk_reqsk_queue_hash_add(sk, req, tcp_timeout_init((struct sock *)req));
        af_ops->send_synack(sk, dst, &fl, req, &foc, !want_cookie ? TCP_SYNACK_NORMAL : TCP_SYNACK_COOKIE);
    }
}
static inline void reqsk_queue_added(struct request_sock_queue *queue)
{
    atomic_inc(&queue->young);
    atomic_inc(&queue->qlen);
}

另外一个问题是,在TCP连接建立之后,将子套接口添加到ACCEPT队列之前,需要将全局TCP套接口链表中删除请求套接口(由函数reqsk_queue_unlink完成),并且要递减请求套接口队列的长度(由函数reqsk_queue_removed完成)。

void inet_csk_reqsk_queue_drop(struct sock *sk, struct request_sock *req)
{
    if (reqsk_queue_unlink(&inet_csk(sk)->icsk_accept_queue, req)) {
        reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
        reqsk_put(req);
    }
}
static inline void reqsk_queue_removed(struct request_sock_queue *queue, const struct request_sock *req)
{        
    if (req->num_timeout == 0)
        atomic_dec(&queue->young);
    atomic_dec(&queue->qlen);

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到