| // 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/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(sizeof(*scmdev), GFP_KERNEL); |
| 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); |
| 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); |