From: Sungho Bae <[email protected]>

Some virtio-mmio devices, such as virtio-clock or virtio-regulator,
must become operational before the regular PM restore callback runs
because other devices may depend on them.

Add the core infrastructure needed to support noirq system-sleep PM
callbacks for virtio transports:

 - virtio_add_status_noirq(): status helper without might_sleep().
 - virtio_features_ok_noirq(): feature negotiation without might_sleep().
 - virtio_reset_device_noirq(): device reset that skips
   virtio_synchronize_cbs() (IRQ handlers are already quiesced in the
   noirq phase).
 - virtio_device_reinit_noirq(): full noirq bring-up sequence using the
   above helpers.
 - virtio_config_core_enable_noirq(): config enable with irqsave
   locking.
 - virtio_device_ready_noirq(): marks DRIVER_OK without
   virtio_synchronize_cbs().

Add freeze_noirq/restore_noirq callbacks to struct virtio_driver and
provide matching helper wrappers in the virtio core:

 - virtio_device_freeze_noirq(): forwards to drv->freeze_noirq().
 - virtio_device_restore_noirq(): runs the noirq bring-up sequence,
   resets existing vrings via the new config_ops->reset_vqs() hook,
   then calls drv->restore_noirq().

Modify virtio_device_restore() so that when a driver provides
restore_noirq, the normal-phase restore skips the re-initialization
that was already done in the noirq phase.

Signed-off-by: Sungho Bae <[email protected]>
---
 drivers/virtio/virtio.c       | 172 +++++++++++++++++++++++++++++++++-
 include/linux/virtio.h        |   8 ++
 include/linux/virtio_config.h |  29 ++++++
 3 files changed, 206 insertions(+), 3 deletions(-)

diff --git a/drivers/virtio/virtio.c b/drivers/virtio/virtio.c
index b0668434ac21..4fcb6b8c797f 100644
--- a/drivers/virtio/virtio.c
+++ b/drivers/virtio/virtio.c
@@ -193,6 +193,17 @@ static void virtio_config_core_enable(struct virtio_device 
*dev)
        spin_unlock_irq(&dev->config_lock);
 }
 
+static void virtio_config_core_enable_noirq(struct virtio_device *dev)
+{
+       unsigned long flags;
+
+       spin_lock_irqsave(&dev->config_lock, flags);
+       dev->config_core_enabled = true;
+       if (dev->config_change_pending)
+               __virtio_config_changed(dev);
+       spin_unlock_irqrestore(&dev->config_lock, flags);
+}
+
 void virtio_add_status(struct virtio_device *dev, unsigned int status)
 {
        might_sleep();
@@ -200,6 +211,20 @@ void virtio_add_status(struct virtio_device *dev, unsigned 
int status)
 }
 EXPORT_SYMBOL_GPL(virtio_add_status);
 
+/*
+ * Same as virtio_add_status() but without the might_sleep() assertion,
+ * so it is safe to call from noirq context.
+ *
+ * This assumes that the device's get_status and set_status operations are
+ * also noirq-safe. Therefore, the device must garantee that get_status and
+ * set_status can be called from noirq context.
+ */
+void virtio_add_status_noirq(struct virtio_device *dev, unsigned int status)
+{
+       dev->config->set_status(dev, dev->config->get_status(dev) | status);
+}
+EXPORT_SYMBOL_GPL(virtio_add_status_noirq);
+
 /* Do some validation, then set FEATURES_OK */
 static int virtio_features_ok(struct virtio_device *dev)
 {
@@ -234,6 +259,38 @@ static int virtio_features_ok(struct virtio_device *dev)
        return 0;
 }
 
+/* noirq-safe variant: no might_sleep(), uses virtio_add_status_noirq() */
+static int virtio_features_ok_noirq(struct virtio_device *dev)
+{
+       unsigned int status;
+
+       if (virtio_check_mem_acc_cb(dev)) {
+               if (!virtio_has_feature(dev, VIRTIO_F_VERSION_1)) {
+                       dev_warn(&dev->dev,
+                                "device must provide VIRTIO_F_VERSION_1\n");
+                       return -ENODEV;
+               }
+
+               if (!virtio_has_feature(dev, VIRTIO_F_ACCESS_PLATFORM)) {
+                       dev_warn(&dev->dev,
+                                "device must provide 
VIRTIO_F_ACCESS_PLATFORM\n");
+                       return -ENODEV;
+               }
+       }
+
+       if (!virtio_has_feature(dev, VIRTIO_F_VERSION_1))
+               return 0;
+
+       virtio_add_status_noirq(dev, VIRTIO_CONFIG_S_FEATURES_OK);
+       status = dev->config->get_status(dev);
+       if (!(status & VIRTIO_CONFIG_S_FEATURES_OK)) {
+               dev_err(&dev->dev, "virtio: device refuses features: %x\n",
+                       status);
+               return -ENODEV;
+       }
+       return 0;
+}
+
 /**
  * virtio_reset_device - quiesce device for removal
  * @dev: the device to reset
@@ -267,6 +324,24 @@ void virtio_reset_device(struct virtio_device *dev)
 }
 EXPORT_SYMBOL_GPL(virtio_reset_device);
 
+/**
+ * virtio_reset_device_noirq - noirq-safe variant of virtio_reset_device()
+ * @dev: the device to reset
+ */
+void virtio_reset_device_noirq(struct virtio_device *dev)
+{
+#ifdef CONFIG_VIRTIO_HARDEN_NOTIFICATION
+       /*
+        * The noirq stage runs with device IRQ handlers disabled, so
+        * virtio_synchronize_cbs() must not be called here.
+        */
+       virtio_break_device(dev);
+#endif
+
+       dev->config->reset(dev);
+}
+EXPORT_SYMBOL_GPL(virtio_reset_device_noirq);
+
 static int virtio_dev_probe(struct device *_d)
 {
        int err, i;
@@ -618,6 +693,41 @@ static int virtio_device_reinit(struct virtio_device *dev)
        return virtio_features_ok(dev);
 }
 
+/* noirq-safe variant of virtio_device_reinit() */
+static int virtio_device_reinit_noirq(struct virtio_device *dev)
+{
+       struct virtio_driver *drv = drv_to_virtio(dev->dev.driver);
+       int ret;
+
+       /*
+        * We always start by resetting the device, in case a previous
+        * driver messed it up.
+        */
+       virtio_reset_device_noirq(dev);
+
+       /* Acknowledge that we've seen the device. */
+       virtio_add_status_noirq(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE);
+
+       /*
+        * Maybe driver failed before freeze.
+        * Restore the failed status, for debugging.
+        */
+       if (dev->failed)
+               virtio_add_status_noirq(dev, VIRTIO_CONFIG_S_FAILED);
+
+       if (!drv)
+               return 0;
+
+       /* We have a driver! */
+       virtio_add_status_noirq(dev, VIRTIO_CONFIG_S_DRIVER);
+
+       ret = dev->config->finalize_features(dev);
+       if (ret)
+               return ret;
+
+       return virtio_features_ok_noirq(dev);
+}
+
 #ifdef CONFIG_PM_SLEEP
 int virtio_device_freeze(struct virtio_device *dev)
 {
@@ -645,9 +755,15 @@ int virtio_device_restore(struct virtio_device *dev)
        struct virtio_driver *drv = drv_to_virtio(dev->dev.driver);
        int ret;
 
-       ret = virtio_device_reinit(dev);
-       if (ret)
-               goto err;
+       /*
+        * If this device was already brought up in the noirq phase,
+        * skip the re-initialization here.
+        */
+       if (!drv || !drv->restore_noirq) {
+               ret = virtio_device_reinit(dev);
+               if (ret)
+                       goto err;
+       }
 
        if (drv && drv->restore) {
                ret = drv->restore(dev);
@@ -668,6 +784,56 @@ int virtio_device_restore(struct virtio_device *dev)
        return ret;
 }
 EXPORT_SYMBOL_GPL(virtio_device_restore);
+
+int virtio_device_freeze_noirq(struct virtio_device *dev)
+{
+       struct virtio_driver *drv = drv_to_virtio(dev->dev.driver);
+
+       if (drv && drv->freeze_noirq)
+               return drv->freeze_noirq(dev);
+
+       return 0;
+}
+EXPORT_SYMBOL_GPL(virtio_device_freeze_noirq);
+
+int virtio_device_restore_noirq(struct virtio_device *dev)
+{
+       struct virtio_driver *drv = drv_to_virtio(dev->dev.driver);
+       int ret;
+
+       if (!drv || !drv->restore_noirq)
+               return 0;
+
+       ret = virtio_device_reinit_noirq(dev);
+       if (ret)
+               goto err;
+
+       if (!list_empty(&dev->vqs)) {
+               if (!dev->config->reset_vqs) {
+                       ret = -EOPNOTSUPP;
+                       goto err;
+               }
+
+               ret = dev->config->reset_vqs(dev);
+               if (ret)
+                       goto err;
+       }
+
+       ret = drv->restore_noirq(dev);
+       if (ret)
+               goto err;
+
+       /* If restore_noirq set DRIVER_OK, enable config now. */
+       if (dev->config->get_status(dev) & VIRTIO_CONFIG_S_DRIVER_OK)
+               virtio_config_core_enable_noirq(dev);
+
+       return 0;
+
+err:
+       virtio_add_status_noirq(dev, VIRTIO_CONFIG_S_FAILED);
+       return ret;
+}
+EXPORT_SYMBOL_GPL(virtio_device_restore_noirq);
 #endif
 
 int virtio_device_reset_prepare(struct virtio_device *dev)
diff --git a/include/linux/virtio.h b/include/linux/virtio.h
index 3bbc4cb6a672..31267334e4de 100644
--- a/include/linux/virtio.h
+++ b/include/linux/virtio.h
@@ -209,8 +209,12 @@ void virtio_config_driver_enable(struct virtio_device 
*dev);
 #ifdef CONFIG_PM_SLEEP
 int virtio_device_freeze(struct virtio_device *dev);
 int virtio_device_restore(struct virtio_device *dev);
+int virtio_device_freeze_noirq(struct virtio_device *dev);
+int virtio_device_restore_noirq(struct virtio_device *dev);
 #endif
 void virtio_reset_device(struct virtio_device *dev);
+void virtio_reset_device_noirq(struct virtio_device *dev);
+void virtio_add_status_noirq(struct virtio_device *dev, unsigned int status);
 int virtio_device_reset_prepare(struct virtio_device *dev);
 int virtio_device_reset_done(struct virtio_device *dev);
 
@@ -237,6 +241,8 @@ size_t virtio_max_dma_size(const struct virtio_device 
*vdev);
  *    changes; may be called in interrupt context.
  * @freeze: optional function to call during suspend/hibernation.
  * @restore: optional function to call on resume.
+ * @freeze_noirq: optional function to call during noirq suspend/hibernation.
+ * @restore_noirq: optional function to call on noirq resume.
  * @reset_prepare: optional function to call when a transport specific reset
  *    occurs.
  * @reset_done: optional function to call after transport specific reset
@@ -258,6 +264,8 @@ struct virtio_driver {
        void (*config_changed)(struct virtio_device *dev);
        int (*freeze)(struct virtio_device *dev);
        int (*restore)(struct virtio_device *dev);
+       int (*freeze_noirq)(struct virtio_device *dev);
+       int (*restore_noirq)(struct virtio_device *dev);
        int (*reset_prepare)(struct virtio_device *dev);
        int (*reset_done)(struct virtio_device *dev);
        void (*shutdown)(struct virtio_device *dev);
diff --git a/include/linux/virtio_config.h b/include/linux/virtio_config.h
index 69f84ea85d71..496897bc417e 100644
--- a/include/linux/virtio_config.h
+++ b/include/linux/virtio_config.h
@@ -70,6 +70,9 @@ struct virtqueue_info {
  *     vqs_info: array of virtqueue info structures
  *     Returns 0 on success or error status
  * @del_vqs: free virtqueues found by find_vqs().
+ * @reset_vqs: reinitialize existing virtqueues without allocating or
+ *     freeing them (optional).  Used during noirq restore.
+ *     Returns 0 on success or error status.
  * @synchronize_cbs: synchronize with the virtqueue callbacks (optional)
  *      The function guarantees that all memory operations on the
  *      queue before it are visible to the vring_interrupt() that is
@@ -123,6 +126,7 @@ struct virtio_config_ops {
                        struct virtqueue_info vqs_info[],
                        struct irq_affinity *desc);
        void (*del_vqs)(struct virtio_device *);
+       int (*reset_vqs)(struct virtio_device *vdev);
        void (*synchronize_cbs)(struct virtio_device *);
        u64 (*get_features)(struct virtio_device *vdev);
        void (*get_extended_features)(struct virtio_device *vdev,
@@ -371,6 +375,31 @@ void virtio_device_ready(struct virtio_device *dev)
        dev->config->set_status(dev, status | VIRTIO_CONFIG_S_DRIVER_OK);
 }
 
+/**
+ * virtio_device_ready_noirq - noirq-safe variant of virtio_device_ready()
+ * @dev: the virtio device
+ *
+ * This assumes that the device's get_status and set_status operations are
+ * noirq-safe.
+ */
+static inline
+void virtio_device_ready_noirq(struct virtio_device *dev)
+{
+       unsigned int status = dev->config->get_status(dev);
+
+       WARN_ON(status & VIRTIO_CONFIG_S_DRIVER_OK);
+
+#ifdef CONFIG_VIRTIO_HARDEN_NOTIFICATION
+       /*
+        * The noirq stage runs with device IRQ handlers disabled, so
+        * virtio_synchronize_cbs() must not be called here.
+        */
+       __virtio_unbreak_device(dev);
+#endif
+
+       dev->config->set_status(dev, status | VIRTIO_CONFIG_S_DRIVER_OK);
+}
+
 static inline
 const char *virtio_bus_name(struct virtio_device *vdev)
 {
-- 
2.34.1


Reply via email to