mirror of
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
synced 2026-09-18 23:09:29 +02:00
virtio_pmem_freeze() currently deletes virtqueues and resets the device without waking threads waiting for a virtqueue descriptor or a host completion. Mark the request virtqueue broken before reset. This makes new submissions fail fast and lets -ENOSPC waiters leave the wait list. Reset the device before draining used and unused request tokens, then delete the virtqueues. This wakes waiters with -EIO. It also keeps the detach call on a quiesced device. Clear req_vq after del_vqs(). Make drain tolerate a NULL queue so remove after freeze does not dereference a stale virtqueue pointer. Also make virtio_pmem_flush() stop checking req_vq once the broken state is visible. A waiter woken by freeze/remove can resume after del_vqs() has cleared req_vq. Signed-off-by: Li Chen <me@linux.beauty> Signed-off-by: Michael S. Tsirkin <mst@redhat.com> Message-ID: <20260630092338.2094628-13-me@linux.beauty>
311 lines
8.0 KiB
C
311 lines
8.0 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* virtio_pmem.c: Virtio pmem Driver
|
|
*
|
|
* Discovers persistent memory range information
|
|
* from host and provides a virtio based flushing
|
|
* interface.
|
|
*/
|
|
#include "virtio_pmem.h"
|
|
#include "nd.h"
|
|
|
|
struct virtio_pmem_flush_work {
|
|
struct work_struct work;
|
|
struct nd_region *nd_region;
|
|
struct bio *bio;
|
|
};
|
|
|
|
static void virtio_pmem_req_release(struct kref *kref)
|
|
{
|
|
struct virtio_pmem_request *req;
|
|
|
|
req = container_of(kref, struct virtio_pmem_request, kref);
|
|
kfree(req);
|
|
}
|
|
|
|
static void virtio_pmem_signal_done(struct virtio_pmem_request *req)
|
|
{
|
|
/* Pairs with smp_load_acquire() in virtio_pmem_req_done(). */
|
|
smp_store_release(&req->done, true);
|
|
wake_up(&req->host_acked);
|
|
}
|
|
|
|
static bool virtio_pmem_req_done(struct virtio_pmem_request *req)
|
|
{
|
|
/* Pairs with smp_store_release() in virtio_pmem_signal_done(). */
|
|
return smp_load_acquire(&req->done);
|
|
}
|
|
|
|
static void virtio_pmem_complete_err(struct virtio_pmem_request *req)
|
|
{
|
|
req->resp.ret = cpu_to_le32(1);
|
|
virtio_pmem_signal_done(req);
|
|
}
|
|
|
|
static void virtio_pmem_wake_one_waiter(struct virtio_pmem *vpmem)
|
|
{
|
|
struct virtio_pmem_request *req_buf;
|
|
|
|
if (list_empty(&vpmem->req_list))
|
|
return;
|
|
|
|
req_buf = list_first_entry(&vpmem->req_list,
|
|
struct virtio_pmem_request, list);
|
|
list_del_init(&req_buf->list);
|
|
WRITE_ONCE(req_buf->wq_buf_avail, true);
|
|
wake_up(&req_buf->wq_buf);
|
|
}
|
|
|
|
static void virtio_pmem_wake_all_waiters(struct virtio_pmem *vpmem)
|
|
{
|
|
struct virtio_pmem_request *req, *tmp;
|
|
|
|
list_for_each_entry_safe(req, tmp, &vpmem->req_list, list) {
|
|
list_del_init(&req->list);
|
|
WRITE_ONCE(req->wq_buf_avail, true);
|
|
wake_up(&req->wq_buf);
|
|
}
|
|
}
|
|
|
|
static void virtio_pmem_clear_inflight(struct virtio_pmem *vpmem,
|
|
struct virtio_pmem_request *req)
|
|
{
|
|
if (vpmem->req_inflight == req)
|
|
vpmem->req_inflight = NULL;
|
|
}
|
|
|
|
static void virtio_pmem_wake_inflight(struct virtio_pmem *vpmem)
|
|
{
|
|
struct virtio_pmem_request *req = vpmem->req_inflight;
|
|
|
|
if (req)
|
|
wake_up(&req->host_acked);
|
|
}
|
|
|
|
void virtio_pmem_mark_broken(struct virtio_pmem *vpmem)
|
|
{
|
|
if (!READ_ONCE(vpmem->broken)) {
|
|
WRITE_ONCE(vpmem->broken, true);
|
|
dev_err_once(&vpmem->vdev->dev, "virtqueue is broken\n");
|
|
}
|
|
|
|
virtio_pmem_wake_inflight(vpmem);
|
|
virtio_pmem_wake_all_waiters(vpmem);
|
|
}
|
|
EXPORT_SYMBOL_GPL(virtio_pmem_mark_broken);
|
|
|
|
void virtio_pmem_drain(struct virtio_pmem *vpmem)
|
|
{
|
|
struct virtio_pmem_request *req;
|
|
unsigned int len;
|
|
|
|
if (!vpmem->req_vq)
|
|
return;
|
|
|
|
while ((req = virtqueue_get_buf(vpmem->req_vq, &len)) != NULL) {
|
|
virtio_pmem_clear_inflight(vpmem, req);
|
|
virtio_pmem_complete_err(req);
|
|
kref_put(&req->kref, virtio_pmem_req_release);
|
|
}
|
|
|
|
while ((req = virtqueue_detach_unused_buf(vpmem->req_vq)) != NULL) {
|
|
virtio_pmem_clear_inflight(vpmem, req);
|
|
virtio_pmem_complete_err(req);
|
|
kref_put(&req->kref, virtio_pmem_req_release);
|
|
}
|
|
}
|
|
EXPORT_SYMBOL_GPL(virtio_pmem_drain);
|
|
|
|
/* The interrupt handler */
|
|
void virtio_pmem_host_ack(struct virtqueue *vq)
|
|
{
|
|
struct virtio_pmem *vpmem = vq->vdev->priv;
|
|
struct virtio_pmem_request *req_data;
|
|
unsigned long flags;
|
|
unsigned int len;
|
|
|
|
spin_lock_irqsave(&vpmem->pmem_lock, flags);
|
|
while ((req_data = virtqueue_get_buf(vq, &len)) != NULL) {
|
|
virtio_pmem_clear_inflight(vpmem, req_data);
|
|
virtio_pmem_wake_one_waiter(vpmem);
|
|
if (READ_ONCE(vpmem->broken))
|
|
virtio_pmem_complete_err(req_data);
|
|
else
|
|
virtio_pmem_signal_done(req_data);
|
|
kref_put(&req_data->kref, virtio_pmem_req_release);
|
|
}
|
|
spin_unlock_irqrestore(&vpmem->pmem_lock, flags);
|
|
}
|
|
EXPORT_SYMBOL_GPL(virtio_pmem_host_ack);
|
|
|
|
/* The request submission function */
|
|
static int virtio_pmem_flush(struct nd_region *nd_region)
|
|
{
|
|
struct virtio_device *vdev = nd_region->provider_data;
|
|
struct virtio_pmem *vpmem = vdev->priv;
|
|
struct virtio_pmem_request *req_data;
|
|
struct scatterlist *sgs[2], sg, ret;
|
|
unsigned long flags;
|
|
int err, err1;
|
|
|
|
guard(mutex)(&vpmem->flush_lock);
|
|
|
|
/*
|
|
* Don't bother to submit the request to the device if the device is
|
|
* not activated.
|
|
*/
|
|
if (vdev->config->get_status(vdev) & VIRTIO_CONFIG_S_NEEDS_RESET) {
|
|
dev_info(&vdev->dev, "virtio pmem device needs a reset\n");
|
|
return -EIO;
|
|
}
|
|
|
|
if (READ_ONCE(vpmem->broken))
|
|
return -EIO;
|
|
|
|
req_data = kmalloc_obj(*req_data, GFP_NOIO);
|
|
if (!req_data)
|
|
return -ENOMEM;
|
|
|
|
kref_init(&req_data->kref);
|
|
WRITE_ONCE(req_data->done, false);
|
|
init_waitqueue_head(&req_data->host_acked);
|
|
init_waitqueue_head(&req_data->wq_buf);
|
|
INIT_LIST_HEAD(&req_data->list);
|
|
req_data->req.type = cpu_to_le32(VIRTIO_PMEM_REQ_TYPE_FLUSH);
|
|
sg_init_one(&sg, &req_data->req, sizeof(req_data->req));
|
|
sgs[0] = &sg;
|
|
sg_init_one(&ret, &req_data->resp.ret, sizeof(req_data->resp));
|
|
sgs[1] = &ret;
|
|
|
|
spin_lock_irqsave(&vpmem->pmem_lock, flags);
|
|
/*
|
|
* If virtqueue_add_sgs returns -ENOSPC then req_vq virtual
|
|
* queue does not have free descriptor. We add the request
|
|
* to req_list and wait for host_ack to wake us up when free
|
|
* slots are available.
|
|
*/
|
|
for (;;) {
|
|
if (READ_ONCE(vpmem->broken)) {
|
|
err = -EIO;
|
|
break;
|
|
}
|
|
|
|
err = virtqueue_add_sgs(vpmem->req_vq, sgs, 1, 1, req_data,
|
|
GFP_ATOMIC);
|
|
if (!err) {
|
|
/*
|
|
* Take the virtqueue reference while @pmem_lock is
|
|
* held so completion cannot run concurrently.
|
|
*/
|
|
kref_get(&req_data->kref);
|
|
vpmem->req_inflight = req_data;
|
|
break;
|
|
}
|
|
|
|
if (err != -ENOSPC)
|
|
break;
|
|
|
|
dev_info_ratelimited(&vdev->dev,
|
|
"failed to send command to virtio pmem device, no free slots in the virtqueue\n");
|
|
WRITE_ONCE(req_data->wq_buf_avail, false);
|
|
list_add_tail(&req_data->list, &vpmem->req_list);
|
|
spin_unlock_irqrestore(&vpmem->pmem_lock, flags);
|
|
|
|
/* A host response results in "host_ack" getting called */
|
|
wait_event(req_data->wq_buf,
|
|
READ_ONCE(req_data->wq_buf_avail) ||
|
|
READ_ONCE(vpmem->broken));
|
|
spin_lock_irqsave(&vpmem->pmem_lock, flags);
|
|
|
|
if (READ_ONCE(vpmem->broken))
|
|
break;
|
|
}
|
|
|
|
if (READ_ONCE(vpmem->broken))
|
|
err = -EIO;
|
|
if (err == -EIO || virtqueue_is_broken(vpmem->req_vq))
|
|
virtio_pmem_mark_broken(vpmem);
|
|
|
|
err1 = true;
|
|
if (!err && !READ_ONCE(vpmem->broken)) {
|
|
err1 = virtqueue_kick(vpmem->req_vq);
|
|
if (!err1)
|
|
virtio_pmem_mark_broken(vpmem);
|
|
}
|
|
spin_unlock_irqrestore(&vpmem->pmem_lock, flags);
|
|
/*
|
|
* virtqueue_add_sgs failed with error different than -ENOSPC, we can't
|
|
* do anything about that.
|
|
*/
|
|
if (READ_ONCE(vpmem->broken) || err || !err1) {
|
|
dev_info(&vdev->dev, "failed to send command to virtio pmem device\n");
|
|
err = -EIO;
|
|
} else {
|
|
/* A host response results in "host_ack" getting called */
|
|
wait_event(req_data->host_acked,
|
|
virtio_pmem_req_done(req_data) ||
|
|
READ_ONCE(vpmem->broken));
|
|
if (virtio_pmem_req_done(req_data))
|
|
err = le32_to_cpu(req_data->resp.ret);
|
|
else
|
|
err = -EIO;
|
|
}
|
|
|
|
kref_put(&req_data->kref, virtio_pmem_req_release);
|
|
return err;
|
|
};
|
|
|
|
static void virtio_pmem_flush_work(struct work_struct *work)
|
|
{
|
|
struct virtio_pmem_flush_work *flush;
|
|
int err;
|
|
|
|
flush = container_of(work, struct virtio_pmem_flush_work, work);
|
|
err = virtio_pmem_flush(flush->nd_region);
|
|
if (err > 0)
|
|
err = -EIO;
|
|
if (err)
|
|
flush->bio->bi_status = errno_to_blk_status(err);
|
|
bio_endio(flush->bio);
|
|
kfree(flush);
|
|
}
|
|
|
|
/* The asynchronous flush callback function */
|
|
int async_pmem_flush(struct nd_region *nd_region, struct bio *bio)
|
|
{
|
|
struct virtio_device *vdev = nd_region->provider_data;
|
|
struct virtio_pmem *vpmem = vdev->priv;
|
|
struct virtio_pmem_flush_work *flush;
|
|
unsigned long flags;
|
|
int err;
|
|
|
|
if (bio && bio->bi_iter.bi_sector != -1) {
|
|
flush = kmalloc_obj(*flush, GFP_NOIO);
|
|
if (!flush)
|
|
return -ENOMEM;
|
|
|
|
INIT_WORK(&flush->work, virtio_pmem_flush_work);
|
|
flush->nd_region = nd_region;
|
|
flush->bio = bio;
|
|
|
|
spin_lock_irqsave(&vpmem->pmem_lock, flags);
|
|
if (READ_ONCE(vpmem->broken)) {
|
|
spin_unlock_irqrestore(&vpmem->pmem_lock, flags);
|
|
kfree(flush);
|
|
return -EIO;
|
|
}
|
|
queue_work(vpmem->flush_wq, &flush->work);
|
|
spin_unlock_irqrestore(&vpmem->pmem_lock, flags);
|
|
return NVDIMM_FLUSH_ASYNC;
|
|
}
|
|
|
|
err = virtio_pmem_flush(nd_region);
|
|
if (err > 0)
|
|
return -EIO;
|
|
|
|
return err;
|
|
};
|
|
EXPORT_SYMBOL_GPL(async_pmem_flush);
|
|
MODULE_DESCRIPTION("Virtio Persistent Memory Driver");
|
|
MODULE_LICENSE("GPL");
|