diff options
Diffstat (limited to 'drivers/s390/block/scm_blk.c')
-rw-r--r-- | drivers/s390/block/scm_blk.c | 591 |
1 files changed, 591 insertions, 0 deletions
diff --git a/drivers/s390/block/scm_blk.c b/drivers/s390/block/scm_blk.c new file mode 100644 index 000000000..e01889394 --- /dev/null +++ b/drivers/s390/block/scm_blk.c @@ -0,0 +1,591 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Block driver for s390 storage class memory. + * + * Copyright IBM Corp. 2012 + * Author(s): Sebastian Ott <sebott@linux.vnet.ibm.com> + */ + +#define KMSG_COMPONENT "scm_block" +#define pr_fmt(fmt) KMSG_COMPONENT ": " fmt + +#include <linux/interrupt.h> +#include <linux/spinlock.h> +#include <linux/mempool.h> +#include <linux/module.h> +#include <linux/blkdev.h> +#include <linux/blk-mq.h> +#include <linux/genhd.h> +#include <linux/slab.h> +#include <linux/list.h> +#include <asm/eadm.h> +#include "scm_blk.h" + +debug_info_t *scm_debug; +static int scm_major; +static mempool_t *aidaw_pool; +static DEFINE_SPINLOCK(list_lock); +static LIST_HEAD(inactive_requests); +static unsigned int nr_requests = 64; +static unsigned int nr_requests_per_io = 8; +static atomic_t nr_devices = ATOMIC_INIT(0); +module_param(nr_requests, uint, S_IRUGO); +MODULE_PARM_DESC(nr_requests, "Number of parallel requests."); + +module_param(nr_requests_per_io, uint, S_IRUGO); +MODULE_PARM_DESC(nr_requests_per_io, "Number of requests per IO."); + +MODULE_DESCRIPTION("Block driver for s390 storage class memory."); +MODULE_LICENSE("GPL"); +MODULE_ALIAS("scm:scmdev*"); + +static void __scm_free_rq(struct scm_request *scmrq) +{ + struct aob_rq_header *aobrq = to_aobrq(scmrq); + + free_page((unsigned long) scmrq->aob); + kfree(scmrq->request); + kfree(aobrq); +} + +static void scm_free_rqs(void) +{ + struct list_head *iter, *safe; + struct scm_request *scmrq; + + spin_lock_irq(&list_lock); + list_for_each_safe(iter, safe, &inactive_requests) { + scmrq = list_entry(iter, struct scm_request, list); + list_del(&scmrq->list); + __scm_free_rq(scmrq); + } + spin_unlock_irq(&list_lock); + + mempool_destroy(aidaw_pool); +} + +static int __scm_alloc_rq(void) +{ + struct aob_rq_header *aobrq; + struct scm_request *scmrq; + + aobrq = kzalloc(sizeof(*aobrq) + sizeof(*scmrq), GFP_KERNEL); + if (!aobrq) + return -ENOMEM; + + scmrq = (void *) aobrq->data; + scmrq->aob = (void *) get_zeroed_page(GFP_DMA); + if (!scmrq->aob) + goto free; + + scmrq->request = kcalloc(nr_requests_per_io, sizeof(scmrq->request[0]), + GFP_KERNEL); + if (!scmrq->request) + goto free; + + INIT_LIST_HEAD(&scmrq->list); + spin_lock_irq(&list_lock); + list_add(&scmrq->list, &inactive_requests); + spin_unlock_irq(&list_lock); + + return 0; +free: + __scm_free_rq(scmrq); + return -ENOMEM; +} + +static int scm_alloc_rqs(unsigned int nrqs) +{ + int ret = 0; + + aidaw_pool = mempool_create_page_pool(max(nrqs/8, 1U), 0); + if (!aidaw_pool) + return -ENOMEM; + + while (nrqs-- && !ret) + ret = __scm_alloc_rq(); + + return ret; +} + +static struct scm_request *scm_request_fetch(void) +{ + struct scm_request *scmrq = NULL; + + spin_lock_irq(&list_lock); + if (list_empty(&inactive_requests)) + goto out; + scmrq = list_first_entry(&inactive_requests, struct scm_request, list); + list_del(&scmrq->list); +out: + spin_unlock_irq(&list_lock); + return scmrq; +} + +static void scm_request_done(struct scm_request *scmrq) +{ + unsigned long flags; + struct msb *msb; + u64 aidaw; + int i; + + for (i = 0; i < nr_requests_per_io && scmrq->request[i]; i++) { + msb = &scmrq->aob->msb[i]; + aidaw = msb->data_addr; + + if ((msb->flags & MSB_FLAG_IDA) && aidaw && + IS_ALIGNED(aidaw, PAGE_SIZE)) + mempool_free(virt_to_page(aidaw), aidaw_pool); + } + + spin_lock_irqsave(&list_lock, flags); + list_add(&scmrq->list, &inactive_requests); + spin_unlock_irqrestore(&list_lock, flags); +} + +static bool scm_permit_request(struct scm_blk_dev *bdev, struct request *req) +{ + return rq_data_dir(req) != WRITE || bdev->state != SCM_WR_PROHIBIT; +} + +static inline struct aidaw *scm_aidaw_alloc(void) +{ + struct page *page = mempool_alloc(aidaw_pool, GFP_ATOMIC); + + return page ? page_address(page) : NULL; +} + +static inline unsigned long scm_aidaw_bytes(struct aidaw *aidaw) +{ + unsigned long _aidaw = (unsigned long) aidaw; + unsigned long bytes = ALIGN(_aidaw, PAGE_SIZE) - _aidaw; + + return (bytes / sizeof(*aidaw)) * PAGE_SIZE; +} + +struct aidaw *scm_aidaw_fetch(struct scm_request *scmrq, unsigned int bytes) +{ + struct aidaw *aidaw; + + if (scm_aidaw_bytes(scmrq->next_aidaw) >= bytes) + return scmrq->next_aidaw; + + aidaw = scm_aidaw_alloc(); + if (aidaw) + memset(aidaw, 0, PAGE_SIZE); + return aidaw; +} + +static int scm_request_prepare(struct scm_request *scmrq) +{ + struct scm_blk_dev *bdev = scmrq->bdev; + struct scm_device *scmdev = bdev->gendisk->private_data; + int pos = scmrq->aob->request.msb_count; + struct msb *msb = &scmrq->aob->msb[pos]; + struct request *req = scmrq->request[pos]; + struct req_iterator iter; + struct aidaw *aidaw; + struct bio_vec bv; + + aidaw = scm_aidaw_fetch(scmrq, blk_rq_bytes(req)); + if (!aidaw) + return -ENOMEM; + + msb->bs = MSB_BS_4K; + scmrq->aob->request.msb_count++; + msb->scm_addr = scmdev->address + ((u64) blk_rq_pos(req) << 9); + msb->oc = (rq_data_dir(req) == READ) ? MSB_OC_READ : MSB_OC_WRITE; + msb->flags |= MSB_FLAG_IDA; + msb->data_addr = (u64) aidaw; + + rq_for_each_segment(bv, req, iter) { + WARN_ON(bv.bv_offset); + msb->blk_count += bv.bv_len >> 12; + aidaw->data_addr = (u64) page_address(bv.bv_page); + aidaw++; + } + + scmrq->next_aidaw = aidaw; + return 0; +} + +static inline void scm_request_set(struct scm_request *scmrq, + struct request *req) +{ + scmrq->request[scmrq->aob->request.msb_count] = req; +} + +static inline void scm_request_init(struct scm_blk_dev *bdev, + struct scm_request *scmrq) +{ + struct aob_rq_header *aobrq = to_aobrq(scmrq); + struct aob *aob = scmrq->aob; + + memset(scmrq->request, 0, + nr_requests_per_io * sizeof(scmrq->request[0])); + memset(aob, 0, sizeof(*aob)); + aobrq->scmdev = bdev->scmdev; + aob->request.cmd_code = ARQB_CMD_MOVE; + aob->request.data = (u64) aobrq; + scmrq->bdev = bdev; + scmrq->retries = 4; + scmrq->error = BLK_STS_OK; + /* We don't use all msbs - place aidaws at the end of the aob page. */ + scmrq->next_aidaw = (void *) &aob->msb[nr_requests_per_io]; +} + +static void scm_request_requeue(struct scm_request *scmrq) +{ + struct scm_blk_dev *bdev = scmrq->bdev; + int i; + + for (i = 0; i < nr_requests_per_io && scmrq->request[i]; i++) + blk_mq_requeue_request(scmrq->request[i], false); + + atomic_dec(&bdev->queued_reqs); + scm_request_done(scmrq); + blk_mq_kick_requeue_list(bdev->rq); +} + +static void scm_request_finish(struct scm_request *scmrq) +{ + struct scm_blk_dev *bdev = scmrq->bdev; + blk_status_t *error; + int i; + + for (i = 0; i < nr_requests_per_io && scmrq->request[i]; i++) { + error = blk_mq_rq_to_pdu(scmrq->request[i]); + *error = scmrq->error; + blk_mq_complete_request(scmrq->request[i]); + } + + atomic_dec(&bdev->queued_reqs); + scm_request_done(scmrq); +} + +static void scm_request_start(struct scm_request *scmrq) +{ + struct scm_blk_dev *bdev = scmrq->bdev; + + atomic_inc(&bdev->queued_reqs); + if (eadm_start_aob(scmrq->aob)) { + SCM_LOG(5, "no subchannel"); + scm_request_requeue(scmrq); + } +} + +struct scm_queue { + struct scm_request *scmrq; + spinlock_t lock; +}; + +static blk_status_t scm_blk_request(struct blk_mq_hw_ctx *hctx, + const struct blk_mq_queue_data *qd) +{ + struct scm_device *scmdev = hctx->queue->queuedata; + struct scm_blk_dev *bdev = dev_get_drvdata(&scmdev->dev); + struct scm_queue *sq = hctx->driver_data; + struct request *req = qd->rq; + struct scm_request *scmrq; + + spin_lock(&sq->lock); + if (!scm_permit_request(bdev, req)) { + spin_unlock(&sq->lock); + return BLK_STS_RESOURCE; + } + + scmrq = sq->scmrq; + if (!scmrq) { + scmrq = scm_request_fetch(); + if (!scmrq) { + SCM_LOG(5, "no request"); + spin_unlock(&sq->lock); + return BLK_STS_RESOURCE; + } + scm_request_init(bdev, scmrq); + sq->scmrq = scmrq; + } + scm_request_set(scmrq, req); + + if (scm_request_prepare(scmrq)) { + SCM_LOG(5, "aidaw alloc failed"); + scm_request_set(scmrq, NULL); + + if (scmrq->aob->request.msb_count) + scm_request_start(scmrq); + + sq->scmrq = NULL; + spin_unlock(&sq->lock); + return BLK_STS_RESOURCE; + } + blk_mq_start_request(req); + + if (qd->last || scmrq->aob->request.msb_count == nr_requests_per_io) { + scm_request_start(scmrq); + sq->scmrq = NULL; + } + spin_unlock(&sq->lock); + return BLK_STS_OK; +} + +static int scm_blk_init_hctx(struct blk_mq_hw_ctx *hctx, void *data, + unsigned int idx) +{ + struct scm_queue *qd = kzalloc(sizeof(*qd), GFP_KERNEL); + + if (!qd) + return -ENOMEM; + + spin_lock_init(&qd->lock); + hctx->driver_data = qd; + + return 0; +} + +static void scm_blk_exit_hctx(struct blk_mq_hw_ctx *hctx, unsigned int idx) +{ + struct scm_queue *qd = hctx->driver_data; + + WARN_ON(qd->scmrq); + kfree(hctx->driver_data); + hctx->driver_data = NULL; +} + +static void __scmrq_log_error(struct scm_request *scmrq) +{ + struct aob *aob = scmrq->aob; + + if (scmrq->error == BLK_STS_TIMEOUT) + SCM_LOG(1, "Request timeout"); + else { + SCM_LOG(1, "Request error"); + SCM_LOG_HEX(1, &aob->response, sizeof(aob->response)); + } + if (scmrq->retries) + SCM_LOG(1, "Retry request"); + else + pr_err("An I/O operation to SCM failed with rc=%d\n", + scmrq->error); +} + +static void scm_blk_handle_error(struct scm_request *scmrq) +{ + struct scm_blk_dev *bdev = scmrq->bdev; + unsigned long flags; + + if (scmrq->error != BLK_STS_IOERR) + goto restart; + + /* For -EIO the response block is valid. */ + switch (scmrq->aob->response.eqc) { + case EQC_WR_PROHIBIT: + spin_lock_irqsave(&bdev->lock, flags); + if (bdev->state != SCM_WR_PROHIBIT) + pr_info("%lx: Write access to the SCM increment is suspended\n", + (unsigned long) bdev->scmdev->address); + bdev->state = SCM_WR_PROHIBIT; + spin_unlock_irqrestore(&bdev->lock, flags); + goto requeue; + default: + break; + } + +restart: + if (!eadm_start_aob(scmrq->aob)) + return; + +requeue: + scm_request_requeue(scmrq); +} + +void scm_blk_irq(struct scm_device *scmdev, void *data, blk_status_t error) +{ + struct scm_request *scmrq = data; + + scmrq->error = error; + if (error) { + __scmrq_log_error(scmrq); + if (scmrq->retries-- > 0) { + scm_blk_handle_error(scmrq); + return; + } + } + + scm_request_finish(scmrq); +} + +static void scm_blk_request_done(struct request *req) +{ + blk_status_t *error = blk_mq_rq_to_pdu(req); + + blk_mq_end_request(req, *error); +} + +static const struct block_device_operations scm_blk_devops = { + .owner = THIS_MODULE, +}; + +static const struct blk_mq_ops scm_mq_ops = { + .queue_rq = scm_blk_request, + .complete = scm_blk_request_done, + .init_hctx = scm_blk_init_hctx, + .exit_hctx = scm_blk_exit_hctx, +}; + +int scm_blk_dev_setup(struct scm_blk_dev *bdev, struct scm_device *scmdev) +{ + unsigned int devindex, nr_max_blk; + struct request_queue *rq; + int len, ret; + + devindex = atomic_inc_return(&nr_devices) - 1; + /* scma..scmz + scmaa..scmzz */ + if (devindex > 701) { + ret = -ENODEV; + goto out; + } + + bdev->scmdev = scmdev; + bdev->state = SCM_OPER; + spin_lock_init(&bdev->lock); + atomic_set(&bdev->queued_reqs, 0); + + bdev->tag_set.ops = &scm_mq_ops; + bdev->tag_set.cmd_size = sizeof(blk_status_t); + bdev->tag_set.nr_hw_queues = nr_requests; + bdev->tag_set.queue_depth = nr_requests_per_io * nr_requests; + bdev->tag_set.flags = BLK_MQ_F_SHOULD_MERGE; + bdev->tag_set.numa_node = NUMA_NO_NODE; + + ret = blk_mq_alloc_tag_set(&bdev->tag_set); + if (ret) + goto out; + + rq = blk_mq_init_queue(&bdev->tag_set); + if (IS_ERR(rq)) { + ret = PTR_ERR(rq); + goto out_tag; + } + bdev->rq = rq; + nr_max_blk = min(scmdev->nr_max_block, + (unsigned int) (PAGE_SIZE / sizeof(struct aidaw))); + + blk_queue_logical_block_size(rq, 1 << 12); + blk_queue_max_hw_sectors(rq, nr_max_blk << 3); /* 8 * 512 = blk_size */ + blk_queue_max_segments(rq, nr_max_blk); + blk_queue_flag_set(QUEUE_FLAG_NONROT, rq); + blk_queue_flag_clear(QUEUE_FLAG_ADD_RANDOM, rq); + + bdev->gendisk = alloc_disk(SCM_NR_PARTS); + if (!bdev->gendisk) { + ret = -ENOMEM; + goto out_queue; + } + rq->queuedata = scmdev; + bdev->gendisk->private_data = scmdev; + bdev->gendisk->fops = &scm_blk_devops; + bdev->gendisk->queue = rq; + bdev->gendisk->major = scm_major; + bdev->gendisk->first_minor = devindex * SCM_NR_PARTS; + + len = snprintf(bdev->gendisk->disk_name, DISK_NAME_LEN, "scm"); + if (devindex > 25) { + len += snprintf(bdev->gendisk->disk_name + len, + DISK_NAME_LEN - len, "%c", + 'a' + (devindex / 26) - 1); + devindex = devindex % 26; + } + snprintf(bdev->gendisk->disk_name + len, DISK_NAME_LEN - len, "%c", + 'a' + devindex); + + /* 512 byte sectors */ + set_capacity(bdev->gendisk, scmdev->size >> 9); + device_add_disk(&scmdev->dev, bdev->gendisk, NULL); + return 0; + +out_queue: + blk_cleanup_queue(rq); +out_tag: + blk_mq_free_tag_set(&bdev->tag_set); +out: + atomic_dec(&nr_devices); + return ret; +} + +void scm_blk_dev_cleanup(struct scm_blk_dev *bdev) +{ + del_gendisk(bdev->gendisk); + blk_cleanup_queue(bdev->gendisk->queue); + blk_mq_free_tag_set(&bdev->tag_set); + put_disk(bdev->gendisk); +} + +void scm_blk_set_available(struct scm_blk_dev *bdev) +{ + unsigned long flags; + + spin_lock_irqsave(&bdev->lock, flags); + if (bdev->state == SCM_WR_PROHIBIT) + pr_info("%lx: Write access to the SCM increment is restored\n", + (unsigned long) bdev->scmdev->address); + bdev->state = SCM_OPER; + spin_unlock_irqrestore(&bdev->lock, flags); +} + +static bool __init scm_blk_params_valid(void) +{ + if (!nr_requests_per_io || nr_requests_per_io > 64) + return false; + + return true; +} + +static int __init scm_blk_init(void) +{ + int ret = -EINVAL; + + if (!scm_blk_params_valid()) + goto out; + + ret = register_blkdev(0, "scm"); + if (ret < 0) + goto out; + + scm_major = ret; + ret = scm_alloc_rqs(nr_requests); + if (ret) + goto out_free; + + scm_debug = debug_register("scm_log", 16, 1, 16); + if (!scm_debug) { + ret = -ENOMEM; + goto out_free; + } + + debug_register_view(scm_debug, &debug_hex_ascii_view); + debug_set_level(scm_debug, 2); + + ret = scm_drv_init(); + if (ret) + goto out_dbf; + + return ret; + +out_dbf: + debug_unregister(scm_debug); +out_free: + scm_free_rqs(); + unregister_blkdev(scm_major, "scm"); +out: + return ret; +} +module_init(scm_blk_init); + +static void __exit scm_blk_cleanup(void) +{ + scm_drv_cleanup(); + debug_unregister(scm_debug); + scm_free_rqs(); + unregister_blkdev(scm_major, "scm"); +} +module_exit(scm_blk_cleanup); |