diff options
| author | Eric Dumazet <edumazet@kernel.org> | 2026-09-29 07:17:43 +0000 |
|---|---|---|
| committer | Jakub Kicinski <kuba@kernel.org> | 2026-10-05 16:04:50 -0700 |
| commit | 4eee6d58ad1497518b59f8f2c0eb42e258d9cc82 (patch) | |
| tree | 205ac2137dc019eb6df6d16a52f5e66e1aaf4413 /include | |
| parent | 5181ac617537474e6e3de40e22bfd009b4f6e231 (diff) | |
| download | linux-next-4eee6d58ad1497518b59f8f2c0eb42e258d9cc82.tar.gz linux-next-4eee6d58ad1497518b59f8f2c0eb42e258d9cc82.zip | |
tcp: add tp->tcp_nospace
tcp_check_space() runs for every incoming ACK and for every packet we
send, and reads SOCK_NOSPACE from sk->sk_socket->flags.
struct socket lives in its own cache line, which the TCP fast paths do
not otherwise touch, so testing sk->sk_socket->flags pulls in an extra
cache line that is cold when the working set is large.
Add tp->tcp_nospace, a mirror of SOCK_NOSPACE placed right after
tp->chrono_type in the tcp_sock_write_tx group, in a cache line both
the transmit and the ACK paths already touch (bytes_sent, data_segs_out,
delivered, bytes_acked, chrono_type). It fits in an existing 3-byte
hole before chrono_start, so no other field moves and sizeof(struct
tcp_sock) is unchanged.
The two flags are now only changed from sk_set_nospace() and
sk_clear_nospace(), which maintain this invariant:
SOCK_NOSPACE set => tp->tcp_nospace set
sk_set_nospace() sets SOCK_NOSPACE before tp->tcp_nospace (using
smp_mb__after_atomic() and smp_store_mb()), while sk_clear_nospace()
clears tp->tcp_nospace before SOCK_NOSPACE (using
smp_mb__before_atomic()). If a lockless sk_set_nospace() from
tcp_poll() races with sk_clear_nospace() and SOCK_NOSPACE ends up set,
set_bit(SOCK_NOSPACE) happened after clear_bit(SOCK_NOSPACE), so
tp->tcp_nospace = 1 happens after tp->tcp_nospace = 0 as well.
tcp_check_space() can thus test tp->tcp_nospace alone and leave the
authoritative SOCK_NOSPACE test to __tcp_check_space(). The
invariant is one directional on purpose: a stale tp->tcp_nospace only
costs an extra call to __tcp_check_space(), which is what we do
unconditionally today, while a stale SOCK_NOSPACE would cost a
missed EPOLLOUT.
Note the smp_mb() is kept. tcp_poll() sets the flag without the
socket lock, and the store-buffer pattern it forms with
tcp_check_space() needs a full barrier on both sides.
MPTCP subflows share the struct socket of their parent, hence its
SOCK_NOSPACE bit, which can not be mirrored in the subflow tcp_sock.
Pin their tp->tcp_nospace in subflow_ulp_init(), so that they always
reach __tcp_check_space() and keep the current behavior.
Microbenchmark on an AMD EPYC 7B13, 64 threads, each thread calling
tcp_check_space() in a loop over a private set of sockets. Numbers
are cycles per call above a baseline loop that performs the work the
callers already did (the cache lines tcp_write_xmit() and
tcp_clean_rtx_queue() touched) but not tcp_check_space() itself, so
they are the marginal cost of the function. Median of 11 runs.
The set size controls whether struct socket is still cached:
sockets/thread before after
1 +0.6 +0.7
256 (64 KB) +8.3 +3.9
4096 (1 MB) +22.4 +1.1
262144 (64 MB) +48.4 -0.8
Signed-off-by: Eric Dumazet <edumazet@kernel.org>
Link: https://patch.msgid.link/20260929071743.23624-10-edumazet@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Diffstat (limited to 'include')
| -rw-r--r-- | include/linux/tcp.h | 3 | ||||
| -rw-r--r-- | include/net/tcp.h | 30 |
2 files changed, 32 insertions, 1 deletions
diff --git a/include/linux/tcp.h b/include/linux/tcp.h index 6a8c77719322..e4d1720f49f0 100644 --- a/include/linux/tcp.h +++ b/include/linux/tcp.h @@ -269,6 +269,9 @@ struct tcp_sock { */ u32 snd_sml; /* Last byte of the most recently transmitted small packet */ u8 chrono_type; /* current chronograph type */ + u8 tcp_nospace; /* mirrors SOCK_NOSPACE, must be set whenever + * SOCK_NOSPACE is set. + */ u32 chrono_start; /* Start time in jiffies of a TCP chrono */ u32 chrono_stat[3]; /* Time in jiffies for chrono_stat stats */ u32 write_seq; /* Tail(+1) of data held in tcp send buffer */ diff --git a/include/net/tcp.h b/include/net/tcp.h index 74f54bfa51f1..0265c27a5fd7 100644 --- a/include/net/tcp.h +++ b/include/net/tcp.h @@ -783,14 +783,42 @@ void tcp_done_with_error(struct sock *sk, int err); void tcp_reset(struct sock *sk, struct sk_buff *skb); void tcp_fin(struct sock *sk); void __tcp_check_space(struct sock *sk); + +/* Mirror of SOCK_NOSPACE in tcp_sock, maintained by sk_set_nospace() + * and sk_clear_nospace(). + * + * MPTCP subflows share the parent socket, and thus its SOCK_NOSPACE bit. + * Keep their mirror always set (see subflow_ulp_init()) so that they + * always reach __tcp_check_space() and behave as before. + */ +static inline void tcp_set_nospace(struct sock *sk) +{ + if (sk_is_tcp(sk)) { + /* pairs with smp_mb__before_atomic() in tcp_clear_nospace() */ + smp_mb__after_atomic(); + /* pairs with smp_mb() in tcp_check_space() */ + smp_store_mb(tcp_sk(sk)->tcp_nospace, 1); + } +} + +static inline void tcp_clear_nospace(struct sock *sk) +{ + if (sk_is_tcp(sk) && !sk_is_mptcp(sk)) { + WRITE_ONCE(tcp_sk(sk)->tcp_nospace, 0); + /* pairs with smp_mb__after_atomic() in tcp_set_nospace() */ + smp_mb__before_atomic(); + } +} + static inline void tcp_check_space(struct sock *sk) { /* pairs with tcp_poll() */ smp_mb(); - if (sk->sk_socket && test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) + if (unlikely(READ_ONCE(tcp_sk(sk)->tcp_nospace))) __tcp_check_space(sk); } + void tcp_sack_compress_send_ack(struct sock *sk); static inline void tcp_cleanup_skb(struct sk_buff *skb) |
