Serial Consoles and Consoles in General

After dissecting the architecture of serial drivers and how a serial device interacts with PPP and SLIP, this month we are going to look at how a serial device can act as a console. Fortunately, console management is independent of the exact nature of serial ports and an interesting feature by itself. This article, therefore, deals more with console management than with serial ports, and is accompanied by the implementation of a UDP console, to show how in practice console management is organized in Linux-2.4. Sample code has been written and tested using version 2.4.4 of the kernel.

The idea of "console"

Once upon a time, when computers were huge boxes with dozens of terminals, one of those terminals had the special role of ``system console''; it was the only terminal that could be used in single-user-mode for system recovery, and the only one that received system error messages. Recent hardware is usually equipped with keyboard and video hardware and doesn't run a plethora of text terminal, so the ``system console'' is physically embodied by the native keyboard and monitor. When the host computer has no own display hardware, on the other hand, the role of the console is usually taken by a serial port: both diagnostic messages and the login prompt can be accessed by plugging a serial cable to the computer. While it makes sense to think of the console as the ``controlling terminal'', where both input and output is performed, being able to fire a root shell and delivering kernel messages are completely different facilities. The former feature is handled by the configuration of the init process and is out of scope here; this kernel-oriented column is rather concerned with the latter issue: how kernel messages are delivered to the interactive system administrator. Note that I'm not going to discuss how kernel messages are collected by user programs (like klogd and syslogd, I'd rather concentrate on delivery of messages to the system console device, be it a serial port or other device.

The default console

When you run the typical text-mode GNU/Linux installation on your computer, the console is by default the current virtual terminal. Thus, kernel messages fall in the middle of your shell session. This behavior may be irritating if, for example, you are accessing a damaged floppy disk and the kernel spits a line of complaint every few seconds; we'll see how to fix the problem in a while. When you run X, the current virtual terminal is in graphic mode and delivery of kernel messages is disabled for this reason; even if the kernel printed the messages, you wouldn't see them anyways.

The mechanism for delivering messages to the console is implemented by the printk function, defined in kernel/printk.c. The function uses vsprintf (defined in lib/vsprintf.c) to create a message string, places the string in the circular buffer of kernel messages and passes it to all active console devices if the priority of the message is less than console_loglevel. The circular buffer is out of scope for this article because it is just temporary storage whence user-space programs can retrieve kernel messages.

The variable console_loglevel, used to select which messages are considered to be ``important enough'' to be worth printing on the system console, defaults to DEFAULT_CONSOLE_LOGLEVEL and the system administrator can change it by writing to /proc/sys/kernel/printk. A value of 8, for example, will force all messages down to debug messages to be printed to the console, and a command like "echo 8 > /proc/sys/kernel/printk" would work to enable all messages. Individual priority values (in the range 0 to 7) are defined in <linux/kernel.h> as macros like KERN_ERROR and KERN_DEBUG.

As stated, if the priority of the current message is numerically less than console_loglevel, the message is passed to all active consoles. The actual code for delivering the message scans a list of console devices and reduces to the few lines shown in listing 1.

if (msg_level < console_loglevel && console_drivers) {
    struct console *c = console_drivers;
    while(c) {
        if ((c->flags & CON_ENABLED) && c->write)
            c->write(c, msg, p - msg + line_feed);
        c = c->next;
    }
}

In the most common Linux configuration there is only one console device registered, and it corresponds to the virtual terminal. The relevant code lives in drivers/char/console.c, and actual printing of messages is performed by vt_console_print. If you look in the function, you'll find that it doesn't always print messages to the foreground virtual terminal but may be configured to elect a specific virtual terminal for messaging. If the variable kmsg_redirect is non-zero, its value is used as the number of the virtual terminal elected to receive kernel messages. The variable can be written by invoking ioctl(TIOCLINUX) (a Linux-specific tty ioctl command) on a file descriptor associated to a virtual console. To this aim, I use a simple tool called setconsole, whose source is included in the sample code associated to this article. By issuing ``setconsole 1'', for example, you can force all kernel messages to be printed to virtual terminal number 1 instead of the one you are using when the message is delivered.

If you are running a ``standard'' PC equipped with serial ports, you can also elect one of your serial ports as a console by passing a proper console= command line option to your kernel. The file Documentation/serial-console.txt, part of the kernel source tree, clearly describes both the overall design and the details you may need, so I won't repeat it here. The only point worth noting is that there may be several console devices at the same time (for example, both a serial port and a virtual terminal), thanks to the loop shown in listing 1.

Declaring and selecting a console

In order to declare a new console device, your kernel code must first include <linux/console.h>. The header defines struct console and a few flags used therein.

Once equipped with a struct console, your code can simply call register_console to get inserted in the list of active consoles. The structure being registered is made up of the following fields:

Listing 2 shows how the device driver for PC serial ports (drivers/char/serial.c) declares and registers the serial console.

static struct console sercons = {
	name:		"ttyS",
	write:		serial_console_write,
	device:		serial_console_device,
	wait_key:	serial_console_wait_key,
	setup:		serial_console_setup,
	flags:		CON_PRINTBUFFER,
	index:		-1,
};

void __init serial_console_init(void)
{
	register_console(&sercons);
}

Listing 2a shows how the device driver for PC parallel ports (drivers/char/lp.c) declares and registers the parallel console (if enabled). As you can see, the code for registering a parallel console is a bit more complicated, as the kernel first checks if the line printer that is being attached is the first one (!nr) and refuses to use it as a console otherwise.

static struct console lpcons = {
	name:		"lp",
	write:		lp_console_write,
	device:		lp_console_device,
	flags:		CON_PRINTBUFFER,
};

static int lp_register(int nr, struct parport *port)
{

[...]

#ifdef CONFIG_LP_CONSOLE
	if (!nr) {
		if (port->modes & PARPORT_MODE_SAFEININT) {
			register_console (&lpcons);
			console_registered = port;
			printk (KERN_INFO "lp%d: console ready\n", CONSOLE_LP);
		} else
			printk (KERN_ERR "lp%d: cannot run console on %s\n",
				CONSOLE_LP, port->name);
	}
#endif

	return 0;
}

It's interesting to note that the console.h header is also concerned with frame-buffer-consoles; that kind of functionality (based on the struct consw data structure) is out of scope in this column as it deals with mapping the virtual-terminal text modes to different video hardware implementations.

Writing a console driver

In order to understand the mechanisms that make up a system console in Linux, we'll see them on work in an implementation that sends kernel messages to the network using UDP. In my opinion, bringing the discussion of serial consoles outside of the realm of serial communication helps in better understanding the ``console'' abstraction; any doubts on how to bring this discussion to the serial device can be solved by looking at drivers/char/serial.c.

This column doesn't show all the details of the UDP console, as that would make the discussion too heavy to be interesting. Ingo Molnar's netconsole patches (http://people.redhat.com/mingo/netconsole-patches/) implement a full-featured UDP console. My skeletal UDP console, which could be a good starting point to grasp the whole mechanism and will be discussed throughout the rest of this article, is available from my own FTP and HTTP space (http://arcana.linux.it/docs/sercons.tar.gz). Both Ingo Molnar's patches and my code are licensed according to the GNU GPL.

Sample code availability

All sample code referenced in this column and previous ones is
available from http://www.linux.it/~rubini/linux-mag/ and
ftp://ftp.linux.it/pub/People/rubini/linux-mag .

I owe an apology to the readership because last time I promised sample
code I haven't been able to deliver it in time. Due to high workload,
I usually complete sample code after the deadline for the column
(although I already have a proof of the idea and I compile and test
everything that's printed in the column itself).  Last time I offered
sample code (ktftpd.tar.gz) I haven't been able to complete it in time
(nor to write any more columns for a while). Now everything is in
place and I hope not to so late with deadlines any more.

---> I can't make it shorter than that. I would promise to at least
upload a skeleton as "beta", but that would make it longer.

The definition of the UDP console device is, as you may expect, reminiscent of the serial console just shown. Listing 3 shows the incantation of struct console for the UDP console.

struct console udpcons = {
    name:     "udp",
    write:    udpcons_write,
    wait_key: udpcons_waitkey,
    setup:    udpcons_setup,
    device:   NULL, /* no tty device associated to UDP */
    flags:    CON_ENABLED, /* always enabled */
    index:    -1, /* unspecified */
};

The purpose of the UDP console device is sending kernel messages through the network. Unlike serial or vt consoles, which are associated to real tty devices, this console has no tty associated, and that's why the device function is not defined.

In a real console device, the device function is used to return the device number associated to this console as a kdev_t value. Only one serial port can be elected as a console, for example, and the device function defined by the serial driver is used to tell the caller which one is. The function is used in drivers/char/tty_io.c to redirect any access to /dev/console. Thus, a process that opens /dev/console will actually open a different device, provided at least one of the active console drivers has a device function and the device returned is known to the tty layer.

Console Initialization

A few fields in struct console exist just to simplify console initialization and customization.

The name field is used in command line parsing, together with setup. The UDP console is designed to be a module, so accessing the kernel command line may not make too much sense; however, to better exemplify how add-on consoles work I chose to implement both fields. The name of this console is "udp", so you could use a kernel command line argument of "console=udp...".

At system boot, console_setup (in kernel/printk.c) is called once for each console= kernel command line argument. The function saves all of these directives in an array. When register_console is later called, if one of the options is found to match with the name field, the setup function is called.

A "console" command-line option can be used to specify the name (and number) of the console as well as an optional argument separated by a comma. For serial ports, for example, "console=ttyS2,9600n8" will select the third serial port at a speed of 9600 baud. For the UDP console, you can specify the UDP port like it was a device number and an optional destination IP; the device number is saved to the index field of struct console and everything after the command is passed as options to the setup function. For example you can tell your kernel "console=udp4000,10.0.0.1" so that when the module is later loaded it will use port 4000 and will transmit to host 10.0.0.1. The default UDP port is 2222 and the default destination IP address is the broadcast address ("255.255.255.255"); if you use those default values, the route taken by generated packets depend on your configuration.

int udpcons_setup(struct console *co, char *options)
{
    if (co->index > 0)
	udpcons_port = co->index;

    if (options)
	udpcons_addr = in_aton(options);

    return 0; /* success */
}

Listing 4 shows the core of the setup function (the real one is slightly more flexible, and you can see it in the file udpcons.c).

Console Flags

The flags field is a bitmask, and linux-2.4 defines three flags:

Our UDP console sets CON_ENABLED in order to run even if no kernel command line option requests an UDP console, while other flags are not interesting in this context.

Output and Input

The main role of a console device, as seen from the kernel, is reporting kernel messages to the user, and the write function as defined in struct console is the engine of such reporting. As shown in listing 1, it is called directly from printk. Since printk can be called at any time, even at interrupt time, the write function of a console should not sleep for any noticeable amount of time. To prevent data corruption due to reentrancy, printk is protected by a spinlock and runs with interrupts disabled; this unfortunately means that any delay in the console output function can be detrimental to system operation.

On the other hand, you may want to see all messages that are printed before a system panic, so for maximum reliability the console print function should not return before data is actually output. For this reason, the serial console driver operates in a busy loop and its write function doesn't return before the last byte has been transmitted. The same applies to the parallel console driver, with the caveat that our line printer could get out of paper. In this case, we have the possibility of either blocking until the printer is feeded some more paper or losing messages; we can choose the preferred behaviour by setting CONSOLE_LP_STRICT to 1 or 0, respectively (see drivers/char/lp.c). Our sample UDP console can't wait for data transmission because it can't manipulate the network device in a busy loop; it therefore just builds a network frame and enqueues it in the kernel's transmit queue. For this reason, you won't be able see UDP packets for any kernel message that is immediately followed by a kernel panic. Fortunately, most Linux ports are very conservative in calling panic (while, for example, the PPC port is not), and you'll be able to use the UDP console in debugging your own device drivers like I do.

As far as input is concerned, struct console also includes a wait_key function (and an unused read function). wait_key is only used at system boot time, for example after asking the user to insert a root floppy. There is no such feature implemented in the sample UDP console, but the comment in the placeholder function udpcons_wait_key describes how you could implement it.

Collecting console data from the UDP console

After reading this description of how a console works, you may want to try out the sample UDP console. To this aim, you'll need a way to collect UDP data packets from the network. The udp-get program included in the UDP console tarball is designed to collect packets from an UDP port and dumping them to stdout. To collect all packets sent to port 2222 you just need to run "udp-get 2222".
Alessandro has two babies and tons of email; he writes Free Software and technical documentation in the small time frames that are left between email and babies.

Thanks to Andrea Glorioso <andrea.glorioso-at-binary-only-dot-com> and Davide Ciminaghi <ciminaghi-at-prosa-dot-it> for helping revising this article.