Re: [RFC PATCH 0/3] iommu: Reduce iommu_probe_device_lock contention
From: Sakac, Pavol
Date: Thu Sep 17 2026 - 16:22:53 EST
On 9/11/26 19:45, Robin Murphy wrote:
> On 11/09/2026 1:58 pm, Pavol Sakac wrote:
>> iommu_probe_device_lock is a file-scope mutex held across
>> __iommu_probe_device(): every device's IOMMU probe serializes against
>> every other. With "PCI/IOV: Initialize virtual functions in
>> parallel" [1] fanning an SR-IOV enable across CPUs, it collects 94.7%
>> of all lock wait. Patch 1 gathers per-member sysfs publication into one
>> all-or-nothing helper (no functional change); patches 2-3 move that
>> publication and first-device default-domain setup into per-group
>> finalisation after the global unlock, under group->mutex, mirroring the
>> removal path and bus_iommu_probe().
>>
>> Lock statistics and SR-IOV init time for 4x PF (NVMe, 255 VFs each), on
>> the reproducer from the parallel VF initialization cover letter [1]:
>>
>> lock_stat:
>> Lock wait: Before After contentions: Before After
>> iommu_probe_device_lock 25507 ms 10471 ms 1143 841
>> &root->kernfs_rwsem 942 ms 1834 ms 93208 116316
>> &vfio.group_lock 425 ms 3614 ms 497 736
>>
>> avg wait per acquisition 6.3 ms -> 2.6 ms (4080 acq., both arms)
>>
>> Stage SR-IOV init time:
>> S0 (baseline) 3027 ms
>> S1 999 ms
>> S2 (this series) 995 ms
>>
>> Reproducer disclaimer:
>> I lean primarily on lock_stat numbers to defend the improvements. In
>> the reproducer, this lock's residual hold dominates the window and
>> masks the later series' wall-time gains, more in [1].
>>
>> This is relief, not removal: the lock still has the highest wait
>> time in the profile after this series. Wait per acquisition drops
>> from 6.3 ms to 2.6 ms, which is what makes the smaller
>> serialization points behind it measurable for the later series.
>>
>> RFC on the direction: The comment in __iommu_probe_device() expects
>> the lock to narrow to device_lock() once the ACPI/OF replay calls
>> are cleaned up. I could not make that work for the whole section:
>> group formation in ops->device_group() is a cross-device decision a
>> per-device lock cannot order. This series instead moves the work
>> that needs no global ordering out of the section. Is that an
>> acceptable step, or is there a scoping or removal plan this should
>> wait for?
>
> The point of probe_device_lock is to prevent multiple threads trying to
> probe the *same* device concurrently; it protects the per-device state
> of dev->iommu and dev->iommu_group until the latter is assigned or the
> former is cleaned up (depending on how the probe goes). The replay calls
> are mostly gone, but the main reason device_lock() still won't work is
> that the same problem exists for driver-model-based IOMMU drivers
> themselves, since we don't have a good way to avoid bus_iommu_probe()
> deadlocking on IOMMU devices that are in the middle of registering
> during their own driver bind (not least the caller itself).
>
Thanks, that makes sense.
>> A second question: I have measured where 90% of the residual hold goes:
>> get_pci_alias_group() walks every PCI device in the system to find
>> same-bus DMA aliases. It runs once per device probed, so once per VF,
>> under this lock, and the VFs keep growing the list it walks. A prototype
>> that skips the walk when no device has a dma_alias_mask and this device
>> has no pci_real_dma_dev() override cuts this lock's hold time by about
>> 90%, for the same acquisitions and the same groups. I am not proposing
>> it here, as I do not have the time to get it right this cycle. How can
>> we optimize this preferably in O(1) time?
>
> TBH that makes it sound like optimising pci_device_group() is the better
> thing to do. We were never really meant to have a global lock here - it
> was just an acceptable compromise for simplicity at the time - so I'm
> still not keen on adding yet more complexity to the probe flow to work
> around it as if global serialisation was necessary when it isn't.
>
> Heck, even if you do just want a quick bodge to ease contention then I'd
> still lean more towards something more self-contained like this
> hometime-on-a-Friday fun I couldn't resist sketching out...
>
> Thanks,
> Robin.
>
I appreciate the sketch :)
I've tested it with table size of 16, but it's not enough and needs also
get_pci_alias_group() walking scope reduction fix so it does not iterate
all devices but just those on the bus - as it itself claims to only need.
I left get_pci_function_alias_group() as is - VFs skip the walk there.
When both are combined (in below table as arm C), they work the best and
finally put kernfs_rwsem to the top. Here are numbers from the reproducer
running with all of my patches from S1-S5 series:
Arm:
- A the 3 iommu patches as posted
- B purely your lock table with 16 entries replacing my 3
- C B + the alias-walk fix
- D just alias-walk fix alone
A B C D
iommu_probe_device_lock wait 10391 ms 19538 ms 1198 ms 8237 ms
iommu_probe_device_lock hold 3338 ms 15074 ms 1002 ms 754 ms
iommu_probe_device_lock cont 836 731 384 1134
&root->kernfs_rwsem wait 660 ms 949 ms 2129 ms 835 ms
gdp_mutex wait 162 ms 189 ms 1139 ms 154 ms
&k->k_lock acquisitions 1195808 1201016 86824 87242
&k->k_lock contentions 2378 997420 4567 2261
all classes, wait 11249 ms 26730 ms 4497 ms 9237 ms
SR-IOV init time 1152 ms 1600 ms 923 ms 1012 ms
runs (min over) 5 3 3 3
Also ran different table sizes sweep of arm C:
4x 16x 64x
iommu_probe_device_lock wait 4638 ms 1198 ms 447 ms
iommu_probe_device_lock hold 854 ms 1002 ms 1423 ms
iommu_probe_device_lock cont 957 384 121
&root->kernfs_rwsem wait 1764 ms 2129 ms 3522 ms
gdp_mutex wait 665 ms 1139 ms 1713 ms
&k->k_lock acquisitions 86975 86824 86876
all classes, wait 7081 ms 4497 ms 5709 ms
SR-IOV init time 1000 ms 923 ms 1007 ms
runs (min over) 3 3 3
Would you like to take the first one? I can post v2 with just the alias
walk fix.
> ----->8-----
>
> From: Robin Murphy <robin.murphy@xxxxxxx>
> Subject: [PATCH] UNTESTED: iommu: Reduce iommu_probe_device_lock contention
>
> The purpose of iommu_probe_device_lock was to prevent multiple threads
> trying to probe the same device concurrently, it's only global for the
> sake of simplicity, as there are still reasons why we can't use
> device_lock(), and adding a whole other lock to struct device itself
> just for this would be unreasonable.
>
> However, we're now getting sufficiently large systems with enough
> devices to start seeing significant contention on this lock, so let's
> scale it to a lock table to reduce contention between unrelated devices.
>
> Signed-off-by: Robin Murphy <robin.murphy@xxxxxxx>
> ---
> drivers/acpi/scan.c | 6 +++---
> drivers/iommu/iommu.c | 37 +++++++++++++++++++++++++------------
> drivers/iommu/of_iommu.c | 6 +++---
> include/linux/iommu.h | 2 +-
> 4 files changed, 32 insertions(+), 19 deletions(-)
>
> diff --git a/drivers/acpi/scan.c b/drivers/acpi/scan.c
> index f48715ed827c..33d6a758042a 100644
> --- a/drivers/acpi/scan.c
> +++ b/drivers/acpi/scan.c
> @@ -1620,10 +1620,10 @@ static int acpi_iommu_configure_id(struct device *dev, const u32 *id_in)
> int err;
>
> /* Serialise to make dev->iommu stable under our potential fwspec */
> - mutex_lock(&iommu_probe_device_lock);
> + mutex_lock(iommu_probe_device_lock(dev));
> /* If we already translated the fwspec there is nothing left to do */
> if (dev_iommu_fwspec_get(dev)) {
> - mutex_unlock(&iommu_probe_device_lock);
> + mutex_unlock(iommu_probe_device_lock(dev));
> return 0;
> }
>
> @@ -1633,7 +1633,7 @@ static int acpi_iommu_configure_id(struct device *dev, const u32 *id_in)
> if (err && err != -EPROBE_DEFER)
> err = viot_iommu_configure(dev);
>
> - mutex_unlock(&iommu_probe_device_lock);
> + mutex_unlock(iommu_probe_device_lock(dev));
>
> return err;
> }
> diff --git a/drivers/iommu/iommu.c b/drivers/iommu/iommu.c
> index cd1bca7ede9a..11519e195aaf 100644
> --- a/drivers/iommu/iommu.c
> +++ b/drivers/iommu/iommu.c
> @@ -381,9 +381,9 @@ int iommu_mock_device_add(struct device *dev, struct iommu_device *iommu)
> {
> int rc;
>
> - mutex_lock(&iommu_probe_device_lock);
> + mutex_lock(iommu_probe_device_lock(dev));
> rc = iommu_fwspec_init(dev, iommu->fwnode);
> - mutex_unlock(&iommu_probe_device_lock);
> + mutex_unlock(iommu_probe_device_lock(dev));
>
> if (rc)
> return rc;
> @@ -400,7 +400,7 @@ static struct dev_iommu *dev_iommu_get(struct device *dev)
> {
> struct dev_iommu *param = dev->iommu;
>
> - lockdep_assert_held(&iommu_probe_device_lock);
> + lockdep_assert_held(iommu_probe_device_lock(dev));
>
> if (param)
> return param;
> @@ -457,7 +457,7 @@ void dev_iommu_priv_set(struct device *dev, void *priv)
> {
> /* FSL_PAMU does something weird */
> if (!IS_ENABLED(CONFIG_FSL_PAMU))
> - lockdep_assert_held(&iommu_probe_device_lock);
> + lockdep_assert_held(iommu_probe_device_lock(dev));
> dev->iommu->priv = priv;
> }
> EXPORT_SYMBOL_GPL(dev_iommu_priv_set);
> @@ -483,9 +483,9 @@ static int iommu_init_device(struct device *dev)
> * found no IOMMU to wait for, so there's no point calling it again.
> */
> if (!dev->iommu->fwspec && !dev->driver && dev->bus->dma_configure) {
> - mutex_unlock(&iommu_probe_device_lock);
> + mutex_unlock(iommu_probe_device_lock(dev));
> dev->bus->dma_configure(dev);
> - mutex_lock(&iommu_probe_device_lock);
> + mutex_lock(iommu_probe_device_lock(dev));
> /* If another instance finished the job for us, skip it */
> if (!dev->iommu || dev->iommu_group)
> return -ENODEV;
> @@ -622,7 +622,20 @@ static struct iommu_domain *pasid_array_entry_to_domain(void *entry)
> return ((struct iommu_attach_handle *)xa_untag_pointer(entry))->domain;
> }
>
> -DEFINE_MUTEX(iommu_probe_device_lock);
> +static struct mutex __iommu_probe_device_lock[4] = {
> + __MUTEX_INITIALIZER(iommu_probe_device_lock),
> + __MUTEX_INITIALIZER(iommu_probe_device_lock),
> + __MUTEX_INITIALIZER(iommu_probe_device_lock),
> + __MUTEX_INITIALIZER(iommu_probe_device_lock),
> +};
> +
> +struct mutex *iommu_probe_device_lock(const struct device *dev)
> +{
> + int hash = ((uintptr_t)dev / roundup_pow_of_two(sizeof(*dev))) %
> + ARRAY_SIZE(__iommu_probe_device_lock);
> +
> + return __iommu_probe_device_lock + hash;
> +}
>
> static int __iommu_probe_device(struct device *dev, struct list_head *group_list)
> {
> @@ -637,7 +650,7 @@ static int __iommu_probe_device(struct device *dev, struct list_head *group_list
> * probably be able to use device_lock() here to minimise the scope,
> * but for now enforcing a simple global ordering is fine.
> */
> - lockdep_assert_held(&iommu_probe_device_lock);
> + lockdep_assert_held(iommu_probe_device_lock(dev));
>
> /* Device is probed already if in a group */
> if (dev->iommu_group)
> @@ -711,9 +724,9 @@ int iommu_probe_device(struct device *dev)
> const struct iommu_ops *ops;
> int ret;
>
> - mutex_lock(&iommu_probe_device_lock);
> + mutex_lock(iommu_probe_device_lock(dev));
> ret = __iommu_probe_device(dev, NULL);
> - mutex_unlock(&iommu_probe_device_lock);
> + mutex_unlock(iommu_probe_device_lock(dev));
> if (ret)
> return ret;
>
> @@ -1803,9 +1816,9 @@ static int probe_iommu_group(struct device *dev, void *data)
> struct list_head *group_list = data;
> int ret;
>
> - mutex_lock(&iommu_probe_device_lock);
> + mutex_lock(iommu_probe_device_lock(dev));
> ret = __iommu_probe_device(dev, group_list);
> - mutex_unlock(&iommu_probe_device_lock);
> + mutex_unlock(iommu_probe_device_lock(dev));
> if (ret == -ENODEV)
> ret = 0;
>
> diff --git a/drivers/iommu/of_iommu.c b/drivers/iommu/of_iommu.c
> index a18bb60f6f3d..b5e3a425ca2d 100644
> --- a/drivers/iommu/of_iommu.c
> +++ b/drivers/iommu/of_iommu.c
> @@ -121,9 +121,9 @@ int of_iommu_configure(struct device *dev, struct device_node *master_np,
> return -ENODEV;
>
> /* Serialise to make dev->iommu stable under our potential fwspec */
> - mutex_lock(&iommu_probe_device_lock);
> + mutex_lock(iommu_probe_device_lock(dev));
> if (dev_iommu_fwspec_get(dev)) {
> - mutex_unlock(&iommu_probe_device_lock);
> + mutex_unlock(iommu_probe_device_lock(dev));
> return 0;
> }
> dev_iommu_present = dev->iommu;
> @@ -151,7 +151,7 @@ int of_iommu_configure(struct device *dev, struct device_node *master_np,
> iommu_fwspec_free(dev);
> else if (err && dev->iommu)
> dev_iommu_free(dev);
> - mutex_unlock(&iommu_probe_device_lock);
> + mutex_unlock(iommu_probe_device_lock(dev));
>
> /*
> * If we're not on the iommu_probe_device() path (as indicated by the
> diff --git a/include/linux/iommu.h b/include/linux/iommu.h
> index ac43b8b93f14..3b876cb285e0 100644
> --- a/include/linux/iommu.h
> +++ b/include/linux/iommu.h
> @@ -1201,7 +1201,7 @@ static inline void *dev_iommu_priv_get(struct device *dev)
>
> void dev_iommu_priv_set(struct device *dev, void *priv);
>
> -extern struct mutex iommu_probe_device_lock;
> +struct mutex *iommu_probe_device_lock(const struct device *dev);
> int iommu_probe_device(struct device *dev);
>
> int iommu_device_use_default_domain(struct device *dev);
> --
> 2.54.0.dirty
>
Here's the alias walk fix prototype (only tested on VFs though):
-----
From: Pavol Sakac <sakacpav@xxxxxxxxx>
Date: Thu, 17 Sep 2026 13:00:00 +0200
Subject: [PATCH] iommu: Find PCI DMA aliases on the device's own bus
get_pci_alias_group() walks every PCI device in the system with
for_each_pci_dev() to find the aliases of one device. It runs once per
probed device under iommu_probe_device_lock and takes the PCI bus
klist lock twice per step.
A DMA alias is a devfn on the same bus, so walk only that bus, with
pci_walk_bus(). The recursion walks the bus itself and cannot nest
inside pci_walk_bus(), so fetch one alias per pass and let the visited
bit the recursion sets move the next pass on; a visited alias is no
longer re-entered only to return NULL.
No functional change intended.
Assisted-by: LLM
Signed-off-by: Pavol Sakac <sakacpav@xxxxxxxxx>
---
drivers/iommu/iommu.c | 55 ++++++++++++++++++++++++++++--------------
1 file changed, 37 insertions(+), 18 deletions(-)
diff --git a/drivers/iommu/iommu.c b/drivers/iommu/iommu.c
index cd1bca7..3bf1020 100644
--- a/drivers/iommu/iommu.c
+++ b/drivers/iommu/iommu.c
@@ -1521,6 +1521,35 @@ static struct iommu_group *get_pci_function_alias_group(struct pci_dev *pdev,
return NULL;
}
+struct pci_alias_search {
+ struct pci_dev *pdev;
+ unsigned long *devfns;
+ struct pci_dev *alias;
+};
+
+static int find_pci_dma_alias(struct pci_dev *tmp, void *data)
+{
+ struct pci_alias_search *s = data;
+
+ /* pci_walk_bus() descends below the bus; aliases are same-bus only */
+ if (tmp->bus != s->pdev->bus ||
+ test_bit(tmp->devfn & 0xff, s->devfns) ||
+ !pci_devs_are_dma_aliases(s->pdev, tmp))
+ return 0;
+
+ s->alias = pci_dev_get(tmp);
+ return 1;
+}
+
+static struct pci_dev *get_pci_dma_alias(struct pci_dev *pdev,
+ unsigned long *devfns)
+{
+ struct pci_alias_search s = { .pdev = pdev, .devfns = devfns };
+
+ pci_walk_bus(pdev->bus, find_pci_dma_alias, &s);
+ return s.alias;
+}
+
/*
* Look for aliases to or from the given device for existing groups. DMA
* aliases are only supported on the same bus, therefore the search
@@ -1533,7 +1562,7 @@ static struct iommu_group *get_pci_function_alias_group(struct pci_dev *pdev,
static struct iommu_group *get_pci_alias_group(struct pci_dev *pdev,
unsigned long *devfns)
{
- struct pci_dev *tmp = NULL;
+ struct pci_dev *tmp;
struct iommu_group *group;
if (test_and_set_bit(pdev->devfn & 0xff, devfns))
@@ -1543,24 +1572,14 @@ static struct iommu_group *get_pci_alias_group(struct pci_dev *pdev,
if (group)
return group;
- for_each_pci_dev(tmp) {
- if (tmp == pdev || tmp->bus != pdev->bus)
- continue;
-
- /* We alias them or they alias us */
- if (pci_devs_are_dma_aliases(pdev, tmp)) {
- group = get_pci_alias_group(tmp, devfns);
- if (group) {
- pci_dev_put(tmp);
- return group;
- }
-
+ /* One alias per walk: the recursion cannot nest in pci_walk_bus() */
+ while ((tmp = get_pci_dma_alias(pdev, devfns))) {
+ group = get_pci_alias_group(tmp, devfns);
+ if (!group)
group = get_pci_function_alias_group(tmp, devfns);
- if (group) {
- pci_dev_put(tmp);
- return group;
- }
- }
+ pci_dev_put(tmp);
+ if (group)
+ return group;
}
return NULL;
--
2.47.1