Ridiculous or not, here is version 3 of the block device trigger for
"freaking blinkenlights". It addresses basically all of the points
raised in response to the v2 patchset.
* The main body of the code is moved from the block directory into
the LED triggers directory (drivers/leds/trigger/ledtrig-blkdev.c)
The downside of this is that it requires adding an API to the
block subsystem - get_disk_by_name() - which allows the trigger
code to resolve a gendisk when asked to monitor it. I know of
no good way to do this today, and I don't know of a good way to
implement the sysfs API requested by Pavel and Marek without
something like this API.
Other than that, changes to the block subsystem are as minimal as
I can make them - a single pointer added to struct gendisk and
init/cleanup calls when a gendisk is added or deleted.
* This also implements Marek's suggestion of periodically checking
devices for activity, rather than directly blinking LEDs in the
I/O path. This change has the unanticipated benefit of making the
trigger work on pretty much all types of virtual block devices
(device mapper, MD RAID, zRAM, etc.), as well as NVMe SSDs.
* Relationships between devices and LEDs are now many-to-many. An
LED can monitor multiple devices, and multiple LEDs can monitor
any one device. The current "associations" are reflected in two
sysfs directories.
- /sys/class/leds/<led>/block_devices contains links to all devices
associated with an LED, and
- /sys/block/<disk>/blkdev_leds contains links to all LEDs with
which the device is associated.
(The latter directory only exists when the device is associated
with at least one LED.)
* Each LED can be set to show read activity, write activity, or both.
Discards and cache flushes are considered to be writes, as they
affect the state of the device's non-volatile storage.
Ian Pilcher (18):
docs: Add block device (blkdev) LED trigger documentation
block: Add get_disk_by_name() for use by blkdev LED trigger
ledtrig-blkdev: Add file (ledtrig-blkdev.c) for block device LED
trigger
ledtrig-blkdev: Add misc. helper functions to blkdev LED trigger
ledtrig-blkdev: Periodically check devices for activity & blink LEDs
block: Add LED trigger pointer to struct gendisk
ledtrig-blkdev: Add function to initialize gendisk ledtrig member
ledtrig-blkdev: Add function to remove LED/device association
ledtrig-blkdev: Add function to disassociate a device from all LEDs
block: Call LED trigger init/cleanup functions
ledtrig-blkdev: Add function to associate a device with an LED
ledtrig-blkdev: Add sysfs attributes to [dis]associate LEDs & devices
ledtrig-blkdev: Add blink_time & interval sysfs attributes
ledtrig-blkdev: Add mode (read/write/rw) sysfs attributue
ledtrig-blkdev: Add function to associate blkdev trigger with LED
ledtrig-blkdev: Add function to disassociate an LED from the trigger
ledtrig-blkdev: Add initialization function
ledtrig-blkdev: Add config option to enable the trigger
Documentation/ABI/testing/sysfs-block | 9 +
.../testing/sysfs-class-led-trigger-blkdev | 48 ++
Documentation/leds/index.rst | 1 +
Documentation/leds/ledtrig-blkdev.rst | 132 +++
block/genhd.c | 28 +
drivers/leds/trigger/Kconfig | 9 +
drivers/leds/trigger/Makefile | 1 +
drivers/leds/trigger/ledtrig-blkdev.c | 770 ++++++++++++++++++
include/linux/genhd.h | 13 +
include/linux/leds.h | 20 +
10 files changed, 1031 insertions(+)
create mode 100644 Documentation/ABI/testing/sysfs-class-led-trigger-blkdev
create mode 100644 Documentation/leds/ledtrig-blkdev.rst
create mode 100644 drivers/leds/trigger/ledtrig-blkdev.c
--
2.31.1
Ensures that gendisk ledtrig member is initialized to NULL, in case the
structure was not allocated with kzalloc() or equivalent
Signed-off-by: Ian Pilcher <[email protected]>
---
include/linux/leds.h | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/include/linux/leds.h b/include/linux/leds.h
index 329fd914cf24..6b67650d8797 100644
--- a/include/linux/leds.h
+++ b/include/linux/leds.h
@@ -10,6 +10,7 @@
#include <dt-bindings/leds/common.h>
#include <linux/device.h>
+#include <linux/genhd.h>
#include <linux/kernfs.h>
#include <linux/list.h>
#include <linux/mutex.h>
@@ -599,4 +600,19 @@ static inline void ledtrig_audio_set(enum led_audio type,
}
#endif
+#ifdef CONFIG_LEDS_TRIGGER_BLKDEV
+/**
+ * ledtrig_blkdev_disk_init - initialize the ledtrig field of a new gendisk
+ * @gd: the gendisk to be initialized
+ */
+static inline void ledtrig_blkdev_disk_init(struct gendisk *const gd)
+{
+ gd->ledtrig = NULL;
+}
+#else /* CONFIG_LEDS_TRIGGER_BLKDEV */
+static inline void ledtrig_blkdev_disk_init(const struct gendisk *gd)
+{
+}
+#endif /* CONFIG_LEDS_TRIGGER_BLKDEV */
+
#endif /* __LINUX_LEDS_H_INCLUDED */
--
2.31.1
Needed by ledtrig_blkdev_disk_cleanup(), which removes all monitoring of a
block device by the blkdev LED trigger when the device is removed
Signed-off-by: Ian Pilcher <[email protected]>
---
include/linux/genhd.h | 3 +++
1 file changed, 3 insertions(+)
diff --git a/include/linux/genhd.h b/include/linux/genhd.h
index b26bbf2d9048..66e2760702cb 100644
--- a/include/linux/genhd.h
+++ b/include/linux/genhd.h
@@ -168,6 +168,9 @@ struct gendisk {
#endif /* CONFIG_BLK_DEV_INTEGRITY */
#if IS_ENABLED(CONFIG_CDROM)
struct cdrom_device_info *cdi;
+#endif
+#ifdef CONFIG_LEDS_TRIGGER_BLKDEV
+ struct ledtrig_blkdev_disk *ledtrig;
#endif
int node_id;
struct badblocks *bb;
--
2.31.1
Add API that gets a "handle" (pointer & incremented reference count) to a
block device (struct gendisk) by name. Used by the block device LED
trigger when configuring which device(s) an LED should monitor.
Signed-off-by: Ian Pilcher <[email protected]>
---
block/genhd.c | 25 +++++++++++++++++++++++++
include/linux/genhd.h | 10 ++++++++++
2 files changed, 35 insertions(+)
diff --git a/block/genhd.c b/block/genhd.c
index 298ee78c1bda..e6d7bb709d62 100644
--- a/block/genhd.c
+++ b/block/genhd.c
@@ -1362,3 +1362,28 @@ int bdev_read_only(struct block_device *bdev)
return bdev->bd_read_only || get_disk_ro(bdev->bd_disk);
}
EXPORT_SYMBOL(bdev_read_only);
+
+static int match_disk_name(struct device *const dev, const void *const name)
+{
+ return dev->type == &disk_type
+ && strcmp(name, dev_to_disk(dev)->disk_name) == 0;
+}
+
+/**
+ * get_disk_by_name - get a gendisk by name
+ * @name: the name of the disk
+ *
+ * Returns a pointer to the gendisk named @name (if it exists), @NULL if not.
+ * Increments the disk's reference count, so caller must call put_device().
+ */
+struct gendisk *get_disk_by_name(const char *const name)
+{
+ struct device *dev;
+
+ dev = class_find_device(&block_class, NULL, name, match_disk_name);
+ if (dev == NULL)
+ return NULL;
+
+ return dev_to_disk(dev);
+}
+EXPORT_SYMBOL_GPL(get_disk_by_name);
diff --git a/include/linux/genhd.h b/include/linux/genhd.h
index 13b34177cc85..b26bbf2d9048 100644
--- a/include/linux/genhd.h
+++ b/include/linux/genhd.h
@@ -342,4 +342,14 @@ static inline void printk_all_partitions(void)
}
#endif /* CONFIG_BLOCK */
+/* for blkdev LED trigger (drivers/leds/trigger/ledtrig-blkdev.c) */
+#ifdef CONFIG_BLOCK
+struct gendisk *get_disk_by_name(const char *name);
+#else
+static inline struct gendisk *get_disk_by_name(const char *name)
+{
+ return NULL;
+}
+#endif
+
#endif /* _LINUX_GENHD_H */
--
2.31.1
Add various helper functions to the block device LED trigger:
* blkdev_mkdir() - create a sysfs directory (and don't swallow error
codes)
* blkdev_streq(), blkdev_skip_space() & blkdev_find_space() - for
parsing writes to sysfs attributes
* blkdev_read_mode() & blkdev_write_mode() - LED mode activity type
helpers
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 74 +++++++++++++++++++++++++++
1 file changed, 74 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index 28ccbd7946ba..fcae7ce63b92 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -6,6 +6,7 @@
* Copyright 2021 Ian Pilcher <[email protected]>
*/
+#include <linux/ctype.h>
#include <linux/leds.h>
#include <linux/list.h>
#include <linux/mutex.h>
@@ -64,3 +65,76 @@ static unsigned int ledtrig_blkdev_count;
/* How often to check for drive activity - in jiffies */
static unsigned int ledtrig_blkdev_interval;
+
+
+/*
+ *
+ * Miscellaneous helper functions
+ *
+ */
+
+/* Like kobject_create_and_add(), but doesn't swallow error codes */
+static struct kobject *blkdev_mkdir(const char *const name,
+ struct kobject *const parent)
+{
+ struct kobject *dir;
+ int ret;
+
+ dir = kobject_create();
+ if (dir == NULL)
+ return ERR_PTR(-ENOMEM);
+
+ ret = kobject_add(dir, parent, "%s", name);
+ if (ret != 0) {
+ kobject_put(dir);
+ return ERR_PTR(ret);
+ }
+
+ return dir;
+}
+
+/*
+ * Compare a null-terminated C string with a non-null-terminated character
+ * sequence of a known length. Returns true if equal, false if not.
+ */
+static bool blkdev_streq(const char *const cstr,
+ const char *const cbuf, const size_t buf_len)
+{
+ return (strlen(cstr) == buf_len) && (memcmp(cstr, cbuf, buf_len) == 0);
+}
+
+/*
+ * Returns a pointer to the first non-whitespace character in s
+ * (or a pointer to the terminating null).
+ */
+static const char *blkdev_skip_space(const char *s)
+{
+ while (*s != 0 && isspace(*s))
+ ++s;
+
+ return s;
+}
+
+/*
+ * Returns a pointer to the first whitespace character in s (or a pointer to the
+ * terminating null), which is effectively a pointer to the position *after* the
+ * last character in the non-whitespace token at the beginning of s. (s is
+ * expected to be the result of a previous call to blkdev_skip_space()).
+ */
+static const char *blkdev_find_space(const char *s)
+{
+ while (*s != 0 && !isspace(*s))
+ ++s;
+
+ return s;
+}
+
+static bool blkdev_read_mode(const enum ledtrig_blkdev_mode mode)
+{
+ return mode != LEDTRIG_BLKDEV_MODE_WO;
+}
+
+static bool blkdev_write_mode(const enum ledtrig_blkdev_mode mode)
+{
+ return mode != LEDTRIG_BLKDEV_MODE_RO;
+}
--
2.31.1
Remove symlinks in /sys/class/leds/<led>/block_devices and
/sys/block/<disk>/blkdev_leds
Decrement reference count on /sys/block/<disk>/blkdev_leds
directory (removes directory when empty)
Cancel delayed work when disassociating last device
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 56 +++++++++++++++++++++++++++
1 file changed, 56 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index e9c23824c33c..447fc81ae0c5 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -225,3 +225,59 @@ static void blkdev_process(struct work_struct *const work)
delay = READ_ONCE(ledtrig_blkdev_interval);
WARN_ON_ONCE(!schedule_delayed_work(&ledtrig_blkdev_work, delay));
}
+
+
+/*
+ *
+ * Disassociate a block device from an LED
+ *
+ */
+
+static void blkdev_disk_del_locked(struct ledtrig_blkdev_led *const led,
+ struct ledtrig_blkdev_link *const link,
+ struct ledtrig_blkdev_disk *const disk)
+{
+ --ledtrig_blkdev_count;
+
+ if (ledtrig_blkdev_count == 0)
+ WARN_ON(!cancel_delayed_work_sync(&ledtrig_blkdev_work));
+
+ sysfs_remove_link(led->dir, disk->gd->disk_name);
+ sysfs_remove_link(disk->dir, led->led_dev->name);
+ kobject_put(disk->dir);
+
+ hlist_del(&link->led_disks_node);
+ hlist_del(&link->disk_leds_node);
+ kfree(link);
+
+ if (hlist_empty(&disk->leds)) {
+ disk->gd->ledtrig = NULL;
+ kfree(disk);
+ }
+
+ put_device(disk_to_dev(disk->gd));
+}
+
+static void blkdev_disk_delete(struct ledtrig_blkdev_led *const led,
+ const char *const disk_name,
+ const size_t name_len)
+{
+ struct ledtrig_blkdev_link *link;
+
+ mutex_lock(&ledtrig_blkdev_mutex);
+
+ hlist_for_each_entry(link, &led->disks, led_disks_node) {
+
+ if (blkdev_streq(link->disk->gd->disk_name,
+ disk_name, name_len)) {
+ blkdev_disk_del_locked(led, link, link->disk);
+ goto exit_unlock;
+ }
+ }
+
+ pr_info("blkdev LED: %.*s not associated with LED %s\n",
+ (int)name_len, disk_name, led->led_dev->name);
+
+exit_unlock:
+ mutex_unlock(&ledtrig_blkdev_mutex);
+}
--
2.31.1
Call ledtrig_blkdev_disk_init() from __device_add_disk() to ensure that
gendisk's ledtrig field is initialized
Call ledtrig_blkdev_disk_cleanup() from del_gendisk() to clean up any
references to the device from the block device LED trigger
Signed-off-by: Ian Pilcher <[email protected]>
---
block/genhd.c | 3 +++
1 file changed, 3 insertions(+)
diff --git a/block/genhd.c b/block/genhd.c
index e6d7bb709d62..091b954ddab3 100644
--- a/block/genhd.c
+++ b/block/genhd.c
@@ -24,6 +24,7 @@
#include <linux/log2.h>
#include <linux/pm_runtime.h>
#include <linux/badblocks.h>
+#include <linux/leds.h>
#include "blk.h"
@@ -539,6 +540,7 @@ static void __device_add_disk(struct device *parent, struct gendisk *disk,
disk_add_events(disk);
blk_integrity_add(disk);
+ ledtrig_blkdev_disk_init(disk);
}
void device_add_disk(struct device *parent, struct gendisk *disk,
@@ -581,6 +583,7 @@ void del_gendisk(struct gendisk *disk)
if (WARN_ON_ONCE(!disk->queue))
return;
+ ledtrig_blkdev_disk_cleanup(disk);
blk_integrity_del(disk);
disk_del_events(disk);
--
2.31.1
Use a delayed workqueue to periodically check configured block devices for
activity since the last check. Blink LEDs associated with devices on which
the configured type of activity (read/write) has occurred.
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 87 +++++++++++++++++++++++++++
1 file changed, 87 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index fcae7ce63b92..e9c23824c33c 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -7,9 +7,11 @@
*/
#include <linux/ctype.h>
+#include <linux/genhd.h>
#include <linux/leds.h>
#include <linux/list.h>
#include <linux/mutex.h>
+#include <linux/part_stat.h>
/* Default blink time & polling interval (milliseconds) */
#define LEDTRIG_BLKDEV_BLINK_MSEC 75
@@ -66,6 +68,9 @@ static unsigned int ledtrig_blkdev_count;
/* How often to check for drive activity - in jiffies */
static unsigned int ledtrig_blkdev_interval;
+static void blkdev_process(struct work_struct *const work);
+static DECLARE_DELAYED_WORK(ledtrig_blkdev_work, blkdev_process);
+
/*
*
@@ -138,3 +143,85 @@ static bool blkdev_write_mode(const enum ledtrig_blkdev_mode mode)
{
return mode != LEDTRIG_BLKDEV_MODE_RO;
}
+
+
+/*
+ *
+ * Periodically check for device acitivity and blink LEDs
+ *
+ */
+
+static void blkdev_blink(const struct ledtrig_blkdev_led *const led)
+{
+ unsigned long delay_on = READ_ONCE(led->blink_msec);
+ unsigned long delay_off = 1; /* 0 leaves LED turned on */
+
+ led_blink_set_oneshot(led->led_dev, &delay_on, &delay_off, 0);
+}
+
+static void blkdev_update_disk(struct ledtrig_blkdev_disk *const disk,
+ const unsigned int generation)
+{
+ const struct block_device *const part0 = disk->gd->part0;
+ const unsigned long read_ios = part_stat_read(part0, ios[STAT_READ]);
+ const unsigned long write_ios = part_stat_read(part0, ios[STAT_WRITE])
+ + part_stat_read(part0, ios[STAT_DISCARD])
+ + part_stat_read(part0, ios[STAT_FLUSH]);
+
+ if (disk->read_ios != read_ios) {
+ disk->read_act = true;
+ disk->read_ios = read_ios;
+ } else {
+ disk->read_act = false;
+ }
+
+ if (disk->write_ios != write_ios) {
+ disk->write_act = true;
+ disk->write_ios = write_ios;
+ } else {
+ disk->write_act = false;
+ }
+
+ disk->generation = generation;
+}
+
+static void blkdev_process(struct work_struct *const work)
+{
+ static unsigned int generation;
+
+ struct ledtrig_blkdev_led *led;
+ struct ledtrig_blkdev_link *link;
+ unsigned long delay;
+
+ if (!mutex_trylock(&ledtrig_blkdev_mutex))
+ goto exit_reschedule;
+
+ hlist_for_each_entry(led, &ledtrig_blkdev_leds, leds_node) {
+
+ hlist_for_each_entry(link, &led->disks, led_disks_node) {
+
+ struct ledtrig_blkdev_disk *const disk = link->disk;
+
+ if (disk->generation != generation)
+ blkdev_update_disk(disk, generation);
+
+ if (disk->read_act && blkdev_read_mode(led->mode)) {
+ blkdev_blink(led);
+ break;
+ }
+
+ if (disk->write_act && blkdev_write_mode(led->mode)) {
+ blkdev_blink(led);
+ break;
+ }
+ }
+ }
+
+ ++generation;
+
+ mutex_unlock(&ledtrig_blkdev_mutex);
+
+exit_reschedule:
+ delay = READ_ONCE(ledtrig_blkdev_interval);
+ WARN_ON_ONCE(!schedule_delayed_work(&ledtrig_blkdev_work, delay));
+}
--
2.31.1
If this is the first LED associated with the device, create the
/sys/block/<disk>/blkdev_leds directory. Otherwise, increment its
reference count.
Create symlinks in /sys/class/leds/<led>/block_devices and
/sys/block/<disk>/blkdev_leds
If this the first device associated with any LED, schedule delayed work
to periodically check associated devices and blink LEDs
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 168 ++++++++++++++++++++++++++
1 file changed, 168 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index 2072cc904616..a1646752b9a0 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -312,3 +312,171 @@ void ledtrig_blkdev_disk_cleanup(struct gendisk *const gd)
mutex_unlock(&ledtrig_blkdev_mutex);
}
EXPORT_SYMBOL_GPL(ledtrig_blkdev_disk_cleanup);
+
+
+/*
+ *
+ * Associate a block device with an LED
+ *
+ */
+
+/* Gets or allocs & initializes the blkdev disk for a gendisk */
+static int blkdev_get_disk(struct gendisk *const gd)
+{
+ struct ledtrig_blkdev_disk *disk;
+ struct kobject *dir;
+
+ if (gd->ledtrig != NULL) {
+ kobject_get(gd->ledtrig->dir);
+ return 0;
+ }
+
+ disk = kmalloc(sizeof(*disk), GFP_KERNEL);
+ if (disk == NULL)
+ return -ENOMEM;
+
+ dir = blkdev_mkdir("blkdev_leds", &disk_to_dev(gd)->kobj);
+ if (IS_ERR(dir)) {
+ kfree(disk);
+ return PTR_ERR(dir);
+ }
+
+ INIT_HLIST_HEAD(&disk->leds);
+ disk->gd = gd;
+ disk->dir = dir;
+ disk->read_ios = 0;
+ disk->write_ios = 0;
+
+ gd->ledtrig = disk;
+
+ return 0;
+}
+
+static void blkdev_put_disk(struct ledtrig_blkdev_disk *const disk)
+{
+ kobject_put(disk->dir);
+
+ if (hlist_empty(&disk->leds)) {
+ disk->gd->ledtrig = NULL;
+ kfree(disk);
+ }
+}
+
+static int blkdev_disk_add_locked(struct ledtrig_blkdev_led *const led,
+ struct gendisk *const gd)
+{
+ struct ledtrig_blkdev_link *link;
+ struct ledtrig_blkdev_disk *disk;
+ unsigned long delay;
+ int ret;
+
+ link = kmalloc(sizeof(*link), GFP_KERNEL);
+ if (link == NULL) {
+ ret = -ENOMEM;
+ goto error_return;
+ }
+
+ ret = blkdev_get_disk(gd);
+ if (ret != 0)
+ goto error_free_link;
+
+ disk = gd->ledtrig;
+
+ ret = sysfs_create_link(disk->dir, &led->led_dev->dev->kobj,
+ led->led_dev->name);
+ if (ret != 0)
+ goto error_put_disk;
+
+ ret = sysfs_create_link(led->dir, &disk_to_dev(gd)->kobj,
+ gd->disk_name);
+ if (ret != 0)
+ goto error_remove_link;
+
+ link->disk = disk;
+ link->led = led;
+ hlist_add_head(&link->led_disks_node, &led->disks);
+ hlist_add_head(&link->disk_leds_node, &disk->leds);
+
+ if (ledtrig_blkdev_count == 0) {
+ delay = READ_ONCE(ledtrig_blkdev_interval);
+ WARN_ON(!schedule_delayed_work(&ledtrig_blkdev_work, delay));
+ }
+
+ ++ledtrig_blkdev_count;
+
+ return 0;
+
+error_remove_link:
+ sysfs_remove_link(disk->dir, led->led_dev->name);
+error_put_disk:
+ blkdev_put_disk(disk);
+error_free_link:
+ kfree(link);
+error_return:
+ return ret;
+}
+
+static bool blkdev_already_linked(const struct ledtrig_blkdev_led *const led,
+ const struct gendisk *const gd)
+{
+ const struct ledtrig_blkdev_link *link;
+
+ if (gd->ledtrig == NULL)
+ return false;
+
+ hlist_for_each_entry(link, &gd->ledtrig->leds, disk_leds_node) {
+
+ if (link->led == led) {
+ pr_info("blkdev LED: %s already associated with %s\n",
+ gd->disk_name, led->led_dev->name);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static int blkdev_disk_add(struct ledtrig_blkdev_led *const led,
+ const char *const disk_name, const size_t name_len)
+{
+ static char name[DISK_NAME_LEN]; /* only used w/ mutex locked */
+ struct gendisk *gd;
+ int ret;
+
+ if (name_len >= DISK_NAME_LEN) {
+ pr_info("blkdev LED: invalid device name %.*s\n",
+ (int)name_len, disk_name);
+ ret = -EINVAL;
+ goto exit_return;
+ }
+
+ ret = mutex_lock_interruptible(&ledtrig_blkdev_mutex);
+ if (ret != 0)
+ goto exit_return;
+
+ memcpy(name, disk_name, name_len);
+ name[name_len] = 0;
+ gd = get_disk_by_name(name); /* increments disk's refcount */
+
+ if (gd == NULL) {
+ pr_info("blkdev LED: no such block device %.*s\n",
+ (int)name_len, disk_name);
+ ret = -ENODEV;
+ goto exit_unlock;
+ }
+
+ if (blkdev_already_linked(led, gd)) {
+ ret = -EEXIST;
+ goto exit_put_dev;
+ }
+
+ ret = blkdev_disk_add_locked(led, gd);
+
+exit_put_dev:
+ if (ret != 0)
+ put_device(disk_to_dev(gd));
+exit_unlock:
+ mutex_unlock(&ledtrig_blkdev_mutex);
+exit_return:
+ return ret;
+}
--
2.31.1
/sys/class/leds/<led>/add_blkdev - to create device/LED associations
/sys/class/leds/<led>/delete_blkdev to remove device/LED associations
For both attributes, accept multiple device names separated by whitespace
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 48 +++++++++++++++++++++++++++
1 file changed, 48 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index a1646752b9a0..15b15aefdfd8 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -480,3 +480,51 @@ static int blkdev_disk_add(struct ledtrig_blkdev_led *const led,
exit_return:
return ret;
}
+
+
+/*
+ *
+ * sysfs attributes to add & delete devices from LEDs
+ *
+ */
+
+static ssize_t blkdev_add_or_del(struct device *const dev,
+ struct device_attribute *const attr,
+ const char *const buf, const size_t count);
+
+static struct device_attribute ledtrig_blkdev_attr_add =
+ __ATTR(add_blkdev, 0200, NULL, blkdev_add_or_del);
+
+static struct device_attribute ledtrig_blkdev_attr_del =
+ __ATTR(delete_blkdev, 0200, NULL, blkdev_add_or_del);
+
+static ssize_t blkdev_add_or_del(struct device *const dev,
+ struct device_attribute *const attr,
+ const char *const buf, const size_t count)
+{
+ struct ledtrig_blkdev_led *const led = led_trigger_get_drvdata(dev);
+ const char *const disk_name = blkdev_skip_space(buf);
+ const char *const endp = blkdev_find_space(disk_name);
+ const ptrdiff_t name_len = endp - disk_name; /* always >= 0 */
+ int ret;
+
+ if (name_len == 0) {
+ pr_info("blkdev LED: empty block device name\n");
+ return -EINVAL;
+ }
+
+ if (attr == &ledtrig_blkdev_attr_del) {
+ blkdev_disk_delete(led, disk_name, name_len);
+ } else { /* attr == &ledtrig_blkdev_attr_add */
+ ret = blkdev_disk_add(led, disk_name, name_len);
+ if (ret != 0)
+ return ret;
+ }
+
+ /*
+ * Consume everything up to the next non-whitespace token (or the end
+ * of the input). Avoids "empty block device name" error if there is
+ * whitespace after the last token (e.g. a newline).
+ */
+ return blkdev_skip_space(endp) - buf;
+}
--
2.31.1
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/Kconfig | 9 +++++++++
drivers/leds/trigger/Makefile | 1 +
2 files changed, 10 insertions(+)
diff --git a/drivers/leds/trigger/Kconfig b/drivers/leds/trigger/Kconfig
index b77a01bd27f4..f15d38b3a632 100644
--- a/drivers/leds/trigger/Kconfig
+++ b/drivers/leds/trigger/Kconfig
@@ -153,4 +153,13 @@ config LEDS_TRIGGER_TTY
When build as a module this driver will be called ledtrig-tty.
+config LEDS_TRIGGER_BLKDEV
+ bool "LED Trigger for block devices"
+ depends on BLOCK
+ help
+ The blkdev LED trigger allows LEDs to be controlled by block device
+ activity (reads and writes).
+
+ See Documentation/leds/ledtrig-blkdev.rst.
+
endif # LEDS_TRIGGERS
diff --git a/drivers/leds/trigger/Makefile b/drivers/leds/trigger/Makefile
index 25c4db97cdd4..d53bab5d93f1 100644
--- a/drivers/leds/trigger/Makefile
+++ b/drivers/leds/trigger/Makefile
@@ -16,3 +16,4 @@ obj-$(CONFIG_LEDS_TRIGGER_NETDEV) += ledtrig-netdev.o
obj-$(CONFIG_LEDS_TRIGGER_PATTERN) += ledtrig-pattern.o
obj-$(CONFIG_LEDS_TRIGGER_AUDIO) += ledtrig-audio.o
obj-$(CONFIG_LEDS_TRIGGER_TTY) += ledtrig-tty.o
+obj-$(CONFIG_LEDS_TRIGGER_BLKDEV) += ledtrig-blkdev.o
--
2.31.1
Remove all device associations with this LED
Remove /sys/class/leds/<led>/block_devices directory
Free per-LED data structure
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 25 +++++++++++++++++++++++++
1 file changed, 25 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index b331e3f24b04..01573c1ad855 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -704,3 +704,28 @@ static int blkdev_activate(struct led_classdev *const led_dev)
exit_return:
return ret;
}
+
+
+/*
+ *
+ * Disassociate an LED from the trigger
+ *
+ */
+
+static void blkdev_deactivate(struct led_classdev *const led_dev)
+{
+ struct ledtrig_blkdev_led *const led = led_get_trigger_data(led_dev);
+ struct ledtrig_blkdev_link *link;
+ struct hlist_node *next;
+
+ mutex_lock(&ledtrig_blkdev_mutex);
+
+ hlist_for_each_entry_safe(link, next, &led->disks, led_disks_node)
+ blkdev_disk_del_locked(led, link, link->disk);
+
+ hlist_del(&led->leds_node);
+ kobject_put(led->dir);
+ kfree(led);
+
+ mutex_unlock(&ledtrig_blkdev_mutex);
+}
--
2.31.1
Show all modes, with current mode in square brackets, in show function
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 67 +++++++++++++++++++++++++++
1 file changed, 67 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index 481b2d142db2..88e2a11af1a9 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -591,3 +591,70 @@ static ssize_t blkdev_time_store(struct device *const dev,
return count;
}
+
+
+/*
+ *
+ * LED mode device attribute
+ *
+ */
+
+static const struct {
+ const char *name;
+ const char *show;
+} blkdev_modes[] = {
+ [LEDTRIG_BLKDEV_MODE_RO] = {
+ .name = "read",
+ .show = "[read] write rw\n",
+ },
+ [LEDTRIG_BLKDEV_MODE_WO] = {
+ .name = "write",
+ .show = "read [write] rw\n",
+ },
+ [LEDTRIG_BLKDEV_MODE_RW] = {
+ .name = "rw",
+ .show = "read write [rw]\n",
+ },
+};
+
+static ssize_t blkdev_mode_show(struct device *const dev,
+ struct device_attribute *const attr,
+ char *const buf)
+{
+ const struct ledtrig_blkdev_led *const led =
+ led_trigger_get_drvdata(dev);
+
+ return sprintf(buf, blkdev_modes[READ_ONCE(led->mode)].show);
+}
+
+static ssize_t blkdev_mode_store(struct device *const dev,
+ struct device_attribute *const attr,
+ const char *const buf, const size_t count)
+{
+ struct ledtrig_blkdev_led *const led = led_trigger_get_drvdata(dev);
+ const char *const mode_name = blkdev_skip_space(buf);
+ const char *const endp = blkdev_find_space(mode_name);
+ const ptrdiff_t name_len = endp - mode_name; /* always >= 0 */
+ enum ledtrig_blkdev_mode mode;
+
+ if (name_len == 0) {
+ pr_info("blkdev LED: empty mode\n");
+ return -EINVAL;
+ }
+
+ for (mode = LEDTRIG_BLKDEV_MODE_RO;
+ mode <= LEDTRIG_BLKDEV_MODE_RW; ++mode) {
+
+ if (blkdev_streq(blkdev_modes[mode].name,
+ mode_name, name_len)) {
+ WRITE_ONCE(led->mode, mode);
+ return count;
+ }
+ }
+
+ pr_info("blkdev LED: invalid mode (%.*s)\n", (int)name_len, mode_name);
+ return -EINVAL;
+}
+
+static struct device_attribute ledtrig_blkdev_attr_mode =
+ __ATTR(mode, 0644, blkdev_mode_show, blkdev_mode_store);
--
2.31.1
/sys/class/leds/<led>/blink_time controls - per-LED blink duration
/sys/class/leds/<led>/interval - global frequency with which devices
are checked for activity and LEDs are blinked
Enforce 25 millisecond minimum for both attributes
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 63 +++++++++++++++++++++++++++
1 file changed, 63 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index 15b15aefdfd8..481b2d142db2 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -528,3 +528,66 @@ static ssize_t blkdev_add_or_del(struct device *const dev,
*/
return blkdev_skip_space(endp) - buf;
}
+
+
+/*
+ *
+ * blink_time & interval device attributes
+ *
+ */
+
+static ssize_t blkdev_time_show(struct device *const dev,
+ struct device_attribute *const attr,
+ char *const buf);
+
+static ssize_t blkdev_time_store(struct device *const dev,
+ struct device_attribute *const attr,
+ const char *const buf, const size_t count);
+
+static struct device_attribute ledtrig_blkdev_attr_blink_time =
+ __ATTR(blink_time, 0644, blkdev_time_show, blkdev_time_store);
+
+static struct device_attribute ledtrig_blkdev_attr_interval =
+ __ATTR(interval, 0644, blkdev_time_show, blkdev_time_store);
+
+static ssize_t blkdev_time_show(struct device *const dev,
+ struct device_attribute *const attr,
+ char *const buf)
+{
+ const struct ledtrig_blkdev_led *const led =
+ led_trigger_get_drvdata(dev);
+ unsigned int value;
+
+ if (attr == &ledtrig_blkdev_attr_blink_time)
+ value = READ_ONCE(led->blink_msec);
+ else // attr == &ledtrig_blkdev_attr_interval
+ value = jiffies_to_msecs(READ_ONCE(ledtrig_blkdev_interval));
+
+ return sprintf(buf, "%u\n", value);
+}
+
+static ssize_t blkdev_time_store(struct device *const dev,
+ struct device_attribute *const attr,
+ const char *const buf, const size_t count)
+{
+ struct ledtrig_blkdev_led *const led = led_trigger_get_drvdata(dev);
+ unsigned int value;
+ int ret;
+
+ ret = kstrtouint(buf, 0, &value);
+ if (ret != 0)
+ return ret;
+
+ if (value < LEDTRIG_BLKDEV_MIN_TIME) {
+ pr_info("blkdev LED: attempt to set time < %s milliseconds\n",
+ __stringify(LEDTRIG_BLKDEV_MIN_TIME));
+ return -ERANGE;
+ }
+
+ if (attr == &ledtrig_blkdev_attr_blink_time)
+ WRITE_ONCE(led->blink_msec, value);
+ else // attr == &ledtrig_blkdev_attr_interval
+ WRITE_ONCE(ledtrig_blkdev_interval, msecs_to_jiffies(value));
+
+ return count;
+}
--
2.31.1
Called when block device is being removed
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 31 +++++++++++++++++++++++++++
include/linux/leds.h | 4 ++++
2 files changed, 35 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index 447fc81ae0c5..2072cc904616 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -281,3 +281,34 @@ static void blkdev_disk_delete(struct ledtrig_blkdev_led *const led,
exit_unlock:
mutex_unlock(&ledtrig_blkdev_mutex);
}
+
+
+/*
+ *
+ * Disassociate all LEDs from a block device (because it's going away)
+ *
+ */
+
+/**
+ * ledtrig_blkdev_disk_cleanup - remove a block device from the blkdev LED
+ * trigger
+ * @disk: the disk to be removed
+ */
+void ledtrig_blkdev_disk_cleanup(struct gendisk *const gd)
+{
+ struct ledtrig_blkdev_link *link;
+ struct hlist_node *next;
+
+ mutex_lock(&ledtrig_blkdev_mutex);
+
+ if (gd->ledtrig != NULL) {
+
+ hlist_for_each_entry_safe(link, next,
+ &gd->ledtrig->leds, disk_leds_node) {
+ blkdev_disk_del_locked(link->led, link, gd->ledtrig);
+ }
+ }
+
+ mutex_unlock(&ledtrig_blkdev_mutex);
+}
+EXPORT_SYMBOL_GPL(ledtrig_blkdev_disk_cleanup);
diff --git a/include/linux/leds.h b/include/linux/leds.h
index 6b67650d8797..98c479814988 100644
--- a/include/linux/leds.h
+++ b/include/linux/leds.h
@@ -609,10 +609,14 @@ static inline void ledtrig_blkdev_disk_init(struct gendisk *const gd)
{
gd->ledtrig = NULL;
}
+void ledtrig_blkdev_disk_cleanup(struct gendisk *const gd);
#else /* CONFIG_LEDS_TRIGGER_BLKDEV */
static inline void ledtrig_blkdev_disk_init(const struct gendisk *gd)
{
}
+static inline void ledtrig_blkdev_disk_cleanup(const struct gendisk *gd)
+{
+}
#endif /* CONFIG_LEDS_TRIGGER_BLKDEV */
#endif /* __LINUX_LEDS_H_INCLUDED */
--
2.31.1
Register the block device LED trigger
Initialize interval (convert default value to jiffies)
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 39 +++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index 01573c1ad855..f152b00a4f1a 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -729,3 +729,42 @@ static void blkdev_deactivate(struct led_classdev *const led_dev)
mutex_unlock(&ledtrig_blkdev_mutex);
}
+
+
+/*
+ *
+ * Initialization - register the trigger
+ *
+ */
+
+static struct attribute *ledtrig_blkdev_attrs[] = {
+ &ledtrig_blkdev_attr_add.attr,
+ &ledtrig_blkdev_attr_del.attr,
+ &ledtrig_blkdev_attr_blink_time.attr,
+ &ledtrig_blkdev_attr_interval.attr,
+ &ledtrig_blkdev_attr_mode.attr,
+ NULL
+};
+
+static const struct attribute_group ledtrig_blkdev_attr_group = {
+ .attrs = ledtrig_blkdev_attrs,
+};
+
+static const struct attribute_group *ledtrig_blkdev_attr_groups[] = {
+ &ledtrig_blkdev_attr_group,
+ NULL
+};
+
+static struct led_trigger ledtrig_blkdev_trigger = {
+ .name = "blkdev",
+ .activate = blkdev_activate,
+ .deactivate = blkdev_deactivate,
+ .groups = ledtrig_blkdev_attr_groups,
+};
+
+static int __init blkdev_init(void)
+{
+ ledtrig_blkdev_interval = msecs_to_jiffies(LEDTRIG_BLKDEV_INTERVAL);
+ return led_trigger_register(&ledtrig_blkdev_trigger);
+}
+device_initcall(blkdev_init);
--
2.31.1
Allocate per-LED data structure and initialize with default values
Create /sys/class/leds/<led>/block_devices directory
Signed-off-by: Ian Pilcher <[email protected]>
---
drivers/leds/trigger/ledtrig-blkdev.c | 46 +++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/drivers/leds/trigger/ledtrig-blkdev.c b/drivers/leds/trigger/ledtrig-blkdev.c
index 88e2a11af1a9..b331e3f24b04 100644
--- a/drivers/leds/trigger/ledtrig-blkdev.c
+++ b/drivers/leds/trigger/ledtrig-blkdev.c
@@ -658,3 +658,49 @@ static ssize_t blkdev_mode_store(struct device *const dev,
static struct device_attribute ledtrig_blkdev_attr_mode =
__ATTR(mode, 0644, blkdev_mode_show, blkdev_mode_store);
+
+
+/*
+ *
+ * Associate an LED with the blkdev trigger
+ *
+ */
+
+static int blkdev_activate(struct led_classdev *const led_dev)
+{
+ struct ledtrig_blkdev_led *led;
+ int ret;
+
+ led = kmalloc(sizeof(*led), GFP_KERNEL);
+ if (led == NULL) {
+ ret = -ENOMEM;
+ goto exit_return;
+ }
+
+ led->led_dev = led_dev;
+ led->blink_msec = LEDTRIG_BLKDEV_BLINK_MSEC;
+ led->mode = LEDTRIG_BLKDEV_MODE_RW;
+ INIT_HLIST_HEAD(&led->disks);
+
+ ret = mutex_lock_interruptible(&ledtrig_blkdev_mutex);
+ if (ret != 0)
+ goto exit_free;
+
+ led->dir = blkdev_mkdir("block_devices", &led_dev->dev->kobj);
+ if (IS_ERR(led->dir)) {
+ ret = PTR_ERR(led->dir);
+ goto exit_unlock;
+ }
+
+ hlist_add_head(&led->leds_node, &ledtrig_blkdev_leds);
+ led_set_trigger_data(led_dev, led);
+ ret = 0;
+
+exit_unlock:
+ mutex_unlock(&ledtrig_blkdev_mutex);
+exit_free:
+ if (ret != 0)
+ kfree(led);
+exit_return:
+ return ret;
+}
--
2.31.1