mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-07-18 08:28:00 -04:00
With an extra event mask we can easily extend the netdev work to also service driver-defined events. For advanced drivers this is probably not a perfect match, but it makes running deferred work easier in simple cases. Expose the netdev_work facility to drivers. Add helpers to schedule work and a dedicated ndo to perform the driver- -scheduled actions. Reviewed-by: Kuniyuki Iwashima <kuniyu@google.com> Acked-by: Stanislav Fomichev <sdf@fomichev.me> Link: https://patch.msgid.link/20260624182018.2445732-3-kuba@kernel.org Signed-off-by: Jakub Kicinski <kuba@kernel.org>
163 lines
4.2 KiB
C
163 lines
4.2 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
#include <linux/export.h>
|
|
#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_pending
|
|
* - work_core_pending
|
|
*/
|
|
static LIST_HEAD(netdev_work_list);
|
|
static DEFINE_SPINLOCK(netdev_work_lock);
|
|
static DECLARE_WORK(netdev_work, netdev_work_proc);
|
|
|
|
static void netdev_work_enqueue(struct net_device *dev, unsigned long events,
|
|
unsigned long core)
|
|
{
|
|
if (!events && !core)
|
|
return;
|
|
|
|
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_pending |= events;
|
|
dev->work_core_pending |= core;
|
|
spin_unlock_bh(&netdev_work_lock);
|
|
|
|
schedule_work(&netdev_work);
|
|
}
|
|
|
|
static unsigned long
|
|
netdev_work_dequeue(struct net_device *dev, unsigned long *pending,
|
|
unsigned long mask)
|
|
{
|
|
unsigned long events;
|
|
|
|
spin_lock_bh(&netdev_work_lock);
|
|
events = *pending & mask;
|
|
*pending &= ~events;
|
|
if (!list_empty(&dev->work_node) &&
|
|
!dev->work_pending && !dev->work_core_pending) {
|
|
list_del_init(&dev->work_node);
|
|
netdev_put(dev, &dev->work_tracker);
|
|
}
|
|
spin_unlock_bh(&netdev_work_lock);
|
|
|
|
return events;
|
|
}
|
|
|
|
void netdev_work_sched(struct net_device *dev, unsigned long events)
|
|
{
|
|
netdev_work_enqueue(dev, events, 0);
|
|
}
|
|
EXPORT_SYMBOL(netdev_work_sched);
|
|
|
|
/**
|
|
* netdev_work_cancel() - cancel selected 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 and its ndo_work won't be called.
|
|
*
|
|
* 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_cancel(struct net_device *dev, unsigned long mask)
|
|
{
|
|
return netdev_work_dequeue(dev, &dev->work_pending, mask);
|
|
}
|
|
EXPORT_SYMBOL(netdev_work_cancel);
|
|
|
|
void __netdev_work_core_sched(struct net_device *dev, unsigned long events)
|
|
{
|
|
netdev_work_enqueue(dev, 0, events);
|
|
}
|
|
|
|
unsigned long
|
|
__netdev_work_core_cancel(struct net_device *dev, unsigned long mask)
|
|
{
|
|
return netdev_work_dequeue(dev, &dev->work_core_pending, mask);
|
|
}
|
|
|
|
static void netdev_work_run(struct net_device *dev, unsigned long events,
|
|
unsigned long core)
|
|
{
|
|
if (!netif_device_present(dev))
|
|
return;
|
|
|
|
if (core & NETDEV_WORK_RX_MODE)
|
|
netif_rx_mode_run(dev);
|
|
if (events && dev->netdev_ops->ndo_work)
|
|
dev->netdev_ops->ndo_work(dev, events);
|
|
}
|
|
|
|
static void netdev_work_proc(struct work_struct *work)
|
|
{
|
|
rtnl_lock();
|
|
|
|
while (true) {
|
|
unsigned long events = 0, core = 0;
|
|
netdevice_tracker tracker;
|
|
struct net_device *dev;
|
|
|
|
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;
|
|
events = dev->work_pending;
|
|
dev->work_pending = 0;
|
|
/* We took another ref above */
|
|
netdev_put(dev, &dev->work_tracker);
|
|
|
|
if (!dev_isalive(dev))
|
|
core = events = 0;
|
|
}
|
|
spin_unlock_bh(&netdev_work_lock);
|
|
|
|
netdev_work_run(dev, events, core);
|
|
netdev_unlock_ops(dev);
|
|
|
|
netdev_put(dev, &tracker);
|
|
}
|
|
|
|
rtnl_unlock();
|
|
}
|