If you are following this free Linux kernel development cour2se, you already registered a video_device and wired it to a v4l2_device in the earlier lectures of this V4L2 series. That gave you a character device node under /dev/videoX, but a node with no working open(), read(), mmap(), or ioctl() is useless to any application. This lecture covers exactly that layer: V4L2 file operations — how user space system calls on /dev/videoX travel down into your driver, how the V4L2 core and videobuf2 core save you from writing most of that plumbing yourself, and how the unlocked_ioctl callback ultimately routes into the much larger v4l2_ioctl_ops table. This is one of the most practical lectures in this free embedded Linux course — get this part wrong and your driver either won’t open, will crash on the first ioctl(), or will leak resources on close.
What You Will Learn
- Why V4L2 drivers use their own
struct v4l2_file_operationsinstead of the raw VFSstruct file_operations - What each field of
v4l2_file_operationsdoes —open,release,read,write,mmap,poll,unlocked_ioctl - How the ready-made
vb2_fop_*helpers from videobuf2 remove almost all of the streaming boilerplate - How to correctly detect the first
open()and the lastclose()usingv4l2_fh_is_singular_file() - How
unlocked_ioctl = video_ioctl2dispatches every V4L2ioctl()into yourv4l2_ioctl_opstable - How to wire up buffer-handling ioctls using the built-in
vb2_ioctl_*helpers instead of writing them by hand - A complete, original demo driver skeleton you can build and load on a real Linux kernel
Prerequisites
- Comfortable with character device basics (from the Character Device Drivers chapter of this free Linux device drivers course)
- Familiar with
v4l2_deviceandvideo_deviceregistration — covered in Lecture 1 and Lecture 2 of this V4L2 series - Basic understanding of
struct file,copy_to_user()/copy_from_user(), and kernel module build basics - A Linux machine (kernel 5.x/6.x) with kernel headers installed, or a VM, to build and load the demo module
Why V4L2 Needs Its Own File Operations Struct
Every character device in Linux is driven by a struct file_operations registered on a struct cdev. In theory you could implement open, release, read, mmap, and unlocked_ioctl directly against that raw struct, the same way you would for any simple char driver. In practice, almost no V4L2 driver does this by hand, because the amount of shared logic every video capture driver needs is huge: allocating and tracking a v4l2_fh per open file descriptor, handling per-queue or per-device locking consistently across every operation, validating that streaming ioctls only run when the queue is in the right state, and copying dozens of different ioctl argument structures between kernel and user space safely.
The V4L2 core solves this by giving you an intermediate layer: struct v4l2_file_operations. You still assign this struct to video_device->fops, and the core itself is the one that plugs a real struct file_operations into the underlying cdev. That internal cdev file_operations struct is what the VFS actually calls; it does its own sanity checking (is the video_device still registered? is this ioctl valid for this node’s vfl_type/vfl_dir?) before forwarding the call into the fields you filled in on v4l2_file_operations. This is exactly the indirection the earlier lectures alluded to when we set vdev->fops during registration.
The v4l2_file_operations Fields, One at a Time
Here is the struct as it stands on current mainline kernels (verified against include/media/v4l2-dev.h):
struct v4l2_file_operations {
struct module *owner;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
__poll_t (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
#ifdef CONFIG_COMPAT
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
#endif
unsigned long (*get_unmapped_area) (struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct file *);
int (*release) (struct file *);
};
- owner — almost always
THIS_MODULE. Keeps the module refcount correct while a file descriptor is open, sormmodcan’t pull the module out from under an open device node. - open — must allocate and attach a
struct v4l2_fhto the file. In nearly every modern driver this is justv4l2_fh_open, which allocates av4l2_fh, callsv4l2_fh_init(), and adds it tovdev->fh_list. If your hardware needs first-open setup (powering on a sensor, resetting a queue), you wrap this call rather than replace it. - release — must eventually call
v4l2_fh_release(), directly or through a helper, or thev4l2_fhis never removed fromfh_listand you leak it. For a videobuf2-based device this is normallyvb2_fop_release, which stops any streaming still in progress for that handle and then callsv4l2_fh_release()for you. - read / write — needed only if your device supports the classic
read()/write()streaming model in addition to (or instead of)mmap()-based buffers.vb2_fop_readandvb2_fop_writeimplement this on top of avb2_queueautomatically. A pure capture (INPUT) device usually only needsread;writeis for OUTPUT-type devices such as virtual video-output loopback drivers. - mmap — maps one previously allocated video buffer into user space at a time, using the offset the application obtained from
VIDIOC_QUERYBUF.vb2_fop_mmaphandles this correctly for you, including the plane lookup against the buffer’s queued offset — you should not write this by hand unless you have a very unusual memory model. - poll — backs
select()/poll()/epoll()so applications can block until a filled buffer is ready.vb2_fop_pollworks as long as eithervdev->queue->lockorvdev->lockis set so it knows what to take before touching the queue; if you leave both NULL you must implement your own poll on top of the lock-freevb2_poll()helper. - unlocked_ioctl — this is the field that matters most and gets its own section below. For any driver using
v4l2_ioctl_ops, this must bevideo_ioctl2.
Detecting First Open and Last Close Correctly
A very common requirement: power on the sensor/codec only when the first application opens the device, and power it down only when the last one closes it — never in between, because V4L2 explicitly allows multiple simultaneous file handles on one video_device (one process streaming video, another just reading controls, for example). Counting opens/closes yourself with a plain integer is fragile once you also have to handle error paths. The core gives you two purpose-built helpers instead:
int v4l2_fh_is_singular_file(struct file *filp);
int v4l2_fh_is_singular(struct v4l2_fh *fh);
Both return 1 if the given file handle is currently the only open handle on that video_device, 0 otherwise. Call it in .open before your fh is added would give the wrong answer — the standard pattern is to call it inside .release before the release helper removes the handle, and inside .open after v4l2_fh_open() has already added the new handle, so a return of 1 genuinely means “I am the first/only user right now.”
Original Demo Driver: File Operations
Below is an original skeleton, ep_vcap (EmbeddedPathashala Video Capture), independent of any textbook driver, built around the helpers above. It assumes a private state struct already holding an initialised vb2_queue and video_device, as covered in the earlier lectures of this chapter.
struct ep_vcap_dev {
struct v4l2_device v4l2_dev;
struct video_device vdev;
struct vb2_queue queue;
struct mutex dev_lock; /* guards non-streaming ioctls */
bool sensor_powered;
};
static int ep_vcap_open(struct file *file)
{
struct ep_vcap_dev *dev = video_drvdata(file);
int ret;
ret = v4l2_fh_open(file);
if (ret)
return ret;
if (v4l2_fh_is_singular_file(file)) {
/* first application to open this node: power the sensor on */
mutex_lock(&dev->dev_lock);
dev->sensor_powered = true;
mutex_unlock(&dev->dev_lock);
pr_info("ep_vcap: first open, sensor powered on\n");
}
return 0;
}
static int ep_vcap_release(struct file *file)
{
struct ep_vcap_dev *dev = video_drvdata(file);
bool was_singular = v4l2_fh_is_singular_file(file);
int ret;
/* stops any ongoing streaming for this handle, then calls
* v4l2_fh_release() internally */
ret = vb2_fop_release(file);
if (was_singular) {
mutex_lock(&dev->dev_lock);
dev->sensor_powered = false;
mutex_unlock(&dev->dev_lock);
pr_info("ep_vcap: last close, sensor powered off\n");
}
return ret;
}
static const struct v4l2_file_operations ep_vcap_fops = {
.owner = THIS_MODULE,
.open = ep_vcap_open,
.release = ep_vcap_release,
.unlocked_ioctl = video_ioctl2,
.read = vb2_fop_read,
.mmap = vb2_fop_mmap,
.poll = vb2_fop_poll,
};
Notice that read, mmap, and poll are simply the ready-made videobuf2 helpers — none of the streaming logic is written by hand. Only open and release got a thin custom wrapper, purely to add the power-management side effect.
From unlocked_ioctl to v4l2_ioctl_ops
Setting .unlocked_ioctl = video_ioctl2 hands every ioctl() call on the node to a V4L2 core function called video_ioctl2(). It does three jobs before your driver ever sees the request:
- Copies the ioctl argument structure between user space and kernel space (sizes differ per ioctl command, and this is handled generically from the command number).
- Runs sanity checks — is this a known V4L2 ioctl command, is it valid for this node’s
vfl_type/vfl_dircombination, does it needCAP_SYS_ADMIN. - Takes
vdev->lock(orqueue->lock, depending on your locking model) if you provided one, so your handler runs under a consistent lock without you managing it per-ioctl.
Internally video_ioctl2() is a thin wrapper around __video_do_ioctl(), which is the function that actually looks at the ioctl command number and calls the matching function pointer inside vdev->ioctl_ops, a struct v4l2_ioctl_ops. That struct is where the real, command-specific behaviour of your driver lives.
struct v4l2_ioctl_ops: Only Fill What Your Device Supports
The full v4l2_ioctl_ops struct has a callback slot for essentially every V4L2 ioctl that exists — format negotiation, controls, streaming, tuner/audio ioctls, and more. You only populate the ones relevant to your device’s vfl_type and capabilities; leaving the rest as NULL makes the core return -ENOTTY automatically for unsupported commands, which is exactly the behaviour applications expect. For a basic capture device the buffer-handling group is mandatory:
struct v4l2_ioctl_ops {
int (*vidioc_querycap)(struct file *file, void *fh,
struct v4l2_capability *cap);
int (*vidioc_reqbufs)(struct file *file, void *fh,
struct v4l2_requestbuffers *b);
int (*vidioc_querybuf)(struct file *file, void *fh,
struct v4l2_buffer *b);
int (*vidioc_qbuf)(struct file *file, void *fh,
struct v4l2_buffer *b);
int (*vidioc_dqbuf)(struct file *file, void *fh,
struct v4l2_buffer *b);
int (*vidioc_expbuf)(struct file *file, void *fh,
struct v4l2_exportbuffer *p);
int (*vidioc_streamon)(struct file *file, void *fh,
enum v4l2_buf_type i);
int (*vidioc_streamoff)(struct file *file, void *fh,
enum v4l2_buf_type i);
/* ... many more optional callbacks: formats, controls, etc. */
};
Just like the file operations, videobuf2 ships ready-made handlers for every buffer-management ioctl so you almost never implement the buffer queueing logic yourself:
| ioctl_ops callback | Ready-made videobuf2 helper | What it does |
|---|---|---|
| vidioc_reqbufs | vb2_ioctl_reqbufs | Allocates/frees the buffer pool for VIDIOC_REQBUFS |
| vidioc_querybuf | vb2_ioctl_querybuf | Returns buffer status/mmap offset for VIDIOC_QUERYBUF |
| vidioc_qbuf | vb2_ioctl_qbuf | Queues a buffer for capture |
| vidioc_dqbuf | vb2_ioctl_dqbuf | Dequeues a filled buffer back to user space |
| vidioc_expbuf | vb2_ioctl_expbuf | Exports a buffer as a DMA-BUF file descriptor |
| vidioc_streamon | vb2_ioctl_streamon | Starts streaming for VIDIOC_STREAMON |
| vidioc_streamoff | vb2_ioctl_streamoff | Stops streaming and returns queued buffers |
vidioc_querycap is the one callback you almost always write yourself, since it just fills in driver/card identification strings — there’s no generic helper for information only your driver knows.
Original Demo Driver: ioctl Operations
static int ep_vcap_querycap(struct file *file, void *fh,
struct v4l2_capability *cap)
{
struct ep_vcap_dev *dev = video_drvdata(file);
strscpy(cap->driver, "ep_vcap", sizeof(cap->driver));
strscpy(cap->card, "EmbeddedPathashala Demo Capture",
sizeof(cap->card));
snprintf(cap->bus_info, sizeof(cap->bus_info),
"platform:%s", dev->vdev.name);
return 0;
}
static const struct v4l2_ioctl_ops ep_vcap_ioctl_ops = {
.vidioc_querycap = ep_vcap_querycap,
.vidioc_reqbufs = vb2_ioctl_reqbufs,
.vidioc_querybuf = vb2_ioctl_querybuf,
.vidioc_qbuf = vb2_ioctl_qbuf,
.vidioc_dqbuf = vb2_ioctl_dqbuf,
.vidioc_expbuf = vb2_ioctl_expbuf,
.vidioc_streamon = vb2_ioctl_streamon,
.vidioc_streamoff = vb2_ioctl_streamoff,
};
/* during probe, after vdev and queue are initialised: */
dev->vdev.fops = &ep_vcap_fops;
dev->vdev.ioctl_ops = &ep_vcap_ioctl_ops;
dev->vdev.lock = &dev->dev_lock;
dev->vdev.queue = &dev->queue;
With vdev->lock set, the core takes dev_lock automatically around every non-streaming ioctl before calling into ep_vcap_ioctl_ops, so none of the handlers above need to take it themselves.
Building and Loading the Demo Driver
Once the demo module above is completed with a platform driver probe()/remove() (from earlier lectures) and a matching Makefile, build and load it the same way as any other kernel module in this course:
$ make -C /lib/modules/$(uname -r)/build M=$PWD modules
$ sudo insmod ep_vcap.ko
$ ls /dev/video*
/dev/video0
$ dmesg | tail -5
[ 1234.001] ep_vcap: v4l2_device registered
[ 1234.002] ep_vcap: video_device registered as /dev/video0
$ v4l2-ctl -d /dev/video0 --all
Driver Info:
Driver name : ep_vcap
Card type : EmbeddedPathashala Demo Capture
Bus info : platform:ep_vcap.0
$ dmesg | tail -2
[ 1240.117] ep_vcap: first open, sensor powered on
$ # after the v4l2-ctl process exits and closes the fd
$ dmesg | tail -1
[ 1240.309] ep_vcap: last close, sensor powered off
Even without real streaming ioctl handlers filled in beyond vidioc_querycap, tools like v4l2-ctl --all already work correctly, and you can see the singular-open/close logging fire exactly once per session — proof that v4l2_fh_is_singular_file() is doing its job regardless of how many times the application opens and closes internally.
Common Mistakes and Troubleshooting
- Forgetting
.unlocked_ioctl = video_ioctl2— without it, none of yourv4l2_ioctl_opscallbacks are ever reached; every ioctl fails withENOTTYat the VFS layer, before it even reaches V4L2 dispatch. - Calling
v4l2_fh_is_singular_file()in the wrong place — check it in.openonly afterv4l2_fh_open()has run, and in.releaseonly beforevb2_fop_release()/v4l2_fh_release()removes the handle. Reversing the order gives an off-by-one count every time. - Not calling
v4l2_fh_release()at all in a custom.release— thev4l2_fhis never removed fromvdev->fh_list, which leaks memory and eventually corrupts the list on module unload. - Mixing lock models — setting both
vdev->lockandqueue->lockwithout understanding the ordering rules, or usingvb2_fop_poll/vb2_fop_mmapwhen neither lock is set, produces intermittent lockups under concurrent access that are hard to reproduce. - Writing your own
mmapbuffer-offset lookup — the exact loop shown in older textbooks is easy to get wrong for multi-planar formats; usevb2_fop_mmapunless you have a documented reason not to.
Best Practices
- Default to the
vb2_fop_*/vb2_ioctl_*helper functions for every operation videobuf2 already implements; write custom code only for the parts that are genuinely device-specific (power sequencing, format validation, control handling). - Use
v4l2_fh_is_singular_file()for any first-open/last-close hardware side effect instead of a hand-rolled reference count. - Pick one locking model — core locking via
vdev->lock/queue->lock, or fully manual locking — and apply it consistently across every callback in the same driver. - Leave unsupported
v4l2_ioctl_opscallbacks asNULLrather than stubbing them to return an error yourself; the core’s automatic-ENOTTYis what user-space V4L2 libraries expect. - Always set
cap->driver,cap->card, andcap->bus_infomeaningfully invidioc_querycap— tools likev4l2-ctland test suites use these strings for identification and bug reports.
Summary and Key Takeaways
This lecture connected the character-device layer of a V4L2 driver end to end: struct v4l2_file_operations sits between the VFS and your driver, and for a videobuf2-based capture device almost every field — read, mmap, poll, and even open/release in their base form — can be satisfied with ready-made vb2_fop_* helpers. The one field that always needs a real value is unlocked_ioctl, set to video_ioctl2, which validates, copies, and dispatches every V4L2 ioctl into your v4l2_ioctl_ops table via __video_do_ioctl(). Buffer-management ioctls in that table again have ready-made vb2_ioctl_* equivalents, leaving vidioc_querycap as essentially the only mandatory callback you write from scratch. Getting first-open/last-close detection right with v4l2_fh_is_singular_file() rounds out a driver that behaves correctly under multiple simultaneous applications — a real requirement in production V4L2 devices, not an edge case. The next lecture in this free Linux kernel development course chapter moves on to format negotiation ioctls (VIDIOC_ENUM_FMT, VIDIOC_G/S_FMT) and how they interact with the vb2_queue you registered earlier.
Frequently Asked Questions
Why can’t I just use the normal struct file_operations for a V4L2 driver?
You technically can implement a plain char device by hand, but you would then be reimplementing v4l2_fh tracking, offset-based buffer mmap, ioctl argument copying, and command validation yourself. struct v4l2_file_operations plus the core’s internal cdev wrapper already do all of this, which is why virtually every in-tree V4L2 driver uses it.
Do I always have to set every field of v4l2_file_operations?
No. Set owner and unlocked_ioctl always, open/release almost always, and read/write/mmap/poll only for the operations your device actually supports — for example, an OUTPUT-only device does not need vb2_fop_read.
What happens if I leave a v4l2_ioctl_ops callback as NULL?
The core’s dispatch in __video_do_ioctl() sees no handler registered for that command and returns -ENOTTY to user space automatically, exactly as if the ioctl were unsupported. You don’t need to write stub functions that return an error yourself.
Can multiple applications have the same /dev/videoX device open at once?
Yes — this is explicitly supported by the v4l2_fh model. Each open() creates its own struct v4l2_fh, and v4l2_fh_is_singular_file()/v4l2_fh_is_singular() are the correct way to detect the specific case where only one handle currently exists.
What is the difference between vdev->lock and vdev->queue->lock?
vdev->lock protects general (non-streaming) ioctls when set, and is taken automatically by video_ioctl2() around your v4l2_ioctl_ops callbacks. vdev->queue->lock (i.e. the vb2_queue’s own lock) protects buffer-queueing operations specifically; some drivers use a separate lock here so long-running control ioctls don’t block VIDIOC_DQBUF.
Why is vidioc_querycap the one ioctl I always have to write myself?
It reports driver-identifying strings (driver name, card name, bus info) that only your driver knows. There is no generic videobuf2 helper for this because the values are inherently device-specific.
Is video_ioctl2 mandatory, or can I write my own unlocked_ioctl?
video_ioctl2 is mandatory if you want to use struct v4l2_ioctl_ops, which is the standard, strongly recommended approach used across the kernel media subsystem. Writing your own unlocked_ioctl means reimplementing argument copying and command validation by hand, which is discouraged for new drivers.
My driver crashes on the first ioctl() call — what should I check first?
Verify .unlocked_ioctl is set to video_ioctl2, that vdev->ioctl_ops points at your populated v4l2_ioctl_ops struct, and that the specific ioctl_ops callback being called is non-NULL for the command being issued. A NULL vidioc_querycap is a very common first crash for a fresh driver skeleton.
Continue the Free Linux Kernel Development Course
Next up: V4L2 format negotiation ioctls and connecting them to your vb2_queue.
Next Lecture Back to Course Index