Skip to contentSkip to conversation

net: Prevent RPS table overwrite of active flows

PATCH RESEND Applied Krishna Kumar 2 participants 4 messages Aug 25, 2025 – Aug 28, 2025

Subsystems (2)
lore
Share & tools
lore
#0 lore raw

v7 cover net: Prevent RPS table overwrite of active flows

As requested, I am resending the same [3] patch after the 6.17 merge window. It applies cleanly to the current net-next tree and has been tested under heavy traffic for 15 minutes.

This series splits the original RPS patch [1] into two patches for net-next. It also addresses a kernel test robot warning by defining rps_flow_is_active() only when aRFS is enabled. I tested v3 with four builds and reboots: two for [PATCH 1/2] with aRFS enabled & disabled, and two for [PATCH 2/2]. There are no code changes in v4 and v5, only documentation. Patch v6 has one line change to keep 'hash' field under #ifdef, and was test built with aRFS=on and aRFS=off. The same two builds were done for v7, along with 15m load testing with aRFS=on to ensure the new changes are correct.

The first patch prevents RPS table overwrite for active flows thereby improving aRFS stability.

The second patch caches hash & flow_id in get_rps_cpu() to avoid recalculating it in set_rps_cpu() (this patch depends on the first).

[1] lore.kernel.org/netdev/[email protected]/
[2] lore.kernel.org/netdev/[email protected]/

---
v7: Improve readability of rps_flow_is_active() and remove some
unnecessary comments.
v6: Keep 'hash' in 'rps_dev_flow' under aRFS config.
v5: Same v4 patch sent with a change in documentation style for "return".
v4: Same v3 patch sent as a new thread instead of a continuation.
v3: Wrapped rps_flow_is_active() in #ifdef CONFIG_RFS_ACCEL to fix
unused function warning reported by kernel test robot.
v2: Split original patch into two: RPS table overwrite prevention and hash/
flow_id caching.

Krishna Kumar (2):
net: Prevent RPS table overwrite of active flows
net: Cache hash and flow_id to avoid recalculation

 include/net/rps.h    |  7 +++--
 net/core/dev.c       | 71 +++++++++++++++++++++++++++++++++++++-------
 net/core/net-sysfs.c |  4 ++-
 3 files changed, 68 insertions(+), 14 deletions(-)
Signature · 1 line
2.39.5
#1 lore raw

PATCH RESEND v7 1/2 net: Prevent RPS table overwrite of active flows

This patch fixes an issue where two different flows on the same RXq produce the same hash resulting in continuous flow overwrites.

Flow #1: A packet for Flow #1 comes in, kernel calls the steering
function. The driver gives back a filter id. The kernel saves this filter id in the selected slot. Later, the driver's service task checks if any filters have expired and then installs the rule for Flow #1.
Flow #2: A packet for Flow #2 comes in. It goes through the same steps.
But this time, the chosen slot is being used by Flow #1. The driver gives a new filter id and the kernel saves it in the same slot. When the driver's service task runs, it runs through all the flows, checks if Flow #1 should be expired, the kernel returns True as the slot has a different filter id, and then the driver installs the rule for Flow #2.
Flow #1: Another packet for Flow #1 comes in. The same thing repeats.
The slot is overwritten with a new filter id for Flow #1.

This causes a repeated cycle of flow programming for missed packets, wasting CPU cycles while not improving performance. This problem happens at higher rates when the RPS table is small, but tests show it still happens even with 12,000 connections and an RPS size of 16K per queue (global table size = 144x16K = 64K).

This patch prevents overwriting an rps_dev_flow entry if it is active. The intention is that it is better to do aRFS for the first flow instead of hurting all flows on the same hash. Without this, two (or more) flows on one RX queue with the same hash can keep overwriting each other. This causes the driver to reprogram the flow repeatedly.

Changes:
1. Add a new 'hash' field to struct rps_dev_flow.
2. Add rps_flow_is_active(): a helper function to check if a flow is active or not, extracted from rps_may_expire_flow(). It is further simplified as per reviewer feedback.
3. In set_rps_cpu():
- Avoid overwriting by programming a new filter if:
- The slot is not in use, or
- The slot is in use but the flow is not active, or
- The slot has an active flow with the same hash, but target CPU differs.
- Save the hash in the rps_dev_flow entry.
4. rps_may_expire_flow(): Use earlier extracted rps_flow_is_active().

Testing & results:
- Driver: ice (E810 NIC), Kernel: net-next
- #CPUs = #RXq = 144 (1:1)
- Number of flows: 12K
- Eight RPS settings from 256 to 32768. Though RPS=256 is not ideal, it is still sufficient to cover 12K flows (256*144 rx-queues = 64K global table slots)
- Global Table Size = 144 * RPS (effectively equal to 256 * RPS)
- Each RPS test duration = 8 mins (org code) + 8 mins (new code).
- Metrics captured on client

Legend for following tables:

Steer-C: #times ndo_rx_flow_steer() was Called by set_rps_cpu()
Steer-L: #times ice_arfs_flow_steer() Looped over aRFS entries
Add:     #times driver actually programmed aRFS (ice_arfs_build_entry())
Del:     #times driver deleted the flow (ice_arfs_del_flow_rules())
Units:   K = 1,000 times, M = 1 million times
  |-------|---------|------|     Org Code    |---------|---------|
  | RPS   | Latency | CPU  | Add    |  Del   | Steer-C | Steer-L |
  |-------|---------|------|--------|--------|---------|---------|
  | 256   | 227.0   | 93.2 | 1.6M   | 1.6M   | 121.7M  | 267.6M  |
  | 512   | 225.9   | 94.1 | 11.5M  | 11.2M  | 65.7M   | 199.6M  |
  | 1024  | 223.5   | 95.6 | 16.5M  | 16.5M  | 27.1M   | 187.3M  |
  | 2048  | 222.2   | 96.3 | 10.5M  | 10.5M  | 12.5M   | 115.2M  |
  | 4096  | 223.9   | 94.1 | 5.5M   | 5.5M   | 7.2M    | 65.9M   |
  | 8192  | 224.7   | 92.5 | 2.7M   | 2.7M   | 3.0M    | 29.9M   |
  | 16384 | 223.5   | 92.5 | 1.3M   | 1.3M   | 1.4M    | 13.9M   |
  | 32768 | 219.6   | 93.2 | 838.1K | 838.1K | 965.1K  | 8.9M    |
  |-------|---------|------|   New Code      |---------|---------|
  | 256   | 201.5   | 99.1 | 13.4K  | 5.0K   | 13.7K   | 75.2K   |
  | 512   | 202.5   | 98.2 | 11.2K  | 5.9K   | 11.2K   | 55.5K   |
  | 1024  | 207.3   | 93.9 | 11.5K  | 9.7K   | 11.5K   | 59.6K   |
  | 2048  | 207.5   | 96.7 | 11.8K  | 11.1K  | 15.5K   | 79.3K   |
  | 4096  | 206.9   | 96.6 | 11.8K  | 11.7K  | 11.8K   | 63.2K   |
  | 8192  | 205.8   | 96.7 | 11.9K  | 11.8K  | 11.9K   | 63.9K   |
  | 16384 | 200.9   | 98.2 | 11.9K  | 11.9K  | 11.9K   | 64.2K   |
  | 32768 | 202.5   | 98.0 | 11.9K  | 11.9K  | 11.9K   | 64.2K   |
  |-------|---------|------|--------|--------|---------|---------|

Some observations:
1. Overall Latency improved: (1790.19-1634.94)/1790.19*100 = 8.67%
2. Overall CPU increased: (777.32-751.49)/751.45*100 = 3.44%
3. Flow Management (add/delete) remained almost constant at ~11K compared to values in millions.

---
 include/net/rps.h    |  7 +++--
 net/core/dev.c       | 64 +++++++++++++++++++++++++++++++++++++++-----
 net/core/net-sysfs.c |  4 ++-
 3 files changed, 65 insertions(+), 10 deletions(-)
include/net/rps.h +5 -2
index d8ab3a08bcc4..73ce56534715 100644
@@ -25,13 +25,16 @@ struct rps_map {
 
 /*
  * The rps_dev_flow structure contains the mapping of a flow to a CPU, the
- * tail pointer for that CPU's input queue at the time of last enqueue, and
- * a hardware filter index.
+ * tail pointer for that CPU's input queue at the time of last enqueue, a
+ * hardware filter index, and the hash of the flow if aRFS is enabled.
  */
 struct rps_dev_flow {
 	u16		cpu;
 	u16		filter;
 	unsigned int	last_qtail;
+#ifdef CONFIG_RFS_ACCEL
+	u32		hash;
+#endif
 };
 #define RPS_NO_FILTER 0xffff
 
net/core/dev.c +57 -7

102 lines not on this page · raw message

net/core/net-sysfs.c +3 -1
index c28cd6665444..5ea9f64adce3 100644
@@ -1120,8 +1120,10 @@ static ssize_t store_rps_dev_flow_table_cnt(struct netdev_rx_queue *queue,
 			return -ENOMEM;
 
 		table->log = ilog2(mask) + 1;
-		for (count = 0; count <= mask; count++)
+		for (count = 0; count <= mask; count++) {
 			table->flows[count].cpu = RPS_NO_CPU;
+			table->flows[count].filter = RPS_NO_FILTER;
+		}
 	} else {
 		table = NULL;
 	}
Signature · 1 line
2.39.5
#2 lore raw

PATCH RESEND v7 2/2 net: Cache hash and flow_id to avoid recalculation

In reply to Krishna Kumar

get_rps_cpu() can cache flow_id and hash as both are required by set_rps_cpu() instead of recalculating them twice.

---
 net/core/dev.c | 15 +++++++--------
 1 file changed, 7 insertions(+), 8 deletions(-)
net/core/dev.c +7 -8
index 656eceb18e67..5083f97801d4 100644
@@ -4869,7 +4869,8 @@ static bool rps_flow_is_active(struct rps_dev_flow *rflow,
 
 static struct rps_dev_flow *
 set_rps_cpu(struct net_device *dev, struct sk_buff *skb,
-	    struct rps_dev_flow *rflow, u16 next_cpu)
+	    struct rps_dev_flow *rflow, u16 next_cpu, u32 hash,
+	    u32 flow_id)
 {
 	if (next_cpu < nr_cpu_ids) {
 		u32 head;
@@ -4880,8 +4881,6 @@ set_rps_cpu(struct net_device *dev, struct sk_buff *skb,
 		struct rps_dev_flow *tmp_rflow;
 		unsigned int tmp_cpu;
 		u16 rxq_index;
-		u32 flow_id;
-		u32 hash;
 		int rc;
 
 		/* Should we steer this flow to a different hardware queue? */
@@ -4897,9 +4896,6 @@ set_rps_cpu(struct net_device *dev, struct sk_buff *skb,
 		if (!flow_table)
 			goto out;
 
-		hash = skb_get_hash(skb);
-		flow_id = rfs_slot(hash, flow_table);
-
 		tmp_rflow = &flow_table->flows[flow_id];
 		tmp_cpu = READ_ONCE(tmp_rflow->cpu);
 
@@ -4947,6 +4943,7 @@ static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
 	struct rps_dev_flow_table *flow_table;
 	struct rps_map *map;
 	int cpu = -1;
+	u32 flow_id;
 	u32 tcpu;
 	u32 hash;
 
@@ -4993,7 +4990,8 @@ static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
 		/* OK, now we know there is a match,
 		 * we can look at the local (per receive queue) flow table
 		 */
-		rflow = &flow_table->flows[rfs_slot(hash, flow_table)];
+		flow_id = rfs_slot(hash, flow_table);
+		rflow = &flow_table->flows[flow_id];
 		tcpu = rflow->cpu;
 
 		/*
@@ -5012,7 +5010,8 @@ static int get_rps_cpu(struct net_device *dev, struct sk_buff *skb,
 		     ((int)(READ_ONCE(per_cpu(softnet_data, tcpu).input_queue_head) -
 		      rflow->last_qtail)) >= 0)) {
 			tcpu = next_cpu;
-			rflow = set_rps_cpu(dev, skb, rflow, next_cpu);
+			rflow = set_rps_cpu(dev, skb, rflow, next_cpu, hash,
+					    flow_id);
 		}
 
 		if (tcpu < nr_cpu_ids && cpu_online(tcpu)) {
Signature · 1 line
2.39.5
#3 lore raw
In reply to Krishna Kumar
This series was applied to netdev/net-next.git (main) by Jakub Kicinski <[email protected]>:

Hello:

This series was applied to netdev/net-next.git (main) by Jakub Kicinski <[email protected]>:

Quoted 14 lines · On Mon, 25 Aug 2025 08:40:03 +0530 you wrote:

As requested, I am resending the same [3] patch after the 6.17 merge window. It applies cleanly to the current net-next tree and has been tested under heavy traffic for 15 minutes.

This series splits the original RPS patch [1] into two patches for net-next. It also addresses a kernel test robot warning by defining rps_flow_is_active() only when aRFS is enabled. I tested v3 with four builds and reboots: two for [PATCH 1/2] with aRFS enabled & disabled, and two for [PATCH 2/2]. There are no code changes in v4 and v5, only documentation. Patch v6 has one line change to keep 'hash' field under #ifdef, and was test built with aRFS=on and aRFS=off. The same two builds were done for v7, along with 15m load testing with aRFS=on to ensure the new changes are correct.

Quoted 1 line

[...]

Here is the summary with links:
- [RESEND,v7,net-next,1/2] net: Prevent RPS table overwrite of active flows

    https://git.kernel.org/netdev/net-next/c/97bcc5b6f454
  - [RESEND,v7,net-next,2/2] net: Cache hash and flow_id to avoid recalculation
    https://git.kernel.org/netdev/net-next/c/48aa30443e52

You are awesome, thank you!

Signature · 2 lines
Deet-doot-dot, I am a bot.
https://korg.docs.kernel.org/patchwork/pwbot.html