| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Recognize and maintain s390 storage class memory. | |
| * | |
| * Copyright IBM Corp. 2012 | |
| * Author(s): Sebastian Ott <sebott@linux.vnet.ibm.com> | |
| */ | |
| #include <linux/device.h> | |
| #include <linux/export.h> | |
| #include <linux/module.h> | |
| #include <linux/mutex.h> | |
| #include <linux/slab.h> | |
| #include <linux/init.h> | |
| #include <linux/err.h> | |
| #include <asm/eadm.h> | |
| #include "chsc.h" | |
| static struct device *scm_root; | |
| #define to_scm_dev(n) container_of(n, struct scm_device, dev) | |
| #define to_scm_drv(d) container_of(d, struct scm_driver, drv) | |
| static int scmdev_probe(struct device *dev) | |
| { | |
| struct scm_device *scmdev = to_scm_dev(dev); | |
| struct scm_driver *scmdrv = to_scm_drv(dev->driver); | |
| return scmdrv->probe ? scmdrv->probe(scmdev) : -ENODEV; | |
| } | |
| static void scmdev_remove(struct device *dev) | |
| { | |
| struct scm_device *scmdev = to_scm_dev(dev); | |
| struct scm_driver *scmdrv = to_scm_drv(dev->driver); | |
| if (scmdrv->remove) | |
| scmdrv->remove(scmdev); | |
| } | |
| static int scmdev_uevent(const struct device *dev, struct kobj_uevent_env *env) | |
| { | |
| return add_uevent_var(env, "MODALIAS=scm:scmdev"); | |
| } | |
| static const struct bus_type scm_bus_type = { | |
| .name = "scm", | |
| .probe = scmdev_probe, | |
| .remove = scmdev_remove, | |
| .uevent = scmdev_uevent, | |
| }; | |
| /** | |
| * scm_driver_register() - register a scm driver | |
| * @scmdrv: driver to be registered | |
| */ | |
| int scm_driver_register(struct scm_driver *scmdrv) | |
| { | |
| struct device_driver *drv = &scmdrv->drv; | |
| drv->bus = &scm_bus_type; | |
| return driver_register(drv); | |
| } | |
| EXPORT_SYMBOL_GPL(scm_driver_register); | |
| /** | |
| * scm_driver_unregister() - deregister a scm driver | |
| * @scmdrv: driver to be deregistered | |
| */ | |
| void scm_driver_unregister(struct scm_driver *scmdrv) | |
| { | |
| driver_unregister(&scmdrv->drv); | |
| } | |
| EXPORT_SYMBOL_GPL(scm_driver_unregister); | |
| void scm_irq_handler(struct aob *aob, blk_status_t error) | |
| { | |
| struct aob_rq_header *aobrq = (void *) aob->request.data; | |
| struct scm_device *scmdev = aobrq->scmdev; | |
| struct scm_driver *scmdrv = to_scm_drv(scmdev->dev.driver); | |
| scmdrv->handler(scmdev, aobrq->data, error); | |
| } | |
| EXPORT_SYMBOL_GPL(scm_irq_handler); | |
| #define scm_attr(name) \ | |
| static ssize_t show_##name(struct device *dev, \ | |
| struct device_attribute *attr, char *buf) \ | |
| { \ | |
| struct scm_device *scmdev = to_scm_dev(dev); \ | |
| int ret; \ | |
| \ | |
| device_lock(dev); \ | |
| ret = sysfs_emit(buf, "%u\n", scmdev->attrs.name); \ | |
| device_unlock(dev); \ | |
| \ | |
| return ret; \ | |
| } \ | |
| static DEVICE_ATTR(name, S_IRUGO, show_##name, NULL); | |
| scm_attr(persistence); | |
| scm_attr(oper_state); | |
| scm_attr(data_state); | |
| scm_attr(rank); | |
| scm_attr(release); | |
| scm_attr(res_id); | |
| static struct attribute *scmdev_attrs[] = { | |
| &dev_attr_persistence.attr, | |
| &dev_attr_oper_state.attr, | |
| &dev_attr_data_state.attr, | |
| &dev_attr_rank.attr, | |
| &dev_attr_release.attr, | |
| &dev_attr_res_id.attr, | |
| NULL, | |
| }; | |
| static struct attribute_group scmdev_attr_group = { | |
| .attrs = scmdev_attrs, | |
| }; | |
| static const struct attribute_group *scmdev_attr_groups[] = { | |
| &scmdev_attr_group, | |
| NULL, | |
| }; | |
| static void scmdev_release(struct device *dev) | |
| { | |
| struct scm_device *scmdev = to_scm_dev(dev); | |
| kfree(scmdev); | |
| } | |
| static void scmdev_setup(struct scm_device *scmdev, struct sale *sale, | |
| unsigned int size, unsigned int max_blk_count) | |
| { | |
| dev_set_name(&scmdev->dev, "%016llx", (unsigned long long) sale->sa); | |
| scmdev->nr_max_block = max_blk_count; | |
| scmdev->address = sale->sa; | |
| scmdev->size = 1UL << size; | |
| scmdev->attrs.rank = sale->rank; | |
| scmdev->attrs.persistence = sale->p; | |
| scmdev->attrs.oper_state = sale->op_state; | |
| scmdev->attrs.data_state = sale->data_state; | |
| scmdev->attrs.rank = sale->rank; | |
| scmdev->attrs.release = sale->r; | |
| scmdev->attrs.res_id = sale->rid; | |
| scmdev->dev.parent = scm_root; | |
| scmdev->dev.bus = &scm_bus_type; | |
| scmdev->dev.release = scmdev_release; | |
| scmdev->dev.groups = scmdev_attr_groups; | |
| } | |
| /* | |
| * Check for state-changes, notify the driver and userspace. | |
| */ | |
| static void scmdev_update(struct scm_device *scmdev, struct sale *sale) | |
| { | |
| struct scm_driver *scmdrv; | |
| bool changed; | |
| device_lock(&scmdev->dev); | |
| changed = scmdev->attrs.rank != sale->rank || | |
| scmdev->attrs.oper_state != sale->op_state; | |
| scmdev->attrs.rank = sale->rank; | |
| scmdev->attrs.oper_state = sale->op_state; | |
| if (!scmdev->dev.driver) | |
| goto out; | |
| scmdrv = to_scm_drv(scmdev->dev.driver); | |
| if (changed && scmdrv->notify) | |
| scmdrv->notify(scmdev, SCM_CHANGE); | |
| out: | |
| device_unlock(&scmdev->dev); | |
| if (changed) | |
| kobject_uevent(&scmdev->dev.kobj, KOBJ_CHANGE); | |
| } | |
| static int check_address(struct device *dev, const void *data) | |
| { | |
| struct scm_device *scmdev = to_scm_dev(dev); | |
| const struct sale *sale = data; | |
| return scmdev->address == sale->sa; | |
| } | |
| static struct scm_device *scmdev_find(struct sale *sale) | |
| { | |
| struct device *dev; | |
| dev = bus_find_device(&scm_bus_type, NULL, sale, check_address); | |
| return dev ? to_scm_dev(dev) : NULL; | |
| } | |
| static int scm_add(struct chsc_scm_info *scm_info, size_t num) | |
| { | |
| struct sale *sale, *scmal = scm_info->scmal; | |
| struct scm_device *scmdev; | |
| int ret; | |
| for (sale = scmal; sale < scmal + num; sale++) { | |
| scmdev = scmdev_find(sale); | |
| if (scmdev) { | |
| scmdev_update(scmdev, sale); | |
| /* Release reference from scm_find(). */ | |
| put_device(&scmdev->dev); | |
| continue; | |
| } | |
| scmdev = kzalloc_obj(*scmdev); | |
| if (!scmdev) | |
| return -ENODEV; | |
| scmdev_setup(scmdev, sale, scm_info->is, scm_info->mbc); | |
| ret = device_register(&scmdev->dev); | |
| if (ret) { | |
| /* Release reference from device_initialize(). */ | |
| put_device(&scmdev->dev); | |
| return ret; | |
| } | |
| } | |
| return 0; | |
| } | |
| int scm_update_information(void) | |
| { | |
| struct chsc_scm_info *scm_info; | |
| u64 token = 0; | |
| size_t num; | |
| int ret; | |
| scm_info = (void *)__get_free_page(GFP_KERNEL | GFP_DMA); | |
| if (!scm_info) | |
| return -ENOMEM; | |
| do { | |
| ret = chsc_scm_info(scm_info, token); | |
| if (ret) | |
| break; | |
| num = (scm_info->response.length - | |
| (offsetof(struct chsc_scm_info, scmal) - | |
| offsetof(struct chsc_scm_info, response)) | |
| ) / sizeof(struct sale); | |
| ret = scm_add(scm_info, num); | |
| if (ret) | |
| break; | |
| token = scm_info->restok; | |
| } while (token); | |
| free_page((unsigned long)scm_info); | |
| return ret; | |
| } | |
| static int scm_dev_avail(struct device *dev, void *unused) | |
| { | |
| struct scm_driver *scmdrv = to_scm_drv(dev->driver); | |
| struct scm_device *scmdev = to_scm_dev(dev); | |
| if (dev->driver && scmdrv->notify) | |
| scmdrv->notify(scmdev, SCM_AVAIL); | |
| return 0; | |
| } | |
| int scm_process_availability_information(void) | |
| { | |
| return bus_for_each_dev(&scm_bus_type, NULL, NULL, scm_dev_avail); | |
| } | |
| static int __init scm_init(void) | |
| { | |
| int ret; | |
| ret = bus_register(&scm_bus_type); | |
| if (ret) | |
| return ret; | |
| scm_root = root_device_register("scm"); | |
| if (IS_ERR(scm_root)) { | |
| bus_unregister(&scm_bus_type); | |
| return PTR_ERR(scm_root); | |
| } | |
| scm_update_information(); | |
| return 0; | |
| } | |
| subsys_initcall_sync(scm_init); |