From nobody Tue Dec 2 02:41:46 2025 Received: from mail-pl1-f178.google.com (mail-pl1-f178.google.com [209.85.214.178]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id E0ADB2E8B7A for ; Tue, 18 Nov 2025 19:07:08 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=209.85.214.178 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1763492830; cv=none; b=NKIBX9sOXs4GEk9yFai1lg2uJJJo0cRZjIzv8MdrzNagu6DSHtYzDkVfw7en60BOt802M/T6mNUVNn1ozLgHwwd4MBzXc9DmKvmBNRmem64XGg0+OYIHO8Nk5NcrEm7z0Q7J1lvRHgiZa4Pt3y/BgQD4RsbPdLJbdiOJWl99K1s= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1763492830; c=relaxed/simple; bh=xKVNnbeSNKrfKn7uvYZI0vYR/Nskc/8flt3OsP8sohw=; h=Date:From:To:Cc:Subject:Message-ID:References:MIME-Version: Content-Type:Content-Disposition:In-Reply-To; b=cWdsRSTsserTgs8ofYkdo9JIWl8Flqwbw3S70PRGnhhQUWffAyo64tS+QMTBohRNWiyl/nPk/OFXWiPrefy/bsHkhztkJwMk9+I9r7GjwsP3vO1rfxNc3LlXllC8+Wm10JDpJ563N83iRPgzBPoLxux90KHgVphHgJ4hHRFfcUs= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=chrisdown.name; spf=pass smtp.mailfrom=chrisdown.name; dkim=pass (1024-bit key) header.d=chrisdown.name header.i=@chrisdown.name header.b=xTQQLhvT; arc=none smtp.client-ip=209.85.214.178 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=quarantine dis=none) header.from=chrisdown.name Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=chrisdown.name Authentication-Results: smtp.subspace.kernel.org; dkim=pass (1024-bit key) header.d=chrisdown.name header.i=@chrisdown.name header.b="xTQQLhvT" Received: by mail-pl1-f178.google.com with SMTP id d9443c01a7336-297dc3e299bso56912515ad.1 for ; Tue, 18 Nov 2025 11:07:08 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=chrisdown.name; s=google; t=1763492828; x=1764097628; darn=vger.kernel.org; h=user-agent:in-reply-to:content-disposition:mime-version:references :message-id:subject:cc:to:from:date:from:to:cc:subject:date :message-id:reply-to; bh=fAXEIOdrPjlE+zwRCMVbogwTJwQnlb80CC1vQLcEkoA=; b=xTQQLhvTmPctEIUzBXfUzRe3RTCYx8LsQZGVU25W/IDA2EzoCkuKOyi+H+PQFnfutB 8cA6ZLbj4LDWFlU+67e79wHrp4xlfO27Q5yPoMn8+7x1MYnU9HvRS/4LJ/jTRDn8DZIW z666IPXhmIlnlJVzMZYOffOzS4bELFO4/wZNA= X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1763492828; x=1764097628; h=user-agent:in-reply-to:content-disposition:mime-version:references :message-id:subject:cc:to:from:date:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to; bh=fAXEIOdrPjlE+zwRCMVbogwTJwQnlb80CC1vQLcEkoA=; b=LPLG91n0WtNzdavEu8Z0PQbLbO9LE89v2s3jFisbUSwQAfUEz/yHcUSc+t6Y5em8CD mfn9bEiNSqWIKiHcqwJAftS2R6ahqKKddAnseiKe81Z5WElHgcCe1Uv0Mz9SW6iwfzdt V2umKjY/cqL0pfh0NaZD0f2JX/z9ZrZGxKu5HadOE29dHBMggGVY5GiTW/2dKtYgA18r C+rF7wREnMMwaSjCW5wdb4JvHXxG0EpT5iPTKKISBF23yaV9yr4HF1Mwn2vzEK4lVJw6 keiDwLgS44jkI87b3jNkIgOEFGmd08cTALPZ4oS8RiVw4ollzpPNNx/5tXQsvUrPEVxM OXoQ== X-Gm-Message-State: AOJu0YzNXX5Vn4q7v5lRvaXCn04AopE4A9jwUaVTMJab+0iIjGEuNPlj ixjndhRDHIqw/RjUAt5TtG6/Ue9jRcdjFRJibgztacfboTnn433VhN9PJ3EByxd5GtE= X-Gm-Gg: ASbGncsHGsBltpZqLHuxA0kPq8wTDq5sEEqFyChmalo3shFHDivpUpEfRIphI+g/19l byKvtaGpgjzC8qGgyf0M47SWj8BeV2qyG7eaNAqXUYpYiNSTayPm6FeKZRB9Owrh/n2WWztxAC7 WQSoJ7HHl9SYx783y7MF1BI0uvS6la5difQkIi/QzEf0/3HLCsO/2XLvNNSbV61TkbUc/keKkOO Imb5jUgDn33PzPQnMkLGStaM1IHzo55Ivyo8rq4PMg9PaPxgsFtTvATD+EOXTtV5HT70y3svbiT HOVPagqJT+MdFnZ6aPjpHw0UzDPeAdteKaplGuYhIX1VeAVDHFp8ajDYjaBMrNGCzxeSGzEVRop wSn7Pn6dbLI4DV8jo8rL6tnvFZpaI4JgZOzfTUkxfF1dQR1E6UzVMwyv1soxoXry0DxeaCsBBlC ma/grkUYcnOx4HIFUkWbE= X-Google-Smtp-Source: AGHT+IFvsO86VAhPM6CptdlIPVzNT0Q89ybPGqkJA89h5VTASkSdKVDNQOnLXSJRqu2YJ+p9FRN0yQ== X-Received: by 2002:a17:903:3b87:b0:297:f8d9:aad7 with SMTP id d9443c01a7336-2986a74ba2fmr183918315ad.50.1763492827872; Tue, 18 Nov 2025 11:07:07 -0800 (PST) Received: from localhost ([116.86.198.140]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-2985c2346c3sm179898925ad.4.2025.11.18.11.07.07 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 18 Nov 2025 11:07:07 -0800 (PST) Date: Wed, 19 Nov 2025 03:07:06 +0800 From: Chris Down To: Petr Mladek Cc: linux-kernel@vger.kernel.org, Greg Kroah-Hartman , Sergey Senozhatsky , Steven Rostedt , John Ogness , Geert Uytterhoeven , Tony Lindgren , kernel-team@fb.com Subject: [PATCH v7 03/13] printk: console: Implement core per-console loglevel infrastructure Message-ID: <201aa88f5c476ba56aa23183d74643275f8b2b41.1763492585.git.chris@chrisdown.name> References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Disposition: inline In-Reply-To: User-Agent: Mutt/2.2.15 (2b349c5e) (2025-10-02) Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Consoles can have vastly different latencies and throughputs. For example, writing a message to the serial console can take on the order of tens of milliseconds to get the UART to successfully write a message. While this might be fine for a single, one-off message, this can cause significant application-level stalls in situations where the kernel writes large amounts of information to the console. This means that while you might want to send at least INFO level messages to (for example) netconsole, which is relatively fast, you may only want to send at least WARN level messages to the serial console. Such an implementation would permit debugging using the serial console in cases that netconsole doesn't receive messages during particularly bad system issues, while still keeping the noise low enough to avoid inducing latency in userspace applications. To mitigate this, add such an interface, extending the existing console loglevel controls to allow each console to have its own loglevel. One can't just disable the serial console, because one may actually need it in situations where the machine is in a bad enough state that nothing is received on netconsole. One also can't just bump the loglevel at runtime after the issue, because usually the machine is already so wedged by this point that it isn't responsive to such requests. This commit adds the internal infrastructure to support per-console log levels, which will be configurable through sysfs and the kernel command line in future commits. The global console_loglevel is preserved and used as the default log level for all consoles. Each console can override this global level with its own specific log level stored in struct console. To override the global level, the per-console log level must be greater than 0; otherwise, the default value of LOGLEVEL_DEFAULT (-1) ensures the global level is used. The existing ignore_loglevel command line parameter will override both the global and per-console log levels. Signed-off-by: Chris Down Reviewed-by: Petr Mladek --- include/linux/console.h | 41 +++++++++-- include/linux/printk.h | 6 +- kernel/printk/internal.h | 12 ++++ kernel/printk/nbcon.c | 4 +- kernel/printk/printk.c | 150 ++++++++++++++++++++++++++++++++++++++- 5 files changed, 203 insertions(+), 10 deletions(-) diff --git a/include/linux/console.h b/include/linux/console.h index 8f10d0a85bb4..c0749a48fc3f 100644 --- a/include/linux/console.h +++ b/include/linux/console.h @@ -314,6 +314,8 @@ struct nbcon_write_context { * @match: Callback for matching a console (Optional) * @flags: Console flags. See enum cons_flags * @index: Console index, e.g. port number + * @level: Per-console loglevel. -1 means use global console_loglevel, + * values > 0 specify console-specific filtering level * @cflag: TTY control mode flags * @ispeed: TTY input speed * @ospeed: TTY output speed @@ -342,6 +344,7 @@ struct console { int (*match)(struct console *co, char *name, int idx, char *options); short flags; short index; + int level; int cflag; uint ispeed; uint ospeed; @@ -518,11 +521,6 @@ extern struct hlist_head console_list; static inline short console_srcu_read_flags(const struct console *con) { WARN_ON_ONCE(!console_srcu_read_lock_is_held()); - - /* - * The READ_ONCE() matches the WRITE_ONCE() when @flags are modified - * for registered consoles with console_srcu_write_flags(). - */ return data_race(READ_ONCE(con->flags)); } =20 @@ -544,6 +542,39 @@ static inline void console_srcu_write_flags(struct con= sole *con, short flags) WRITE_ONCE(con->flags, flags); } =20 +/** + * console_srcu_read_loglevel - Locklessly read the console specific logle= vel + * of a possibly registered console + * @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 on= ly + * read-modify-write operations to do so. + * + * Requires console_srcu_read_lock to be held, which implies that @con mig= ht + * be a registered console. The purpose of holding console_srcu_read_lock = is + * to guarantee that the console state is valid (CON_SUSPENDED/CON_ENABLED) + * and that no exit/cleanup routines will run if the console is currently + * undergoing unregistration. + * + * If the caller is holding the console_list_lock or it is _certain_ that + * @con is not and will not become registered, the caller may read + * @con->level directly instead. + * + * Context: Any context. + * Return: The current value of the @con->level field. + */ +static inline int console_srcu_read_loglevel(const struct console *con) +{ + WARN_ON_ONCE(!console_srcu_read_lock_is_held()); + + /* + * The READ_ONCE() matches the WRITE_ONCE() when @level is modified + * for registered consoles. + */ + return data_race(READ_ONCE(con->level)); +} + /* Variant of console_is_registered() when the console_list_lock is held. = */ static inline bool console_is_registered_locked(const struct console *con) { diff --git a/include/linux/printk.h b/include/linux/printk.h index 45c663124c9b..644584edf3e9 100644 --- a/include/linux/printk.h +++ b/include/linux/printk.h @@ -209,6 +209,7 @@ void printk_legacy_allow_panic_sync(void); extern bool nbcon_device_try_acquire(struct console *con); extern void nbcon_device_release(struct console *con); void nbcon_atomic_flush_unsafe(void); +bool has_per_console_loglevel(const struct console *con); bool pr_flush(int timeout_ms, bool reset_on_progress); #else static inline __printf(1, 0) @@ -322,7 +323,10 @@ static inline void nbcon_device_release(struct console= *con) static inline void nbcon_atomic_flush_unsafe(void) { } - +static inline bool has_per_console_loglevel(const struct console *con) +{ + return false; +} static inline bool pr_flush(int timeout_ms, bool reset_on_progress) { return true; diff --git a/kernel/printk/internal.h b/kernel/printk/internal.h index 1ed86577896c..41e37b44778f 100644 --- a/kernel/printk/internal.h +++ b/kernel/printk/internal.h @@ -15,6 +15,18 @@ int devkmsg_sysctl_set_loglvl(const struct ctl_table *ta= ble, int write, #define printk_sysctl_init() do { } while (0) #endif =20 +enum loglevel_source { + LLS_GLOBAL, + LLS_LOCAL, + 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); + #define con_printk(lvl, con, fmt, ...) \ printk(lvl pr_fmt("%s%sconsole [%s%d] " fmt), \ (con->flags & CON_NBCON) ? "" : "legacy ", \ diff --git a/kernel/printk/nbcon.c b/kernel/printk/nbcon.c index eb4c8faa213d..fb99aaa8c5d8 100644 --- a/kernel/printk/nbcon.c +++ b/kernel/printk/nbcon.c @@ -961,6 +961,7 @@ static bool nbcon_emit_next_record(struct nbcon_write_c= ontext *wctxt, bool use_a struct nbcon_context *ctxt =3D &ACCESS_PRIVATE(wctxt, ctxt); struct console *con =3D ctxt->console; bool is_extended =3D console_srcu_read_flags(con) & CON_EXTENDED; + int con_level =3D console_srcu_read_loglevel(con); struct printk_message pmsg =3D { .pbufs =3D ctxt->pbufs, }; @@ -993,7 +994,8 @@ static bool nbcon_emit_next_record(struct nbcon_write_c= ontext *wctxt, bool use_a if (!nbcon_context_enter_unsafe(ctxt)) return false; =20 - ctxt->backlog =3D printk_get_next_message(&pmsg, ctxt->seq, is_extended, = console_loglevel); + ctxt->backlog =3D printk_get_next_message(&pmsg, ctxt->seq, is_extended, + console_effective_loglevel(con_level)); if (!ctxt->backlog) return nbcon_context_exit_unsafe(ctxt); =20 diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c index 745f89a18189..928d77c56c77 100644 --- a/kernel/printk/printk.c +++ b/kernel/printk/printk.c @@ -197,6 +197,24 @@ static int __init control_devkmsg(char *str) } __setup("printk.devkmsg=3D", control_devkmsg); =20 +/** + * console_clamp_loglevel - Clamp a loglevel to valid console loglevel ran= ge + * + * @level: The loglevel to clamp + * + * Console loglevels must be within the range [LOGLEVEL_ALERT, LOGLEVEL_DE= BUG + 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] =3D "ratelimit"; #if defined(CONFIG_PRINTK) && defined(CONFIG_SYSCTL) int devkmsg_sysctl_set_loglvl(const struct ctl_table *table, int write, @@ -1280,11 +1298,120 @@ module_param(ignore_loglevel, bool, S_IRUGO | S_IW= USR); MODULE_PARM_DESC(ignore_loglevel, "ignore loglevel setting (prints all kernel messages to the console)"); =20 +bool has_per_console_loglevel(const struct console *con) +{ + return con && (console_srcu_read_loglevel(con) > 0); +} + +/** + * is_valid_per_console_loglevel - Check if a loglevel is valid for per-co= nsole + * + * @con_level: The loglevel to check + * + * Per-console loglevels must be strictly positive (> 0). Level 0 (KERN_EM= ERG) + * is reserved for emergency messages that should go to all consoles (and = so is + * disallowed), and -1 (LOGLEVEL_DEFAULT) means use the global console_log= level. + * + * Return: true if con_level is a valid per-console loglevel (> 0), false + * otherwise + */ +static bool is_valid_per_console_loglevel(int con_level) +{ + return (con_level > 0); +} + +/** + * console_effective_loglevel_source - Determine the source of effective l= oglevel + * + * @con_level: The console's per-console loglevel value + * + * This function determines which loglevel authority is in effect for a co= nsole, + * based on the hierarchy of controls: + * + * 1. ignore_loglevel (overrides everything - prints all messages) + * 2. per-console loglevel (if set and not ignored) + * 3. global console_loglevel (fallback) + * + * Return: The loglevel source (LLS_IGNORE_LOGLEVEL, LLS_LOCAL, or LLS_GLO= BAL) + */ +enum loglevel_source +console_effective_loglevel_source(int con_level) +{ + if (ignore_loglevel) + return LLS_IGNORE_LOGLEVEL; + + if (is_valid_per_console_loglevel(con_level)) + return LLS_LOCAL; + + return LLS_GLOBAL; +} + +/** + * console_effective_loglevel - Get the effective loglevel for a console + * + * @con_level: The console's per-console loglevel value + * + * This function returns the actual loglevel value that should be used for + * message filtering for a console, taking into account all loglevel contr= ols + * (global, per-console, and ignore_loglevel). + * + * The effective loglevel is used to determine which messages get printed = to + * the console. Messages with priority less than the effective level are p= rinted. + * + * Return: The effective loglevel value to use for filtering + */ +int console_effective_loglevel(int con_level) +{ + enum loglevel_source source; + int level; + + source =3D console_effective_loglevel_source(con_level); + + switch (source) { + case LLS_IGNORE_LOGLEVEL: + level =3D CONSOLE_LOGLEVEL_MOTORMOUTH; + break; + case LLS_LOCAL: + level =3D con_level; + break; + case LLS_GLOBAL: + level =3D console_loglevel; + break; + default: + pr_warn("Unhandled console loglevel source: %d", source); + level =3D console_loglevel; + break; + } + + return level; +} + static bool suppress_message_printing(int level, int con_eff_level) { return (level >=3D con_eff_level && !ignore_loglevel); } =20 +static bool suppress_message_printing_everywhere(int level) +{ + bool suppress_everywhere =3D true; + struct console *con; + int cookie; + + cookie =3D console_srcu_read_lock(); + + for_each_console_srcu(con) { + int con_level =3D console_srcu_read_loglevel(con); + + if (!suppress_message_printing(level, console_effective_loglevel(con_lev= el))) { + suppress_everywhere =3D false; + break; + } + } + console_srcu_read_unlock(cookie); + + return suppress_everywhere; +} + #ifdef CONFIG_BOOT_PRINTK_DELAY =20 static int boot_delay; /* msecs delay after each printk during bootup */ @@ -2116,7 +2243,8 @@ int printk_delay_msec __read_mostly; static inline void printk_delay(int level) { /* If the message is forced (e.g. panic), we must delay */ - if (!is_printk_force_console() && suppress_message_printing(level, consol= e_loglevel)) + if (!is_printk_force_console() && + suppress_message_printing_everywhere(level)) return; =20 boot_delay_msec(); @@ -3059,6 +3187,7 @@ struct printk_buffers printk_shared_pbufs; static bool console_emit_next_record(struct console *con, bool *handover, = int cookie) { bool is_extended =3D console_srcu_read_flags(con) & CON_EXTENDED; + int con_level =3D console_srcu_read_loglevel(con); char *outbuf =3D &printk_shared_pbufs.outbuf[0]; struct printk_message pmsg =3D { .pbufs =3D &printk_shared_pbufs, @@ -3068,7 +3197,7 @@ static bool console_emit_next_record(struct console *= con, bool *handover, int co *handover =3D false; =20 if (!printk_get_next_message(&pmsg, con->seq, is_extended, - console_loglevel)) + console_effective_loglevel(con_level))) return false; =20 con->dropped +=3D pmsg.dropped; @@ -3817,6 +3946,9 @@ static int try_enable_preferred_console(struct consol= e *newcon, if (newcon->index < 0) newcon->index =3D c->index; =20 + /* TODO: will be configurable in a later patch */ + newcon->level =3D LOGLEVEL_DEFAULT; + if (_braille_register_console(newcon, c)) return 0; =20 @@ -3835,8 +3967,12 @@ static int try_enable_preferred_console(struct conso= le *newcon, * without matching. Accept the pre-enabled consoles only when match() * and setup() had a chance to be called. */ - if (newcon->flags & CON_ENABLED && c->user_specified =3D=3D user_specifie= d) + if (newcon->flags & CON_ENABLED && c->user_specified =3D=3D user_specifie= d) { + /* Ensure level is initialized for pre-enabled consoles */ + if (newcon->level =3D=3D 0) + newcon->level =3D LOGLEVEL_DEFAULT; return 0; + } =20 return -ENOENT; } @@ -4039,6 +4175,14 @@ void register_console(struct console *newcon) } =20 newcon->dropped =3D 0; + + /* + * Don't unconditionally overwrite, it may have been set on the command + * line already. + */ + if (newcon->level =3D=3D 0) + newcon->level =3D LOGLEVEL_DEFAULT; + init_seq =3D get_init_console_seq(newcon, bootcon_registered); =20 if (newcon->flags & CON_NBCON) { --=20 2.51.2