PRINTK_RINGBUFFER OVERVIEW(9)

πŸ“¦ version 7.3.1, in nixpkgs unstable

πŸ’– printk_ringbuffer overview(9) πŸ’–

Kernel API Manual · part 1 of 2 ✨

πŸŽ€ NAME πŸŽ€

printk_ringbuffer overview β€” bestie, this is the full vibe check on the printk ringbuffer: the data structure, the implementation, how writers and readers use it, ABA issues, and memory barriers. No cap, every detail is staying. πŸ’…

πŸŒ€ DATA STRUCTURE πŸŒ€

The printk_ringbuffer is made up of 2 internal ringbuffers:

  • desc_ring β€” a ring of descriptors and their meta data (such as sequence number, timestamp, loglevel, etc.) as well as internal state information about the record and logical positions specifying where in the other ringbuffer the text strings are located. πŸŽ€
  • text_data_ring β€” a ring of data blocks. A data block consists of an unsigned long integer (ID) that maps to a desc_ring index followed by the text string of the record. πŸ¦‹

The internal state information of a descriptor is the key element to allow readers and writers to locklessly synchronize access to the data. Periodt. ✨

✨ IMPLEMENTATION ✨

🌸 Descriptor Ring 🌸

The descriptor ring is an array of descriptors. A descriptor contains essential meta data to track the data of a printk record using blk_lpos structs pointing to associated text data blocks (see "Data Rings" below). Each descriptor is assigned an ID that maps directly to index values of the descriptor array and has a state. The ID and the state are bitwise combined into a single descriptor field named state_var, allowing ID and state to be synchronously and atomically updated. πŸ’«

Descriptors have four states:

  • reserved β€” A writer is modifying the record. ✍️
  • committed β€” The record and all its data are written. A writer can reopen the descriptor (transitioning it back to reserved), but in the committed state the data is consistent. βœ…
  • finalized β€” The record and all its data are complete and available for reading. A writer cannot reopen the descriptor. πŸ”’
  • reusable β€” The record exists, but its text and/or meta data may no longer be available. ♻️

Querying the state_var of a record requires providing the ID of the descriptor to query. This can yield a possible fifth (pseudo) state:

  • miss β€” The descriptor being queried has an unexpected ID. 🫠

The descriptor ring has a tail_id that contains the ID of the oldest descriptor and head_id that contains the ID of the newest descriptor. πŸ‘‘

When a new descriptor should be created (and the ring is full), the tail descriptor is invalidated by first transitioning to the reusable state and then invalidating all tail data blocks up to and including the data blocks associated with the tail descriptor (for the text ring). Then tail_id is advanced, followed by advancing head_id. And finally the state_var of the new descriptor is initialized to the new ID and reserved state. πŸŒ€

The tail_id can only be advanced if the new tail_id would be in the committed or reusable queried state. This makes it possible that a valid sequence number of the tail is always available. πŸ’–

πŸŽ€ Descriptor Finalization πŸŽ€

When a writer calls the commit function prb_commit, record data is fully stored and is consistent within the ringbuffer. However, a writer can reopen that record, claiming exclusive access (as with prb_reserve), and modify that record. When finished, the writer must again commit the record. 😀

In order for a record to be made available to readers (and also become recyclable for writers), it must be finalized. A finalized record cannot be reopened and can never become "unfinalized". Record finalization can occur in three different scenarios:

  1. A writer can simultaneously commit and finalize its record by calling prb_final_commit instead of prb_commit. ⚑
  2. When a new record is reserved and the previous record has been committed via prb_commit, that previous record is automatically finalized. πŸͺ„
  3. When a record is committed via prb_commit and a newer record already exists, the record being committed is automatically finalized. 🌟

πŸ¦‹ Data Ring πŸ¦‹

The text data ring is a byte array composed of data blocks. Data blocks are referenced by blk_lpos structs that point to the logical position of the beginning of a data block and the beginning of the next adjacent data block. Logical positions are mapped directly to index values of the byte array ringbuffer. 🧚

Each data block consists of an ID followed by the writer data. The ID is the identifier of a descriptor that is associated with the data block. A given data block is considered valid if all of the following conditions are met:

  1. The descriptor associated with the data block is in the committed or finalized queried state. βœ…
  2. The blk_lpos struct within the descriptor associated with the data block references back to the same data block. πŸ”
  3. The data block is within the head/tail logical position range. πŸ“

If the writer data of a data block would extend beyond the end of the byte array, only the ID of the data block is stored at the logical position and the full data block (ID and writer data) is stored at the beginning of the byte array. The referencing blk_lpos will point to the ID before the wrap and the next data block will be at the logical position adjacent the full data block after the wrap. 🌯

Data rings have a tail_lpos that points to the beginning of the oldest data block and a head_lpos that points to the logical position of the next (not yet existing) data block. πŸ›

When a new data block should be created (and the ring is full), tail data blocks will first be invalidated by putting their associated descriptors into the reusable state and then pushing the tail_lpos forward beyond them. Then the head_lpos is pushed forward and is associated with a new descriptor. If a data block is not valid, the tail_lpos cannot be advanced beyond it. 🚫

⭐ Info Array ⭐

The general meta data of printk records are stored in printk_info structs, stored in an array with the same number of elements as the descriptor ring. Each info corresponds to the descriptor of the same index in the descriptor ring. Info validity is confirmed by evaluating the corresponding descriptor before and after loading the info. πŸ”

πŸ’… USAGE πŸ’…

Here are some simple examples demonstrating writers and readers. For the examples a global ringbuffer (test_rb) is available (which is not the actual ringbuffer used by printk):

DEFINE_PRINTKRB(test_rb, 15, 5);

This ringbuffer allows up to 32768 records (2 ^ 15) and has a size of 1 MiB (2 ^ (15 + 5)) for text data. 🀯

Sample writer code:

const char *textstr = "message text";
struct prb_reserved_entry e;
struct printk_record r;

// specify how much to allocate
prb_rec_init_wr(r, strlen(textstr) + 1);

if (prb_reserve(e, test_rb, r)) {
        snprintf(r.text_buf, r.text_buf_size, "s", textstr);

        r.info->text_len = strlen(textstr);
        r.info->ts_nsec = local_clock;
        r.info->caller_id = printk_caller_id;

        // commit and finalize the record
        prb_final_commit(e);
}

Note that additional writer functions are available to extend a record after it has been committed but not yet finalized. This can be done as long as no new records have been reserved and the caller is the same. πŸ‘―β€β™€οΈ

Sample writer code (record extending):

        // alternate rest of previous example

        r.info->text_len = strlen(textstr);
        r.info->ts_nsec = local_clock;
        r.info->caller_id = printk_caller_id;

        // commit the record (but do not finalize yet)
        prb_commit(e);
}

...

// specify additional 5 bytes text space to extend
prb_rec_init_wr(r, 5);

// try to extend, but only if it does not exceed 32 bytes
if (prb_reserve_in_last(e, test_rb, r, printk_caller_id, 32)) {
        snprintf(r.text_buf[r.info->text_len],
                 r.text_buf_size - r.info->text_len, "hello");

        r.info->text_len += 5;

        // commit and finalize the record
        prb_final_commit(e);
}

Sample reader code:

struct printk_info info;
struct printk_record r;
char text_buf[32];
u64 seq;

prb_rec_init_rd(r, info, text_buf[0], sizeof(text_buf));

prb_for_each_record(0, test_rb, seq, r) {
        if (info.seq != seq)
                pr_warn("lost llu records0, info.seq - seq);

        if (info.text_len > r.text_buf_size) {
                pr_warn("record llu text truncated0, info.seq);
                text_buf[r.text_buf_size - 1] = 0;
        }

        pr_info("llu: llu: s0, info.seq, info.ts_nsec,
                text_buf[0]);
}

Note that additional less convenient reader functions are available to allow complex record access. 🧐

😭 ABA Issues 😭

To help avoid ABA issues, descriptors are referenced by IDs (array index values combined with tagged bits counting array wraps) and data blocks are referenced by logical positions (array index values combined with tagged bits counting array wraps). However, on 32-bit systems the number of tagged bits is relatively small such that an ABA incident is (at least theoretically) possible. For example, if 4 million maximally sized (1KiB) printk messages were to occur in NMI context on a 32-bit system, the interrupted context would not be able to recognize that the 32-bit integer completely wrapped and thus represents a different data block than the one the interrupted context expects. πŸ’€

To help combat this possibility, additional state checking is performed (such as using cmpxchg even though set would suffice). These extra checks are commented as such and will hopefully catch any ABA issue that a 32-bit system might experience. πŸ™

🧚 Memory Barriers 🧚

Multiple memory barriers are used. To simplify proving correctness and generating litmus tests, lines of code related to memory barriers (loads, stores, and the associated memory barriers) are labeled:

LMM(function:letter)

Comments reference the labels using only the "function:letter" part. 🏷️

The memory barrier pairs and their ordering are:

  • desc_reserve:D / desc_reserve:B β€” push descriptor tail (id), then push descriptor head (id) 🫢
  • desc_reserve:D / data_push_tail:B β€” push data tail (lpos), then set new descriptor reserved (state) πŸŽ€
  • desc_reserve:D / desc_push_tail:C β€” push descriptor tail (id), then set new descriptor reserved (state) 🌸
  • desc_reserve:D / prb_first_seq:C β€” push descriptor tail (id), then set new descriptor reserved (state) πŸ¦‹
  • desc_reserve:F / desc_read:D β€” set new descriptor id and reserved (state), then allow writer changes ✨
  • data_alloc:A (or data_realloc:A) / desc_read:D β€” set old descriptor reusable (state), then modify new data block area πŸ’«
  • data_alloc:A (or data_realloc:A) / data_push_tail:B β€” push data tail (lpos), then modify new data block area πŸ”₯
  • _prb_commit:B / desc_read:B β€” store the writer changes first, then set the new descriptor committed (state) ✨
  • desc_reopen_last:A / _prb_commit:B β€” set the descriptor reserved (state) first, then read the descriptor data πŸŽ€
  • _prb_commit:B / desc_reserve:D β€” set the new descriptor committed (state) first, then check the descriptor head (id) πŸ’…
  • data_push_tail:D / data_push_tail:A β€” set the descriptor reusable (state) first, then push the data tail (lpos) πŸ¦‹
  • desc_push_tail:B / desc_reserve:D β€” set the descriptor reusable (state) first, then push the descriptor tail (id) 🌸
  • desc_update_last_finalized:A / desc_last_finalized_seq:A β€” store the finalized record first, then set the new highest finalized sequence number ⭐
lowkey the whole point of this list is the order, bestie. Store this, then set that. Flip them around and the vibe check fails catastrophically 😭 so please don't.

πŸ‘€ SEE ALSO

Kernel file ./kernel/printk/printk_ringbuffer.c, plus all these iconic little functions that live there β€” go read them, they're literally right there 🫢


okay that's the whole page, you made it πŸŽ‰ ngl this one's a lot but you ate it and left no crumbs. source is the kernel dir <code>./kernel/printk</code>, the version stamp says October 2026, and this absolute icon is filed under <code>printk_ringbuffer overview(9)</code>. go forth and ringbuffer responsibly, bestie πŸ’–

πŸ“¦ where this page comes from