| // 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"); |