mirror of
https://github.com/torvalds/linux.git
synced 2024-12-02 00:51:44 +00:00
Merge branch 'implement-kthread-based-napi-poll'
Wei Wang says: ==================== implement kthread based napi polle The idea of moving the napi poll process out of softirq context to a kernel thread based context is not new. Paolo Abeni and Hannes Frederic Sowa have proposed patches to move napi poll to kthread back in 2016. And Felix Fietkau has also proposed patches of similar ideas to use workqueue to process napi poll just a few weeks ago. The main reason we'd like to push forward with this idea is that the scheduler has poor visibility into cpu cycles spent in softirq context, and is not able to make optimal scheduling decisions of the user threads. For example, we see in one of the application benchmark where network load is high, the CPUs handling network softirqs has ~80% cpu util. And user threads are still scheduled on those CPUs, despite other more idle cpus available in the system. And we see very high tail latencies. In this case, we have to explicitly pin away user threads from the CPUs handling network softirqs to ensure good performance. With napi poll moved to kthread, scheduler is in charge of scheduling both the kthreads handling network load, and the user threads, and is able to make better decisions. In the previous benchmark, if we do this and we pin the kthreads processing napi poll to specific CPUs, scheduler is able to schedule user threads away from these CPUs automatically. And the reason we prefer 1 kthread per napi, instead of 1 workqueue entity per host, is that kthread is more configurable than workqueue, and we could leverage existing tuning tools for threads, like taskset, chrt, etc to tune scheduling class and cpu set, etc. Another reason is if we eventually want to provide busy poll feature using kernel threads for napi poll, kthread seems to be more suitable than workqueue. Furthermore, for large platforms with 2 NICs attached to 2 sockets, kthread is more flexible to be pinned to different sets of CPUs. In this patch series, I revived Paolo and Hannes's patch in 2016 and made modifications. Then there are changes proposed by Felix, Jakub, Paolo and myself on top of those, with suggestions from Eric Dumazet. In terms of performance, I ran tcp_rr tests with 1000 flows with various request/response sizes, with RFS/RPS disabled, and compared performance between softirq vs kthread vs workqueue (patchset proposed by Felix Fietkau). Host has 56 hyper threads and 100Gbps nic, 8 rx queues and only 1 numa node. All threads are unpinned. req/resp QPS 50%tile 90%tile 99%tile 99.9%tile softirq 1B/1B 2.75M 337us 376us 1.04ms 3.69ms kthread 1B/1B 2.67M 371us 408us 455us 550us workq 1B/1B 2.56M 384us 435us 673us 822us softirq 5KB/5KB 1.46M 678us 750us 969us 2.78ms kthread 5KB/5KB 1.44M 695us 789us 891us 1.06ms workq 5KB/5KB 1.34M 720us 905us 1.06ms 1.57ms softirq 1MB/1MB 11.0K 79ms 166ms 306ms 630ms kthread 1MB/1MB 11.0K 75ms 177ms 303ms 596ms workq 1MB/1MB 11.0K 79ms 180ms 303ms 587ms When running workqueue implementation, I found the number of threads used is usually twice as much as kthread implementation. This probably introduces higher scheduling cost, which results in higher tail latencies in most cases. I also ran an application benchmark, which performs fixed qps remote SSD read/write operations, with various sizes. Again, both with RFS/RPS disabled. The result is as follows: op_size QPS 50%tile 95%tile 99%tile 99.9%tile softirq 4K 572.6K 385us 1.5ms 3.16ms 6.41ms kthread 4K 572.6K 390us 803us 2.21ms 6.83ms workq 4k 572.6K 384us 763us 3.12ms 6.87ms softirq 64K 157.9K 736us 1.17ms 3.40ms 13.75ms kthread 64K 157.9K 745us 1.23ms 2.76ms 9.87ms workq 64K 157.9K 746us 1.23ms 2.76ms 9.96ms softirq 1M 10.98K 2.03ms 3.10ms 3.7ms 11.56ms kthread 1M 10.98K 2.13ms 3.21ms 4.02ms 13.3ms workq 1M 10.98K 2.13ms 3.20ms 3.99ms 14.12ms In this set of tests, the latency is predominant by the SSD operation. Also, the user threads are much busier compared to tcp_rr tests. We have to pin the kthreads/workqueue threads to limit to a few CPUs, to not disturb user threads, and provide some isolation. Changes since v9: Small change in napi_poll() in patch 1. Split napi_kthread_stop() functionality to add separately in napi_disable() and netif_napi_del() in patch 2. Add description for napi_set_threaded() and return dev->threaded when dev->napi_list is empty for threaded sysfs in patch 3. Changes since v8: Added description for threaded param in struct net_device in patch 2. Changes since v7: Break napi_set_threaded() into 2 parts, one to create kthread called from netif_napi_add(), the other to set threaded bit in napi_enable(), to get rid of inconsistency through all napi in 1 dev. Added documentation for /sys/class/net/<dev>/threaded. Changes since v6: Added memory barrier in napi_set_threaded(). Changed /sys/class/net/<dev>/thread to a ternary value. Change dev->threaded to a bit instead of bool. Changes since v5: Removed ASSERT_RTNL() from napi_set_threaded() and removed rtnl_lock() operation from napi_enable(). Changes since v4: Recorded the threaded setting in dev and restore it in napi_enable(). Changes since v3: Merged and rearranged patches in a logical order for easier review. Changed sysfs control to be per device. Changes since v2: Corrected typo in patch 1, and updated the cover letter with more detailed and updated test results. Changes since v1: Replaced kthread_create() with kthread_run() in patch 5 as suggested by Felix Fietkau. Changes since RFC: Renamed the kthreads to be napi/<dev>-<napi_id> in patch 5 as suggested by Hannes Frederic Sowa. ==================== Signed-off-by: David S. Miller <davem@davemloft.net>
This commit is contained in:
commit
adbb4fb028
@ -337,3 +337,18 @@ Contact: netdev@vger.kernel.org
|
||||
Description:
|
||||
32-bit unsigned integer counting the number of times the link has
|
||||
been down
|
||||
|
||||
What: /sys/class/net/<iface>/threaded
|
||||
Date: Jan 2021
|
||||
KernelVersion: 5.12
|
||||
Contact: netdev@vger.kernel.org
|
||||
Description:
|
||||
Boolean value to control the threaded mode per device. User could
|
||||
set this value to enable/disable threaded mode for all napi
|
||||
belonging to this device, without the need to do device up/down.
|
||||
|
||||
Possible values:
|
||||
== ==================================
|
||||
0 threaded mode disabled for this dev
|
||||
1 threaded mode enabled for this dev
|
||||
== ==================================
|
||||
|
@ -347,6 +347,7 @@ struct napi_struct {
|
||||
struct list_head dev_list;
|
||||
struct hlist_node napi_hash_node;
|
||||
unsigned int napi_id;
|
||||
struct task_struct *thread;
|
||||
};
|
||||
|
||||
enum {
|
||||
@ -358,6 +359,7 @@ enum {
|
||||
NAPI_STATE_NO_BUSY_POLL, /* Do not add in napi_hash, no busy polling */
|
||||
NAPI_STATE_IN_BUSY_POLL, /* sk_busy_loop() owns this NAPI */
|
||||
NAPI_STATE_PREFER_BUSY_POLL, /* prefer busy-polling over softirq processing*/
|
||||
NAPI_STATE_THREADED, /* The poll is performed inside its own thread*/
|
||||
};
|
||||
|
||||
enum {
|
||||
@ -369,6 +371,7 @@ enum {
|
||||
NAPIF_STATE_NO_BUSY_POLL = BIT(NAPI_STATE_NO_BUSY_POLL),
|
||||
NAPIF_STATE_IN_BUSY_POLL = BIT(NAPI_STATE_IN_BUSY_POLL),
|
||||
NAPIF_STATE_PREFER_BUSY_POLL = BIT(NAPI_STATE_PREFER_BUSY_POLL),
|
||||
NAPIF_STATE_THREADED = BIT(NAPI_STATE_THREADED),
|
||||
};
|
||||
|
||||
enum gro_result {
|
||||
@ -494,6 +497,8 @@ static inline bool napi_complete(struct napi_struct *n)
|
||||
return napi_complete_done(n, 0);
|
||||
}
|
||||
|
||||
int dev_set_threaded(struct net_device *dev, bool threaded);
|
||||
|
||||
/**
|
||||
* napi_disable - prevent NAPI from scheduling
|
||||
* @n: NAPI context
|
||||
@ -503,20 +508,7 @@ static inline bool napi_complete(struct napi_struct *n)
|
||||
*/
|
||||
void napi_disable(struct napi_struct *n);
|
||||
|
||||
/**
|
||||
* napi_enable - enable NAPI scheduling
|
||||
* @n: NAPI context
|
||||
*
|
||||
* Resume NAPI from being scheduled on this context.
|
||||
* Must be paired with napi_disable.
|
||||
*/
|
||||
static inline void napi_enable(struct napi_struct *n)
|
||||
{
|
||||
BUG_ON(!test_bit(NAPI_STATE_SCHED, &n->state));
|
||||
smp_mb__before_atomic();
|
||||
clear_bit(NAPI_STATE_SCHED, &n->state);
|
||||
clear_bit(NAPI_STATE_NPSVC, &n->state);
|
||||
}
|
||||
void napi_enable(struct napi_struct *n);
|
||||
|
||||
/**
|
||||
* napi_synchronize - wait until NAPI is not running
|
||||
@ -1827,6 +1819,8 @@ enum netdev_priv_flags {
|
||||
*
|
||||
* @wol_enabled: Wake-on-LAN is enabled
|
||||
*
|
||||
* @threaded: napi threaded mode is enabled
|
||||
*
|
||||
* @net_notifier_list: List of per-net netdev notifier block
|
||||
* that follow this device when it is moved
|
||||
* to another network namespace.
|
||||
@ -2145,6 +2139,7 @@ struct net_device {
|
||||
struct lock_class_key *qdisc_running_key;
|
||||
bool proto_down;
|
||||
unsigned wol_enabled:1;
|
||||
unsigned threaded:1;
|
||||
|
||||
struct list_head net_notifier_list;
|
||||
|
||||
|
192
net/core/dev.c
192
net/core/dev.c
@ -91,6 +91,7 @@
|
||||
#include <linux/etherdevice.h>
|
||||
#include <linux/ethtool.h>
|
||||
#include <linux/skbuff.h>
|
||||
#include <linux/kthread.h>
|
||||
#include <linux/bpf.h>
|
||||
#include <linux/bpf_trace.h>
|
||||
#include <net/net_namespace.h>
|
||||
@ -1494,6 +1495,27 @@ void netdev_notify_peers(struct net_device *dev)
|
||||
}
|
||||
EXPORT_SYMBOL(netdev_notify_peers);
|
||||
|
||||
static int napi_threaded_poll(void *data);
|
||||
|
||||
static int napi_kthread_create(struct napi_struct *n)
|
||||
{
|
||||
int err = 0;
|
||||
|
||||
/* Create and wake up the kthread once to put it in
|
||||
* TASK_INTERRUPTIBLE mode to avoid the blocked task
|
||||
* warning and work with loadavg.
|
||||
*/
|
||||
n->thread = kthread_run(napi_threaded_poll, n, "napi/%s-%d",
|
||||
n->dev->name, n->napi_id);
|
||||
if (IS_ERR(n->thread)) {
|
||||
err = PTR_ERR(n->thread);
|
||||
pr_err("kthread_run failed with err %d\n", err);
|
||||
n->thread = NULL;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
static int __dev_open(struct net_device *dev, struct netlink_ext_ack *extack)
|
||||
{
|
||||
const struct net_device_ops *ops = dev->netdev_ops;
|
||||
@ -4265,6 +4287,22 @@ int gro_normal_batch __read_mostly = 8;
|
||||
static inline void ____napi_schedule(struct softnet_data *sd,
|
||||
struct napi_struct *napi)
|
||||
{
|
||||
struct task_struct *thread;
|
||||
|
||||
if (test_bit(NAPI_STATE_THREADED, &napi->state)) {
|
||||
/* Paired with smp_mb__before_atomic() in
|
||||
* napi_enable()/dev_set_threaded().
|
||||
* Use READ_ONCE() to guarantee a complete
|
||||
* read on napi->thread. Only call
|
||||
* wake_up_process() when it's not NULL.
|
||||
*/
|
||||
thread = READ_ONCE(napi->thread);
|
||||
if (thread) {
|
||||
wake_up_process(thread);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
list_add_tail(&napi->poll_list, &sd->poll_list);
|
||||
__raise_softirq_irqoff(NET_RX_SOFTIRQ);
|
||||
}
|
||||
@ -6701,6 +6739,49 @@ static void init_gro_hash(struct napi_struct *napi)
|
||||
napi->gro_bitmask = 0;
|
||||
}
|
||||
|
||||
int dev_set_threaded(struct net_device *dev, bool threaded)
|
||||
{
|
||||
struct napi_struct *napi;
|
||||
int err = 0;
|
||||
|
||||
if (dev->threaded == threaded)
|
||||
return 0;
|
||||
|
||||
if (threaded) {
|
||||
list_for_each_entry(napi, &dev->napi_list, dev_list) {
|
||||
if (!napi->thread) {
|
||||
err = napi_kthread_create(napi);
|
||||
if (err) {
|
||||
threaded = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dev->threaded = threaded;
|
||||
|
||||
/* Make sure kthread is created before THREADED bit
|
||||
* is set.
|
||||
*/
|
||||
smp_mb__before_atomic();
|
||||
|
||||
/* Setting/unsetting threaded mode on a napi might not immediately
|
||||
* take effect, if the current napi instance is actively being
|
||||
* polled. In this case, the switch between threaded mode and
|
||||
* softirq mode will happen in the next round of napi_schedule().
|
||||
* This should not cause hiccups/stalls to the live traffic.
|
||||
*/
|
||||
list_for_each_entry(napi, &dev->napi_list, dev_list) {
|
||||
if (threaded)
|
||||
set_bit(NAPI_STATE_THREADED, &napi->state);
|
||||
else
|
||||
clear_bit(NAPI_STATE_THREADED, &napi->state);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
|
||||
int (*poll)(struct napi_struct *, int), int weight)
|
||||
{
|
||||
@ -6728,6 +6809,12 @@ void netif_napi_add(struct net_device *dev, struct napi_struct *napi,
|
||||
set_bit(NAPI_STATE_NPSVC, &napi->state);
|
||||
list_add_rcu(&napi->dev_list, &dev->napi_list);
|
||||
napi_hash_add(napi);
|
||||
/* Create kthread for this napi if dev->threaded is set.
|
||||
* Clear dev->threaded if kthread creation failed so that
|
||||
* threaded mode will not be enabled in napi_enable().
|
||||
*/
|
||||
if (dev->threaded && napi_kthread_create(napi))
|
||||
dev->threaded = 0;
|
||||
}
|
||||
EXPORT_SYMBOL(netif_napi_add);
|
||||
|
||||
@ -6745,9 +6832,28 @@ void napi_disable(struct napi_struct *n)
|
||||
|
||||
clear_bit(NAPI_STATE_PREFER_BUSY_POLL, &n->state);
|
||||
clear_bit(NAPI_STATE_DISABLE, &n->state);
|
||||
clear_bit(NAPI_STATE_THREADED, &n->state);
|
||||
}
|
||||
EXPORT_SYMBOL(napi_disable);
|
||||
|
||||
/**
|
||||
* napi_enable - enable NAPI scheduling
|
||||
* @n: NAPI context
|
||||
*
|
||||
* Resume NAPI from being scheduled on this context.
|
||||
* Must be paired with napi_disable.
|
||||
*/
|
||||
void napi_enable(struct napi_struct *n)
|
||||
{
|
||||
BUG_ON(!test_bit(NAPI_STATE_SCHED, &n->state));
|
||||
smp_mb__before_atomic();
|
||||
clear_bit(NAPI_STATE_SCHED, &n->state);
|
||||
clear_bit(NAPI_STATE_NPSVC, &n->state);
|
||||
if (n->dev->threaded && n->thread)
|
||||
set_bit(NAPI_STATE_THREADED, &n->state);
|
||||
}
|
||||
EXPORT_SYMBOL(napi_enable);
|
||||
|
||||
static void flush_gro_hash(struct napi_struct *napi)
|
||||
{
|
||||
int i;
|
||||
@ -6773,18 +6879,18 @@ void __netif_napi_del(struct napi_struct *napi)
|
||||
|
||||
flush_gro_hash(napi);
|
||||
napi->gro_bitmask = 0;
|
||||
|
||||
if (napi->thread) {
|
||||
kthread_stop(napi->thread);
|
||||
napi->thread = NULL;
|
||||
}
|
||||
}
|
||||
EXPORT_SYMBOL(__netif_napi_del);
|
||||
|
||||
static int napi_poll(struct napi_struct *n, struct list_head *repoll)
|
||||
static int __napi_poll(struct napi_struct *n, bool *repoll)
|
||||
{
|
||||
void *have;
|
||||
int work, weight;
|
||||
|
||||
list_del_init(&n->poll_list);
|
||||
|
||||
have = netpoll_poll_lock(n);
|
||||
|
||||
weight = n->weight;
|
||||
|
||||
/* This NAPI_STATE_SCHED test is for avoiding a race
|
||||
@ -6804,7 +6910,7 @@ static int napi_poll(struct napi_struct *n, struct list_head *repoll)
|
||||
n->poll, work, weight);
|
||||
|
||||
if (likely(work < weight))
|
||||
goto out_unlock;
|
||||
return work;
|
||||
|
||||
/* Drivers must not modify the NAPI state if they
|
||||
* consume the entire weight. In such cases this code
|
||||
@ -6813,7 +6919,7 @@ static int napi_poll(struct napi_struct *n, struct list_head *repoll)
|
||||
*/
|
||||
if (unlikely(napi_disable_pending(n))) {
|
||||
napi_complete(n);
|
||||
goto out_unlock;
|
||||
return work;
|
||||
}
|
||||
|
||||
/* The NAPI context has more processing work, but busy-polling
|
||||
@ -6826,7 +6932,7 @@ static int napi_poll(struct napi_struct *n, struct list_head *repoll)
|
||||
*/
|
||||
napi_schedule(n);
|
||||
}
|
||||
goto out_unlock;
|
||||
return work;
|
||||
}
|
||||
|
||||
if (n->gro_bitmask) {
|
||||
@ -6844,17 +6950,79 @@ static int napi_poll(struct napi_struct *n, struct list_head *repoll)
|
||||
if (unlikely(!list_empty(&n->poll_list))) {
|
||||
pr_warn_once("%s: Budget exhausted after napi rescheduled\n",
|
||||
n->dev ? n->dev->name : "backlog");
|
||||
goto out_unlock;
|
||||
return work;
|
||||
}
|
||||
|
||||
list_add_tail(&n->poll_list, repoll);
|
||||
*repoll = true;
|
||||
|
||||
return work;
|
||||
}
|
||||
|
||||
static int napi_poll(struct napi_struct *n, struct list_head *repoll)
|
||||
{
|
||||
bool do_repoll = false;
|
||||
void *have;
|
||||
int work;
|
||||
|
||||
list_del_init(&n->poll_list);
|
||||
|
||||
have = netpoll_poll_lock(n);
|
||||
|
||||
work = __napi_poll(n, &do_repoll);
|
||||
|
||||
if (do_repoll)
|
||||
list_add_tail(&n->poll_list, repoll);
|
||||
|
||||
out_unlock:
|
||||
netpoll_poll_unlock(have);
|
||||
|
||||
return work;
|
||||
}
|
||||
|
||||
static int napi_thread_wait(struct napi_struct *napi)
|
||||
{
|
||||
set_current_state(TASK_INTERRUPTIBLE);
|
||||
|
||||
while (!kthread_should_stop() && !napi_disable_pending(napi)) {
|
||||
if (test_bit(NAPI_STATE_SCHED, &napi->state)) {
|
||||
WARN_ON(!list_empty(&napi->poll_list));
|
||||
__set_current_state(TASK_RUNNING);
|
||||
return 0;
|
||||
}
|
||||
|
||||
schedule();
|
||||
set_current_state(TASK_INTERRUPTIBLE);
|
||||
}
|
||||
__set_current_state(TASK_RUNNING);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int napi_threaded_poll(void *data)
|
||||
{
|
||||
struct napi_struct *napi = data;
|
||||
void *have;
|
||||
|
||||
while (!napi_thread_wait(napi)) {
|
||||
for (;;) {
|
||||
bool repoll = false;
|
||||
|
||||
local_bh_disable();
|
||||
|
||||
have = netpoll_poll_lock(napi);
|
||||
__napi_poll(napi, &repoll);
|
||||
netpoll_poll_unlock(have);
|
||||
|
||||
__kfree_skb_flush();
|
||||
local_bh_enable();
|
||||
|
||||
if (!repoll)
|
||||
break;
|
||||
|
||||
cond_resched();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static __latent_entropy void net_rx_action(struct softirq_action *h)
|
||||
{
|
||||
struct softnet_data *sd = this_cpu_ptr(&softnet_data);
|
||||
|
@ -538,6 +538,45 @@ static ssize_t phys_switch_id_show(struct device *dev,
|
||||
}
|
||||
static DEVICE_ATTR_RO(phys_switch_id);
|
||||
|
||||
static ssize_t threaded_show(struct device *dev,
|
||||
struct device_attribute *attr, char *buf)
|
||||
{
|
||||
struct net_device *netdev = to_net_dev(dev);
|
||||
ssize_t ret = -EINVAL;
|
||||
|
||||
if (!rtnl_trylock())
|
||||
return restart_syscall();
|
||||
|
||||
if (dev_isalive(netdev))
|
||||
ret = sprintf(buf, fmt_dec, netdev->threaded);
|
||||
|
||||
rtnl_unlock();
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int modify_napi_threaded(struct net_device *dev, unsigned long val)
|
||||
{
|
||||
int ret;
|
||||
|
||||
if (list_empty(&dev->napi_list))
|
||||
return -EOPNOTSUPP;
|
||||
|
||||
if (val != 0 && val != 1)
|
||||
return -EOPNOTSUPP;
|
||||
|
||||
ret = dev_set_threaded(dev, val);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static ssize_t threaded_store(struct device *dev,
|
||||
struct device_attribute *attr,
|
||||
const char *buf, size_t len)
|
||||
{
|
||||
return netdev_store(dev, attr, buf, len, modify_napi_threaded);
|
||||
}
|
||||
static DEVICE_ATTR_RW(threaded);
|
||||
|
||||
static struct attribute *net_class_attrs[] __ro_after_init = {
|
||||
&dev_attr_netdev_group.attr,
|
||||
&dev_attr_type.attr,
|
||||
@ -570,6 +609,7 @@ static struct attribute *net_class_attrs[] __ro_after_init = {
|
||||
&dev_attr_proto_down.attr,
|
||||
&dev_attr_carrier_up_count.attr,
|
||||
&dev_attr_carrier_down_count.attr,
|
||||
&dev_attr_threaded.attr,
|
||||
NULL,
|
||||
};
|
||||
ATTRIBUTE_GROUPS(net_class);
|
||||
|
Loading…
Reference in New Issue
Block a user