← Previous Lecture | Next Lecture →
What You Will Learn
- How to create a directory and a file under /proc from a kernel module
- Why the old file_operations-based proc code you’ll find online is outdated
- How to use the modern proc_ops structure introduced in kernel 5.6
- How to safely copy data between kernel space and user space
- How to build, load, test, and clean up a proc-based module end to end
Prerequisites
- Completed the previous lecture on the /proc filesystem, or already know what /proc is for
- Basic C programming
- Can build and load a simple “Hello World” kernel module (insmod/rmmod)
- A Linux VM running kernel 5.6 or newer, with kernel headers installed
This lecture is a practical follow-up to our proc filesystem overview. Here we actually build something: a small kernel module that creates its own entry under /proc, so a user-space program can read from it and write to it using plain cat and echo. Along the way, this kernel module proc file tutorial flags one thing almost every older guide gets wrong for anyone running a current kernel — the API you need to use changed in 5.6, and a lot of tutorials online still teach the pre-5.6 version.
A Quick Word on Why We’re Still Learning This
As covered in the previous lecture, procfs isn’t where the community wants new driver interfaces built anymore — sysfs is. So why learn to create a /proc entry at all? Two honest reasons: first, plenty of real, still-maintained code in the kernel tree does register its own /proc files, so you’ll run into this pattern reading kernel source. Second, it’s genuinely the simplest possible way to learn the read/write callback model before you tackle sysfs’s more structured attribute system in the next lecture. Treat this as a stepping stone, not a production pattern.
The API Changed in Kernel 5.6 — Here’s What You Need to Know
If you search for “proc_create example” right now, most results you’ll find were written for kernels older than 5.6, and they’ll show you a struct file_operations being passed as the fourth argument to proc_create(). On any kernel from 5.6 onward, that code won’t compile. The kernel maintainers split proc’s callback table away from the general VFS file_operations struct into a dedicated, smaller struct built specifically for procfs: struct proc_ops.
| Before kernel 5.6 | Kernel 5.6 and later |
|---|---|
| struct file_operations | struct proc_ops |
| .read | .proc_read |
| .write | .proc_write |
| .open | .proc_open |
| .owner field required | .owner field removed (handled automatically) |
The good news: the rest of the workflow — proc_mkdir to create a folder, proc_create to register a file, remove_proc_entry or proc_remove to clean up — hasn’t changed shape at all. Only the callback table’s type and field names did.
Step 1: Create a Directory Under /proc
If you want your module’s files grouped under their own folder instead of scattered directly in /proc, start with proc_mkdir(). It takes the folder name and a parent entry — pass NULL for the parent to create it directly under /proc itself. Hang on to the pointer it returns; every file you create afterward needs it as their parent.
Step 2: Register a File with proc_create() and proc_ops
Next, register the actual file using proc_create(), pointing its fourth argument at a struct proc_ops you’ve filled in with your read and write callbacks.
Step 3: Copy Data Safely Between Kernel and User Space
Your read and write callbacks never touch a user-space pointer directly — the kernel and a user process live in separate address spaces, and dereferencing a user pointer from kernel code is both unsafe and, on most configurations, will simply crash or fail. Instead you always go through copy_to_user() when sending data out, and copy_from_user() when receiving it.
| User runs echo “5 1” > /proc/epmod/config |
→ | VFS calls .proc_write callback |
→ | Module runs copy_from_user() |
→ | Kernel state updated in module memory |
Complete, Working Example
Here’s a full module that creates /proc/epmod/config. Reading it prints two integers the module stores internally; writing two integers to it updates them. This is written from scratch for this lecture and targets kernel 5.6 and above.
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#include <linux/string.h>
#define EP_BUF_SIZE 64
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedPathashala");
MODULE_DESCRIPTION("Demo module: create a /proc file using proc_ops");
static struct proc_dir_entry *ep_dir;
static struct proc_dir_entry *ep_file;
static int level = 1;
static int mode = 0;
static ssize_t ep_proc_read(struct file *fp, char __user *ubuf,
size_t count, loff_t *ppos)
{
char kbuf[EP_BUF_SIZE];
int len;
if (*ppos > 0)
return 0; /* signal EOF after first read */
len = scnprintf(kbuf, EP_BUF_SIZE, "level=%d mode=%d\n", level, mode);
if (copy_to_user(ubuf, kbuf, len))
return -EFAULT;
*ppos = len;
return len;
}
static ssize_t ep_proc_write(struct file *fp, const char __user *ubuf,
size_t count, loff_t *ppos)
{
char kbuf[EP_BUF_SIZE];
int new_level, new_mode;
if (count >= EP_BUF_SIZE)
return -EINVAL;
if (copy_from_user(kbuf, ubuf, count))
return -EFAULT;
kbuf[count] = '\0';
if (sscanf(kbuf, "%d %d", &new_level, &new_mode) != 2)
return -EINVAL;
level = new_level;
mode = new_mode;
return count;
}
static const struct proc_ops ep_proc_fops = {
.proc_read = ep_proc_read,
.proc_write = ep_proc_write,
};
static int __init ep_proc_init(void)
{
ep_dir = proc_mkdir("epmod", NULL);
if (!ep_dir)
return -ENOMEM;
ep_file = proc_create("config", 0664, ep_dir, &ep_proc_fops);
if (!ep_file) {
remove_proc_entry("epmod", NULL);
return -ENOMEM;
}
pr_info("epmod: /proc/epmod/config is ready\n");
return 0;
}
static void __exit ep_proc_exit(void)
{
remove_proc_entry("config", ep_dir);
remove_proc_entry("epmod", NULL);
pr_info("epmod: cleaned up /proc/epmod\n");
}
module_init(ep_proc_init);
module_exit(ep_proc_exit);
Building and Testing the Module
Use a minimal Makefile alongside the source file:
obj-m += epmod.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
Then build, load, and test it:
$ make
$ sudo insmod epmod.ko
$ cat /proc/epmod/config
level=1 mode=0
$ sudo sh -c 'echo "7 2" > /proc/epmod/config'
$ cat /proc/epmod/config
level=7 mode=2
$ sudo rmmod epmod
$ dmesg | tail
If cat /proc/epmod/config prints your updated values after the write, the whole read/write/copy_to_user/copy_from_user loop is working correctly.
Common Mistakes When Writing proc Callbacks
- Forgetting the *ppos EOF check on read. Without it, tools like
catwill call your read function in an infinite loop, since they keep asking for more data until they get a zero-length read. - Using file_operations on a 5.6+ kernel. You’ll get a compiler error about incompatible pointer types — that’s your cue to switch to proc_ops.
- Not bounds-checking count in the write callback. Always confirm the incoming size fits your kernel buffer before you copy into it.
- Dereferencing the user buffer pointer directly. Always go through copy_to_user/copy_from_user, never touch a __user pointer as if it were a normal kernel pointer.
- Removing the parent directory before the file inside it. Clean up child entries first, then the directory, mirroring the order you created them in.
Security Considerations
Anything you expose under /proc is potentially readable or writable by every user on the box, subject to the file mode you set. Think carefully about the permission bits you pass to proc_create — 0664 in the example above allows group writes, which may be more permissive than you actually want on a shared system. Also validate every byte coming from copy_from_user; that data originates from user space and should never be trusted blindly, even in a learning exercise like this one.
Best Practices
- Always check kernel version compatibility (proc_ops vs file_operations) if you’re supporting multiple kernel releases, using
LINUX_VERSION_CODEguards. - Keep proc callbacks small and predictable; push real logic into separate helper functions.
- Free every proc entry you create, in reverse order of creation, inside your module’s exit function.
- Treat this pattern as a learning tool — for anything you intend to maintain long term, build the interface with sysfs instead.
Frequently Asked Questions
1. Why did my proc_create() code fail to compile?
You’re almost certainly on kernel 5.6 or newer and still passing a struct file_operations pointer. Switch to struct proc_ops.
2. Do I need .owner in proc_ops?
No. Unlike file_operations, proc_ops has no .owner field — the kernel manages module reference counting for you.
3. What does the mode argument to proc_create() actually do?
It sets the file permission bits shown in a directory listing, same as chmod values — for example 0444 for read-only, 0664 for read/write with group access.
4. Can a proc file support poll or ioctl too?
Yes, proc_ops has slots for those as well (.proc_poll, .proc_ioctl), following the same naming pattern as .proc_read and .proc_write.
5. Why is my read callback being called more than once?
That’s almost always the missing *ppos EOF check — return 0 once you’ve already sent your data.
6. Is it safe to store state in module-level global variables like this example does?
For a single learning module, yes. In real drivers with concurrent access, you’d protect shared state with a mutex or spinlock.
7. Should I use this pattern for a real driver I plan to upstream?
No — use sysfs. This tutorial exists to teach the callback model, not to recommend procfs for new driver interfaces.
Key Takeaways
- On kernel 5.6 and later, proc_create() takes a struct proc_ops, not a struct file_operations.
- proc_mkdir(), proc_create(), and remove_proc_entry() together form the full lifecycle of a custom /proc entry.
- Every byte moving between kernel and user space must go through copy_to_user() or copy_from_user().
- Always signal EOF in your read callback to avoid infinite reads from tools like cat.
- This pattern is a great learning exercise, but sysfs is the community-recommended choice for real driver interfaces.
Conclusion
You’ve now built a working /proc-based interface from scratch, using the current proc_ops API that actually compiles on modern kernels — not the outdated file_operations pattern still floating around in older tutorials. More importantly, you’ve seen the full lifecycle: create a directory, register a file, move data safely across the user/kernel boundary, and clean everything up on exit. That mental model — register, read/write via safe copy functions, clean up — carries forward almost unchanged into sysfs, which is exactly where we’re headed next in this free Linux device drivers course.
Keep Building Your Kernel Programming Skills
Next up: exposing the same kind of interface the modern way, using sysfs. More free lectures at EmbeddedPathashala.
Browse the Full Course Next Lecture →
2 Comments