viommu_replay_mappings() sent every mapping of a domain to the device
with a synchronous request, holding mappings_lock - and with it
interrupts - across the whole walk, so the IRQ-off window grew with the
number of mappings: 233 ms for 8192 4K mappings, 29 s for a million.

Queue them instead and wait once. One mapping per mappings_lock section,
so that a full virtqueue - the only wait in there - bounds a single lock
hold rather than the whole walk; the walk is a single pass, because the
endpoint was published before it (a mapping inserted while it runs is
queued by map_pages() itself) and a mapping removed meanwhile has its
UNMAP queued in the section that removed it. The requests carry the
device's errno back - the first error it reported - so a rejected replay
fails the attach with the device's own status, and a device that goes
away fails it with -ENODEV rather than reporting a replay that never
happened; a duplicate MAP, which a replay can legitimately produce, is
answered S_INVAL - the spec says a duplicate MAP SHOULD be rejected and
MUST NOT change the existing mapping - and is not treated as a rejection.
A rejected MAP on the map path is still ignored, as before.

Measured, N=8192 4K mappings: max IRQ-off window 233.7 ms -> 1.4-2.5 ms,
and no longer growing with N (1.4-2.5 ms from N=64 to 32768, against
3.6/45/233 ms on the unpatched driver); attach ioctl 218.9/249.0 ->
50.3/48.8 ms (48-67 ms across runs); viommu_send_req_sync() calls 8193 ->
1; the device receives exactly the same MAPs (8244).

Signed-off-by: Anlai Lu <[email protected]>
---
 drivers/iommu/virtio-iommu.c | 149 +++++++++++++++++++++++++++--------
 1 file changed, 117 insertions(+), 32 deletions(-)

diff --git a/drivers/iommu/virtio-iommu.c b/drivers/iommu/virtio-iommu.c
index 4e578d4255f6..7b883f59221f 100644
--- a/drivers/iommu/virtio-iommu.c
+++ b/drivers/iommu/virtio-iommu.c
@@ -73,6 +73,19 @@ struct viommu_mapping {
        struct viommu_request           *unmap_req;
 };
 
+/* Where a queued request reports a rejection, or NULL */
+struct viommu_req_status {
+       bool                            *rejected;
+       /*
+        * Errno to treat as success: a replayed MAP can be a duplicate,
+        * which the spec says the device SHOULD reject with S_INVAL and
+        * MUST NOT change the existing mapping.
+        */
+       int                             ignore_errno;
+       /* The first error the device reported, for the caller to return */
+       int                             err;
+};
+
 struct viommu_domain {
        struct iommu_domain             domain;
        struct viommu_dev               *viommu;
@@ -104,6 +117,8 @@ struct viommu_request {
        void                            *writeback;
        unsigned int                    write_offset;
        unsigned int                    len;
+       /* Where to report a failure of this request, or NULL */
+       struct viommu_req_status        *status;
        char                            buf[] __counted_by(len);
 };
 
@@ -144,6 +159,19 @@ static bool viommu_domain_has_endpoint(struct 
viommu_domain *vdomain)
        return has;
 }
 
+/* Same, for callers that do not hold request_lock */
+static bool viommu_device_alive(struct viommu_dev *viommu)
+{
+       unsigned long flags;
+       bool live;
+
+       spin_lock_irqsave(&viommu->request_lock, flags);
+       live = viommu_device_live(viommu);
+       spin_unlock_irqrestore(&viommu->request_lock, flags);
+
+       return live;
+}
+
 static int viommu_get_req_errno(void *buf, size_t len)
 {
        struct virtio_iommu_req_tail *tail = buf + len - sizeof(*tail);
@@ -216,6 +244,16 @@ static int __viommu_sync_req(struct viommu_dev *viommu)
                        viommu_set_req_status(req->buf, req->len,
                                              VIRTIO_IOMMU_S_IOERR);
 
+               if (req->status) {
+                       int err = viommu_get_req_errno(req->buf, req->len);
+
+                       if (err && err != req->status->ignore_errno) {
+                               *req->status->rejected = true;
+                               if (!req->status->err)
+                                       req->status->err = err;
+                       }
+               }
+
                write_len = req->len - req->write_offset;
                if (req->writeback && len == write_len)
                        memcpy(req->writeback, req->buf + req->write_offset,
@@ -286,6 +324,7 @@ static int __viommu_queue_req(struct viommu_dev *viommu,
  * @buf: pointer to the request buffer
  * @len: length of the request buffer
  * @writeback: copy data back to the buffer when the request completes.
+ * @status: where to report a rejection of this request, or NULL
  *
  * Allocate a request object, fill it and queue it.  When @writeback is true,
  * data written by the device, including the request status, is copied into
@@ -295,7 +334,7 @@ static int __viommu_queue_req(struct viommu_dev *viommu,
  * Return 0 if the request was successfully added to the queue.
  */
 static int __viommu_add_req(struct viommu_dev *viommu, void *buf, size_t len,
-                           bool writeback)
+                           bool writeback, struct viommu_req_status *status)
 {
        int ret;
        off_t write_offset;
@@ -312,6 +351,7 @@ static int __viommu_add_req(struct viommu_dev *viommu, void 
*buf, size_t len,
                return -ENOMEM;
 
        req->len = len;
+       req->status = status;
        if (writeback) {
                req->writeback = buf + write_offset;
                req->write_offset = write_offset;
@@ -325,13 +365,14 @@ static int __viommu_add_req(struct viommu_dev *viommu, 
void *buf, size_t len,
        return ret;
 }
 
-static int viommu_add_req(struct viommu_dev *viommu, void *buf, size_t len)
+static int viommu_add_req(struct viommu_dev *viommu, void *buf, size_t len,
+                         struct viommu_req_status *status)
 {
        int ret;
        unsigned long flags;
 
        spin_lock_irqsave(&viommu->request_lock, flags);
-       ret = __viommu_add_req(viommu, buf, len, false);
+       ret = __viommu_add_req(viommu, buf, len, false, status);
        if (ret)
                dev_dbg(viommu->dev, "could not add request: %d\n", ret);
        spin_unlock_irqrestore(&viommu->request_lock, flags);
@@ -354,6 +395,7 @@ static int viommu_queue_prealloc_req(struct viommu_dev 
*viommu,
 
        spin_lock_irqsave(&viommu->request_lock, flags);
        req->len = len;
+       req->status = NULL;
        req->writeback = NULL;
        memcpy(req->buf, buf, write_offset);
        ret = __viommu_queue_req(viommu, req, write_offset);
@@ -374,7 +416,7 @@ static int viommu_send_req_sync(struct viommu_dev *viommu, 
void *buf,
 
        spin_lock_irqsave(&viommu->request_lock, flags);
 
-       ret = __viommu_add_req(viommu, buf, len, true);
+       ret = __viommu_add_req(viommu, buf, len, true, NULL);
        if (ret) {
                dev_dbg(viommu->dev, "could not add request (%d)\n", ret);
                goto out_unlock;
@@ -462,7 +504,7 @@ static int viommu_add_mapping(struct viommu_domain 
*vdomain, u64 iova, u64 end,
                        .flags          = cpu_to_le32(flags),
                };
 
-               ret = viommu_add_req(vdomain->viommu, &map, sizeof(map));
+               ret = viommu_add_req(vdomain->viommu, &map, sizeof(map), NULL);
                if (ret) {
                        interval_tree_remove(&mapping->iova, 
&vdomain->mappings);
                        spin_unlock_irqrestore(&vdomain->mappings_lock, 
irqflags);
@@ -656,23 +698,57 @@ static int viommu_domain_map_identity(struct 
viommu_endpoint *vdev,
 }
 
 /*
- * viommu_replay_mappings - re-send MAP requests
+ * viommu_replay_mappings - send every mapping of the domain again
  *
- * When reattaching a domain that was previously detached from all endpoints,
- * mappings were deleted from the device. Re-create the mappings available in
- * the internal tree.
+ * Used when reattaching a domain that was previously detached from all
+ * endpoints: the device dropped its copy then (it frees a domain together with
+ * its last endpoint).
+ *
+ * The device is not waited for between mappings: they are all queued, one per
+ * mappings_lock section, and waited for once at the end.  A full virtqueue is
+ * the only wait in the loop, so it bounds a single lock hold rather than the
+ * whole walk.  A mapping that was sent concurrently is harmless: the spec says
+ * a duplicate MAP SHOULD be rejected with S_INVAL and MUST NOT change the
+ * existing mapping, and the replay treats that rejection as success.
+ *
+ * A rejected MAP is reported by returning an error, which fails the attach: 
the
+ * domain would be missing a mapping its driver believes in.  Nothing else is
+ * done about it.
  */
 static int viommu_replay_mappings(struct viommu_domain *vdomain)
 {
-       int ret = 0;
+       bool rejected = false;
+       struct viommu_req_status status = {
+               .rejected       = &rejected,
+               .ignore_errno   = -EINVAL,
+       };
        unsigned long flags;
-       struct viommu_mapping *mapping;
-       struct interval_tree_node *node;
-       struct virtio_iommu_req_map map;
+       int ret = 0;
+       struct interval_tree_node *node = NULL;
+
+       /*
+        * A single pass is enough: the endpoint was published before this ran,
+        * so a mapping inserted while the walk progresses is queued by
+        * map_pages() itself, and one inserted before it is in the tree when
+        * the walk starts.  A mapping removed meanwhile is simply not there any
+        * more, and its UNMAP was queued in the same section that removed it,
+        * so it cannot end up after a MAP this walk sends.
+        */
+       u64 next_iova = 0;
+       bool done = false;
+
+       while (!done) {
+               struct viommu_mapping *mapping;
+               struct virtio_iommu_req_map map;
+
+               spin_lock_irqsave(&vdomain->mappings_lock, flags);
+               node = interval_tree_iter_first(&vdomain->mappings, next_iova,
+                                               -1UL);
+               if (!node) {
+                       spin_unlock_irqrestore(&vdomain->mappings_lock, flags);
+                       break;
+               }
 
-       spin_lock_irqsave(&vdomain->mappings_lock, flags);
-       node = interval_tree_iter_first(&vdomain->mappings, 0, -1UL);
-       while (node) {
                mapping = container_of(node, struct viommu_mapping, iova);
                map = (struct virtio_iommu_req_map) {
                        .head.type      = VIRTIO_IOMMU_T_MAP,
@@ -682,26 +758,35 @@ static int viommu_replay_mappings(struct viommu_domain 
*vdomain)
                        .phys_start     = cpu_to_le64(mapping->paddr),
                        .flags          = cpu_to_le32(mapping->flags),
                };
+               ret = viommu_add_req(vdomain->viommu, &map, sizeof(map), 
&status);
 
-               ret = viommu_send_req_sync(vdomain->viommu, &map, sizeof(map));
-               if (ret) {
-                       /*
-                        * The endpoint was published before this walk, so a
-                        * map_pages() racing it can have sent the same MAP
-                        * already: the spec says a duplicate MAP SHOULD be
-                        * rejected with S_INVAL, and the device MUST NOT
-                        * change the existing mapping.
-                        */
-                       if (ret != -EINVAL)
-                               break;
-                       ret = 0;
-               }
+               if (mapping->iova.last == ULONG_MAX)
+                       done = true;
+               else
+                       next_iova = mapping->iova.last + 1;
+               spin_unlock_irqrestore(&vdomain->mappings_lock, flags);
 
-               node = interval_tree_iter_next(node, 0, -1UL);
+               if (ret)
+                       break;
        }
-       spin_unlock_irqrestore(&vdomain->mappings_lock, flags);
 
-       return ret;
+       /* Wait for all of them outside the lock: this is the batched drain */
+       viommu_sync_req(vdomain->viommu);
+
+       /*
+        * A device that went away did not take anything: the status pointers of
+        * the requests still queued are on this frame, so report it here rather
+        * than pretending the replay delivered the mappings.
+        */
+       if (!viommu_device_alive(vdomain->viommu))
+               return -ENODEV;
+
+       /* The queueing failure, or what the device said */
+       if (ret)
+               return ret;
+
+       /* Report what the device said, like the synchronous replay did */
+       return rejected ? (status.err ?: -EIO) : 0;
 }
 
 static int viommu_add_resv_mem(struct viommu_endpoint *vdev,
-- 
2.55.0


Reply via email to