In Part 1 of this lecture we created the /proc/procdemo directory and registered a procdemo_loglevel pseudo-file using the modern struct proc_ops API. That file does nothing useful yet, because we never wrote the callbacks. This lecture in our free Linux kernel development course completes the picture: we implement the read and write handlers for a working procfs read write callback in a kernel module, then test the whole thing live on a running system.
What You Will Learn
- How the offset argument in a procfs read callback prevents an infinite
catloop - How to safely copy data between kernel space and user space with
copy_to_user()/copy_from_user() - How to parse and validate a value written from the shell
- How to reject an out-of-range write and report the error back to user space
- How to test a procfs interface live with
cat,echo, anddmesg
Prerequisites
This lecture continues directly from Part 1 of this series, where we built the procdemo_loglevel proc entry with proc_mkdir() and proc_create(). If you have not gone through that lecture yet, do that first — everything below builds on the loglevel_fops structure defined there.
Understanding the Read Callback Signature
Every time a process calls read() on our procfs file, the kernel invokes our proc_read callback with a user-space buffer, the number of bytes requested, and a pointer to an offset. The offset is the part beginners most often get wrong: cat keeps calling read() until it receives zero bytes back, so if you do not advance and check the offset yourself, cat will loop forever reading the same data over and over.
| 1 | Shell process calls open() then read() on the procfs file |
| 2 | Kernel invokes loglevel_read() with offset = 0 |
| 3 | Callback formats the current log level into a small kernel buffer and copies it to user space, then advances the offset |
| 4 | Kernel calls loglevel_read() again; since offset is now non-zero, the callback returns 0 (EOF) and cat stops |
static ssize_t loglevel_read(struct file *filp, char __user *ubuf,
size_t count, loff_t *offp)
{
char kbuf[16];
int len;
if (*offp > 0) /* we already sent our one line of data */
return 0;
len = scnprintf(kbuf, sizeof(kbuf), "%d\n", log_level);
if (copy_to_user(ubuf, kbuf, len))
return -EFAULT;
*offp += len;
return len;
}
The if (*offp > 0) return 0; line is the whole trick. It tells the kernel “there is nothing more to give you” on the second call, which is exactly what cat is waiting to hear before it stops reading.
Implementing the Write Callback With Validation
The write callback receives whatever bytes user space sent — in our case, whatever echo wrote. We copy those bytes into a kernel buffer, convert the text to an integer, and reject the write outright if the value is outside our allowed range of 0 to 3:
#define LOGLEVEL_MAX 3
static ssize_t loglevel_write(struct file *filp, const char __user *ubuf,
size_t count, loff_t *offp)
{
char kbuf[16];
int new_level;
int ret;
if (count >= sizeof(kbuf))
return -EINVAL;
if (copy_from_user(kbuf, ubuf, count))
return -EFAULT;
kbuf[count] = '\0';
ret = kstrtoint(kbuf, 10, &new_level);
if (ret)
return ret; /* not a valid integer */
if (new_level LOGLEVEL_MAX) {
pr_warn("procdemo: rejected log level %d (allowed 0-%d)\n",
new_level, LOGLEVEL_MAX);
return -EINVAL;
}
log_level = new_level;
pr_info("procdemo: log level changed to %d\n", log_level);
return count;
}
kstrtoint() is the modern, safe way to convert a user-supplied string to an integer inside the kernel — always prefer it over hand-rolled parsing with simple_strtol(), which is deprecated.
Testing the procfs Interface Live
After building and inserting the module, read the current value:
$ cat /proc/procdemo/procdemo_loglevel
0
Now raise it to a valid level:
$ sudo sh -c "echo 2 > /proc/procdemo/procdemo_loglevel"
$ cat /proc/procdemo/procdemo_loglevel
2
And confirm that an out-of-range value is rejected cleanly instead of crashing anything:
$ sudo sh -c "echo 9 > /proc/procdemo/procdemo_loglevel"
sh: echo: write error: Invalid argument
$ dmesg | tail -1
procdemo: rejected log level 9 (allowed 0-3)
Common Mistakes and Troubleshooting
| Symptom | Likely Cause |
|---|---|
cat hangs or repeats output forever |
Read callback never checks or advances the offset argument |
| Kernel oops on write | Copying more bytes than the kernel buffer can hold; always bounds-check count first |
echo silently does nothing |
Write callback returns 0 instead of the number of bytes consumed |
| Permission denied on write | File permission bits do not include write access, or the command was not run as root |
Performance Considerations
procfs callbacks run in process context and are not on any hot data path in a well-designed driver, so raw speed is rarely a concern. The one thing to watch is holding locks for longer than necessary inside a read or write callback — keep the critical section that touches shared driver state as short as possible, and do any formatting or parsing outside the lock wherever you can.
Real-World Use Cases
This exact pattern — a small, permission-guarded procfs file that toggles a debug or verbosity level — shows up throughout the mainline kernel and in countless out-of-tree drivers, because it lets a field engineer or support technician adjust logging on a running system without recompiling or reloading anything.
Key Takeaways
- Always check and advance the offset in a procfs read callback, or
catwill loop forever. - Use
copy_to_user()andcopy_from_user()for every crossing of the kernel/user-space boundary. - Use
kstrtoint()to parse integers from user-supplied text safely. - Reject invalid writes with a proper negative error code instead of silently ignoring bad input.
Conclusion
You now have a complete, working procfs interface: a directory, a permission-guarded file, and a pair of callbacks that correctly handle the read offset and validate every write. This same pattern scales directly to real driver work — swap the single integer for whatever state your own driver needs to expose, and you have a debugging interface that any engineer can drive from a plain shell.
Frequently Asked Questions
Q1. Why does struct proc_ops exist instead of just reusing file_operations?
proc_ops is a smaller, procfs-specific structure introduced in kernel 5.6 that removes fields procfs never needed and lets the kernel manage module reference counting automatically, closing off a class of bugs.
Q2. What happens if I forget the offset check in my read callback?
Commands like cat will call read() in a loop forever because they never see a zero-byte return signalling end of data.
Q3. Can a procfs file have only a read callback and no write callback?
Yes. Simply leave .proc_write unset in the proc_ops structure, and set the file’s permission bits to remove write access, as we did with procdemo_devstate in Part 1.
Q4. Is procfs still relevant, or should new drivers use sysfs or debugfs instead?
sysfs is preferred for one-value-per-file device attributes and debugfs for pure development debugging, but procfs remains widely used for module-level, human-readable control interfaces like the one built here.
Q5. Why use kstrtoint() instead of atoi() or simple_strtol()?
Kernel code has no C library, atoi() does not exist, and simple_strtol() is deprecated because it does not properly validate its input; kstrtoint() is the current, safe replacement.
Q6. What error should a write callback return for invalid input?
A negative errno value such as -EINVAL for a malformed or out-of-range value, or -EFAULT if copying from user space itself fails.
More lectures on kernel module interfacing, IPC, and device drivers are coming up next in this series.

2 Comments