Proc Interface#
/proc is the classic way a driver hands kernel-side information to userspace without going through a real block device. There are three ways to implement the read side of a proc entry, depends on whether you’re exposing a single fixed value or walking a list.
Keep a note that this method is depreciated for device driver.
1. Raw file_operations - the old way#
This is the original method: you wire up .read, .write, .open, .llseek, and .release yourself, exactly like a normal character device.
| |
Random access via llseek is supported, but this approach is also likely to more error-prone, which is why raw file_operations is rarely used for /proc today.
In older tutorial: this struct used to be
struct file_operations, the same one character devices use. Since Linux 5.6,/procentries register against a dedicatedstruct proc_opsinstead smaller surface, and it no longer inherits fields (likemmap-related security checks) that don’t apply to proc files.
Function trace for this method:
Start (open) -> llseek -> read -> llseek -> read -> llseek -> read -> End (close)2. Using seq_file + single_open#
A method to handle this by simply connecting the sequence file ( seq_file ) interface to file_operations and using single_open() which does not use seq_operations.
| |
single_open() allocates a seq_file, calls your show() once, and buffers the whole result, that’s what lets seq_read/seq_lseek handle chunked reads and real llseek correctly without you writing any offset-tracking code.
Function trace:
Start (open) -> Show -> End (close)[ 62.587970] :proc_open: invoked
[ 62.587996] :proc_show: invoked
[ 62.588047] :proc_release: invoked3. Using seq_file + seq_operations - iterating output#
| |
Note that when you’re iterating over real kernel state rather than a static array: use locking mechanism in
start()/stop()to protext the data. see example
Also the
void *fromstart()/next()doesn’t have to be *pos.seq_filedoesn’t care what it is, as long asshow()knows how to use it. In this example it’s just a pointer intodata[]. If you were walking a real linked list instead, it’d usually be astruct list_head *pointing at the current node.
Function trace:
Start (open) -> next -> show -> next -> show -> next -> show -> End (close)[ 899.261954] proc_fs_iterator is loaded
[ 904.533344] :proc_open: invoked
[ 904.533358] :proc_seq_start: invoked, pos=0
[ 904.533359] :proc_seq_show: invoked
[ 904.533362] :proc_seq_next: invoked, pos=0
[ 904.533363] :proc_seq_show: invoked
[...]
[ 904.533369] :proc_seq_next: invoked, pos=6
[ 904.533369] :proc_seq_next: position requested exceeds the maximum length
[ 904.533370] :proc_seq_stop: invoked
[ 904.538378] :proc_seq_start: invoked, pos=7
[ 904.538388] :proc_seq_start: position requested exceeds the maximum length
[ 904.538388] :proc_seq_stop: invokedWhy does
proc_seq_startget invoked twice at the end?read()on a/procfile isn’t a single call.catkeeps callingread()until it gets 0 bytes back, since that’s the EOF signal. So after the iterator exhaustsdata[]andstop()runs, userspace issues one moreread(), which reopens the sequence at the last*pos(here, 7) just to confirm there’s really nothing left.start()returnsNULL,seq_read()returns 0, and only then doescatstop calling. Putting the three side by side:
file_operations | single_show | seq_operations | |
|---|---|---|---|
Random access (llseek) | Yes | No (single blob) | No, sequential only |
| State management | Manual (own position tracking) | None needed | start/next/stop cursor |
| Use case | Legacy, arbitrary seek/read | Fixed, one-shot output | Iterating kernel object lists |
| Error surface | High, positions/offsets by hand | Low | Low, but iterator logic must be correct |
Rule of thumb: if the output is a single formatted string, single_show is the least code for the same result. Use seq_operations only when you’re iterating over data structure that userspace expects to read top to bottom.