diff options
| author | Jakub Kicinski <kuba@kernel.org> | 2026-06-24 11:20:15 -0700 |
|---|---|---|
| committer | Jakub Kicinski <kuba@kernel.org> | 2026-06-25 10:18:40 -0700 |
| commit | 12c765be84d28f22deca10e775889f54bd571a85 (patch) | |
| tree | 7948df147704d04fd0a9fb3669575407b365f298 /net/core/netdev_work.c | |
| parent | 1105ef941c1a28e115d1b97f17e1c85576884100 (diff) | |
net: turn the rx_mode work into a generic netdev_work facility
The rx_mode update runs from a workqueue: drivers have their
ndo_set_rx_mode_async() callback executed by a single global
work item under RTNL and ops lock. This is a useful pattern.
Support multiple "events" that need to be serviced and make RX_MODE
sync the first one. Call the events "core" because later on
we will let drivers define and schedule their own.
Reviewed-by: Kuniyuki Iwashima <kuniyu@google.com>
Acked-by: Stanislav Fomichev <sdf@fomichev.me>
Link: https://patch.msgid.link/20260624182018.2445732-2-kuba@kernel.org
Signed-off-by: Jakub Kicinski <kuba@kernel.org>
Diffstat (limited to 'net/core/netdev_work.c')
| -rw-r--r-- | net/core/netdev_work.c | 117 |
1 files changed, 117 insertions, 0 deletions
diff --git a/net/core/netdev_work.c b/net/core/netdev_work.c new file mode 100644 index 000000000000..c121c24dc493 --- /dev/null +++ b/net/core/netdev_work.c @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include <linux/list.h> +#include <linux/netdevice.h> +#include <linux/rtnetlink.h> +#include <linux/spinlock.h> +#include <linux/workqueue.h> +#include <net/netdev_lock.h> + +#include "dev.h" + +static void netdev_work_proc(struct work_struct *work); + +/* @netdev_work_lock protects: + * - @netdev_work_list + * - within the list entries (struct net_device fields): + * - work_node + * - work_tracker + * - work_core_pending + */ +static LIST_HEAD(netdev_work_list); +static DEFINE_SPINLOCK(netdev_work_lock); +static DECLARE_WORK(netdev_work, netdev_work_proc); + +void __netdev_work_core_sched(struct net_device *dev, unsigned long event) +{ + spin_lock_bh(&netdev_work_lock); + if (list_empty(&dev->work_node)) { + list_add_tail(&dev->work_node, &netdev_work_list); + netdev_hold(dev, &dev->work_tracker, GFP_ATOMIC); + } + dev->work_core_pending |= event; + spin_unlock_bh(&netdev_work_lock); + + schedule_work(&netdev_work); +} + +/** + * __netdev_work_core_cancel() - cancel selected core work for a netdev + * @dev: net_device + * @mask: events to cancel + * + * Clear @mask from the device's work pending mask. If no work is left pending + * the device is dequeued. + * + * No expectations on locking, but also no guarantees provided. If the caller + * wants to touch @dev afterwards (e.g. call the work that got canceled) + * they have to ensure @dev does not get freed. + * + * Returns: the subset of @mask that was actually pending, so the caller can run + * those events inline. + */ +unsigned long +__netdev_work_core_cancel(struct net_device *dev, unsigned long mask) +{ + unsigned long event; + + spin_lock_bh(&netdev_work_lock); + event = dev->work_core_pending & mask; + dev->work_core_pending &= ~mask; + if (!list_empty(&dev->work_node) && !dev->work_core_pending) { + list_del_init(&dev->work_node); + netdev_put(dev, &dev->work_tracker); + } + spin_unlock_bh(&netdev_work_lock); + + return event; +} + +static void netdev_work_proc(struct work_struct *work) +{ + rtnl_lock(); + + while (true) { + netdevice_tracker tracker; + struct net_device *dev; + unsigned long core = 0; + + spin_lock_bh(&netdev_work_lock); + if (list_empty(&netdev_work_list)) { + spin_unlock_bh(&netdev_work_lock); + break; + } + dev = list_first_entry(&netdev_work_list, struct net_device, + work_node); + /* Take a temporary reference so @dev can't be freed while we + * drop the lock to grab its ops lock; the work reference is + * only released once we claim the work below. + * The re-locking dance is to ensure that ops lock is enough + * to ensure canceling work is not racy with dequeue. + */ + netdev_hold(dev, &tracker, GFP_ATOMIC); + spin_unlock_bh(&netdev_work_lock); + + netdev_lock_ops(dev); + spin_lock_bh(&netdev_work_lock); + if (!list_empty(&dev->work_node)) { + list_del_init(&dev->work_node); + core = dev->work_core_pending; + dev->work_core_pending = 0; + /* We took another ref above */ + netdev_put(dev, &dev->work_tracker); + + if (!dev_isalive(dev)) + core = 0; + } + spin_unlock_bh(&netdev_work_lock); + + if (core & NETDEV_WORK_RX_MODE) + netif_rx_mode_run(dev); + netdev_unlock_ops(dev); + + netdev_put(dev, &tracker); + } + + rtnl_unlock(); +} |
