Every time an application opens or closes your device file, the kernel calls your driver’s open() and release() methods. In this lecture of our free Linux device drivers course you will learn exactly what each method is responsible for, how to fetch your device’s private data using container_of(), and how to build a character driver open release method pair that allocates resources cleanly on open and frees every single one of them on release, on a modern Linux kernel 6.x system.
free linux kernel development course
free embedded systems course
character driver open release method
linux character device driver
What You Will Learn
Using container_of() to recover your device’s private struct from struct inode
Allocating and freeing per-open resources safely
An original character driver open release method example on kernel 6.x
Common mistakes, best practices, and command-line testing
Prerequisites
copy_to_user() / copy_from_user() basics (previous lecture)
A working Linux kernel 6.x build environment
What the open() Method Does in a Character Driver
The open() method runs every single time a process opens your device file. If you do not define it, the kernel simply lets the open succeed with no extra work, but most real drivers use this callback to initialize per-device or per-open state, validate the requested minor number, and allocate any buffers the device will need. Its prototype has not changed in modern kernels:
Returning 0 tells the kernel the open succeeded. Returning a negative error code, such as -ENODEV or -ENOMEM, aborts the open and that error code is what the calling application sees from its own open() syscall.
Recovering Private Data With container_of()
Your open() callback receives a struct inode, not your device struct directly. The kernel’s lower-level inode carries an i_cdev field pointing at the struct cdev you embedded inside your own device structure, so the container_of() macro walks backward from that pointer to recover your full struct. This is the standard character driver open release method pattern used across the kernel:
i_cdev pointer
offset calculation
your full device struct
Original Example: An ep_chardev Open Release Method Pair
Here is an original character driver, written for kernel 6.x, that embeds a cdev, allocates a per-device buffer on first open, tracks open count with a mutex instead of a global lock, and stores the recovered pointer in filp->private_data for later use by read() and write():
struct cdev cdev;
struct mutex lock;
unsigned char *buf;
size_t buf_size;
unsigned int open_count;
};static struct ep_chardev *ep_dev;static int ep_chardev_open(struct inode *inode, struct file *filp)
{
struct ep_chardev *dev;dev = container_of(inode->i_cdev, struct ep_chardev, cdev);mutex_lock(&dev->lock);
if (!dev->buf) {
dev->buf = kzalloc(dev->buf_size, GFP_KERNEL);
if (!dev->buf) {
mutex_unlock(&dev->lock);
pr_err(“ep_chardev: buffer allocation failed\n”);
return -ENOMEM;
}
}
dev->open_count++;
mutex_unlock(&dev->lock);
filp->private_data = dev;
pr_info(“ep_chardev: opened, open_count=%u\n”, dev->open_count);
return 0;
}
What the release() Method Does
release() is called when the last reference to an open file descriptor is closed, and it is where you undo whatever open() set up. A correct character driver open release method implementation frees only what it allocated, and only once the last user has actually closed the device, not on every close() syscall if the file was duplicated:
{
struct ep_chardev *dev = filp->private_data;mutex_lock(&dev->lock);
dev->open_count–;if (dev->open_count == 0) {
kfree(dev->buf);
dev->buf = NULL;
pr_info(“ep_chardev: last close, buffer released\n”);
}
mutex_unlock(&dev->lock);filp->private_data = NULL;
return 0;
}
Notice the pattern is a mirror image of open(): a mutex protects the shared open_count and buffer pointer instead of a single global lock, and the buffer is only freed once open_count reaches zero. This modernizes the old single global lock approach into a per-device mutex, which scales far better when a system has multiple instances of the same driver.
open() vs release() Responsibilities
| Method | Called When | Typical Responsibility |
|---|---|---|
| open() | Application calls open() on the device file | Validate device, allocate buffers, initialize private_data |
| release() | Last reference to the file descriptor is closed | Free buffers, reset state, clear private_data |
Common Mistakes in Character Driver Open Release Methods
| Mistake | Consequence |
|---|---|
| Freeing the buffer on every release() call regardless of open_count | Use-after-free if another process still has the device open |
| Allocating memory in open() without checking kzalloc’s return value | NULL pointer dereference the first time the buffer is used |
| Forgetting to clear filp->private_data in release() | Stale pointer reused if the same file struct is reopened |
| Using a single global lock instead of a per-device mutex | Unnecessary contention when multiple device instances exist |
Best Practices
Protect open_count and shared buffers with a per-device mutex
Return meaningful negative error codes from open() on failure
Clear filp->private_data in release() to avoid stale references
Security and Performance Considerations
Because open() often runs with elevated trust relative to a random syscall, validate the requesting minor number defensively rather than assuming it is always correct, and avoid doing expensive or blocking work inside open() while holding a lock, since that directly delays every other process trying to open the same device. On the performance side, allocating the buffer lazily on first open, as shown above, avoids wasting memory for devices that are registered but never actually used.
Testing From the Command Line
$ sudo insmod ep_chardev.ko
$ sudo mknod /dev/ep_chardev c 240 0
$ sudo cat /dev/ep_chardev > /dev/null
$ dmesg | tail -n 4
[ 2001.111222] ep_chardev: opened, open_count=1
[ 2001.111400] ep_chardev: last close, buffer released
$ sudo rmmod ep_chardev
Summary and Key Takeaways
container_of() recovers your device struct from the struct inode passed to open()
A per-device mutex protecting open_count is safer and scales better than a single global lock
Always free in release() exactly what you allocated in open(), and only once
Conclusion
The open() and release() methods form the lifecycle backbone of every character device driver, and getting the character driver open release method pattern right, with container_of(), a per-device mutex, and matched allocation and cleanup, will save you from some of the most common kernel driver bugs. Combined with copy_to_user()/copy_from_user() from the previous lecture, you now have everything needed to build a fully working character device driver on Linux kernel 6.x as part of this free Linux kernel development course.
Frequently Asked Questions
What happens if I don’t define an open() method?
The kernel lets the open() syscall succeed automatically with no extra driver-side work, which is fine only if your device needs no initialization at all.
Why use container_of() instead of a global device pointer?
container_of() lets a single driver support multiple device instances correctly, since each open() call recovers the exact device struct tied to the inode that was opened, rather than assuming there is only one device.
Is release() called every time close() is called?
No. release() is only called when the last reference to the open file description is dropped, which matters if the file descriptor was duplicated with dup() or inherited across a fork().
Why use a mutex instead of a global lock in open()/release()?
A per-device mutex only blocks operations on that specific device, while a single global lock would unnecessarily serialize opens and releases across every instance of the driver on the system.
Should memory be allocated in open() or in the module’s init function?
Allocating lazily in open(), as shown in this lecture, avoids wasting memory on a device that is registered but never actually opened by any application.
What error code should open() return if the device is unavailable?
A negative errno value such as -ENODEV for a missing device or -ENOMEM for an allocation failure is standard, and this value is what the calling application’s open() syscall will return.
Continue the Free Linux Device Drivers Course
Next up: implementing the write() method and handling write requests safely.
