printk/console: Clean up boot console handling in register_console()
authorPetr Mladek <pmladek@suse.com>
Mon, 22 Nov 2021 13:26:49 +0000 (14:26 +0100)
committerPetr Mladek <pmladek@suse.com>
Mon, 6 Dec 2021 13:07:57 +0000 (14:07 +0100)
The variable @bcon has two meanings. It is used several times for iterating
the list of registered consoles. In the meantime, it holds the information
whether a boot console is first in @console_drivers list.

The information about the 1st console driver used to be important for
the decision whether to install the new console by default or not.
It allowed to re-evaluate the variable @need_default_console when
a real console with tty binding has been unregistered in the meantime.

The decision about the default console is not longer affected by @bcon
variable. The current code checks whether the first driver is real
and has tty binding directly.

The information about the first console is still used for two more
decisions:

  1. It prevents duplicate output on non-boot consoles with
     CON_CONSDEV flag set.

  2. Early/boot consoles are unregistered when a real console with
     CON_CONSDEV is registered and @keep_bootcon is not set.

The behavior in the real life is far from obvious. @bcon is set according
to the first console @console_drivers list. But the first position in
the list is special:

  1. Consoles with CON_CONSDEV flag are put at the beginning of
     the list. It is either the preferred console or any console
     with tty binding registered by default.

  2. Another console might become the first in the list when
     the first console in the list is unregistered. It might
     happen either explicitly or automatically when boot
     consoles are unregistered.

There is one more important rule:

  + Boot consoles can't be registered when any real console
    is already registered.

It is a puzzle. The main complication is the dependency on the first
position is the list and the complicated rules around it.

Let's try to make it easier:

1. Add variable @bootcon_enabled and set it by iterating all registered
   consoles. The variable has obvious meaning and more predictable
   behavior. Any speed optimization and other tricks are not worth it.

2. Use a generic name for the variable that is used to iterate
   the list on registered console drivers.

Behavior change:

No, maybe surprisingly, there is _no_ behavior change!

Let's provide the proof by contradiction. Both operations, duplicate
output prevention and boot consoles removal, are done only when
the newly added console has CON_CONSDEV flag set. The behavior
would change when the new @bootcon_enabled has different value
than the original @bcon.

By other words, the behavior would change when the following conditions
are true:

   + a console with CON_CONSDEV flag is added
   + a real (non-boot) console is the first in the list
   + a boot console is later in the list

Now, a real console might be first in the list only when:

   + It was the first registered console. In this case, there can't be
     any boot console because any later ones were rejected.

   + It was put at the first position because it had CON_CONSDEV flag
     set. It was either the preferred console or it was a console with
     tty binding registered by default. We are interested only in
     a real consoles here. And real console with tty binding fulfills
     conditions of the default console.

     Now, there is always only one console that is either preferred
     or fulfills conditions of the default console. It can't be already
     in the list and being registered at the same time.

As a result, the above three conditions could newer be "true" at
the same time. Therefore the behavior can't change.

Final dilemma:

OK, the new code has the same behavior. But is the change in the right
direction? What if the handling of @console_drivers is updated in
the future?

OK, let's look at it from another angle:

1. The ordering of @console_drivers list is important only in
   console_device() function. The first console driver with tty
   binding gets associated with /dev/console.

2. CON_CONSDEV flag is shown in /proc/consoles. And it should be set
   for the driver that is returned by console_device().

3. A boot console is removed and the duplicated output is prevented
   when the real console with CON_CONSDEV flag is registered.

Now, in the ideal world:

+ The driver associated with /dev/console should be either a console
  preferred via the command line, device tree, or SPCR. Or it should
  be the first real console with tty binding registered by default.

+ The code should match the related boot and real console drivers.
  It should unregister only the obsolete boot driver. And the duplicated
  output should be prevented only on the related real driver.

It is clear that it is not guaranteed by the current code. Instead,
the current code looks like a maze of heuristics that try to achieve
the above.

It is result of adding several features over last few decades. For example,
a possibility to register more consoles, unregister consoles, boot
consoles, consoles without tty binding, device tree, SPCR, braille
consoles.

Anyway, there is no reason why the decision, about removing boot consoles
and preventing duplicated output, should depend on the first console
in the list. The current code does the decisions primary by CON_CONSDEV
flag that is used for the preferred console. It looks like a
good compromise. And the change seems to be in the right direction.

Signed-off-by: Petr Mladek <pmladek@suse.com>
Link: https://lore.kernel.org/r/20211122132649.12737-6-pmladek@suse.com
kernel/printk/printk.c

index 6591da2..155229f 100644 (file)
@@ -2943,31 +2943,30 @@ static void try_enable_default_console(struct console *newcon)
  */
 void register_console(struct console *newcon)
 {
-       struct console *bcon = NULL;
+       struct console *con;
+       bool bootcon_enabled = false;
+       bool realcon_enabled = false;
        int err;
 
-       for_each_console(bcon) {
-               if (WARN(bcon == newcon, "console '%s%d' already registered\n",
-                                        bcon->name, bcon->index))
+       for_each_console(con) {
+               if (WARN(con == newcon, "console '%s%d' already registered\n",
+                                        con->name, con->index))
                        return;
        }
 
-       /*
-        * before we register a new CON_BOOT console, make sure we don't
-        * already have a valid console
-        */
-       if (newcon->flags & CON_BOOT) {
-               for_each_console(bcon) {
-                       if (!(bcon->flags & CON_BOOT)) {
-                               pr_info("Too late to register bootconsole %s%d\n",
-                                       newcon->name, newcon->index);
-                               return;
-                       }
-               }
+       for_each_console(con) {
+               if (con->flags & CON_BOOT)
+                       bootcon_enabled = true;
+               else
+                       realcon_enabled = true;
        }
 
-       if (console_drivers && console_drivers->flags & CON_BOOT)
-               bcon = console_drivers;
+       /* Do not register boot consoles when there already is a real one. */
+       if (newcon->flags & CON_BOOT && realcon_enabled) {
+               pr_info("Too late to register bootconsole %s%d\n",
+                       newcon->name, newcon->index);
+               return;
+       }
 
        /*
         * See if we want to enable this console driver by default.
@@ -3005,8 +3004,10 @@ void register_console(struct console *newcon)
         * the real console are the same physical device, it's annoying to
         * see the beginning boot messages twice
         */
-       if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV))
+       if (bootcon_enabled &&
+           ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) {
                newcon->flags &= ~CON_PRINTBUFFER;
+       }
 
        /*
         *      Put this console in the list - keep the
@@ -3062,15 +3063,15 @@ void register_console(struct console *newcon)
        pr_info("%sconsole [%s%d] enabled\n",
                (newcon->flags & CON_BOOT) ? "boot" : "" ,
                newcon->name, newcon->index);
-       if (bcon &&
+       if (bootcon_enabled &&
            ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV) &&
            !keep_bootcon) {
                /* We need to iterate through all boot consoles, to make
                 * sure we print everything out, before we unregister them.
                 */
-               for_each_console(bcon)
-                       if (bcon->flags & CON_BOOT)
-                               unregister_console(bcon);
+               for_each_console(con)
+                       if (con->flags & CON_BOOT)
+                               unregister_console(con);
        }
 }
 EXPORT_SYMBOL(register_console);