[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <84b8d91693e56f6026271bc40553bcb6657ab24d.1764272407.git.chris@chrisdown.name>
Date: Fri, 28 Nov 2025 03:44:06 +0800
From: Chris Down <chris@...isdown.name>
To: Petr Mladek <pmladek@...e.com>
Cc: linux-kernel@...r.kernel.org,
Greg Kroah-Hartman <gregkh@...uxfoundation.org>,
Sergey Senozhatsky <senozhatsky@...omium.org>,
Steven Rostedt <rostedt@...dmis.org>,
John Ogness <john.ogness@...utronix.de>,
Geert Uytterhoeven <geert@...ux-m68k.org>,
Tony Lindgren <tony.lindgren@...ux.intel.com>, kernel-team@...com
Subject: [PATCH v8 14/21] printk: console: Introduce sysfs interface for
per-console loglevels
A sysfs interface under /sys/class/console/ is created that permits
viewing and configuring per-console attributes. This is the main
interface with which we expect users to interact with and configure
per-console loglevels.
Each console device now has its own directory (for example,
/sys/class/console/ttyS0/) containing the following attributes:
- effective_loglevel (ro): The effective loglevel for the console after
considering all loglevel authorities (e.g., global loglevel,
per-console loglevel).
- effective_loglevel_source (ro): The source of the effective loglevel
(e.g., local, global, ignore_loglevel).
- loglevel (rw): The per-console loglevel. Writing a value between 0
(KERN_EMERG) and 8 (KERN_DEBUG + 1) sets the per-console loglevel.
Writing -1 disables the per-console loglevel.
In terms of technical implementation, we embed a device pointer in the
console struct, and register each console using it so we can expose
attributes in sysfs. We currently expose the following attributes:
% ls -l /sys/class/console/ttyS0/
total 0
lrwxrwxrwx 1 root root 0 Oct 23 13:17 subsystem -> ../../../../class/console/
-r--r--r-- 1 root root 4096 Oct 23 13:18 effective_loglevel
-r--r--r-- 1 root root 4096 Oct 23 13:18 effective_loglevel_source
-rw-r--r-- 1 root root 4096 Oct 23 13:18 loglevel
-rw-r--r-- 1 root root 4096 Oct 23 13:17 uevent
The lifecycle of this classdev looks like this on registration:
register_console(con)/printk_late_init()
console_register_device(con)
device_initialize(con->classdev) # kref_init: refcount = 1
device_add(con->classdev)
get_device(con->classdev) # temporary: refcount++ (to 2)
...
put_device(con->classdev) # drop temporary refcount-- (to 1)
At steady state the class device holds a single persistent reference.
Unregistration:
unregister_console_locked(con)
struct device *dev = console->classdev;
console->classdev = NULL;
device_unregister(dev)
device_del(dev)
device_remove_class_symlinks(dev)
sysfs_delete_link()
kernfs_remove_by_name_ns()
__kernfs_remove()
kernfs_drain()
kernfs_drain_open_files() # waits for open file handles
...
kobject_del(&dev->kobj) # removes from sysfs
put_device(dev) # final kref_put: refcount-- (1 -> 0)
kobject_release()
kobject_cleanup()
device_release()
console_classdev_release(dev)
kfree(dev)
Signed-off-by: Chris Down <chris@...isdown.name>
---
Documentation/ABI/testing/sysfs-class-console | 58 ++++
Documentation/core-api/printk-basics.rst | 35 ++-
Documentation/networking/netconsole.rst | 12 +
MAINTAINERS | 1 +
include/linux/console.h | 4 +
kernel/printk/Makefile | 2 +-
kernel/printk/internal.h | 8 +
kernel/printk/printk.c | 32 ++
kernel/printk/sysfs.c | 290 ++++++++++++++++++
9 files changed, 424 insertions(+), 18 deletions(-)
create mode 100644 Documentation/ABI/testing/sysfs-class-console
create mode 100644 kernel/printk/sysfs.c
diff --git a/Documentation/ABI/testing/sysfs-class-console b/Documentation/ABI/testing/sysfs-class-console
new file mode 100644
index 000000000000..8c0f0cf3f6c5
--- /dev/null
+++ b/Documentation/ABI/testing/sysfs-class-console
@@ -0,0 +1,58 @@
+What: /sys/class/console/
+Date: November 2025
+Contact: Chris Down <chris@...isdown.name>
+Description: Interface for viewing and setting per-console attributes, like
+ the per-console loglevel. For a high-level document describing
+ the motivations for this interface and related non-sysfs
+ controls, see
+ Documentation/admin-guide/per-console-loglevel.rst.
+
+What: /sys/class/console/<C>/effective_loglevel
+Date: November 2025
+Contact: Chris Down <chris@...isdown.name>
+Permissions: 0444 (world readable)
+Description: Read only. The currently effective loglevel for this console.
+ All messages emitted with a loglevel below the effective value
+ will be emitted to the console.
+
+What: /sys/class/console/<C>/effective_loglevel_source
+Date: November 2025
+Contact: Chris Down <chris@...isdown.name>
+Permissions: 0444 (world readable)
+Description: Read only. The currently effective loglevel source for this
+ console -- for example, whether it was set globally, or whether
+ it was set locally for this console.
+
+ Possible values are:
+ =============== ============================================
+ local The loglevel comes from the console's
+ per-console loglevel setting.
+ global The loglevel comes from the global
+ console_loglevel.
+ ignore_loglevel Both the per-console loglevel and global
+ loglevel are ignored as ignore_loglevel is
+ present on the kernel command line.
+ =============== ============================================
+
+What: /sys/class/console/<C>/loglevel
+Date: November 2025
+Contact: Chris Down <chris@...isdown.name>
+Permissions: 0644 (root read/write, user read)
+Description: Read write. The current per-console loglevel, which will take
+ effect if not overridden by other non-sysfs controls (see
+ Documentation/admin-guide/per-console-loglevel.rst).
+
+ Valid values:
+ 1-8: LOGLEVEL_ALERT (1) to LOGLEVEL_DEBUG + 1 (8)
+ -1: Use global console_loglevel (default)
+ 0: Explicitly rejected (KERN_EMERG not allowed)
+
+ Error codes:
+ EINVAL: Non-numeric input
+ ERANGE: Value out of valid range (< 1 or > 8, excluding -1)
+ ERANGE: Value is 0 (KERN_EMERG not allowed for per-console)
+ ERANGE: Value below system minimum_console_loglevel
+
+ The special value -1 disables the per-console loglevel, making
+ the console use the global loglevel instead.
+
diff --git a/Documentation/core-api/printk-basics.rst b/Documentation/core-api/printk-basics.rst
index 2dde24ca7d9f..bfad359505bb 100644
--- a/Documentation/core-api/printk-basics.rst
+++ b/Documentation/core-api/printk-basics.rst
@@ -54,32 +54,33 @@ string, the log level is not a separate argument). The available log levels are:
The log level specifies the importance of a message. The kernel decides whether
to show the message immediately (printing it to the current console) depending
-on its log level and the current *console_loglevel* (a kernel variable). If the
-message priority is higher (lower log level value) than the *console_loglevel*
-the message will be printed to the console.
+on its log level and the current global *console_loglevel* or local per-console
+loglevel (kernel variables). If the message priority is higher (lower log level
+value) than the effective loglevel the message will be printed to the console.
If the log level is omitted, the message is printed with ``KERN_DEFAULT``
level.
-You can check the current *console_loglevel* with::
+You can check the current console's loglevel -- for example if you want to
+check the loglevel for serial consoles:
- $ cat /proc/sys/kernel/printk
- 4 4 1 7
+ $ cat /sys/class/console/ttyS0/effective_loglevel
+ 6
+ $ cat /sys/class/console/ttyS0/effective_loglevel_source
+ local
-The result shows the *current*, *default*, *minimum* and *boot-time-default* log
-levels.
+To change the default loglevel for all consoles, simply write the desired level
+to ``/proc/sys/kernel/console_loglevel``. For example::
-To change the current console_loglevel simply write the desired level to
-``/proc/sys/kernel/printk``. For example, to print all messages to the console::
+ # echo 5 > /proc/sys/kernel/console_loglevel
- # echo 8 > /proc/sys/kernel/printk
+This sets the console_loglevel to print KERN_WARNING (4) or more severe
+messages to console. Consoles with a per-console loglevel set will ignore it
+unless ``ignore_per_console_loglevel`` is set on the kernel command line or at
+``/sys/module/printk/parameters/ignore_per_console_loglevel``.
-Another way, using ``dmesg``::
-
- # dmesg -n 5
-
-sets the console_loglevel to print KERN_WARNING (4) or more severe messages to
-console. See ``dmesg(1)`` for more information.
+For more information on per-console loglevels, see
+Documentation/admin-guide/per-console-loglevel.rst.
As an alternative to printk() you can use the ``pr_*()`` aliases for
logging. This family of macros embed the log level in the macro names. For
diff --git a/Documentation/networking/netconsole.rst b/Documentation/networking/netconsole.rst
index 59cb9982afe6..5ff12e88e5b8 100644
--- a/Documentation/networking/netconsole.rst
+++ b/Documentation/networking/netconsole.rst
@@ -78,6 +78,18 @@ Built-in netconsole starts immediately after the TCP stack is
initialized and attempts to bring up the supplied dev at the supplied
address.
+You can also set a loglevel at runtime::
+
+ $ ls -l /sys/class/console/netcon0/
+ total 0
+ lrwxrwxrwx 1 root root 0 May 18 13:28 subsystem -> ../../../../class/console/
+ -r--r--r-- 1 root root 4096 May 18 13:28 effective_loglevel
+ -r--r--r-- 1 root root 4096 May 18 13:28 effective_loglevel_source
+ -rw-r--r-- 1 root root 4096 May 18 13:28 loglevel
+ -rw-r--r-- 1 root root 4096 May 18 13:28 uevent
+
+See Documentation/admin-guide/per-console-loglevel.rst for more information.
+
The remote host has several options to receive the kernel messages,
for example:
diff --git a/MAINTAINERS b/MAINTAINERS
index e56494c7a956..2e6faa647b43 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -20505,6 +20505,7 @@ R: John Ogness <john.ogness@...utronix.de>
R: Sergey Senozhatsky <senozhatsky@...omium.org>
S: Maintained
T: git git://git.kernel.org/pub/scm/linux/kernel/git/printk/linux.git
+F: Documentation/ABI/testing/sysfs-class-console
F: Documentation/core-api/printk-basics.rst
F: include/linux/printk.h
F: kernel/printk/
diff --git a/include/linux/console.h b/include/linux/console.h
index a670c40623ad..a97235550668 100644
--- a/include/linux/console.h
+++ b/include/linux/console.h
@@ -16,6 +16,7 @@
#include <linux/atomic.h>
#include <linux/bits.h>
+#include <linux/device.h>
#include <linux/irq_work.h>
#include <linux/rculist.h>
#include <linux/rcuwait.h>
@@ -323,6 +324,8 @@ struct nbcon_write_context {
* @dropped: Number of unreported dropped ringbuffer records
* @data: Driver private data
* @node: hlist node for the console list
+ * @classdev: sysfs class device for this console, used to expose
+ * per-console controls in /sys/class/console/<name>/
*
* @nbcon_state: State for nbcon consoles
* @nbcon_seq: Sequence number of the next record for nbcon to print
@@ -352,6 +355,7 @@ struct console {
unsigned long dropped;
void *data;
struct hlist_node node;
+ struct device *classdev;
/* nbcon console specific members */
diff --git a/kernel/printk/Makefile b/kernel/printk/Makefile
index f8004ac3983d..19e4919a13a7 100644
--- a/kernel/printk/Makefile
+++ b/kernel/printk/Makefile
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: GPL-2.0-only
obj-y = printk.o
-obj-$(CONFIG_PRINTK) += printk_safe.o nbcon.o
+obj-$(CONFIG_PRINTK) += sysfs.o printk_safe.o nbcon.o
obj-$(CONFIG_A11Y_BRAILLE_CONSOLE) += braille.o
obj-$(CONFIG_PRINTK_INDEX) += index.o
diff --git a/kernel/printk/internal.h b/kernel/printk/internal.h
index f2ebaa2a6aa2..3b3a3c982412 100644
--- a/kernel/printk/internal.h
+++ b/kernel/printk/internal.h
@@ -21,6 +21,8 @@ enum loglevel_source {
LLS_IGNORE_LOGLEVEL,
};
+int console_clamp_loglevel(int level);
+
enum loglevel_source
console_effective_loglevel_source(int con_level);
int console_effective_loglevel(int con_level);
@@ -46,6 +48,9 @@ int console_effective_loglevel(int con_level);
#ifdef CONFIG_PRINTK
+void console_register_device(struct console *new);
+void console_setup_class(void);
+
#ifdef CONFIG_PRINTK_CALLER
#define PRINTK_PREFIX_MAX 48
#else
@@ -217,6 +222,9 @@ static inline void nbcon_kthreads_wake(void) { }
static inline bool console_is_usable(struct console *con, short flags,
bool use_atomic) { return false; }
+static inline void console_register_device(struct console *new) { }
+static inline void console_setup_class(void) { }
+
#endif /* CONFIG_PRINTK */
extern bool have_boot_console;
diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
index 1d28887e7218..605e0811cfc6 100644
--- a/kernel/printk/printk.c
+++ b/kernel/printk/printk.c
@@ -200,6 +200,24 @@ static int __init control_devkmsg(char *str)
}
__setup("printk.devkmsg=", control_devkmsg);
+/**
+ * console_clamp_loglevel - Clamp a loglevel to valid console loglevel range
+ *
+ * @level: The loglevel to clamp
+ *
+ * Console loglevels must be within the range [LOGLEVEL_ALERT, LOGLEVEL_DEBUG + 1].
+ * This function clamps a given level to this valid range.
+ *
+ * Note: This does not allow LOGLEVEL_EMERG (0) for per-console loglevels, as
+ * level 0 is reserved for emergency messages that should always go to all consoles.
+ *
+ * Return: The clamped loglevel value
+ */
+int console_clamp_loglevel(int level)
+{
+ return clamp(level, LOGLEVEL_ALERT, LOGLEVEL_DEBUG + 1);
+}
+
char devkmsg_log_str[DEVKMSG_STR_MAX_SIZE] = "ratelimit";
#if defined(CONFIG_PRINTK) && defined(CONFIG_SYSCTL)
int devkmsg_sysctl_set_loglvl(const struct ctl_table *table, int write,
@@ -4185,6 +4203,9 @@ void register_console(struct console *newcon)
u64 init_seq;
int err;
+ if (newcon->level == 0)
+ newcon->level = LOGLEVEL_DEFAULT;
+
console_list_lock();
for_each_console(con) {
@@ -4314,6 +4335,7 @@ void register_console(struct console *newcon)
if (use_device_lock)
newcon->device_unlock(newcon, flags);
+ console_register_device(newcon);
console_sysfs_notify();
/*
@@ -4429,6 +4451,13 @@ static int unregister_console_locked(struct console *console)
if (console->flags & CON_NBCON)
nbcon_free(console);
+ if (console->classdev) {
+ struct device *dev = console->classdev;
+
+ console->classdev = NULL;
+ device_unregister(dev);
+ }
+
console_sysfs_notify();
if (console->exit)
@@ -4578,6 +4607,9 @@ static int __init printk_late_init(void)
console_cpu_notify, NULL);
WARN_ON(ret < 0);
printk_sysctl_init();
+
+ console_setup_class();
+
return 0;
}
late_initcall(printk_late_init);
diff --git a/kernel/printk/sysfs.c b/kernel/printk/sysfs.c
new file mode 100644
index 000000000000..38d03046c45d
--- /dev/null
+++ b/kernel/printk/sysfs.c
@@ -0,0 +1,290 @@
+// SPDX-License-Identifier: GPL-2.0
+#include <linux/kernel.h>
+#include <linux/console.h>
+#include <linux/device.h>
+#include <linux/printk.h>
+#include <linux/slab.h>
+#include "internal.h"
+
+/**
+ * console_sysfs_read_loglevel - Locklessly read the console specific loglevel
+ * when accessing the related sysfs interface
+ * @con: struct console pointer of console to read loglevel from
+ *
+ * Locklessly reading @con->level provides a consistent read value because
+ * there is at most one CPU modifying @con->level and that CPU is using only
+ * read-modify-write operations to do so.
+ *
+ * Only use this function to read the loglevel via the related sysfs interface.
+ * The sysfs API makes sure that the structure cannot disappear while the
+ * interface is used.
+ *
+ * Context: Sysfs interface for the given console.
+ * Return: The current value of the @con->level field.
+ */
+static inline int console_sysfs_read_loglevel(const struct console *con)
+{
+ /*
+ * The READ_ONCE() matches the WRITE_ONCE() when @level is modified
+ * for registered consoles.
+ */
+ return data_race(READ_ONCE(con->level));
+}
+
+/**
+ * console_sysfs_write_loglevel - Write the console specific loglevel via
+ * sysfs interface.
+ * @con: struct console pointer of console to write loglevel to
+ * @con_level: new loglevel value to write
+ *
+ * Only use this function to write the loglevel via the related sysfs interface.
+ * The sysfs API makes sure that the structure cannot disappear while the
+ * interface is used.
+ *
+ * Context: Any context.
+ */
+static inline void console_sysfs_write_loglevel(struct console *con, int con_level)
+{
+ /* This matches the READ_ONCE() in console_sysfs_read_loglevel(). */
+ WRITE_ONCE(con->level, con_level);
+}
+
+/**
+ * console_effective_loglevel_source_str - Get string name of loglevel source
+ *
+ * @con: The console to query
+ *
+ * Returns a human-readable string describing the source of the console's
+ * effective loglevel (e.g., "local", "global", "ignore_loglevel").
+ *
+ * Return: String name of the loglevel source
+ */
+static const char *
+console_effective_loglevel_source_str(const struct console *con)
+{
+ enum loglevel_source source;
+ const char *str;
+ int con_level;
+
+ con_level = console_sysfs_read_loglevel(con);
+ source = console_effective_loglevel_source(con_level);
+
+ switch (source) {
+ case LLS_IGNORE_LOGLEVEL:
+ str = "ignore_loglevel";
+ break;
+ case LLS_LOCAL:
+ str = "local";
+ break;
+ case LLS_GLOBAL:
+ str = "global";
+ break;
+ default:
+ str = "unknown";
+ break;
+ }
+
+ return str;
+}
+
+static ssize_t effective_loglevel_source_show(struct device *dev,
+ struct device_attribute *attr,
+ char *buf)
+{
+ struct console *con = dev_get_drvdata(dev);
+ const char *str;
+
+ str = console_effective_loglevel_source_str(con);
+ return sysfs_emit(buf, "%s\n", str);
+}
+
+static DEVICE_ATTR_RO(effective_loglevel_source);
+
+static ssize_t effective_loglevel_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct console *con = dev_get_drvdata(dev);
+ int con_level;
+
+ con_level = console_sysfs_read_loglevel(con);
+ return sysfs_emit(buf, "%d\n", console_effective_loglevel(con_level));
+}
+
+static DEVICE_ATTR_RO(effective_loglevel);
+
+static ssize_t loglevel_show(struct device *dev, struct device_attribute *attr,
+ char *buf)
+{
+ struct console *con = dev_get_drvdata(dev);
+ int con_level;
+
+ con_level = console_sysfs_read_loglevel(con);
+ return sysfs_emit(buf, "%d\n", con_level);
+}
+
+static ssize_t loglevel_store(struct device *dev, struct device_attribute *attr,
+ const char *buf, size_t size)
+{
+ struct console *con = dev_get_drvdata(dev);
+ ssize_t ret;
+ int level;
+
+ ret = kstrtoint(buf, 10, &level);
+ if (ret < 0)
+ return ret;
+
+ /* -1 means "use global loglevel" */
+ if (level == -1)
+ goto out;
+
+ /*
+ * Reject level 0 (KERN_EMERG) - per-console loglevel must be > 0.
+ * Emergency messages should go to all consoles, so they cannot be
+ * filtered per-console.
+ */
+ if (level == 0)
+ return -ERANGE;
+
+ if (console_clamp_loglevel(level) != level)
+ return -ERANGE;
+
+ /*
+ * If the system has a minimum console loglevel set (via sysctl or
+ * kernel parameter), enforce it. This prevents setting per-console
+ * loglevels below the system minimum.
+ */
+ if (minimum_console_loglevel > CONSOLE_LOGLEVEL_MIN &&
+ level < minimum_console_loglevel)
+ return -ERANGE;
+
+out:
+ console_sysfs_write_loglevel(con, level);
+ return size;
+}
+
+static DEVICE_ATTR_RW(loglevel);
+
+static struct attribute *console_sysfs_attrs[] = {
+ &dev_attr_loglevel.attr,
+ &dev_attr_effective_loglevel_source.attr,
+ &dev_attr_effective_loglevel.attr,
+ NULL,
+};
+
+ATTRIBUTE_GROUPS(console_sysfs);
+
+static const struct class console_class = {
+ .name = "console",
+ .dev_groups = console_sysfs_groups,
+};
+static bool console_class_registered;
+
+/**
+ * console_classdev_release - Release callback for console class devices
+ *
+ * @dev: The device being released
+ *
+ * Called when the last reference to a console class device is dropped.
+ * Frees the memory allocated for the device structure.
+ */
+static void console_classdev_release(struct device *dev)
+{
+ kfree(dev);
+}
+
+/**
+ * console_register_device - Register a console's sysfs class device
+ *
+ * @con: The console to register
+ *
+ * Creates a sysfs class device for the given console under /sys/class/console/.
+ * This enables userspace access to per-console attributes like loglevel.
+ *
+ * If called before the console class is registered (during early boot),
+ * this function returns early and the device will be registered later
+ * by console_setup_class().
+ */
+void console_register_device(struct console *con)
+{
+ /*
+ * We might be called from register_console() before the class is
+ * registered. If that happens, we'll take care of it in
+ * printk_late_init.
+ */
+ if (!console_class_registered)
+ return;
+
+ if (WARN_ON(con->classdev))
+ return;
+
+ con->classdev = kzalloc(sizeof(struct device), GFP_KERNEL);
+ if (!con->classdev)
+ return;
+
+ device_initialize(con->classdev);
+ con->classdev->release = console_classdev_release;
+ con->classdev->class = &console_class;
+ dev_set_drvdata(con->classdev, con);
+ if (dev_set_name(con->classdev, "%s%d", con->name, con->index)) {
+ put_device(con->classdev);
+ con->classdev = NULL;
+ return;
+ }
+
+ /*
+ * This class device exists solely to expose attributes (like loglevel)
+ * and does not control physical power states. Power is managed by the
+ * underlying hardware device. Disable PM entirely to prevent the
+ * creation of confusing and unused power sysfs attributes.
+ */
+ device_set_pm_not_required(con->classdev);
+
+ if (device_add(con->classdev)) {
+ put_device(con->classdev);
+ con->classdev = NULL;
+ }
+}
+
+/**
+ * console_setup_class - Initialize the console sysfs class
+ *
+ * Registers the console class with sysfs and creates class devices for all
+ * currently registered consoles. Called during late init after sysfs is
+ * available.
+ *
+ * Consoles registered before this function is called will have their class
+ * devices created here. Consoles registered afterwards will have their
+ * devices created by console_register_device() during register_console().
+ */
+void console_setup_class(void)
+{
+ struct console *con;
+ int cookie;
+ int err;
+
+ /*
+ * printk exists for the lifetime of the kernel, it cannot be unloaded,
+ * so we should never end up back in here.
+ */
+ if (WARN_ON(console_class_registered))
+ return;
+
+ err = class_register(&console_class);
+ if (err) {
+ pr_err("console: failed to register class: %pe\n", ERR_PTR(err));
+ return;
+ }
+
+ /*
+ * Take console_list_lock() before exposing the class globally.
+ * This ensures register_console() (which holds the lock) cannot
+ * see the class until it's fully initialised with dev_groups.
+ */
+ console_list_lock();
+ console_class_registered = true;
+ cookie = console_srcu_read_lock();
+ for_each_console_srcu(con)
+ console_register_device(con);
+ console_srcu_read_unlock(cookie);
+ console_list_unlock();
+}
--
2.51.2
Powered by blists - more mailing lists