/* SPDX-License-Identifier: (BSD-2-Clause OR GPL-2.0-or-later) * (C) 2019-2021 AVM GmbH * * vim:set noexpandtab shiftwidth=8 softtabstop=8 fileencoding=utf-8: * * Helper to create char device (region), add the needed device files. */ #define MODNAME "cchardevice" #define pr_fmt(fmt) MODNAME ": " fmt #include #include // struct module, THIS_MODULE #include // struct file_operations #include // cdev_*() #include // device_ #include // kzmalloc /* cprocfs */ #include /* self */ #include struct cchardevice { const char *name; unsigned int minorcount; struct module *module; const struct file_operations *fops; /* runtime */ struct list_head list; dev_t devno; dev_t major; struct cdev *cdev; struct class *devclass; unsigned int devcount; }; /* --------------------------------------------------------------------- */ static const struct file_operations test_fops = { .owner = THIS_MODULE, }; static struct { struct cprocfs_dir *cprocfs_root; struct list_head dev_list; struct cchardevice test_device; } glob = { .test_device = { .name = MODNAME "_test", .minorcount = 2, .module = THIS_MODULE, .list = LIST_HEAD_INIT(glob.test_device.list), .fops = &test_fops, } }; /* --------------------------------------------------------------------- */ static void cchardevice_internal_unregister(struct cchardevice *p) { while (p->devcount > 0) { unsigned int minor = p->devcount - 1; pr_info("device_destroy(%s %u,%u)\n", p->name, p->major, minor); device_destroy(p->devclass, MKDEV(p->major, minor)); p->devcount--; } if (p->devclass) { pr_info("class_destroy(%s)\n", p->name); class_destroy(p->devclass); p->devclass = 0; } if (p->cdev) { pr_info("cdev_del(%s)\n", p->name); cdev_del(p->cdev); kfree(p->cdev); p->cdev = 0; } if (p->devno) { pr_info("unregister_chrdev_region(%s %u,%u (%u))\n", p->name, MAJOR(p->devno), MINOR(p->devno), p->minorcount); unregister_chrdev_region(p->devno, p->minorcount); p->devno = 0; } list_del(&p->list); } static int cchardevice_internal_register(struct cchardevice *p) { struct device *dev; char devname[64]; int ret; if (strlen(p->name) + 3 > sizeof(devname)) { pr_err("name len check failed for %s\n", p->name); return -1; } p->devcount = 0; ret = alloc_chrdev_region(&p->devno, 0, p->minorcount, p->name); if (ret < 0) { pr_err("alloc_chrdev_region() for %s failed (%d)\n", p->name, ret); return ret; } p->major = MAJOR(p->devno); p->cdev = cdev_alloc(); if (IS_ERR(p->cdev)) { pr_err("cdev_alloc() for %s failed (%ld)\n", p->name, PTR_ERR(p->cdev)); p->cdev = NULL; cchardevice_internal_unregister(p); return -1; } cdev_init(p->cdev, p->fops); p->cdev->owner = p->module; ret = cdev_add(p->cdev, p->devno, p->minorcount); if (ret) { pr_err("cdev_add() for %s failed (%d)\n", p->name, ret); cchardevice_internal_unregister(p); return -1; } p->devclass = class_create(p->module, p->name); if (IS_ERR(p->devclass)) { pr_err("class_create() for %s failed (%ld)\n", p->name, PTR_ERR(p->devclass)); p->devclass = NULL; cchardevice_internal_unregister(p); return -1; } if (p->minorcount > 1) { dev_t devno = p->devno; snprintf(devname, sizeof(devname), "%s%%d", p->name); while (p->devcount < p->minorcount) { pr_info("device_create(%s): %u,%u\n", devname, MAJOR(devno), MINOR(devno)); dev = device_create(p->devclass, NULL, devno, NULL, devname, MINOR(devno)); if (IS_ERR(dev)) { pr_err("device_create() for %s failed (%ld)\n", p->name, PTR_ERR(p->devclass)); cchardevice_internal_unregister(p); return -1; } p->devcount++; devno++; } } else { pr_info("device_create(%s): %u,%u\n", p->name, MAJOR(p->devno), MINOR(p->devno)); dev = device_create(p->devclass, NULL, p->devno, NULL, p->name); if (IS_ERR(dev)) { pr_err("device_create() for %s failed (%ld)\n", p->name, PTR_ERR(p->devclass)); cchardevice_internal_unregister(p); return -1; } p->devcount++; } list_add_tail(&p->list, &glob.dev_list); return 0; } /* --------------------------------------------------------------------- */ struct cchardevice *cchardevice_register(const char *name, unsigned int minorcount, struct module *module, const struct file_operations *fops) { struct cchardevice *p = kzalloc(sizeof(struct cchardevice), GFP_KERNEL); if (!p) return NULL; p->name = name; p->minorcount = minorcount ? minorcount : 1; p->module = module; p->fops = fops; if (cchardevice_internal_register(p) == 0) return p; kfree(p); return NULL; } EXPORT_SYMBOL(cchardevice_register); void cchardevice_unregister(struct cchardevice *p) { cchardevice_internal_unregister(p); kfree(p); } EXPORT_SYMBOL(cchardevice_unregister); /* --------------------------------------------------------------------- */ /* -------- procfs ----------------------------------------------------- */ /* --------------------------------------------------------------------- */ static void cchardevice_show_devices(void *userdata, cprocfs_walk_cb_t cb, void *arg) { struct cchardevice *p, *n; list_for_each_entry_safe(p, n, &glob.dev_list, list) { if (p->minorcount > 1) { cb(arg, "%-16s %4d 0-%u (%s)\n", p->name, p->major, p->minorcount, p->module->name); } else { cb(arg, "%-16s %4d (%s)\n", p->name, p->major, p->module->name); } } } static void cchardevice_control_help(void *userdata, cprocfs_walk_cb_t cb, void *arg) { cb(arg, "possible commands:\n"); cb(arg, " addtest\n"); cb(arg, " deltest\n"); } static int word_matches(const char *haystack, const char *needle) { size_t len = strlen(needle); return len && strncasecmp(haystack, needle, len) == 0; } static void cchardevice_control(void *userdata, int argc, char *argv[]) { if (argc == 0) { pr_err("%s(): empty command\n", __func__); return; } if (word_matches("addtest", argv[0])) (void) cchardevice_internal_register(&glob.test_device); else if (word_matches("deltest", argv[0])) cchardevice_internal_unregister(&glob.test_device); else pr_err("%s(): unknown command '%s'\n", __func__, argv[0]); } /* --------------------------------------------------------------------- */ static int cchardevice_procfs_init(void) { glob.cprocfs_root = cprocfs_create_root_dir(MODNAME); if (glob.cprocfs_root == 0) return -ENOMEM; if (cprocfs_register_file(glob.cprocfs_root, "devices", cchardevice_show_devices, 0) < 0) return -ENOMEM; if (cprocfs_register_control(glob.cprocfs_root, "control", cchardevice_control_help, cchardevice_control, 0) < 0) return -ENOMEM; return 0; } /* --------------------------------------------------------------------- */ /* -------- init & exit ------------------------------------------------ */ /* --------------------------------------------------------------------- */ static int __init cchardevice_init_module(void) { int ret; INIT_LIST_HEAD(&glob.dev_list); ret = cchardevice_procfs_init(); if (ret < 0) return ret; pr_info("loaded\n"); return 0; } static void __exit cchardevice_exit_module(void) { if (glob.cprocfs_root) { cprocfs_destroy(glob.cprocfs_root); glob.cprocfs_root = 0; } if (!list_empty(&glob.dev_list)) pr_err("WARNING: some devices not unregistered\n"); pr_info("unloaded\n"); } MODULE_DESCRIPTION("AVM: easy creation of char devices"); MODULE_LICENSE("Dual BSD/GPL"); module_init(cchardevice_init_module); module_exit(cchardevice_exit_module);