Trace the Linux kernel boot logging lifecycle from start_kernel() through memory initialization, comparing statically allocated log buffers, early console drivers, memblock initialization, and the exact buffer migration sequence used to preserve kernel messages
1. Compile-time baseline: the static log buffer
Before any memory allocator exists, printk() must already work. The kernel therefore embeds a static ring buffer:
__LOG_BUF_LEN = 1 << CONFIG_LOG_BUF_SHIFTstatic char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN)- static ring-buffer metadata
printk_rb_staticdescribes that array.
So the first messages after start_kernel() — the Linux banner, command-line notices, early architecture messages — are stored in __log_buf without touching memblock or the page allocator.
This static buffer is deliberately small. On large systems, the numerous ACPI/EFI/memblock messages can overflow it before a larger buffer is available, which is why the kernel needs an early migration path.
2. start_kernel() before and around setup_arch()
The high-level boot order is:
start_kernel()
pr_notice("%s", linux_banner) // writes into static __log_buf
...
setup_arch(&command_line)
early param parsing, including log_buf_len=
memblock initialization
setup_log_buf(1) // x86/several arches
...
parse_early_param()
...
setup_log_buf(0) // generic fallback
vfs_caches_init_early()
sort_main_extable()
...
console_init()
Early boot messages produced before console_init() are not lost: they are stored in the static printk ring buffer. A boot console may print them immediately, but the canonical log record remains in the ring buffer.
3. setup_arch(), memblock, and the log_buf_len= parameter
setup_arch() is where architecture code:
- Discovers the physical memory map.
- Initializes/reserves memblock regions.
- Sets memblock allocation limits.
- Parses early parameters.
- Calls the early log-buffer migration.
The log_buf_len= early parameter does not allocate immediately. Its handler merely records the requested size:
static int __init log_buf_len_setup(char *str)
{
unsigned size = memparse(str, &str);
log_buf_len_update(size);
return 0;
}
early_param("log_buf_len", log_buf_len_setup);
The actual allocation is deferred until memblock is usable.
On x86, the early call is made from setup_arch():
/* Allocate bigger log buffer */
setup_log_buf(1);
This happens after memblock has been initialized and after the memblock limit has been configured, so memblock_alloc() is usable.
4. Exact early migration: setup_log_buf(1)
Modern kernels do not simply memcpy() the static buffer. They migrate printk records into a new dynamically allocated printk ring buffer.
The core sequence in kernel/printk/printk.c is:
void __init setup_log_buf(int early)
{
...
if (!early)
set_percpu_data_ready();
if (log_buf != __log_buf)
return; // already migrated
if (!early && !new_log_buf_len)
log_buf_add_cpu();
if (!new_log_buf_len)
return; // no larger buffer requested
new_descs_count = new_log_buf_len >> PRB_AVGBITS;
new_log_buf = memblock_alloc(new_log_buf_len, LOG_ALIGN);
new_descs = memblock_alloc(new_descs_size, LOG_ALIGN);
new_infos = memblock_alloc(new_infos_size, LOG_ALIGN);
prb_init(&printk_rb_dynamic,
new_log_buf, ilog2(new_log_buf_len),
new_descs, ilog2(new_descs_count),
new_infos);
local_irq_save(flags);
log_buf_len = new_log_buf_len;
log_buf = new_log_buf;
new_log_buf_len = 0;
free = __LOG_BUF_LEN;
prb_for_each_record(0, &printk_rb_static, seq, &r)
free -= add_to_rb(&printk_rb_dynamic, &r);
prb = &printk_rb_dynamic;
local_irq_restore(flags);
/*
* Copy any remaining messages that might have appeared from
* NMI context after copying but before switching to the
* dynamic buffer.
*/
prb_for_each_record(seq, &printk_rb_static, seq, &r)
add_to_rb(&printk_rb_dynamic, &r);
if (seq != prb_next_seq(&printk_rb_static))
pr_err("dropped %llu messages\n", ...);
pr_info("log_buf_len: %u bytes\n", log_buf_len);
pr_info("early log buf free: %u(%u%%)\n", ...);
}
This exact structure is visible in the v5.14 source and in newer printk code with the NMI second-pass copy.
Key properties of the migration
| Step | Purpose |
|---|---|
log_buf != __log_buf check |
Prevents double migration when setup_log_buf() is called more than once. |
memblock_alloc() for text, descriptors, and info arrays |
The new printk ring buffer needs three allocations: text storage, descriptor array, and printk_info array. |
prb_init(&printk_rb_dynamic, ...) |
Initializes the dynamic ring buffer before any records are copied. |
| IRQ save / printk-safe entry | Ensures the boot CPU is not interrupted while the global buffer pointer is being switched. |
prb_for_each_record(0, &printk_rb_static, ...) |
Iterates the old static ring buffer from sequence 0, preserving message order, timestamps, and metadata. |
add_to_rb(&printk_rb_dynamic, &r) |
Copies each record into the new ring buffer; this is a record-level migration, not a raw byte copy. |
prb = &printk_rb_dynamic |
Atomically switches the global printk ring-buffer pointer. |
Second prb_for_each_record(seq, ...) |
Picks up messages that arrived from NMI context during the first copy. |
dropped %llu messages check |
Detects a race where the static buffer wrapped before migration completed. |
Historical kernels used a much simpler approach:
new_log_buf = memblock_virt_alloc(new_log_buf_len, PAGE_SIZE);
...
memcpy(log_buf, __log_buf, __LOG_BUF_LEN);
That raw memcpy() preserved bytes but not the modern printk ringbuffer metadata. Modern kernels must use record migration because the ring buffer now separates text, descriptors, and printk_info metadata.
5. Generic fallback: setup_log_buf(0)
Not every architecture performs the early setup_log_buf(1) call. Later, start_kernel() calls:
setup_log_buf(0);
vfs_caches_init_early();
sort_main_extable();
This is the generic fallback before kmem_cache_init() and before the buddy allocator is fully usable, so it also uses memblock/bootmem-style allocation rather than kmalloc().
If the architecture already migrated the buffer early, setup_log_buf(0) returns immediately because log_buf != __log_buf. If not, it performs the same migration at this later point.
6. Early console drivers are separate from the log buffer
An early console / boot console is not a log buffer. It is a console driver that writes directly to hardware, typically a UART, very early in boot:
earlycon=or architectureearlyprintkregisters astruct consolewithCON_BOOT.CON_PRINTBUFFERmeans that when the boot console is registered,register_console()dumps the existing contents of the kernel log buffer to it.- Messages are still stored in
__log_buf; the early console is only a sink. - Later,
console_init()registers the real console drivers. The real console takes over, and the boot console is unregistered.
The kernel documentation describes the two console classes directly: “There are two types of consoles — bootconsoles (early_printk) and [real consoles].”
A typical early console definition looks like:
static struct console early_console_prom = {
.name = "early",
.write = early_console_write,
.flags = CON_PRINTBUFFER | CON_BOOT,
};
So the early console can print boot messages before console_init(), but the authoritative log lifecycle is still:
printk record → static __log_buf → (optional early console output)
→ dynamic ring buffer migration
→ console_init() registers real consoles
→ boot console handover/unregister
7. Comparison table
| Component | When available | Allocation | Role |
|---|---|---|---|
__log_buf / printk_rb_static |
Compile time, before start_kernel() |
Static .bss/data array sized by CONFIG_LOG_BUF_SHIFT |
Holds every early printk() record before memblock and before any console driver. |
| Early console / bootconsole | Very early, after early parameter parsing | No buffer allocation; direct hardware write | Immediately prints boot messages and dumps the existing static log buffer when registered. |
printk_rb_dynamic |
After memblock initialization, via setup_log_buf(1) or setup_log_buf(0) |
memblock_alloc() for text buffer, descriptors, and printk_info array |
Long-term kernel log ring buffer; receives all records migrated from the static buffer. |
| Real console | After console_init() |
Console-driver specific | Prints current and backlogged log records; takes over from the boot console. |
8. Condensed lifecycle
Bootloader / decompressor
│
▼
start_kernel()
├─ pr_notice(linux_banner) ──────────────► static __log_buf
├─ setup_arch()
│ ├─ memblock init / memory map
│ ├─ parse early params (log_buf_len=)
│ ├─ early console may register ──────► direct UART output,
│ │ dumps __log_buf backlog
│ └─ setup_log_buf(1)
│ ├─ memblock_alloc(new text/descs/infos)
│ ├─ prb_init(dynamic)
│ ├─ IRQ-safe record copy static → dynamic
│ ├─ switch global prb to dynamic
│ └─ NMI second-pass copy + dropped-message check
├─ parse_early_param()
├─ setup_log_buf(0) ── fallback / no-op if already migrated
├─ ...
└─ console_init()
├─ register real consoles
├─ print remaining log buffer
└─ unregister boot consoles
The critical design point is that the kernel never loses early messages during the transition from static to dynamic storage: it migrates records, not raw bytes, under IRQ-safe conditions, and performs a second pass for NMI-context records that could race the switch.