Character Driver Open Release Methods- Free Linux Device Drivers Course

 

Character Driver Open Release Methods
Character Device Drivers · Free Linux Kernel Development Course
Chapter 4 · Lecture 4
Kernel 6.x Updated
Beginner Friendly

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 device drivers course
free linux kernel development course
free embedded systems course
character driver open release method
linux character device driver

What You Will Learn

What open() and release() are responsible for in a character driver
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

struct cdev registration and major/minor numbers (previous lecture)
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:

open() Method Prototype
int (*open)(struct inode *inode, struct file *filp);

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:

container_of Recovery Diagram
struct inode
i_cdev pointer
container_of()
offset calculation
struct ep_chardev
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():

ep_chardev.c: Device Structure and open()
struct ep_chardev {
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:

release() Method Prototype
int (*release)(struct inode *inode, struct file *filp);
ep_chardev.c: release()
static int ep_chardev_release(struct inode *inode, struct file *filp)
{
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

Always pair every open() allocation with a matching release() free
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

Build, Load, and Exercise the Driver
$ make
$ 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

open() runs on every open() syscall; release() runs when the last reference is closed
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.

 

Leave a Reply

Your email address will not be published. Required fields are marked *