Linux V4L2 File Operations Explained-Free Linux Device Drivers Course

PREV_LEC  |  NEXT_LEC

Linux V4L2 File Operations Explained
Free Linux Kernel Development Course — V4L2 Video Capture Device Drivers, Lecture 3

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.

v4l2_file_operations video_ioctl2 v4l2_ioctl_ops vb2_fop helpers v4l2_fh_is_singular_file free linux kernel development course

What You Will Learn

  • Why V4L2 drivers use their own struct v4l2_file_operations instead of the raw VFS struct file_operations
  • What each field of v4l2_file_operations does — 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 last close() using v4l2_fh_is_singular_file()
  • How unlocked_ioctl = video_ioctl2 dispatches every V4L2 ioctl() into your v4l2_ioctl_ops table
  • 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_device and video_device registration — 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.

Call Path: User Space to Your Driver
open(“/dev/video0”) in user space
VFS: struct file_operations (internal, set up by V4L2 core on cdev)
↓ sanity checks: vdev registered? correct vfl_type/vfl_dir?
struct v4l2_file_operations (your vdev->fops)
↓ .open / .read / .mmap / .poll / .unlocked_ioctl
your driver code, or a vb2_fop_* helper

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, so rmmod can’t pull the module out from under an open device node.
  • open — must allocate and attach a struct v4l2_fh to the file. In nearly every modern driver this is just v4l2_fh_open, which allocates a v4l2_fh, calls v4l2_fh_init(), and adds it to vdev->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 the v4l2_fh is never removed from fh_list and you leak it. For a videobuf2-based device this is normally vb2_fop_release, which stops any streaming still in progress for that handle and then calls v4l2_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_read and vb2_fop_write implement this on top of a vb2_queue automatically. A pure capture (INPUT) device usually only needs read; write is 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_mmap handles 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_poll works as long as either vdev->queue->lock or vdev->lock is 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-free vb2_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 be video_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:

  1. 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).
  2. Runs sanity checks — is this a known V4L2 ioctl command, is it valid for this node’s vfl_type/vfl_dir combination, does it need CAP_SYS_ADMIN.
  3. Takes vdev->lock (or queue->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.

ioctl() Dispatch Chain
ioctl(fd, VIDIOC_QBUF, &buf) in user space
v4l2_file_operations.unlocked_ioctl = video_ioctl2
↓ copies args, validates command, takes vdev->lock
__video_do_ioctl()
↓ looks up VIDIOC_QBUF in the dispatch table
vdev->ioctl_ops->vidioc_qbuf() <– your driver / vb2_ioctl_qbuf

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 callbackReady-made videobuf2 helperWhat it does
vidioc_reqbufsvb2_ioctl_reqbufsAllocates/frees the buffer pool for VIDIOC_REQBUFS
vidioc_querybufvb2_ioctl_querybufReturns buffer status/mmap offset for VIDIOC_QUERYBUF
vidioc_qbufvb2_ioctl_qbufQueues a buffer for capture
vidioc_dqbufvb2_ioctl_dqbufDequeues a filled buffer back to user space
vidioc_expbufvb2_ioctl_expbufExports a buffer as a DMA-BUF file descriptor
vidioc_streamonvb2_ioctl_streamonStarts streaming for VIDIOC_STREAMON
vidioc_streamoffvb2_ioctl_streamoffStops 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 your v4l2_ioctl_ops callbacks are ever reached; every ioctl fails with ENOTTY at the VFS layer, before it even reaches V4L2 dispatch.
  • Calling v4l2_fh_is_singular_file() in the wrong place — check it in .open only after v4l2_fh_open() has run, and in .release only before vb2_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 — the v4l2_fh is never removed from vdev->fh_list, which leaks memory and eventually corrupts the list on module unload.
  • Mixing lock models — setting both vdev->lock and queue->lock without understanding the ordering rules, or using vb2_fop_poll/vb2_fop_mmap when neither lock is set, produces intermittent lockups under concurrent access that are hard to reproduce.
  • Writing your own mmap buffer-offset lookup — the exact loop shown in older textbooks is easy to get wrong for multi-planar formats; use vb2_fop_mmap unless 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_ops callbacks as NULL rather than stubbing them to return an error yourself; the core’s automatic -ENOTTY is what user-space V4L2 libraries expect.
  • Always set cap->driver, cap->card, and cap->bus_info meaningfully in vidioc_querycap — tools like v4l2-ctl and 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

PREV_LEC  |  NEXT_LEC

Leave a Reply

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