From nobody Thu Nov 5 07:29:18 2026 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793486691; b=T6DH3JHbWunngPQO/+fxBtTqCOO+r1W7gWbqlR4/EeCmqVeJeYeccD1gVm38xvm9AbA0 VJlGUZEPs183lAz27UuZUFR1xZNa/lZaEZJDxsnVtvquVCRj8GcwcXKc6RbVYO7v2MtBL g7f4TpfdJuIG6BXS727BcY0EWhpr1+zLoif5NCX7rjfkOw3yUDA+O4rQY4pUl4LHlUNcP Xmx/xZ5UveYZHj6DLdoFcMQ6j+kMzXGB0U+GD77psnRUnlYTOCH0vBH/DodO+ZsY36zx1 DJ4ixZsSJ7RTgnZxbjHCn8dgi/l0chQFrzNqIpvG9OlHdawWuala9uUU/VA1lyNhD1Q== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793486691; h=Received:DKIM-Signature:From:Date:Subject:MIME-Version:Content-Type: Content-Transfer-Encoding:Message-Id:References:In-Reply-To:To:Cc: X-Mailer:X-Developer-Signature:X-Developer-Key; bh=eu8rB7/B9jehHVyR5w59GGdXuac08HDFYLvVcT+vCJI=; b=T1bHrNF5myrbXinOehYnnEdrTwbDKn89lmakUPZLO3/kFDIq8liHojvnjGwxaGv7wHbS aZX9b3TClA8vEi7NIvL+pYWW36zS1gwwriqDkTfFnSTuB+NcsJuKEyKcWcSHuItoaF3IY gqc6WRMctbIrcIo4BJJ2iJ5XUMSXFsFsu4IM50Tfy4iTKI/0GYuKZJ0rsMtY+vmVhmSr4 uyxfgUj3Zki5RE689D2jzoef8wL3siVoYLSgF0P7HarjKUrKimpE7xBpwHrzZMHnFpjvR BfmgHSXpu0j1hE0J2EGEQgqI6aV1I5l1tuicc6LGqCvRwjPByuFiCmhWEgcidUKLNZw== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b="VN+mV/PE"; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 68B69606A31A for ; Sat, 31 Oct 2026 22:44:51 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=VN+mV/PE Received: by smtp.kernel.org (Postfix) with ESMTPSA id 8AC371F0093A; Sat, 31 Oct 2026 22:44:50 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793486691; bh=eu8rB7/B9jehHVyR5w59GGdXuac08HDFYLvVcT+vCJI=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=VN+mV/PEhaPVn/prVTA5KKEs6o8s9zjHwQDPPfeKDQJNBgyiRi7iu+y0H/CE9wqL3 Ot3ZC/LJmDe2t56YZd6UudJWqQVOg9FeicjhKCzwfb+9v/F3T8fdo5fhoqzF0Yi8Qo AKCHnjMK61lROLLjB9f8KM/vvhlwLZBzUKlA2HZEDqkHi7GuetVPvW2IrOdSc1lx6x HXiGQdz7DTwLkaCQP8z3nOL/WgPKXEbZvdOYSVysJb1L29tGg0FABTH9WYR1aM7GXH kT+dwCLaifwJf7ui3xNTk0iDo/9tdXBwm8ApVk9g/yCJji9WQyW2vzcun3wLUlBwm5 0Clq+CnEE8erA== From: "Matthieu Baerts (NGI0)" Date: Sat, 31 Oct 2026 23:44:33 +0100 Subject: [PATCH mptcp-next v2 1/8] mptcp: do not use a raised snd_wnd for the send window X-Mailing-List: mptcp@lists.linux.dev List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20261031-mptcp-sched-opp-retrans-v2-1-e9c4a30f6841@kernel.org> References: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> In-Reply-To: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> To: MPTCP Linux Cc: Abdul Wasey , "Matthieu Baerts (NGI0)" X-Mailer: b4 0.16.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=2969; i=matttbe@kernel.org; h=from:subject:message-id; bh=aZGT/rxawpdMxpCWU75u0u+d0BPRijwb9nQ7fnPxrM8=; b=owGbwMvMwCVWo/Th0Gd3rumMp9WSGLKe5UfXPditej9hvcfZHqGkp9NcUkOkDlbJf6yt9l1Sz nnuSYRjRykLgxgXg6yYIot0W2T+zOdVvCVefhYwc1iZQIYwcHEKwEQOFjIyTP2s8FP0QbuF6lT+ N1X2V57aSGt/XrkxTtIkbe3COfVP1zP8d3SJeJnupRXlseMaz4Rv1+ZwTBF3+Rnzr2q+ieIzGTs LXgA= X-Developer-Key: i=matttbe@kernel.org; a=openpgp; fpr=E8CB85F76877057A6E27F77AF6B7824F4269A073 ack_update_msk() sets the right edge of the MPTCP send window from the DATA_ACK and tcp_sk(ssk)->snd_wnd, but this is not always what the peer announced: mptcp_check_allowed_size() raises it to window_end - data_seq, so that TCP lets through what the MPTCP-level window allows. That is fine for new data. For data sent again, data_seq can be below snd_una, and snd_wnd ends up too large by the difference. TCP only overwrites it when an ACK moves things forward on this subflow, see tcp_may_update_window(). If a DATA_ACK moved by another subflow comes in on this subflow before that, wnd_end goes past what the peer offered. It only moves forward, so it stays there. The sender then transmits data beyond the peer's window: the peer drops it (MPTCP_MIB_NODSSWINDOW) after TCP acked it, and only an MPTCP-level retransmission can recover it, which raises snd_wnd again. The connection can then stay stuck at a low rate for several seconds, with nothing in flight on any subflow. Use the window announced in the packet carrying the DATA_ACK instead. In my case with BBR, a small TCP rmem, and a second path with a much higher latency, a lower throughput and some losses, this small change can double the throughput in the worst cases. Fixes: b713d0067574 ("mptcp: really share subflow snd_wnd") Signed-off-by: Matthieu Baerts (NGI0) --- net/mptcp/options.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/net/mptcp/options.c b/net/mptcp/options.c index adbf8e0773db..b0e8b1200cfe 100644 --- a/net/mptcp/options.c +++ b/net/mptcp/options.c @@ -1121,9 +1121,11 @@ static void rwin_update(struct mptcp_sock *msk, stru= ct sock *ssk, =20 static void ack_update_msk(struct mptcp_sock *msk, struct sock *ssk, + const struct sk_buff *skb, struct mptcp_options_received *mp_opt) { u64 new_wnd_end, new_snd_una, snd_nxt =3D READ_ONCE(msk->snd_nxt); + u32 win =3D ntohs(tcp_hdr(skb)->window); struct sock *sk =3D (struct sock *)msk; u64 old_snd_una; =20 @@ -1140,7 +1142,12 @@ static void ack_update_msk(struct mptcp_sock *msk, if (unlikely(after64(new_snd_una, snd_nxt))) new_snd_una =3D old_snd_una; =20 - new_wnd_end =3D new_snd_una + tcp_sk(ssk)->snd_wnd; + /* What the peer announced: tcp_sk(ssk)->snd_wnd can be larger, see + * mptcp_check_allowed_size(), and is not always updated. + */ + if (likely(!tcp_hdr(skb)->syn)) + win <<=3D tcp_sk(ssk)->rx_opt.snd_wscale; + new_wnd_end =3D new_snd_una + win; =20 if (after64(new_wnd_end, msk->wnd_end)) WRITE_ONCE(msk->wnd_end, new_wnd_end); @@ -1310,7 +1317,7 @@ bool mptcp_incoming_options(struct sock *sk, struct s= k_buff *skb) * monodirectional flows will stuck */ if (mp_opt.use_ack) - ack_update_msk(msk, sk, &mp_opt); + ack_update_msk(msk, sk, skb, &mp_opt); rwin_update(msk, sk, skb); =20 /* Zero-data-length packets are dropped by the caller and not --=20 2.55.0 From nobody Thu Nov 5 07:29:18 2026 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793486692; b=c7KYf6DeulLh4F+WoHzLj/7khOjjHFIf3gnL9fZNXMaAreOP5yJLfcWNmu8TNeQIA7hi ioOerbbdH0nxlEEaNoUT340GZC2x5XCbST0AooGhmqWZy0FG5205i6RGDNbVSJZm3A/qp 9NnnWPz/gEqdGwT32e1kkUaXArnVpSeDq2tj+k9qA8/q8CsjFGh6DYV214+1eiam3NdDh g2S8fvAA8tiOcVEDvZYkbXMNkxWiegz+35MGFbdkN8zSIY/Zx3FQlVKI3fXj5EQtoUrAH RaVU/vuPf0Ioqj4Gmd1oUQP+eChHLFPJKQrKHGQzpZp42rPC2zihThq2DgDRDN2PGtQ== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793486692; h=Received:DKIM-Signature:From:Date:Subject:MIME-Version:Content-Type: Content-Transfer-Encoding:Message-Id:References:In-Reply-To:To:Cc: X-Mailer:X-Developer-Signature:X-Developer-Key; bh=Cu1+g4Vv0tl6byy+uUameFJYvdXNgHhqgSuGvv3DksU=; b=4tNrKaD+HVIuULnBaqg5OLaPs0c9WGC0ApW9uvXAVI9CQ4wFog7L3+dZ7DJHHoDL+Lda lI8/7wFpqCnrmOYayZsGaI8Syy/eXvZSPrqh8EVQfaLpiNrCG4rPv0hWWWHpXeZOmM145 FT793p8P4kOg19iSmfpKM78rmLcZv7kAZyNHGJRMWV280bwdlUkYQILiuMwxBin2Wuttv 6AlOS2PT90qGSwi8byiLq1GK7IXStuZBPy/nTuqoRej98PD76bDS9VRZOtnk9B6Dm0rrY ZHWLkMu4Yh6XNLHL1MO5uSUNyl+/LNXtcnWwor11KRNUPUcyAhJumbVEKiJexjVcPng== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=greBMJ8b; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 61555606A31A for ; Sat, 31 Oct 2026 22:44:52 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=greBMJ8b Received: by smtp.kernel.org (Postfix) with ESMTPSA id 829481F00919; Sat, 31 Oct 2026 22:44:51 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793486692; bh=Cu1+g4Vv0tl6byy+uUameFJYvdXNgHhqgSuGvv3DksU=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=greBMJ8b+JP0IVfAZrw2C9uAHU8h+cySo8o0P+qHO3q1TU0xLybAyo2NlZUt1kB1j M7LsofTe3cAjCs5D5N1dlv/qJWcpvMQr8cB8eBfclvqxx50515brIw+urMw5rUp5+8 U2jHLgvuJFgw1iwI/F7RN51Ggs/H599mCTmB+k5CF+H/MmHWdjp12Uw2NMciXaECcy +fWYAF0W3YsSnqeSJ0nCnGa4pkcoQnBx1Wl3HqjWuBzG3LB4KDDKqjE+w1Nb5lHQEE pbm7d281z2xLBm1gsUVB161s2fScnrvDkO2OxawcVmd8e/BXgR+9+Cut8QhaVKueMe CIBL1qULJv8XA== From: "Matthieu Baerts (NGI0)" Date: Sat, 31 Oct 2026 23:44:34 +0100 Subject: [PATCH mptcp-next v2 2/8] mptcp: sched: let get_send limit what one decision sends X-Mailing-List: mptcp@lists.linux.dev List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20261031-mptcp-sched-opp-retrans-v2-2-e9c4a30f6841@kernel.org> References: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> In-Reply-To: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> To: MPTCP Linux Cc: Abdul Wasey , "Matthieu Baerts (NGI0)" , Gregory Detal X-Mailer: b4 0.16.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=4776; i=matttbe@kernel.org; h=from:subject:message-id; bh=xRwb9fex/kT1ZTdKpzmIDilnCWsXA1mgxwtCsflOCSM=; b=owGbwMvMwCVWo/Th0Gd3rumMp9WSGLKe5cd8iVdxeK9+eU5y0BKvJMWrOoe1ojxjwyZZ5BrOe puw/JJkRykLgxgXg6yYIot0W2T+zOdVvCVefhYwc1iZQIYwcHEKwETMpzD8d3jF0KLh/SWFJ+i3 puqHFb8djt4PfnT+in37A/YwhsiwWYwMkxUf7Hp/2lRLcOa0U2VTLk5RieuZaavN/H2ZDyN7nGQ kPwA= X-Developer-Key: i=matttbe@kernel.org; a=openpgp; fpr=E8CB85F76877057A6E27F77AF6B7824F4269A073 From: Gregory Detal A scheduler picks the subflows for the pending data in get_send, then __subflow_push_pending() sends on each of them until msk->snd_burst is used up, the subflow has no room, or it is not active. snd_burst is only checked once a whole dfrag is sent, and one dfrag can hold up to a page fragment, 32 KB, so a scheduler cannot ask for less than that. Sending one MSS per subflow in turn, for example, is not possible. Add send_limit to the subflow context, which a scheduler can set in get_send: at most that many bytes are sent on the subflow for this decision, then the scheduler is asked again. 0, the default, keeps the current behaviour. The limit applies to this decision only: __subflow_push_pending() reads and clears it, and mptcp_sched_get_send() clears the limits before asking a scheduler: a retransmission can leave one behind, as it calls get_send when the scheduler has no get_retrans, without using the limit. Nothing sets it yet: a later patch lets a BPF scheduler write it. When the limit is reached inside a dfrag, the dfrag stays the send head, partly sent, as it already does when the subflow runs out of space in the middle of it. first_pending then moves on only once the dfrag is fully sent, as before. This is based on Gregory Detal's RFC called "mptcp: update scheduler API" [1] but applying Mat Martineau's suggestion. Link: https://lore.kernel.org/mptcp/20240527-sched_per_packet-v1-0-09a41d40= 5f7c@gmail.com/ [1] Signed-off-by: Gregory Detal Co-developed-by: Abdul Wasey Signed-off-by: Abdul Wasey Co-developed-by: Matthieu Baerts (NGI0) Signed-off-by: Matthieu Baerts (NGI0) --- net/mptcp/protocol.c | 22 ++++++++++++++++++++-- net/mptcp/protocol.h | 4 ++++ net/mptcp/sched.c | 4 ++++ 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c index 8f3c6ebd49ee..d960ab9c76b3 100644 --- a/net/mptcp/protocol.c +++ b/net/mptcp/protocol.c @@ -1823,17 +1823,25 @@ void mptcp_check_and_set_pending(struct sock *sk) static int __subflow_push_pending(struct sock *sk, struct sock *ssk, struct mptcp_sendmsg_info *info) { + struct mptcp_subflow_context *subflow =3D mptcp_subflow_ctx(ssk); struct mptcp_sock *msk =3D mptcp_sk(sk); struct mptcp_data_frag *dfrag; int len, copied =3D 0, err =3D 0; + u32 limit; =20 - if (mptcp_subflow_ctx(ssk)->penalise) + if (subflow->penalise) mptcp_penalise_cwnd(ssk); =20 + /* the scheduler's limit for this decision only */ + limit =3D subflow->send_limit; + subflow->send_limit =3D 0; + while ((dfrag =3D mptcp_send_head(sk))) { info->sent =3D dfrag->already_sent; info->limit =3D dfrag->data_len; - len =3D dfrag->data_len - dfrag->already_sent; + if (limit && dfrag->data_len - info->sent > limit - copied) + info->limit =3D info->sent + limit - copied; + len =3D info->limit - info->sent; while (len > 0) { int ret =3D 0; =20 @@ -1849,6 +1857,16 @@ static int __subflow_push_pending(struct sock *sk, s= truct sock *ssk, =20 mptcp_update_post_push(msk, dfrag, ret); } + + /* Stopped inside the dfrag: it stays the send head, as when the + * subflow runs out of space, and the scheduler is asked again. + */ + if (limit && copied >=3D limit) { + if (dfrag->already_sent =3D=3D dfrag->data_len) + msk->first_pending =3D mptcp_send_next(sk); + err =3D copied; + goto out; + } msk->first_pending =3D mptcp_send_next(sk); =20 if (msk->snd_burst <=3D 0 || diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h index 1d41ab1da2e0..4e8920747682 100644 --- a/net/mptcp/protocol.h +++ b/net/mptcp/protocol.h @@ -606,6 +606,10 @@ struct mptcp_subflow_context { bool data_avail; bool scheduled; bool penalise; /* scheduler flagged this subflow for cwnd halving */ + u32 send_limit; /* set by the scheduler in get_send: at + * most this many bytes before it is + * asked again; 0 for no limit + */ bool pm_listener; /* a listener managed by the kernel PM? */ bool fully_established; /* path validated */ bool close_event_done; /* has done the post-closed part, diff --git a/net/mptcp/sched.c b/net/mptcp/sched.c index 1e59072d478c..c8d193cb0e4c 100644 --- a/net/mptcp/sched.c +++ b/net/mptcp/sched.c @@ -189,6 +189,10 @@ int mptcp_sched_get_send(struct mptcp_sock *msk) =20 if (msk->sched =3D=3D &mptcp_sched_default || !msk->sched) return mptcp_sched_default_get_send(msk); + + /* limits are per decision: a retransmission can leave some behind */ + mptcp_for_each_subflow(msk, subflow) + subflow->send_limit =3D 0; return msk->sched->get_send(msk); } =20 --=20 2.55.0 From nobody Thu Nov 5 07:29:18 2026 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793486694; b=pkxUIlMOmZebXq2nI7nRcEMIV89e595c/mWnxwwpwtSEyvVM9bIfe45Itsj9jnW0/HBM h4sbvDK0RUcBKA2xDBNg1fPoDCRL5E8wm3+nRQBWOe2wBsqCnYkp64FU6bnjoIiSSnw4d sBoeGmnmFAINsEoRgt4QuDPNPfgUErWL/zaeE3YYtscUmxtk6MPqf1eeyEUhiCL2qlEmn ZddtVgKczv6jXKEPQEK5IV9x/5YuMvxYVBYyD0RNMe4Q1cTIAj9YZS2Dn5+/Uh29g50ob pAXKa4eCfsE/kVc+t+4J1WEw6pS98T/l/VoLSqrbED4X+jR9eoom19RMsrdw4QGxlRg== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793486694; h=Received:DKIM-Signature:From:Date:Subject:MIME-Version:Content-Type: Content-Transfer-Encoding:Message-Id:References:In-Reply-To:To:Cc: X-Mailer:X-Developer-Signature:X-Developer-Key; bh=N2P3sdTNYDif5zE6Qzd/988hWvobLBWE6gLCCKI9fAw=; b=E8lVPz7SpfHbfYEv0hZ1qHAwXrXeKsRtnf+EPDQN/Q1iSUf71c9hcDzZiHPFdLg6alu3 wqO95GVjhKDlQr6NOShzgfHMkSmOeANpFRpz166AJzSEkNoG1gRxjX+mCJsHNuJnbBMEc Ovrgy+mc0FfB2T4BCXwhQwixUXluMyXakPZvKp3isXpJgX2ZMV08iUcDGeDITnmu82NMY FFA+D+Fjy0IRk3iWwpYbJ058kjlxqchJPiVPMb3S+K0dlu52/aCMhfiZieVdw6x48jUrx nw2jQRc2cvH9mvFmu02ETjJdbqnORQF9xHFgSWMfanrRb3TK9HDa3+S6jIXsrnPLLwA== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=HhqoU7Kw; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7FDB0606A31A for ; Sat, 31 Oct 2026 22:44:53 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=HhqoU7Kw Received: by smtp.kernel.org (Postfix) with ESMTPSA id 9F3E61F000C1; Sat, 31 Oct 2026 22:44:52 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793486693; bh=N2P3sdTNYDif5zE6Qzd/988hWvobLBWE6gLCCKI9fAw=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=HhqoU7KwOad2PcgO2uiJZebnjTEBJ8YV997f/gMVFuB+lDabv60hBAVxW2GLo/elx DXcBK9mmPqHsVjS46Xr7m5n4iDAXwexsbSzh1bc9YaY6BQ41bA+QInseEooWLESK5X v+YAe1Qvs3FFHowTwPAeMvY5RNZyIcyTQCdt86pCHN8NsZ6fGwroiI5/Jc9VGbS/mx FvfV+nn54QLdTaXbeLah+oNKxfnx7vi4fouWF0cvEU17O8WaSLKJ6af64EOGV+NaxL yKa25cFl2yFZwpl/FF296KTxKxK70YhTaA+cMV/ut8BurM93pq4s9vGtJjNDdzP8bd A+8WnE4IdvHpQ== From: "Matthieu Baerts (NGI0)" Date: Sat, 31 Oct 2026 23:44:35 +0100 Subject: [PATCH mptcp-next v2 3/8] mptcp: sched: let get_retrans reinject on an idle subflow X-Mailing-List: mptcp@lists.linux.dev List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20261031-mptcp-sched-opp-retrans-v2-3-e9c4a30f6841@kernel.org> References: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> In-Reply-To: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> To: MPTCP Linux Cc: Abdul Wasey , "Matthieu Baerts (NGI0)" X-Mailer: b4 0.16.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=18754; i=matttbe@kernel.org; h=from:subject:message-id; bh=C9LANHss9ce0aTBbd5VFxz0D+ujit+ktH6azHMaAu0g=; b=kA0DAAoWfCLwwvNHCpcByyZiAGrmb13IZd+id8h8YQWMWCuLuxMqcppnOw3tQhTe3L7EkBKRZ 4h1BAAWCgAdFiEEG4ZZb5nneg10Sk44fCLwwvNHCpcFAmrmb10ACgkQfCLwwvNHCpc1RQD/TErA fzplFYQQjoR2pQqJlS3xP6AgZYPBeP2ofnL1gjYBAKAEQdHM0C/C7s3s2S/1NWhAWGP5LU8s4j+ wrccG1HwM X-Developer-Key: i=matttbe@kernel.org; a=openpgp; fpr=E8CB85F76877057A6E27F77AF6B7824F4269A073 From: Abdul Wasey With subflows of different speeds, the data sent on a slow subflow holds back the DATA_ACK while the fast one keeps delivering what comes after it. Once the receive window or the send buffer is used up, nothing new can be sent: the fast subflow drains and stays idle until the slow one delivers, or until the MPTCP-level RTO fires. The same happens when the application has nothing more to write for now: the end of what it wrote waits on the slow subflow. The old out-of-tree kernel resent that data on the idle subflow ("opportunistic retransmission", Raiciu et al., NSDI 2012), upstream has nothing for it [1]. Do it as a scheduler action, through get_retrans, so that there is still one call per decision: get_retrans is now also called when an active non-backup subflow has nothing queued or in flight while nothing new can be sent, and msk->sched_retrans says why it is called: - MPTCP_SCHED_RETRANS_RTO: the MPTCP-level timer fired, as before; - MPTCP_SCHED_RETRANS_IDLE_WND: the peer's receive window is nearly full; - MPTCP_SCHED_RETRANS_IDLE_SNDBUF: the application waits for send buffer space; - MPTCP_SCHED_RETRANS_IDLE_APP: the application has nothing more to send for now. On the IDLE reasons, the scheduler marks a subflow and sets the new reinject_limit there. The core then resends on it, up to that many bytes and one cwnd, the DSN ranges that the other subflows still hold, taken from the DSS mappings of the skbs in their queues: a BPF program cannot walk them. Most of the data after snd_una already reached the peer, so this is less than resending everything from snd_una. A limit of 0, the default, sends nothing. This is done at most once per snd_una value. This is not done with the MPTCP checksum: a re-injection must then spool a complete DSS, see commit 4e14867d5e91 ("mptcp: tune re-injections for csum enabled mode"). This runs in the worker, under the msk socket lock, as the MPTCP-level retransmission does. If no subflow is idle there, or if a send is pending, nothing is decided for this snd_una. Otherwise, the answer of the scheduler is final for this snd_una. Nothing wakes the worker yet: that is the next patch. Only a scheduler with its own get_retrans is asked: the default one does not reinject yet, see a later patch. A BPF scheduler cannot set reinject_limit yet either: a later patch allows it. One that does not want to reinject can ignore the IDLE reasons. Link: https://github.com/multipath-tcp/mptcp_net-next/issues/332 [1] Signed-off-by: Abdul Wasey Co-developed-by: Matthieu Baerts (NGI0) Signed-off-by: Matthieu Baerts (NGI0) --- net/mptcp/mib.c | 2 + net/mptcp/mib.h | 2 + net/mptcp/protocol.c | 89 +++++++++++++++++++ net/mptcp/protocol.h | 44 +++++++++ net/mptcp/sched.c | 245 +++++++++++++++++++++++++++++++++++++++++++++++= ++++ 5 files changed, 382 insertions(+) diff --git a/net/mptcp/mib.c b/net/mptcp/mib.c index b39b5511345d..7fe0fd71cb43 100644 --- a/net/mptcp/mib.c +++ b/net/mptcp/mib.c @@ -97,6 +97,8 @@ static const struct snmp_mib mptcp_snmp_list[] =3D { SNMP_MIB_ITEM("OFOPruned", MPTCP_MIB_OFOPRUNED), SNMP_MIB_ITEM("CwndPenalised", MPTCP_MIB_CWNDPENALISED), SNMP_MIB_ITEM("PenalCandidate", MPTCP_MIB_PENALCAND), + SNMP_MIB_ITEM("OppReinject", MPTCP_MIB_OPPREINJ), + SNMP_MIB_ITEM("OppReinjectBytes", MPTCP_MIB_OPPREINJBYTES), }; =20 /* mptcp_mib_alloc - allocate percpu mib counters diff --git a/net/mptcp/mib.h b/net/mptcp/mib.h index a7b5bf684af7..615666050f51 100644 --- a/net/mptcp/mib.h +++ b/net/mptcp/mib.h @@ -100,6 +100,8 @@ enum linux_mptcp_mib_field { MPTCP_MIB_OFOPRUNED, /* MPTCP-level OoO queue pruned */ MPTCP_MIB_CWNDPENALISED, /* subflow cwnd halved by the scheduler */ MPTCP_MIB_PENALCAND, /* scheduler picked a slow (low-rate) subflow */ + MPTCP_MIB_OPPREINJ, /* Opportunistic reinjections on an idle subflow */ + MPTCP_MIB_OPPREINJBYTES, /* Bytes sent by opportunistic reinjections */ __MPTCP_MIB_MAX }; =20 diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c index d960ab9c76b3..7117786e5251 100644 --- a/net/mptcp/protocol.c +++ b/net/mptcp/protocol.c @@ -3057,6 +3057,92 @@ static int __mptcp_push_retrans(struct sock *sk, str= uct mptcp_data_frag *dfrag, return len; } =20 +/* resend [start, end) on @ssk from the msk rtx queue; < 0 on a send error= */ +static int mptcp_oppr_send_range(struct sock *sk, struct sock *ssk, u64 st= art, + u64 end, int budget, + struct mptcp_sendmsg_info *info) +{ + struct mptcp_data_frag *dfrag; + int copied =3D 0, ret; + + list_for_each_entry(dfrag, &mptcp_sk(sk)->rtx_queue, list) { + if (!dfrag->already_sent || !before64(dfrag->data_seq, end)) + break; + if (!after64(dfrag->data_seq + dfrag->already_sent, start)) + continue; + + info->sent =3D before64(dfrag->data_seq, start) ? + start - dfrag->data_seq : 0; + info->limit =3D min_t(u64, dfrag->already_sent, + end - dfrag->data_seq); + info->limit =3D min_t(u64, info->limit, + info->sent + budget - copied); + while (info->sent < info->limit && copied < budget) { + ret =3D mptcp_sendmsg_frag(sk, ssk, dfrag, info); + if (ret <=3D 0) + return copied ? : -1; + copied +=3D ret; + info->sent +=3D ret; + } + if (copied >=3D budget) + break; + } + return copied; +} + +/* Opportunistic reinjection: the scheduling part is in sched.c */ +static void __mptcp_oppr(struct sock *sk) +{ + struct mptcp_sendmsg_info info =3D { .data_lock_held =3D true, }; + struct mptcp_sock *msk =3D mptcp_sk(sk); + int copied =3D 0, budget, ret, i; + struct mptcp_sched_oppr oppr; + struct sock *ssk; + + mptcp_data_lock(sk); + __mptcp_clean_una_wakeup(sk); + oppr.una =3D msk->snd_una; + mptcp_data_unlock(sk); + + if (!mptcp_sched_oppr_get(msk, &oppr)) + return; + + ssk =3D oppr.ssk; + lock_sock(ssk); + /* the scheduler's limit, and no more than one cwnd */ + budget =3D min_t(u64, oppr.limit, + tcp_snd_cwnd(tcp_sk(ssk)) * tcp_sk(ssk)->mss_cache); + + spin_lock_bh(&msk->fallback_lock); + if (__mptcp_check_fallback(msk) || !msk->allow_subflows) + goto unlock; + + for (i =3D 0; i < oppr.n && copied < budget; i++) { + ret =3D mptcp_oppr_send_range(sk, ssk, oppr.r[i].start, + oppr.r[i].end, budget - copied, + &info); + if (ret < 0) + break; + copied +=3D ret; + } + + if (copied) { + /* once per snd_una value; after a failed send, the next + * DATA_ACK tries again + */ + WRITE_ONCE(msk->oppr_una, oppr.una); + tcp_push(ssk, 0, info.mss_now, tcp_sk(ssk)->nonagle, + info.size_goal); + msk->allow_infinite_fallback =3D false; + msk->bytes_retrans +=3D copied; + MPTCP_INC_STATS(sock_net(sk), MPTCP_MIB_OPPREINJ); + MPTCP_ADD_STATS(sock_net(sk), MPTCP_MIB_OPPREINJBYTES, copied); + } +unlock: + spin_unlock_bh(&msk->fallback_lock); + release_sock(ssk); +} + static void __mptcp_retrans(struct sock *sk) { struct mptcp_sock *msk =3D mptcp_sk(sk); @@ -3295,6 +3381,9 @@ static void mptcp_worker(struct work_struct *work) if (test_and_clear_bit(MPTCP_WORK_RTX, &msk->flags)) __mptcp_retrans(sk); =20 + if (test_and_clear_bit(MPTCP_WORK_OPPR, &msk->flags)) + __mptcp_oppr(sk); + fail_tout =3D msk->first ? READ_ONCE(mptcp_subflow_ctx(msk->first)->fail_= tout) : 0; if (fail_tout && time_after(jiffies, fail_tout)) mptcp_mp_fail_no_response(msk); diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h index 4e8920747682..39dd56d882ce 100644 --- a/net/mptcp/protocol.h +++ b/net/mptcp/protocol.h @@ -117,6 +117,7 @@ #define MPTCP_FALLBACK_DONE 2 #define MPTCP_WORK_CLOSE_SUBFLOW 3 #define MPTCP_RTX_ENABLED 4 +#define MPTCP_WORK_OPPR 5 =20 /* MPTCP socket release cb flags */ #define MPTCP_PUSH_PENDING 1 @@ -294,6 +295,40 @@ struct mptcp_data_frag { */ #define MPTCP_RTT_SAMPLES 5 =20 +/* Why get_retrans is called, in msk->sched_retrans */ +enum mptcp_sched_retrans { + /* the MPTCP-level retransmission timer fired */ + MPTCP_SCHED_RETRANS_RTO, + /* A subflow is idle, nothing new can be sent, and other subflows + * hold data the peer is waiting for. The core resends that data on + * the subflow the scheduler marks, up to the reinject_limit bytes it + * sets there: 0, the default, sends nothing. Nothing new can be sent + * because: + */ + /* the peer's receive window is nearly full */ + MPTCP_SCHED_RETRANS_IDLE_WND, + /* the application waits for send buffer space */ + MPTCP_SCHED_RETRANS_IDLE_SNDBUF, + /* the application has nothing more to send for now */ + MPTCP_SCHED_RETRANS_IDLE_APP, +}; + +#define MPTCP_OPPR_MAX_RANGES 32 + +struct mptcp_oppr_range { + u64 start; + u64 end; +}; + +/* What __mptcp_oppr() resends: see mptcp_sched_oppr_get() */ +struct mptcp_sched_oppr { + struct sock *ssk; + u32 limit; + u64 una; + int n; + struct mptcp_oppr_range r[MPTCP_OPPR_MAX_RANGES]; +}; + /* MPTCP connection sock */ struct mptcp_sock { /* inet_connection_sock must be the first member */ @@ -310,6 +345,10 @@ struct mptcp_sock { atomic64_t rcv_wnd_sent; u64 rcv_data_fin_seq; u64 bytes_retrans; + u64 oppr_una; /* snd_una of the last opportunistic + * reinjection, or of its refusal + */ + u8 sched_retrans; /* enum mptcp_sched_retrans */ u64 bytes_consumed; int snd_burst; int old_wspace; @@ -610,6 +649,9 @@ struct mptcp_subflow_context { * most this many bytes before it is * asked again; 0 for no limit */ + u32 reinject_limit; /* set by the scheduler, on an + * MPTCP_SCHED_RETRANS_IDLE_* reason + */ bool pm_listener; /* a listener managed by the kernel PM? */ bool fully_established; /* path validated */ bool close_event_done; /* has done the post-closed part, @@ -888,6 +930,8 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *= msk); struct sock *mptcp_subflow_get_retrans(struct mptcp_sock *msk); int mptcp_sched_get_send(struct mptcp_sock *msk); int mptcp_sched_get_retrans(struct mptcp_sock *msk); +bool mptcp_sched_oppr_get(struct mptcp_sock *msk, + struct mptcp_sched_oppr *oppr); =20 static inline u64 mptcp_data_avail(const struct mptcp_sock *msk) { diff --git a/net/mptcp/sched.c b/net/mptcp/sched.c index c8d193cb0e4c..da5fa8c0fb12 100644 --- a/net/mptcp/sched.c +++ b/net/mptcp/sched.c @@ -28,6 +28,31 @@ static int mptcp_sched_default_get_send(struct mptcp_soc= k *msk) return 0; } =20 +/* the active subflow with nothing to send or retransmit and the lowest RT= T */ +static struct sock *mptcp_sched_oppr_idle_ssk(struct mptcp_sock *msk) +{ + struct mptcp_subflow_context *subflow; + struct sock *pick =3D NULL; + u32 best =3D U32_MAX; + + mptcp_for_each_subflow(msk, subflow) { + struct sock *ssk =3D mptcp_subflow_tcp_sock(subflow); + u32 rtt =3D READ_ONCE(tcp_sk(ssk)->srtt_us); + + if (!__mptcp_subflow_active(subflow) || subflow->backup || + subflow->request_bkup || subflow->stale || !rtt) + continue; + /* racy on a DATA_ACK, only a hint: the worker checks again */ + if (!data_race(tcp_rtx_and_write_queues_empty(ssk))) + continue; + if (rtt < best) { + best =3D rtt; + pick =3D ssk; + } + } + return pick; +} + static int mptcp_sched_default_get_retrans(struct mptcp_sock *msk) { struct sock *ssk; @@ -217,3 +242,223 @@ int mptcp_sched_get_retrans(struct mptcp_sock *msk) return msk->sched->get_retrans(msk); return msk->sched->get_send(msk); } + +/* Opportunistic reinjection, "opportunistic retransmission" in section 4.2 + * of Raiciu et al., NSDI 2012. + * + * With paths of different speeds, data sent on a slow or lossy path holds + * back the DATA_ACK while the fast path keeps delivering what comes after + * it. Once the receive window or the send buffer is used up, nothing new + * can be sent: the fast path drains and stays idle until the slow path + * delivers, or until the MPTCP-level RTO fires. The same happens when the + * application has nothing more to send for now: the end of what it wrote + * waits on the slow path, and so does the application if it expects an + * answer. + * + * That state can be seen when a DATA_ACK comes in: no new data can go out, + * and an active subflow has nothing queued or in flight. The data at + * snd_una is then held by another subflow, so resend on the idle subflow + * the ranges that the other subflows still hold, taken from the DSS + * mappings in their queues, instead of everything from snd_una: most of + * that already reached the peer. + * + * The scheduler decides on which subflow, and how much, through get_retra= ns + * with the reason in msk->sched_retrans. This is done at most once per + * snd_una value. + */ +#define MPTCP_OPPR_MIN (16 * 1024) + +/* can the scheduler be asked to reinject: see mptcp_sched_get_reinject() = */ +static bool mptcp_sched_has_reinject(const struct mptcp_sock *msk) +{ + const struct mptcp_sched_ops *sched =3D READ_ONCE(msk->sched); + + return sched && sched !=3D &mptcp_sched_default && sched->get_retrans; +} + +/* Ask the scheduler where to resend the data other subflows hold, because= a + * subflow is idle while nothing new can be sent, for @reason: see + * MPTCP_SCHED_RETRANS_IDLE_WND. Only a scheduler with its own get_retrans= is + * asked; the default one does not reinject. + */ +static int mptcp_sched_get_reinject(struct mptcp_sock *msk, u8 reason) +{ + struct mptcp_subflow_context *subflow; + int err; + + msk_owned_by_me(msk); + + if (__mptcp_check_fallback(msk)) + return -EINVAL; + + mptcp_for_each_subflow(msk, subflow) { + if (READ_ONCE(subflow->scheduled)) + return -EBUSY; + subflow->reinject_limit =3D 0; + } + + if (!mptcp_sched_has_reinject(msk)) + return -EOPNOTSUPP; + + msk->sched_retrans =3D reason; + err =3D msk->sched->get_retrans(msk); + msk->sched_retrans =3D MPTCP_SCHED_RETRANS_RTO; + return err; +} + +/* Why nothing new can go out while the peer waits for data: an + * MPTCP_SCHED_RETRANS_IDLE_* reason, or 0 when new data can still go out + * (the scheduler will then use the idle subflow) or nothing is waiting. + * Read without the msk socket lock when called from a DATA_ACK. + */ +static u8 mptcp_sched_oppr_reason(struct sock *sk) +{ + struct mptcp_sock *msk =3D mptcp_sk(sk); + u64 snd_una =3D READ_ONCE(msk->snd_una); + struct socket *sock; + + /* with csum, a resend must spool a complete DSS */ + if (snd_una =3D=3D READ_ONCE(msk->snd_nxt) || + snd_una =3D=3D READ_ONCE(msk->oppr_una) || + READ_ONCE(msk->csum_enabled) || __mptcp_check_fallback(msk)) + return 0; + + /* only the DATA_FIN is waiting */ + if (snd_una + 1 =3D=3D READ_ONCE(msk->snd_nxt) && + mptcp_data_fin_enabled(msk)) + return 0; + + if (mptcp_send_head(sk)) { + if (mptcp_wnd_end(msk) - READ_ONCE(msk->snd_nxt) >=3D MPTCP_OPPR_MIN) + return 0; + return MPTCP_SCHED_RETRANS_IDLE_WND; + } + + /* everything written so far was handed to a subflow */ + sock =3D READ_ONCE(sk->sk_socket); + if (sock && test_bit(SOCK_NOSPACE, &sock->flags)) + return MPTCP_SCHED_RETRANS_IDLE_SNDBUF; + return MPTCP_SCHED_RETRANS_IDLE_APP; +} + +/* Add what @skb holds to @r, sorted by start. When @r is full, keep the l= owest + * ones: the data at snd_una is the one the peer is waiting for. + */ +static int mptcp_sched_oppr_add_skb(const struct sk_buff *skb, u64 una, + u64 *last, struct mptcp_oppr_range *r, + int n) +{ + const struct mptcp_ext *mpext =3D mptcp_get_ext(skb); + u64 start, end; + int i; + + if (!mpext || !mpext->use_map || !mpext->data_len || + mpext->data_seq =3D=3D *last) + return n; + + /* one mapping can span several skbs after a TCP split */ + *last =3D mpext->data_seq; + start =3D mpext->data_seq; + end =3D start + mpext->data_len; + if (!after64(end, una)) + return n; + if (before64(start, una)) + start =3D una; + + if (n =3D=3D MPTCP_OPPR_MAX_RANGES) { + if (!before64(start, r[n - 1].start)) + return n; + n--; + } + + for (i =3D n; i > 0 && after64(r[i - 1].start, start); i--) + r[i] =3D r[i - 1]; + r[i].start =3D start; + r[i].end =3D end; + return n + 1; +} + +/* The DSN ranges at or after @una that the subflows other than @idle still + * hold, sent or not: the data the peer is waiting for. Sorted by start, a= nd + * merged: data resent before can sit on more than one subflow. + */ +static int mptcp_sched_oppr_collect(struct mptcp_sock *msk, struct sock *i= dle, + u64 una, struct mptcp_oppr_range *r) +{ + struct mptcp_subflow_context *subflow; + int n =3D 0, i, k; + + mptcp_for_each_subflow(msk, subflow) { + struct sock *ssk =3D mptcp_subflow_tcp_sock(subflow); + struct sk_buff *skb; + u64 last =3D 0; + + if (ssk =3D=3D idle) + continue; + + lock_sock(ssk); + skb_rbtree_walk(skb, &ssk->tcp_rtx_queue) + n =3D mptcp_sched_oppr_add_skb(skb, una, &last, r, n); + skb_queue_walk(&ssk->sk_write_queue, skb) + n =3D mptcp_sched_oppr_add_skb(skb, una, &last, r, n); + release_sock(ssk); + } + + for (i =3D 1, k =3D 0; i < n; i++) { + if (!after64(r[i].start, r[k].end)) { + if (after64(r[i].end, r[k].end)) + r[k].end =3D r[i].end; + } else { + r[++k] =3D r[i]; + } + } + return n ? k + 1 : 0; +} + +/* What to resend from @oppr->una for an opportunistic reinjection, if any= thing: + * ask the scheduler, then take the ranges from the subflows. Called from = the + * worker. + */ +bool mptcp_sched_oppr_get(struct mptcp_sock *msk, + struct mptcp_sched_oppr *oppr) +{ + struct mptcp_subflow_context *subflow; + struct sock *sk =3D (struct sock *)msk; + u8 reason; + int err; + + /* no subflow to use yet: the next DATA_ACK checks again */ + reason =3D mptcp_sched_oppr_reason(sk); + if (!reason || !mptcp_sched_oppr_idle_ssk(msk)) + return false; + + /* same with a send pending: new data is about to go out */ + err =3D mptcp_sched_get_reinject(msk, reason); + if (err =3D=3D -EBUSY) + return false; + + oppr->ssk =3D NULL; + oppr->limit =3D 0; + oppr->n =3D 0; + mptcp_for_each_subflow(msk, subflow) { + if (!READ_ONCE(subflow->scheduled)) + continue; + if (!err && !oppr->ssk && subflow->reinject_limit) { + oppr->ssk =3D mptcp_subflow_tcp_sock(subflow); + oppr->limit =3D subflow->reinject_limit; + } + mptcp_subflow_set_scheduled(subflow, false); + } + + /* the scheduler declined, or nothing to resend: not again for this + * snd_una + */ + if (oppr->ssk) + oppr->n =3D mptcp_sched_oppr_collect(msk, oppr->ssk, oppr->una, + oppr->r); + if (!oppr->n) { + WRITE_ONCE(msk->oppr_una, oppr->una); + return false; + } + return true; +} --=20 2.55.0 From nobody Thu Nov 5 07:29:18 2026 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793486694; b=DSi6Po9dtSZ0ijy4lQPyEKqPZK0xKyL9De1a/i/28pvT6lk654Ny64xxXlwoTaLaQ5L3 Zik6Vj4VT6FcUg+AHVhr8ENMULlRQko6uJTpzQnvnL7GgbCsSZFXUxC0jkeHimfVH6z+w 7y331n2lrUg+ATecSyPA4YEQPpOklTD+uRVPLp03FZbPPFymJjuaWlM2F7Ra6T+sZPtMq a+8BMFdPuWjgvbzayEK0lvIMerZxBJURC4PDmtwKjHRqU+vwNot69T5DNBLNT386kRwcI kRsg4LPBaCInfKp5zJPIxhaXiHLcNYds4W+kVyO/kEhER+rFLekQUVbU4HCNMLXNoMQ== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793486694; h=Received:DKIM-Signature:From:Date:Subject:MIME-Version:Content-Type: Content-Transfer-Encoding:Message-Id:References:In-Reply-To:To:Cc: X-Mailer:X-Developer-Signature:X-Developer-Key; bh=gkmLSQSFtv4tVo++I83+bIAIRTxcu5sERX3HH9RosCY=; b=YIE027s0HVcANeduFKNypGgbKJxkK+sfadbjXQ4x3P6l3BL0a572ouKcfEya4vLWPaim Kwo0A3Ufu/CT4el/ATU2KHTt5KbZgzFeoyc+8bstZfFUlw05ZI/Af5zKre0mzsuYycMp9 u7JksgALVfXPXVYOc10B/HrNoCvrmvsCxF+iimAH2XuhrG4xHvA0lnSjMYcooBaZQdaD6 hUTtlXbbkNiKFxmMj9SvhxevJ3BZPFoqBbWUcpmFteoUSvlDiu9Qn3H9s0TfLzGcQItc/ RfQxirtIOmrYTLQCgI5vMBDTMl9LdJj8+i8Ca/lvn5cRZpAnedmCrbZzjkSG436lvNQ== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=RPboL+m8; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 7D423606A31A for ; Sat, 31 Oct 2026 22:44:54 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=RPboL+m8 Received: by smtp.kernel.org (Postfix) with ESMTPSA id BDD381F00919; Sat, 31 Oct 2026 22:44:53 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793486694; bh=gkmLSQSFtv4tVo++I83+bIAIRTxcu5sERX3HH9RosCY=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=RPboL+m86KRM3XUT9I1OvhF+3Eioi5dKi6fN6fNwCztIBsdyTVP5+Lw3+K+0EEtm+ 48B5eikiuScIoOw0dn5+DINiEp9PJvBfL3FHrOSWewuSmbPeetkRyaPtZxEWv9WwXA Wk1YK9bYLjxV+6+llGC+Bd11z/ptytc92A6YfVhK3gmSsWi3nXp3jg1kr1O46ABpvr iMoBWeH+lDHYCnH1lZLDiU9uywZjjOlT4GHtnqhOKqkVNLWfmYKZ9xdGwr6K0miG+A iZ+dL3u7AHcwhPKwJC9oFZF+ojZl0pT4t3uhUWQ/tsWmn6lhxSgqmqdDEohjOi1NR/ JTEQ3R+9mLZzg== From: "Matthieu Baerts (NGI0)" Date: Sat, 31 Oct 2026 23:44:36 +0100 Subject: [PATCH mptcp-next v2 4/8] mptcp: sched: look for an idle subflow on each DATA_ACK X-Mailing-List: mptcp@lists.linux.dev List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20261031-mptcp-sched-opp-retrans-v2-4-e9c4a30f6841@kernel.org> References: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> In-Reply-To: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> To: MPTCP Linux Cc: Abdul Wasey , "Matthieu Baerts (NGI0)" X-Mailer: b4 0.16.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=4611; i=matttbe@kernel.org; h=from:subject:message-id; bh=OsCOHoeDtDtpQmXvJ5g4TdLqv99XsnoE0atEHOeJLV4=; b=owGbwMvMwCVWo/Th0Gd3rumMp9WSGLKe5ce5rVgm1DPpdKVt54u5C1fphaQ17z2REGaimXnjg D6r4eKKjlIWBjEuBlkxRRbptsj8mc+reEu8/Cxg5rAygQxh4OIUgImolDD8U4p89jJ3QtielvdW PWUHeu59V2AKnTf1eoX/Rc3ypnf3djAyTJI7W3JkzYb4pFyuyKeb62tqZqndTdfbfXhyYIzqJP1 ZzAA= X-Developer-Key: i=matttbe@kernel.org; a=openpgp; fpr=E8CB85F76877057A6E27F77AF6B7824F4269A073 From: Abdul Wasey The reinjection added by the previous patch runs in the worker, which takes the msk socket lock and delays the receive path. Only wake it when it can do something: when a DATA_ACK leaves nothing new to send, with a subflow idle. Checking the subflows on a DATA_ACK needs a stable subflow list, so only while no process owns the msk. Otherwise, defer the check to the release of the msk socket lock, as MPTCP_CLEAN_UNA does. Waking the worker instead would do it for most DATA_ACKs, even with only one subflow, and often before a subflow is idle. Signed-off-by: Abdul Wasey Co-developed-by: Matthieu Baerts (NGI0) Signed-off-by: Matthieu Baerts (NGI0) --- net/mptcp/options.c | 1 + net/mptcp/protocol.c | 11 +++++++++++ net/mptcp/protocol.h | 3 +++ net/mptcp/sched.c | 17 +++++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/net/mptcp/options.c b/net/mptcp/options.c index b0e8b1200cfe..626a1e2fc4de 100644 --- a/net/mptcp/options.c +++ b/net/mptcp/options.c @@ -1161,6 +1161,7 @@ static void ack_update_msk(struct mptcp_sock *msk, __mptcp_data_acked(sk); } msk->last_ack_recv =3D tcp_jiffies32; + __mptcp_check_oppr(sk); mptcp_data_unlock(sk); =20 trace_ack_update_msk(mp_opt->data_ack, diff --git a/net/mptcp/protocol.c b/net/mptcp/protocol.c index 7117786e5251..800c2a4ae3a0 100644 --- a/net/mptcp/protocol.c +++ b/net/mptcp/protocol.c @@ -4057,6 +4057,15 @@ void __mptcp_check_push(struct sock *sk, struct sock= *ssk) __set_bit(MPTCP_PUSH_PENDING, &mptcp_sk(sk)->cb_flags); } =20 +/* the subflow list is only stable while no process owns the msk */ +void __mptcp_check_oppr(struct sock *sk) +{ + if (!sock_owned_by_user(sk)) + mptcp_sched_oppr_check(sk); + else + __set_bit(MPTCP_OPPR_CHECK, &mptcp_sk(sk)->cb_flags); +} + #define MPTCP_FLAGS_PROCESS_CTX_NEED (BIT(MPTCP_PUSH_PENDING) | \ BIT(MPTCP_RETRANSMIT) | \ BIT(MPTCP_FLUSH_JOIN_LIST)) @@ -4121,6 +4130,8 @@ static void mptcp_release_cb(struct sock *sk) __mptcp_error_report(sk); if (__test_and_clear_bit(MPTCP_SYNC_SNDBUF, &msk->cb_flags)) __mptcp_sync_sndbuf(sk); + if (__test_and_clear_bit(MPTCP_OPPR_CHECK, &msk->cb_flags)) + mptcp_sched_oppr_check(sk); } } =20 diff --git a/net/mptcp/protocol.h b/net/mptcp/protocol.h index 39dd56d882ce..32985f74ce35 100644 --- a/net/mptcp/protocol.h +++ b/net/mptcp/protocol.h @@ -127,6 +127,7 @@ #define MPTCP_FLUSH_JOIN_LIST 5 #define MPTCP_SYNC_STATE 6 #define MPTCP_SYNC_SNDBUF 7 +#define MPTCP_OPPR_CHECK 8 =20 struct mptcp_skb_cb { u64 map_seq; @@ -930,6 +931,7 @@ struct sock *mptcp_subflow_get_send(struct mptcp_sock *= msk); struct sock *mptcp_subflow_get_retrans(struct mptcp_sock *msk); int mptcp_sched_get_send(struct mptcp_sock *msk); int mptcp_sched_get_retrans(struct mptcp_sock *msk); +void mptcp_sched_oppr_check(struct sock *sk); bool mptcp_sched_oppr_get(struct mptcp_sock *msk, struct mptcp_sched_oppr *oppr); =20 @@ -1053,6 +1055,7 @@ static inline u64 mptcp_expand_seq(u64 old_seq, u64 c= ur_seq, bool use_64bit) } void __mptcp_check_push(struct sock *sk, struct sock *ssk); void __mptcp_data_acked(struct sock *sk); +void __mptcp_check_oppr(struct sock *sk); void __mptcp_error_report(struct sock *sk); bool mptcp_update_rcv_data_fin(struct mptcp_sock *msk, u64 data_fin_seq, b= ool use_64bit); static inline bool mptcp_data_fin_enabled(const struct mptcp_sock *msk) diff --git a/net/mptcp/sched.c b/net/mptcp/sched.c index da5fa8c0fb12..087b36381e7a 100644 --- a/net/mptcp/sched.c +++ b/net/mptcp/sched.c @@ -341,6 +341,23 @@ static u8 mptcp_sched_oppr_reason(struct sock *sk) return MPTCP_SCHED_RETRANS_IDLE_APP; } =20 +/* called on each DATA_ACK, or when the msk socket lock is released if a p= rocess + * owned it: see __mptcp_check_oppr(). Under the msk data lock, with a sta= ble + * subflow list. The worker takes the msk socket lock, which delays the re= ceive + * path, so wake it only when the scheduler may reinject: if a subflow is = idle. + */ +void mptcp_sched_oppr_check(struct sock *sk) +{ + struct mptcp_sock *msk =3D mptcp_sk(sk); + + if (!mptcp_sched_has_reinject(msk) || !mptcp_sched_oppr_reason(sk) || + !mptcp_sched_oppr_idle_ssk(msk)) + return; + + if (!test_and_set_bit(MPTCP_WORK_OPPR, &msk->flags)) + mptcp_schedule_work(sk); +} + /* Add what @skb holds to @r, sorted by start. When @r is full, keep the l= owest * ones: the data at snd_una is the one the peer is waiting for. */ --=20 2.55.0 From nobody Thu Nov 5 07:29:18 2026 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793486696; b=ImLo0566eqvNI5kuS92GbG1VMlcg0jSiKza/tuq1UqDFjCZCQAinBfplXf7eBKi6RnZh Rfbn2HbYHAe0QEp27apmsiRFYbsf2byfY7mKubE6yreisNOfvza/cR0Elu8B7CxSxb+HZ IQiJHr1hwA3UGcX9zp/yF5uIKPZ2smKYmpaa8HaWODqrN2FQacFazwx/kXZkoVzQXi56K Lj3bGLQgu9Zx6c5keEYMWza/62E5u20d9Nz07nSdYkBavrO16ttdnmaKCjFKmw18dHGRL gIrUlfdKQwl/xwgJpOKlVRgCAX+yJuPNjT9t57S2mCL6coCNCcRfraWb6Vw1xsfsibw== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793486696; h=Received:DKIM-Signature:From:Date:Subject:MIME-Version:Content-Type: Content-Transfer-Encoding:Message-Id:References:In-Reply-To:To:Cc: X-Mailer:X-Developer-Signature:X-Developer-Key; bh=7/bh29xQqE8r8O9dTn62Z7e0qkplchHRC19jY7cOqlI=; b=jJhUG07WDleSepXpqa44OzsmaRmfmQ2O9Dnp5olp/A0fXmBXMiHr13IzBgWjnjICC18q L39mtshPCKWTZhrJZfKC1ZTM8A9oPABZuvZmL9II6qi5CYU5n6dO74s4CzXdulAD0s1yS bSe906xJL4WKe6xn48Mqx1VnT75s58aOHjID3YapjBY50ugUEPBQUV4qplz/fFi3nIIHF AFQf1XmGgEi8M60gtE8XR12LWJj/w9QqqjRIXO9tnIWIEBqt5PtT3d5f0KgwvNjPxMhOl C9diydIpIRKwE2zdVLjw8bYOGdbcmt1j68GcMPo85ToIsnkXBbtmebNMwJteGXyzfzw== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=JGqmZ3w6; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id AC1B9606A31A for ; Sat, 31 Oct 2026 22:44:55 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=JGqmZ3w6 Received: by smtp.kernel.org (Postfix) with ESMTPSA id B607D1F000C1; Sat, 31 Oct 2026 22:44:54 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793486695; bh=7/bh29xQqE8r8O9dTn62Z7e0qkplchHRC19jY7cOqlI=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=JGqmZ3w6/fgv57oo5hpTczUkZUYucHTo61hVzZph+z8aNHjMYBOhBJil6Aq/jPAs+ QA7lwQ42bdSRrhuR0s7NvL14JWELnm4rfP8kftvXTj5aavLcSFcHS3RQtYF9Rcm5NK MBrbJ+PT/ftmcYqkJ8jrfdLtg71e9iqFznBZG6IvXXoJGIvo9sIjP+u31C0PweyZxC 5s6B7S1FmQxlMA9449kaW3jrG3Tl2Y1IEqQEO8HokbIyHefjz+FzNmLFsOgSoA/a3S /4kGCwf7yJwKGacA4Xb4TndHGCOfclropeUQ4LrV+5hDUL4bFH+KvtwLs1u0C+yL0N G8m3xNE7og4UQ== From: "Matthieu Baerts (NGI0)" Date: Sat, 31 Oct 2026 23:44:37 +0100 Subject: [PATCH mptcp-next v2 5/8] mptcp: sched: reinject on an idle subflow with the default scheduler X-Mailing-List: mptcp@lists.linux.dev List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20261031-mptcp-sched-opp-retrans-v2-5-e9c4a30f6841@kernel.org> References: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> In-Reply-To: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> To: MPTCP Linux Cc: Abdul Wasey , "Matthieu Baerts (NGI0)" X-Mailer: b4 0.16.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=10330; i=matttbe@kernel.org; h=from:subject:message-id; bh=mPer2Dt9JbZP4xvhLsgCxM7P3fH/vWFtvT4UR9qZzc0=; b=owGbwMvMwCVWo/Th0Gd3rumMp9WSGLKe5cf9s1sZwDvxneD1O+I3BFg0Em9yVottFMrgD9F/l rvg0Iy0jlIWBjEuBlkxRRbptsj8mc+reEu8/Cxg5rAygQxh4OIUgIm8fcTwv25vgVnE+q9sAmJH DQtEX/d8unS4ovb9juaTL0xkrI59fcjI0PLY83nt7V+xvdu821//zWeZ9PjBP4VfPT4FbFNWG91 9wAkA X-Developer-Key: i=matttbe@kernel.org; a=openpgp; fpr=E8CB85F76877057A6E27F77AF6B7824F4269A073 From: Abdul Wasey Let the default scheduler use the reinjection added by the previous patches. On an IDLE reason, it picks the active non-backup subflow with nothing queued or in flight and the lowest RTT, and resends there only the data held by a subflow that is gone, or slower than the idle one with an RTT long enough for a copy to arrive at least one RTT of the idle subflow sooner. Below that, the copies cost the idle subflow more than they save, and on subflows of similar RTT nothing is resent. The limit is what the idle subflow can send in the time the copies save, at most 256 KB, or 256 KB when the slow subflow is gone. A subflow that had a retransmission timeout without progress since is not counted as slow: it may be broken, and the stale detection needs the MPTCP-level timeouts that copies would avoid. A subflow is gone when it cannot send anymore but still holds data: not when it is joining, as it only holds a SYN, nor when it is closed, even if its queues do not look empty after a tcp_disconnect(). With more than two subflows, only what these subflows hold is resent, and the check on each DATA_ACK uses the same condition: the worker is only woken when there is something worth resending. Two subflows in netns, netem on both directions, one download, at least 5 runs each, tcp_rmem max 6 MB: - 40M/10ms + 2M/250ms with 5% loss: BBR 27-30 -> 37-38 Mbit/s, CUBIC 9-35 -> 37-38. - 400M/10ms + 15M/120ms: BBR 67-82 -> 320-332 Mbit/s, CUBIC 30-142 -> 322-345. - 1 MB request/response, p95 completion time in ms: 40M/10 + 2M/250 5%: 2396 -> 317; 400M/10 + 15M/120: 530 -> 104. The copies are 1 to 3% of the bytes sent. - 50M/10 + 50M/10: nothing resent, results unchanged. Closes: https://github.com/multipath-tcp/mptcp_net-next/issues/332 Signed-off-by: Abdul Wasey Co-developed-by: Matthieu Baerts (NGI0) Signed-off-by: Matthieu Baerts (NGI0) --- net/mptcp/sched.c | 140 +++++++++++++++++++++++++++++++++++++++++++++++++-= ---- 1 file changed, 128 insertions(+), 12 deletions(-) diff --git a/net/mptcp/sched.c b/net/mptcp/sched.c index 087b36381e7a..b70e4a690817 100644 --- a/net/mptcp/sched.c +++ b/net/mptcp/sched.c @@ -28,6 +28,9 @@ static int mptcp_sched_default_get_send(struct mptcp_sock= *msk) return 0; } =20 +#define MPTCP_OPPR_BUDGET (256 * 1024) +#define MPTCP_OPPR_GONE U32_MAX + /* the active subflow with nothing to send or retransmit and the lowest RT= T */ static struct sock *mptcp_sched_oppr_idle_ssk(struct mptcp_sock *msk) { @@ -53,11 +56,98 @@ static struct sock *mptcp_sched_oppr_idle_ssk(struct mp= tcp_sock *msk) return pick; } =20 +/* How much earlier, in us, a copy sent now on @idle would reach the peer + * than the data held by @subflow: about half an RTT of each, the current = one + * of @subflow and the base one of @idle, whose queue is empty. 0 if @subf= low + * holds nothing, is not slower than @idle, or if the copy would not save = at + * least one RTT of @idle: with a smaller gain, the copies cost more than = they + * save. MPTCP_OPPR_GONE if @subflow is gone. + */ +static u32 mptcp_sched_oppr_gain(struct mptcp_subflow_context *subflow, + const struct sock *idle) +{ + struct sock *ssk =3D mptcp_subflow_tcp_sock(subflow); + u32 srtt, min_rtt; + + /* racy on a DATA_ACK, only a hint: the worker checks again */ + if (ssk =3D=3D idle || data_race(tcp_rtx_and_write_queues_empty(ssk))) + return 0; + + /* A joining subflow only holds its SYN, and a closed one nothing, even + * if its queues look used: tcp_disconnect() moves the write_seq. + */ + if ((1 << inet_sk_state_load(ssk)) & + (TCPF_SYN_SENT | TCPF_SYN_RECV | TCPF_CLOSE)) + return 0; + + if (!__mptcp_subflow_active(subflow)) + return MPTCP_OPPR_GONE; + + /* A retransmission timeout without progress since: the subflow may + * be broken, which the stale detection handles (it needs the + * MPTCP-level timeouts that copies would avoid). + */ + if (READ_ONCE(inet_csk(ssk)->icsk_retransmits)) + return 0; + + srtt =3D READ_ONCE(tcp_sk(ssk)->srtt_us) >> 3; + /* same here, @idle is not locked */ + min_rtt =3D data_race(tcp_min_rtt(tcp_sk(idle))); + if (READ_ONCE(ssk->sk_pacing_rate) >=3D READ_ONCE(idle->sk_pacing_rate) || + min_rtt =3D=3D ~0U || srtt <=3D 3 * min_rtt) + return 0; + return (srtt - min_rtt) / 2; +} + +/* The largest gain of the subflows, 0 if nothing is worth resending on @i= dle */ +static u32 mptcp_sched_oppr_max_gain(struct mptcp_sock *msk, + const struct sock *idle) +{ + struct mptcp_subflow_context *subflow; + u32 gain =3D 0; + + mptcp_for_each_subflow(msk, subflow) + gain =3D max(gain, mptcp_sched_oppr_gain(subflow, idle)); + return gain; +} + +/* The default scheduler's choice on an MPTCP_SCHED_RETRANS_IDLE_* reason, + * whichever it is: the idle subflow with the lowest RTT, if another subfl= ow + * holds data worth resending on it. The limit is what the idle subflow can + * send in the time the copies save; a subflow that is gone saves an MPTCP + * RTO. + */ +static struct sock *mptcp_sched_oppr_default(struct mptcp_sock *msk) +{ + struct sock *idle; + u64 limit; + u32 gain; + + idle =3D mptcp_sched_oppr_idle_ssk(msk); + if (!idle) + return NULL; + + gain =3D mptcp_sched_oppr_max_gain(msk, idle); + if (!gain) + return NULL; + + limit =3D MPTCP_OPPR_BUDGET; + if (gain !=3D MPTCP_OPPR_GONE) + limit =3D min_t(u64, limit, + div_u64((u64)READ_ONCE(idle->sk_pacing_rate) * gain, + USEC_PER_SEC)); + mptcp_subflow_ctx(idle)->reinject_limit =3D limit; + return idle; +} + static int mptcp_sched_default_get_retrans(struct mptcp_sock *msk) { struct sock *ssk; =20 - ssk =3D mptcp_subflow_get_retrans(msk); + if (msk->sched_retrans !=3D MPTCP_SCHED_RETRANS_RTO) + ssk =3D mptcp_sched_oppr_default(msk); + else + ssk =3D mptcp_subflow_get_retrans(msk); if (!ssk) return -EINVAL; =20 @@ -263,23 +353,36 @@ int mptcp_sched_get_retrans(struct mptcp_sock *msk) * that already reached the peer. * * The scheduler decides on which subflow, and how much, through get_retra= ns - * with the reason in msk->sched_retrans. This is done at most once per - * snd_una value. + * with the reason in msk->sched_retrans. The default one resends only data + * held by a subflow that is gone, or that is slower than the idle one and + * whose RTT is long enough for a copy to arrive at least one RTT of the + * idle subflow sooner. Below that, the copies cost the idle subflow more + * than they save. This is done at most once per snd_una value, and at most + * what the idle subflow can send in the time saved, or MPTCP_OPPR_BUDGET = for + * a subflow that is gone. */ #define MPTCP_OPPR_MIN (16 * 1024) =20 +/* the in-kernel default scheduler */ +static bool mptcp_sched_is_default(const struct mptcp_sock *msk) +{ + const struct mptcp_sched_ops *sched =3D READ_ONCE(msk->sched); + + return !sched || sched =3D=3D &mptcp_sched_default; +} + /* can the scheduler be asked to reinject: see mptcp_sched_get_reinject() = */ static bool mptcp_sched_has_reinject(const struct mptcp_sock *msk) { const struct mptcp_sched_ops *sched =3D READ_ONCE(msk->sched); =20 - return sched && sched !=3D &mptcp_sched_default && sched->get_retrans; + return !sched || sched->get_retrans; } =20 /* Ask the scheduler where to resend the data other subflows hold, because= a * subflow is idle while nothing new can be sent, for @reason: see - * MPTCP_SCHED_RETRANS_IDLE_WND. Only a scheduler with its own get_retrans= is - * asked; the default one does not reinject. + * MPTCP_SCHED_RETRANS_IDLE_WND. Only the default scheduler and one with i= ts + * own get_retrans are asked. */ static int mptcp_sched_get_reinject(struct mptcp_sock *msk, u8 reason) { @@ -301,7 +404,10 @@ static int mptcp_sched_get_reinject(struct mptcp_sock = *msk, u8 reason) return -EOPNOTSUPP; =20 msk->sched_retrans =3D reason; - err =3D msk->sched->get_retrans(msk); + if (mptcp_sched_is_default(msk)) + err =3D mptcp_sched_default_get_retrans(msk); + else + err =3D msk->sched->get_retrans(msk); msk->sched_retrans =3D MPTCP_SCHED_RETRANS_RTO; return err; } @@ -344,14 +450,20 @@ static u8 mptcp_sched_oppr_reason(struct sock *sk) /* called on each DATA_ACK, or when the msk socket lock is released if a p= rocess * owned it: see __mptcp_check_oppr(). Under the msk data lock, with a sta= ble * subflow list. The worker takes the msk socket lock, which delays the re= ceive - * path, so wake it only when the scheduler may reinject: if a subflow is = idle. + * path, so wake it only when the scheduler may reinject: when a subflow i= s idle + * and, for the default one, when mptcp_sched_oppr_default() would pick it. */ void mptcp_sched_oppr_check(struct sock *sk) { struct mptcp_sock *msk =3D mptcp_sk(sk); + struct sock *idle; =20 - if (!mptcp_sched_has_reinject(msk) || !mptcp_sched_oppr_reason(sk) || - !mptcp_sched_oppr_idle_ssk(msk)) + if (!mptcp_sched_has_reinject(msk) || !mptcp_sched_oppr_reason(sk)) + return; + + idle =3D mptcp_sched_oppr_idle_ssk(msk); + if (!idle || (mptcp_sched_is_default(msk) && + !mptcp_sched_oppr_max_gain(msk, idle))) return; =20 if (!test_and_set_bit(MPTCP_WORK_OPPR, &msk->flags)) @@ -396,8 +508,9 @@ static int mptcp_sched_oppr_add_skb(const struct sk_buf= f *skb, u64 una, } =20 /* The DSN ranges at or after @una that the subflows other than @idle still - * hold, sent or not: the data the peer is waiting for. Sorted by start, a= nd - * merged: data resent before can sit on more than one subflow. + * hold, sent or not: the data the peer is waiting for. With the default + * scheduler, only from the subflows this is worth it for. Sorted by start, + * and merged: data resent before can sit on more than one subflow. */ static int mptcp_sched_oppr_collect(struct mptcp_sock *msk, struct sock *i= dle, u64 una, struct mptcp_oppr_range *r) @@ -412,6 +525,9 @@ static int mptcp_sched_oppr_collect(struct mptcp_sock *= msk, struct sock *idle, =20 if (ssk =3D=3D idle) continue; + if (mptcp_sched_is_default(msk) && + !mptcp_sched_oppr_gain(subflow, idle)) + continue; =20 lock_sock(ssk); skb_rbtree_walk(skb, &ssk->tcp_rtx_queue) --=20 2.55.0 From nobody Thu Nov 5 07:29:18 2026 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793486697; b=bCYsVFsKUpkTB9UdY2EVqfsLO3a5g1XrcaAng4beKWYnwFymXjXCGeqBGejMhWgrKGuY xiEhWzNP+PRWOe0WnquvOEfU9Uc5SHQ3M4JFe65HI0IUCpHjtc7To+WThu+/yx1nnYJ1f AlV4kPY9Kc5UoIt/n89csz1Awtayx7ZbQTS5TWtWdlhstoQMIvSbOF7lQ+71auVSRiT74 AFYtG6wrnALOSVQWlNbKOHdIN/aS+5EmpcWYOIUr137pEiSg90tvEEAekKs7JR4/Q42bR vCuqWkvPaX9LmGzzxeFMUxpEX4qkrKOfFfVVvExxNUkK+W45M4k0ifrRJF0EH3FJCog== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793486697; h=Received:DKIM-Signature:From:Date:Subject:MIME-Version:Content-Type: Content-Transfer-Encoding:Message-Id:References:In-Reply-To:To:Cc: X-Mailer:X-Developer-Signature:X-Developer-Key; bh=olDO0WMW9E7339UNa/dF5nkzOhMW9qqN5s+oJew3Rl8=; b=FwcJswpX6bQkWXboE3D3x80yb89LiA0OHN0rwjEORwMubU8AA7yp7y70i0vvd6wTfi3e fqNR7+/4hFu6ANr8V3Dwy8Ep/SJEhep+qVwG6spiePoVrHOY9NS60ExIUqzaVnKzS81nP vs5PHVQGHGdOHPgTmF8UgwEJyCLaiFScnzFI/LCIbCHx0slYcYvFHgVgx52nJWtTM45On T7C8J/Y31YktwC5O2wUAb3B52sJYQx7d584RuTW8iSC7tL6GEfDJO6KEKdQBSc2rZoNpd 0TAih9liAKBeMyIxpeujs+GGt5kSWN6RBm2TQXQG6AcT23G/xQTLuDtqoi68JdHlL9w== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=NytY45dj; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id AA53C606A31A for ; Sat, 31 Oct 2026 22:44:56 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=NytY45dj Received: by smtp.kernel.org (Postfix) with ESMTPSA id AD35B1F00919; Sat, 31 Oct 2026 22:44:55 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793486696; bh=olDO0WMW9E7339UNa/dF5nkzOhMW9qqN5s+oJew3Rl8=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=NytY45djeRdfLvfMvaIk/eZ0K2W2B1+pimxTVKSSkcIy6wdfG395KnYFSHL9y0RWs zFMEd9jm+6pjB84oFPMmUgEFmqrNecoMxBG/ht8W0bZbC+62YVHNngKszqQMVde296 cs78jOnNkq+K0kZY0zYjd9zCd5ep0sM3WFQ0wuTZ/vhtsZYF28L1Dr838FdNohXDxQ gpLtSbqQ/WcLJIgYwxM5mapL5guy9a3hnOouiYhUHutgFcDGaKJXgEhr0CxbKXDb6m 2Xr1BEnc+NW4moGBwU2l9WL6BX8YyD/+uByP8te/pqUMp7oQU7VPCNrPzzNd6reInU mkRf9RmKdw/CA== From: "Matthieu Baerts (NGI0)" Date: Sat, 31 Oct 2026 23:44:38 +0100 Subject: [PATCH mptcp-next v2 6/8] mptcp: sched: let a BPF scheduler set the send and reinject limits X-Mailing-List: mptcp@lists.linux.dev List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20261031-mptcp-sched-opp-retrans-v2-6-e9c4a30f6841@kernel.org> References: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> In-Reply-To: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> To: MPTCP Linux Cc: Abdul Wasey , "Matthieu Baerts (NGI0)" X-Mailer: b4 0.16.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=1170; i=matttbe@kernel.org; h=from:subject:message-id; bh=DiZMaCeYF/ikUIPynJuYh3wBqd3wmXM7j1aSizM9Blw=; b=owGbwMvMwCVWo/Th0Gd3rumMp9WSGLKe5ccnn7rd431ShdkxfDdPhK2oWNjl7g9nOqyb9jsfd nAIv8DeUcrCIMbFICumyCLdFpk/83kVb4mXnwXMHFYmkCEMXJwCMBHLHIZ/9g5b2tyf+aT17jqn vN6T+5TAMYnTn8/e2JYpe+aOrgTPIkaGU+fMbucJCAh8dpr+XqC361/jvJs2K1euufnshdOhV88 zeQA= X-Developer-Key: i=matttbe@kernel.org; a=openpgp; fpr=E8CB85F76877057A6E27F77AF6B7824F4269A073 From: Abdul Wasey A scheduler can limit what is sent for one decision with send_limit, and what is resent on an idle subflow with reinject_limit, but a BPF scheduler cannot write these fields of the subflow context yet. Allow it, so that it can set them in get_send and get_retrans. Signed-off-by: Abdul Wasey --- net/mptcp/bpf.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/net/mptcp/bpf.c b/net/mptcp/bpf.c index 53111d7a9eee..832dd807f672 100644 --- a/net/mptcp/bpf.c +++ b/net/mptcp/bpf.c @@ -63,6 +63,12 @@ static int bpf_mptcp_sched_btf_struct_access(struct bpf_= verifier_log *log, case offsetof(struct mptcp_subflow_context, avg_pacing_rate): end =3D offsetofend(struct mptcp_subflow_context, avg_pacing_rate); break; + case offsetof(struct mptcp_subflow_context, send_limit): + end =3D offsetofend(struct mptcp_subflow_context, send_limit); + break; + case offsetof(struct mptcp_subflow_context, reinject_limit): + end =3D offsetofend(struct mptcp_subflow_context, reinject_limit); + break; default: bpf_log(log, "no write support to mptcp_subflow_context at off %d\n", off); --=20 2.55.0 From nobody Thu Nov 5 07:29:18 2026 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793486697; b=Tt6gvBZCphzgSPkXjx5XNCncc7JYeNbwuA1oSGcAbdXlBhcqhcGVDGML4KfZKh1H0vwQ vv0psz5z8AoWnXpO8xzqoEwry4ozJM6ER/QtteocJdIKZPUaScB/6+7aDp/FlRpM45/sV /IoZIZPWp5lMvd/DCGgXwrCh8eMq/TobLLXoHY4GudQvnpqWoFv2nN2fukpxdOcmHoGFo fIW8Y4FyHihK0WBXJiSFrRhBsKjG3zj0SkErMGcL5zQoyVnajKZM3VXvW90AS0FV2E4RR hB6+tIHM7BNPl5kJhq7RPWS/WzfRw6D+vD5tUYLMWaRdfLXksdf8rfpQBhpGOxbOFQg== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793486697; h=Received:DKIM-Signature:From:Date:Subject:MIME-Version:Content-Type: Content-Transfer-Encoding:Message-Id:References:In-Reply-To:To:Cc: X-Mailer:X-Developer-Signature:X-Developer-Key; bh=V2WZ/Ki4/Mq0LrUBHHhSD5OcHa9ZckCLSE/1phyhIYk=; b=GIOYFS0quWeATE3pg+Zq2cT2DFHqfD7Q9csEve4piLVei5s+KJjdX2iPSwym7lYStHbU n9FvuHnn4+HxUsM3pXRLNb9nnS40p2sN10lXbwGJqiFlmKdwf4KCRYEZw+q6Aorj4EP+A tEJaQMMv3GCDD1BOdlcb0ICU0MF8dQ7F+4LF7jBd6YUMYE+gh2ebHccKxM2vGX3eV0Z5V CzfJlBnnl6j3FxD4PTKKrehn3EWJwTlw95J1B+EjX794Y6lVfTOq625Zey0wQwkkIsnAo bDjT6kKKv4m4nrbRF+YubvKb1yRB6s7cyyGO/D9kwkMTIhsPmZOYG6KMYtZFJDXEtzw== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=SE1fbL3s; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 63DCF606A31A for ; Sat, 31 Oct 2026 22:44:57 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=SE1fbL3s Received: by smtp.kernel.org (Postfix) with ESMTPSA id A4AA11F00A15; Sat, 31 Oct 2026 22:44:56 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793486697; bh=V2WZ/Ki4/Mq0LrUBHHhSD5OcHa9ZckCLSE/1phyhIYk=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=SE1fbL3sm6QZ2ovm8yeK9oJfZ7DNoCy1+26PVwSK4bhqFhVfZqCX+iiu4KnUWDam3 /glUGo9Cd4Du/SEFGZnrnu+n9rXK456Roh9KrzA3CL11bFr3i3uDZCBxPYSkhwEt31 o8iQu2JOxPx/tRDi485XPgoWsgq9jAzBQUnIokGm3LsVjBAjn5tnHMPBZMWB7j6pO4 bApMFue+BaTF98kBJDZFv1BhWPpo+V1+oy8L/lxvDzxAq8ps4taHaPQhaO2OFyVahH 6LJN1Fe0vPZDux6FUqMNNg77gmKrYZGoCuBpXN4g1ImZizQfXlDLLaQ0wVaSQoyvdL R+1HoF/P0MCnw== From: "Matthieu Baerts (NGI0)" Date: Sat, 31 Oct 2026 23:44:39 +0100 Subject: [PATCH mptcp-next v2 7/8] selftests: bpf: mptcp: test the send limit of a scheduler X-Mailing-List: mptcp@lists.linux.dev List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20261031-mptcp-sched-opp-retrans-v2-7-e9c4a30f6841@kernel.org> References: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> In-Reply-To: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> To: MPTCP Linux Cc: Abdul Wasey , "Matthieu Baerts (NGI0)" X-Mailer: b4 0.16.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=7036; i=matttbe@kernel.org; h=from:subject:message-id; bh=K/1YrW4aZ4fWr6dD9zpdbZUEockaVQeD1DXbN6gQVOQ=; b=owGbwMvMwCVWo/Th0Gd3rumMp9WSGLKe5Sdcali4i6U7/orxRK62HTwMbe1Vr+y+dUbfODW9K 7mpoHFGRykLgxgXg6yYIot0W2T+zOdVvCVefhYwc1iZQIYwcHEKwERk+BgZPuyQz5u/V7S1Vnyq kqLqgp+Puzcl/U05Old17jXTV6IMqgz/M9n1j2zy2T9JeN2SgPXrGp4ea3IPePw2iXGJ68PvX/0 u8wMA X-Developer-Key: i=matttbe@kernel.org; a=openpgp; fpr=E8CB85F76877057A6E27F77AF6B7824F4269A073 From: Abdul Wasey Add bpf_limit, a round robin scheduler that sets send_limit to 4 KB on the subflow it picks, and a test checking that no subflow gets more than that between two calls of get_send. The scheduler measures it from the subflow's write_seq, at each call, for the subflow it picked last time. An MPTCP-level retransmission is not counted: it does not come from get_send. The data is written in 64 KB chunks, so that a dfrag is larger than the limit. With 1.5 KB writes, as send_recv_data() does, no decision is given more than 4 KB anyway, and the limit is not tested. Without the limit applied in __subflow_push_pending(), a subflow got 32728 bytes in one decision. Signed-off-by: Abdul Wasey --- tools/testing/selftests/bpf/prog_tests/mptcp.c | 106 +++++++++++++++++= ++++ .../testing/selftests/bpf/progs/mptcp_bpf_limit.c | 94 ++++++++++++++++++ 2 files changed, 200 insertions(+) diff --git a/tools/testing/selftests/bpf/prog_tests/mptcp.c b/tools/testing= /selftests/bpf/prog_tests/mptcp.c index d77c9f8c53c7..4302c16cb6bb 100644 --- a/tools/testing/selftests/bpf/prog_tests/mptcp.c +++ b/tools/testing/selftests/bpf/prog_tests/mptcp.c @@ -18,6 +18,7 @@ #include "mptcp_bpf_rr.skel.h" #include "mptcp_bpf_red.skel.h" #include "mptcp_bpf_burst.skel.h" +#include "mptcp_bpf_limit.skel.h" =20 #define NS_TEST "mptcp_ns" #define ADDR_1 "10.0.1.1" @@ -820,6 +821,109 @@ static void test_burst(void) mptcp_bpf_burst__destroy(skel); } =20 +struct big_send_arg { + int lfd; + int err; +}; + +/* send total_bytes in 64 KB writes, so that data fragments get larger than + * the scheduler's limit + */ +static void *big_send(void *data) +{ + struct big_send_arg *a =3D data; + static char buf[64 * 1024]; + unsigned int sent =3D 0; + ssize_t ret; + int fd; + + fd =3D accept(a->lfd, NULL, NULL); + if (fd < 0) { + a->err =3D -errno; + return NULL; + } + while (sent < total_bytes) { + ret =3D send(fd, buf, MIN(total_bytes - sent, sizeof(buf)), 0); + if (ret < 0 && errno =3D=3D EINTR) + continue; + if (ret < 0) { + a->err =3D -errno; + break; + } + sent +=3D ret; + } + close(fd); + return NULL; +} + +/* At most 4 KB per decision, also inside a data fragment: no subflow gets + * more than that between two calls of the scheduler. + */ +static void test_limit(void) +{ + struct big_send_arg arg =3D {}; + struct mptcp_bpf_limit *skel; + int server_fd =3D -1, client_fd =3D -1; + struct netns_obj *netns =3D NULL; + unsigned int recvd =3D 0; + struct bpf_link *link; + pthread_t thread; + char buf[64 * 1024]; + ssize_t ret; + + skel =3D mptcp_bpf_limit__open_and_load(); + if (!ASSERT_OK_PTR(skel, "open_and_load: limit")) + return; + + link =3D bpf_map__attach_struct_ops(skel->maps.limit); + if (!ASSERT_OK_PTR(link, "attach_struct_ops")) + goto destroy; + + netns =3D netns_new(NS_TEST, true); + if (!ASSERT_OK_PTR(netns, "netns_new")) + goto detach; + if (!ASSERT_OK(sched_init("subflow", "bpf_limit"), "sched_init")) + goto close_netns; + + server_fd =3D start_mptcp_server(AF_INET, ADDR_1, PORT_1, 0); + if (!ASSERT_OK_FD(server_fd, "start_mptcp_server")) + goto close_netns; + client_fd =3D connect_to_fd(server_fd, 0); + if (!ASSERT_OK_FD(client_fd, "connect_to_fd")) + goto close_server; + + arg.lfd =3D server_fd; + if (!ASSERT_OK(pthread_create(&thread, NULL, big_send, &arg), + "pthread_create")) + goto close_client; + while (recvd < total_bytes) { + ret =3D recv(client_fd, buf, sizeof(buf), 0); + if (ret < 0 && errno =3D=3D EINTR) + continue; + if (ret <=3D 0) + break; + recvd +=3D ret; + } + pthread_join(thread, NULL); + + ASSERT_OK(arg.err, "send"); + ASSERT_EQ(recvd, total_bytes, "recv"); + CHECK(has_bytes_sent(ADDR_2), "limit", "should have bytes_sent on addr2\n= "); + ASSERT_GT(skel->bss->max_given, 0, "bytes given per decision"); + ASSERT_LE(skel->bss->max_given, 4096, "bytes given per decision"); + +close_client: + close(client_fd); +close_server: + close(server_fd); +close_netns: + netns_free(netns); +detach: + bpf_link__destroy(link); +destroy: + mptcp_bpf_limit__destroy(skel); +} + void test_mptcp(void) { if (test__start_subtest("base")) @@ -842,4 +946,6 @@ void test_mptcp(void) test_red(); if (test__start_subtest("burst")) test_burst(); + if (test__start_subtest("limit")) + test_limit(); } diff --git a/tools/testing/selftests/bpf/progs/mptcp_bpf_limit.c b/tools/te= sting/selftests/bpf/progs/mptcp_bpf_limit.c new file mode 100644 index 000000000000..431f07d6b669 --- /dev/null +++ b/tools/testing/selftests/bpf/progs/mptcp_bpf_limit.c @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include "mptcp_bpf.h" +#include + +char _license[] SEC("license") =3D "GPL"; + +#define LIMIT 4096 + +__u64 calls =3D 0; +/* the most bytes a subflow was given for one decision */ +__u32 max_given =3D 0; + +/* the subflow picked last time, by position, and its write_seq then */ +int last_pick =3D -1; +__u32 last_seq =3D 0; + +SEC("struct_ops") +void BPF_PROG(mptcp_sched_limit_init, struct mptcp_sock *msk) +{ +} + +SEC("struct_ops") +void BPF_PROG(mptcp_sched_limit_release, struct mptcp_sock *msk) +{ +} + +/* round robin, at most LIMIT bytes per decision */ +SEC("struct_ops") +int BPF_PROG(bpf_limit_get_send, struct mptcp_sock *msk) +{ + struct mptcp_subflow_context *subflow, *first =3D NULL, *pick =3D NULL; + const struct tcp_sock *tp; + __u64 n =3D calls++; + struct sock *ssk; + int i =3D 0, p =3D 0; + + bpf_for_each(mptcp_subflow, subflow, (struct sock *)msk) { + if (i =3D=3D last_pick) { + ssk =3D bpf_mptcp_subflow_tcp_sock(subflow); + tp =3D ssk ? bpf_skc_to_tcp_sock(ssk) : NULL; + if (tp && tp->write_seq - last_seq > max_given) + max_given =3D tp->write_seq - last_seq; + } + if (!first) + first =3D subflow; + if (!pick && i =3D=3D (n & 1)) { + pick =3D subflow; + p =3D i; + } + i++; + } + /* a single subflow so far */ + if (!pick) { + pick =3D first; + p =3D 0; + } + if (!pick) + return -1; + + ssk =3D bpf_mptcp_subflow_tcp_sock(pick); + tp =3D ssk ? bpf_skc_to_tcp_sock(ssk) : NULL; + last_pick =3D tp ? p : -1; + last_seq =3D tp ? tp->write_seq : 0; + + pick->send_limit =3D LIMIT; + mptcp_subflow_set_scheduled(pick, true); + return 0; +} + +/* An MPTCP-level retransmission is not one of our decisions: do not count + * what it sends. + */ +SEC("struct_ops") +int BPF_PROG(bpf_limit_get_retrans, struct mptcp_sock *msk) +{ + struct mptcp_subflow_context *subflow; + + last_pick =3D -1; + bpf_for_each(mptcp_subflow, subflow, (struct sock *)msk) { + mptcp_subflow_set_scheduled(subflow, true); + return 0; + } + return -1; +} + +SEC(".struct_ops.link") +struct mptcp_sched_ops limit =3D { + .init =3D (void *)mptcp_sched_limit_init, + .release =3D (void *)mptcp_sched_limit_release, + .get_send =3D (void *)bpf_limit_get_send, + .get_retrans =3D (void *)bpf_limit_get_retrans, + .name =3D "bpf_limit", +}; --=20 2.55.0 From nobody Thu Nov 5 07:29:18 2026 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=100.103.45.18 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793486699; b=4Km2iL8KdZOduiQ7GDWrgvOt+59yGVaeP60zVI25q2UNOuFKqNrYmQvXhrQopUFCaijS LJjnwzCh+rKgZ0ig3/PdwOT7YlRKHC6EZs/upt71shpUhw6k8e8xmapA7ERa46mMKYcm/ 0kFjd74CCD8p/TosrUCwkPs9xfjuCVBefdivkbdBNji7GmpzsrztJ9kMPc1mZDei7Ftqk rQVJDdUNlRrzhwr8cAcm6eDWEUwFqBh1lGi/k1QQNq5e2nL1KoEY/jyxb7dM9DVwyDvXg FrQgk5T0v4YO4l+w3VtxeDGxIS9qFJAk7vF7RYUtCIyZWGpKAjLuqb3rM/wlZtpn5hg== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793486699; h=Received:DKIM-Signature:From:Date:Subject:MIME-Version:Content-Type: Content-Transfer-Encoding:Message-Id:References:In-Reply-To:To:Cc: X-Mailer:X-Developer-Signature:X-Developer-Key; bh=5hUBN6+1jDOWJ0vdcV75i1F+TIrOX66n/UiAFZSwlvs=; b=yfH8MzsxclbEqx4ckHEXfn+rtchcyJlCnxIX+hBBmoL7Vo9oW+Sc5g4LmCtDCNj1r4bE aWyzNgsjA2xD0YuwoSOOmAondkZn+kHY0KvaIw/gg74cjw/9QTGl2FypWQCfD5+9Zyd4G qXv6f6bKizVY7MKED8J5jv2GIFcPbeAmbL80rufuZzhx4dH0ipNHW/ZMEHk/8jSchyf2l ojmlNvm8CSVwEjEDr0MJ2dPD05lDFFScHy8SHpRMvux+27AHcwaZXmd1dgFNHhzlEN67q 7XfUW/lqbCVhedMn1wZ1Zmr9H88cDwGcHKhM6rQlhQyemwMlmpLjoZ08HshcqZV9msA== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=Jhog6En7; arc=none smtp.remote-ip=100.103.45.18 Received: from smtp.kernel.org (quasi.space.kernel.org [100.103.45.18]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature ECDSA (prime256v1)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id ACF9E606A31A for ; Sat, 31 Oct 2026 22:44:58 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=kernel.org header.i=@kernel.org header.a=rsa-sha256 header.s=k20260515 header.b=Jhog6En7 Received: by smtp.kernel.org (Postfix) with ESMTPSA id 9BF0A1F00919; Sat, 31 Oct 2026 22:44:57 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=kernel.org; s=k20260515; t=1793486698; bh=5hUBN6+1jDOWJ0vdcV75i1F+TIrOX66n/UiAFZSwlvs=; h=From:Date:Subject:References:In-Reply-To:To:Cc; b=Jhog6En7HRwIOm0rPvv1ei0rGkggz/z5STKUBLYyGn6+nnSNBT7nCvy08gMUQaBlA hhXj2gCZ6M1DO2ZudmUJwONlm8T0DU9elPPaH5MxArFsY5OPuJthYPm1UTk1UzLQI4 v0iou5Vzi7PEedjlIOfj2WI6S6+kTvZiDjUN3E3aIvt5TDm4c/DHM/AE7oGEGdHg5B ddMKOZtEmEbnl1n/eY2J1FCeqfF2cG+lhNsA2LvymBvM+scXxb7e//BmhBsIkURIjo zkPP/lZpeM2Rwh9LU9ChdBzaU900+f6ORS/bS1OIWtlgNcKe3Cqs2retxrmvJncVhe WVbqOCcY+E8ew== From: "Matthieu Baerts (NGI0)" Date: Sat, 31 Oct 2026 23:44:40 +0100 Subject: [PATCH mptcp-next v2 8/8] selftests: bpf: mptcp: test a scheduler reinjecting on an idle subflow X-Mailing-List: mptcp@lists.linux.dev List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Message-Id: <20261031-mptcp-sched-opp-retrans-v2-8-e9c4a30f6841@kernel.org> References: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> In-Reply-To: <20261031-mptcp-sched-opp-retrans-v2-0-e9c4a30f6841@kernel.org> To: MPTCP Linux Cc: Abdul Wasey , "Matthieu Baerts (NGI0)" X-Mailer: b4 0.16.0 X-Developer-Signature: v=1; a=openpgp-sha256; l=4408; i=matttbe@kernel.org; h=from:subject:message-id; bh=YAnRGy+eXCA9l/+DMAAHPkfjni1AGhmQqQqDjDWMwAk=; b=owGbwMvMwCVWo/Th0Gd3rumMp9WSGLKe5Sec2jjX7lqBmPWsRQY5dmfi/rLsLmtxnppwcCbDe dac9CaTjlIWBjEuBlkxRRbptsj8mc+reEu8/Cxg5rAygQxh4OIUgIksfMfwv4DBV63YynUvw7wX ibcfzwsI+mAcPO2Lf+6V46nadx/yHmdkeLKZ/Z8k04Nvu8WKk2dzpBtJRHPaimY8iK7iuWp08bk FIwA= X-Developer-Key: i=matttbe@kernel.org; a=openpgp; fpr=E8CB85F76877057A6E27F77AF6B7824F4269A073 From: Abdul Wasey Add bpf_reinject, a scheduler that sends new data on the first subflow only, and on an IDLE reason of get_retrans marks the other subflow with a reinject_limit of 64 KB. The second address then only carries data resent by the core. The test checks that it did, and that get_retrans was called with an IDLE reason; the scheduler counts its calls by reason. Signed-off-by: Abdul Wasey --- tools/testing/selftests/bpf/prog_tests/mptcp.c | 24 ++++++++ .../selftests/bpf/progs/mptcp_bpf_reinject.c | 70 ++++++++++++++++++= ++++ 2 files changed, 94 insertions(+) diff --git a/tools/testing/selftests/bpf/prog_tests/mptcp.c b/tools/testing= /selftests/bpf/prog_tests/mptcp.c index 4302c16cb6bb..723f42b45827 100644 --- a/tools/testing/selftests/bpf/prog_tests/mptcp.c +++ b/tools/testing/selftests/bpf/prog_tests/mptcp.c @@ -18,6 +18,7 @@ #include "mptcp_bpf_rr.skel.h" #include "mptcp_bpf_red.skel.h" #include "mptcp_bpf_burst.skel.h" +#include "mptcp_bpf_reinject.skel.h" #include "mptcp_bpf_limit.skel.h" =20 #define NS_TEST "mptcp_ns" @@ -821,6 +822,27 @@ static void test_burst(void) mptcp_bpf_burst__destroy(skel); } =20 +/* New data only goes on the first subflow, so the second one only carries + * what the scheduler asks the core to reinject when it goes idle. + */ +static void test_reinject(void) +{ + struct mptcp_bpf_reinject *skel; + __u64 idle; + + skel =3D mptcp_bpf_reinject__open_and_load(); + if (!ASSERT_OK_PTR(skel, "open_and_load: reinject")) + return; + + test_bpf_sched(skel->maps.reinject, "reinject", WITH_DATA, WITH_DATA); + + /* calls[0] counts the MPTCP RTOs, the others the idle reasons */ + idle =3D skel->bss->calls[1] + skel->bss->calls[2] + skel->bss->calls[3]; + ASSERT_GT(idle, 0, "get_retrans called on an idle subflow"); + + mptcp_bpf_reinject__destroy(skel); +} + struct big_send_arg { int lfd; int err; @@ -946,6 +968,8 @@ void test_mptcp(void) test_red(); if (test__start_subtest("burst")) test_burst(); + if (test__start_subtest("reinject")) + test_reinject(); if (test__start_subtest("limit")) test_limit(); } diff --git a/tools/testing/selftests/bpf/progs/mptcp_bpf_reinject.c b/tools= /testing/selftests/bpf/progs/mptcp_bpf_reinject.c new file mode 100644 index 000000000000..3e315c7ead67 --- /dev/null +++ b/tools/testing/selftests/bpf/progs/mptcp_bpf_reinject.c @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include "mptcp_bpf.h" +#include + +char _license[] SEC("license") =3D "GPL"; + +/* get_retrans calls, by msk->sched_retrans */ +__u64 calls[4] =3D {}; + +SEC("struct_ops") +void BPF_PROG(mptcp_sched_reinject_init, struct mptcp_sock *msk) +{ +} + +SEC("struct_ops") +void BPF_PROG(mptcp_sched_reinject_release, struct mptcp_sock *msk) +{ +} + +/* new data only on the first subflow, as bpf_first */ +SEC("struct_ops") +int BPF_PROG(bpf_reinject_get_send, struct mptcp_sock *msk) +{ + struct mptcp_subflow_context *subflow; + + bpf_for_each(mptcp_subflow, subflow, (struct sock *)msk) { + if (bpf_mptcp_subflow_tcp_sock(subflow) =3D=3D msk->first) { + mptcp_subflow_set_scheduled(subflow, true); + return 0; + } + } + return -1; +} + +/* On an MPTCP RTO, resend on the first subflow. When the core asks because + * a subflow is idle, resend up to 64 KB of what the first subflow holds on + * another one, so the second address only carries reinjected data. + */ +SEC("struct_ops") +int BPF_PROG(bpf_reinject_get_retrans, struct mptcp_sock *msk) +{ + struct mptcp_subflow_context *subflow; + __u8 reason =3D msk->sched_retrans; + bool idle =3D reason !=3D MPTCP_SCHED_RETRANS_RTO; + + if (reason < 4) + __sync_fetch_and_add(&calls[reason], 1); + + bpf_for_each(mptcp_subflow, subflow, (struct sock *)msk) { + bool first =3D bpf_mptcp_subflow_tcp_sock(subflow) =3D=3D msk->first; + + if (first =3D=3D idle) + continue; + if (idle) + subflow->reinject_limit =3D 64 * 1024; + mptcp_subflow_set_scheduled(subflow, true); + return 0; + } + return -1; +} + +SEC(".struct_ops.link") +struct mptcp_sched_ops reinject =3D { + .init =3D (void *)mptcp_sched_reinject_init, + .release =3D (void *)mptcp_sched_reinject_release, + .get_send =3D (void *)bpf_reinject_get_send, + .get_retrans =3D (void *)bpf_reinject_get_retrans, + .name =3D "bpf_reinject", +}; --=20 2.55.0