syn队列和accept队列的混淆
Confusion about syn queue and accept queue
在阅读TCP源码的时候,发现一个迷惑的地方:
我知道 TCP 在 3 次握手中有两个队列:
- 第一个队列存储服务器收到
SYN
并发回ACK + SYN
的连接,我们称之为syn队列.
- 第二个队列存放3WHS成功并建立连接的连接,我们称之为接受队列。
但是在阅读代码时,我发现listen()
会调用inet_csk_listen_start()
,这会调用reqsk_queue_alloc()
创建icsk_accept_queue
。而那个队列是在accept()
中使用的,当我们发现队列不为空时,我们会从中获取一个连接并return.
此外,跟踪接收过程后,调用堆栈是这样的
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
,不是 syn 队列。
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()
会return建立连接,也就是说icsk_accept_queue
是第二个队列,但是第一个队列在哪里呢?
从第一个队列到第二个队列的连接在哪里变化?
为什么 Linux 将新请求添加到 icsk_accept_queue
中?
简短的回答是 SYN 队列很危险。它们危险的原因是通过发送单个数据包 (SYN),发送方可以让接收方提交资源(SYN 队列条目的内存)。如果您以足够快的速度发送足够多的此类数据包(可能使用伪造的起始地址),您将导致接收方耗尽其内存资源或开始拒绝接受合法连接。
由于这个原因,现代操作系统没有 SYN 队列。相反,他们将采用各种技术(最常见的称为 SYN cookie),这些技术将允许他们只为已经回答初始 SYN ACK 数据包的连接建立一个队列,从而证明他们自己拥有用于此连接的专用资源。
所以,你是对的 - 没有 SYN 队列。
接下来我们将遵循最典型的代码路径,忽略丢包、重传和使用非典型特性(如 TCP 快速打开(代码注释中的 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_head
和rskq_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 泛滥时不会)。
通过调用 inet_csk_reqsk_queue_hash_add
然后调用 send_synack
,SYN 在 tcp_conn_request
中处理,其中存储连接请求(不是完整的套接字)(我们很快就会看到)响应 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_req
和 inet_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
处理,它创建一个完整的 child 套接字,然后调用 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_drop
和 reqsk_queue_removed
,在 child 上调用 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
,它保存着accept队列中套接字的数量)。
在阅读TCP源码的时候,发现一个迷惑的地方:
我知道 TCP 在 3 次握手中有两个队列:
- 第一个队列存储服务器收到
SYN
并发回ACK + SYN
的连接,我们称之为syn队列. - 第二个队列存放3WHS成功并建立连接的连接,我们称之为接受队列。
但是在阅读代码时,我发现listen()
会调用inet_csk_listen_start()
,这会调用reqsk_queue_alloc()
创建icsk_accept_queue
。而那个队列是在accept()
中使用的,当我们发现队列不为空时,我们会从中获取一个连接并return.
此外,跟踪接收过程后,调用堆栈是这样的
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
,不是 syn 队列。
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()
会return建立连接,也就是说icsk_accept_queue
是第二个队列,但是第一个队列在哪里呢?
从第一个队列到第二个队列的连接在哪里变化?
为什么 Linux 将新请求添加到 icsk_accept_queue
中?
简短的回答是 SYN 队列很危险。它们危险的原因是通过发送单个数据包 (SYN),发送方可以让接收方提交资源(SYN 队列条目的内存)。如果您以足够快的速度发送足够多的此类数据包(可能使用伪造的起始地址),您将导致接收方耗尽其内存资源或开始拒绝接受合法连接。
由于这个原因,现代操作系统没有 SYN 队列。相反,他们将采用各种技术(最常见的称为 SYN cookie),这些技术将允许他们只为已经回答初始 SYN ACK 数据包的连接建立一个队列,从而证明他们自己拥有用于此连接的专用资源。
所以,你是对的 - 没有 SYN 队列。
接下来我们将遵循最典型的代码路径,忽略丢包、重传和使用非典型特性(如 TCP 快速打开(代码注释中的 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_head
和rskq_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 泛滥时不会)。
通过调用 inet_csk_reqsk_queue_hash_add
然后调用 send_synack
,SYN 在 tcp_conn_request
中处理,其中存储连接请求(不是完整的套接字)(我们很快就会看到)响应 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_req
和 inet_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
处理,它创建一个完整的 child 套接字,然后调用 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_drop
和 reqsk_queue_removed
,在 child 上调用 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
,它保存着accept队列中套接字的数量)。