【问题标题】:Confusion about syn queue and accept queue关于同步队列和接受队列的混淆
【发布时间】:2020-11-23 17:22:32
【问题描述】:

在阅读TCP源码的时候,发现一个迷茫的事情:

我知道 TCP 在 3 次握手中有两个队列:

  • 第一个队列存储服务器接收到SYN并发回ACK + SYN的连接,我们称之为syn队列
  • 第二个队列存储3WHS成功并建立连接的连接,我们称之为accept queue

但是在阅读代码的时候,我发现listen()会调用inet_csk_listen_start(),而reqsk_queue_alloc()会调用reqsk_queue_alloc()来创建icsk_accept_queue。而那个队列用在accept()中,当我们发现队列不为空时,我们会从中获取一个连接并返回。

还有,跟踪接收过程后,调用栈是这样的

tcp_v4_rcv()->tcp_v4_do_rcv()->tcp_rcv_state_process()

接收到第一次握手时服务器状态为 LISTEN。所以它会调用

`tcp_v4_conn_request()->tcp_conn_request()`

tcp_conn_request()

if (!want_cookie)
    // Add the req into the queue
    inet_csk_reqsk_queue_hash_add(sk, req, tcp_timeout_init((struct sock *)req));

但这里的队列正是icsk_accept_queue,而不是同步队列。

void inet_csk_reqsk_queue_hash_add(struct sock *sk, struct request_sock *req,
                   unsigned long timeout)
{
    reqsk_queue_hash_req(req, timeout);
    inet_csk_reqsk_queue_added(sk);
}

static inline void inet_csk_reqsk_queue_added(struct sock *sk)
{
    reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);
}

accept()会返回建立的连接,也就是说icsk_accept_queue是第二个队列,但是第一个队列在哪里呢?

从第一个队列到第二个队列的连接在哪里变化?

Linux为什么要在icsk_accept_queue中添加新的req?

【问题讨论】:

  • This 是我们在 Cloudflare 的朋友写的一篇关于 SYN 数据包处理的非常有用的博客文章。
  • @Jim D. 感谢您的链接。但我认为他们无法回答我关于源代码中的队列是如何实现和工作的。
  • @Remy Lebeau 感谢您的链接。但我认为他们无法回答我关于源代码中的队列是如何实现和工作的。

标签: linux sockets tcp linux-kernel handshake


【解决方案1】:

在下文中,我们将遵循最典型的代码路径,并将忽略丢包、重传以及使用 TCP 快速打开(代码 cmets 中的 TFO)等非典型功能所产生的问题。

accept 调用由intet_csk_accept 处理,它调用reqsk_queue_remove 从侦听套接字的接受队列&icsk->icsk_accept_queue 中取出一个套接字:

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;
    int error;

    lock_sock(sk);

    [...]

    req = reqsk_queue_remove(queue, sk);
    newsk = req->sk;

    [...]

    return newsk;

    [...]
}

reqsk_queue_remove 中,它使用rskq_accept_headrskq_accept_tail 将套接字拉出队列并调用sk_acceptq_removed

static inline struct request_sock *reqsk_queue_remove(struct request_sock_queue *queue,
                              struct sock *parent)
{
    struct request_sock *req;

    spin_lock_bh(&queue->rskq_lock);
    req = queue->rskq_accept_head;
    if (req) {
        sk_acceptq_removed(parent);
        WRITE_ONCE(queue->rskq_accept_head, req->dl_next);
        if (queue->rskq_accept_head == NULL)
            queue->rskq_accept_tail = NULL;
    }
    spin_unlock_bh(&queue->rskq_lock);
    return req;
}

sk_acceptq_removed 减少了sk_ack_backlog 中等待接受的套接字队列的长度:

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

我认为,提问者完全理解这一点。现在让我们看看收到 SYN 时会发生什么,以及 3WH 的最终 ACK 到达时会发生什么。

首先收到 SYN。同样,让我们​​假设 TFO 和 SYN cookie 没有发挥作用,并查看最常见的路径(至少在出现 SYN 洪水时不会)。

SYN 在tcp_conn_request 中处理,其中存储连接请求(不是完整的套接字)(我们很快就会看到),方法是调用inet_csk_reqsk_queue_hash_add,然后调用send_synack 来响应SYN:

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 (!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);

   [...]

   return 0;

   [...]
}

inet_csk_reqsk_queue_hash_add 调用 reqsk_queue_hash_reqinet_csk_reqsk_queue_added 来存储请求。

void inet_csk_reqsk_queue_hash_add(struct sock *sk, struct request_sock *req,
                   unsigned long timeout)
{
    reqsk_queue_hash_req(req, timeout);
    inet_csk_reqsk_queue_added(sk);
}

reqsk_queue_hash_req 将请求放入 ehash

static void reqsk_queue_hash_req(struct request_sock *req,
                 unsigned long timeout)
{
    [...]

    inet_ehash_insert(req_to_sk(req), NULL);

    [...]
}

然后inet_csk_reqsk_queue_added 使用icsk_accept_queue 调用reqsk_queue_added

static inline void inet_csk_reqsk_queue_added(struct sock *sk)
{
    reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue);
}

增加qlen(不是sk_ack_backlog):

static inline void reqsk_queue_added(struct request_sock_queue *queue)
{
    atomic_inc(&queue->young);
    atomic_inc(&queue->qlen);
}

ehash 是所有 ESTABLISHED 和 TIMEWAIT 套接字的存储位置,以及最近存储 SYN“队列”的位置。

请注意,将到达的连接请求存储在适当的队列中实际上没有任何意义。它们的顺序无关紧要(最终的 ACK 可以按任何顺序到达),通过将它们移出侦听套接字,无需锁定侦听套接字来处理最终的 ACK。

请参阅this commit 了解影响此更改的代码。

最后,我们可以看到请求从 ehash 中移除,并作为一个完整的套接字添加到接受队列中。

3WH 的最终 ACK 由tcp_check_req 处理,它创建一个完整的子套接字,然后调用inet_csk_complete_hashdance

struct sock *tcp_check_req(struct sock *sk, struct sk_buff *skb,
               struct request_sock *req,
               bool fastopen, bool *req_stolen)
{

    [...]

    /* OK, ACK is valid, create big socket and
     * feed this segment to it. It will repeat all
     * the tests. THIS SEGMENT MUST MOVE SOCKET TO
     * ESTABLISHED STATE. If it will be dropped after
     * socket is created, wait for troubles.
     */
    child = inet_csk(sk)->icsk_af_ops->syn_recv_sock(sk, skb, req, NULL,
                             req, &own_req);

    [...]

    return inet_csk_complete_hashdance(sk, child, req, own_req);

    [...]

}

然后inet_csk_complete_hashdance 对请求调用inet_csk_reqsk_queue_dropreqsk_queue_removed,对子调用inet_csk_reqsk_queue_add

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;
    }
    [...]
}

inet_csk_reqsk_queue_drop 调用 reqsk_queue_unlink,从 ehash 中删除请求,reqsk_queue_removed 减少 qlen:

void inet_csk_reqsk_queue_drop(struct sock *sk, struct request_sock *req)
{
    if (reqsk_queue_unlink(req)) {
        reqsk_queue_removed(&inet_csk(sk)->icsk_accept_queue, req);
        reqsk_put(req);
    }
}

最后,inet_csk_reqsk_queue_add 将完整的套接字添加到接受队列中。

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;

    spin_lock(&queue->rskq_lock);
    if (unlikely(sk->sk_state != TCP_LISTEN)) {
        inet_child_forget(sk, req, child);
        child = NULL;
    } else {
        req->sk = child;
        req->dl_next = NULL;
        if (queue->rskq_accept_head == NULL)
            WRITE_ONCE(queue->rskq_accept_head, req);
        else
            queue->rskq_accept_tail->dl_next = req;
        queue->rskq_accept_tail = req;
        sk_acceptq_added(sk);
    }
    spin_unlock(&queue->rskq_lock);
    return child;
}

TL;DR 它在 ehash 中,并且此类 SYN 的数量是 qlen(而不是 sk_ack_backlog,它保存了接受队列中的套接字数量)。

【讨论】:

  • 谢谢你的回复,真的很清楚。还有一个问题,为什么我们在inet_csk_reqsk_queue_added(sk)接收SYN时添加icsk_accept_queue。这就是让我困惑的地方,我认为添加同步队列长度更合理。
  • 而另一个答案认为现在没有同步队列。他错了吗?
  • @tyChen 不再有显式队列,但会跟踪 SYN 连接请求的数量并将请求存储在 ehash 中。如果 SYN 请求的数量超过阈值,它们将被丢弃或使用 SYN cookie,具体取决于配置。在我看来,说它根本不存在是不准确的。
  • 感谢您的回复,我上面提到的增加 icsk_accept_queue 呢?
  • 我认为你被这些名字误导了。有两个“队列”,SYN“队列”并没有真正存储在实际队列中,而是存储在 ehash 中。此类 SYN 的数量保存在 qlen 中。接受队列是rskq_accept_head 指向的实际队列。它的长度存储在sk_ack_backlog 中。当我们收到一个 SYN 时,我们调用inet_csk_reqsk_queue_added(sk),它调用reqsk_queue_added(&inet_csk(sk)->icsk_accept_queue) 增加qlen,SYN 队列的大小,而不是接受队列的大小。事情当然可以更好地命名。
【解决方案2】:

简短的回答是 SYN 队列很危险。它们危险的原因是,通过发送单个数据包 (SYN),发送方可以让接收方提交资源(SYN 队列条目的内存)。如果您发送足够快的此类数据包,可能使用伪造的原始地址,您将导致接收方耗尽其内存资源或开始拒绝接受合法连接。

因此,现代操作系统没有 SYN 队列。相反,他们将使用各种技术(最常见的是称为 SYN cookie),这些技术将允许他们只为已经回答初始 SYN ACK 数据包的连接建立一个队列,从而证明他们自己拥有用于此连接的专用资源。

所以,你是对的 - 没有 SYN 队列。

【讨论】:

猜你喜欢
  • 1970-01-01
  • 2015-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多