Non-Blocking and Signal-Aware Mutex Variants in the Linux Kernel-Linux Device Drivers Course

Non-Blocking and Signal-Aware Mutex Variants in the Linux Kernel
mutex_trylock, mutex_lock_interruptible, mutex_lock_killable and mutex_lock_io explained with kernel 6.x examples

« Previous Lecture  |  Next Lecture »

In the earlier lecture of this free Linux kernel programming course we protected a driver’s shared context structure with a plain mutex_lock() / mutex_unlock() pair. A plain mutex lock always puts the calling thread to sleep when the lock is busy, and that sleep cannot be interrupted by a signal. That behaviour is fine for most drivers, but it is not always what you want. This lecture, part of our free Linux device drivers course, covers four mutex variants the kernel gives you for exactly these situations: a non-blocking try, a signal-interruptible wait, a fatal-signal-only wait, and an I/O-accounted wait.

Why a Non-Blocking Lock Attempt Is Sometimes Needed

A plain mutex_lock() call blocks the calling thread until the lock becomes free. That is usually correct, but there are code paths — interrupt-adjacent work, deadlock-avoidance retry loops, or a driver that must never stall a particular call — where blocking indefinitely is not acceptable. For those paths the kernel provides a non-blocking variant that reports failure immediately instead of sleeping.

int mutex_trylock(struct mutex *lock);

The return value tells you what happened, and it is the opposite convention of many other kernel calls:

  • Return value 1 — the lock was free and has now been acquired by the caller.
  • Return value 0 — the lock is currently held by someone else; the caller did not block and does not own the lock.
mutex_trylock decision flow
start
|
v
[ mutex_trylock(&lock) ] ——————–+
| |
| returns 1 (acquired) | returns 0 (busy)
v v
[ critical section ] [ caller does NOT own lock ]
| |
v v
[ mutex_unlock(&lock) ] [ take alternate action ]
Important Caution

Do not use mutex_trylock() purely to check whether a mutex is currently locked. The return value is only a snapshot; another thread can lock or unlock the mutex the instant after the call returns, so treating the result as the lock’s “current state” is a race condition, not a check. Also note that a highly contended lock will make mutex_trylock() fail often, since it never waits — the classic use case is deadlock-avoidance retry logic, where a thread backs out of one lock ordering and retries with another.

mutex_trylock Still Requires Process Context

Even though the “try” in the name suggests something lightweight, mutex_trylock() is subject to the same rule as every other mutex API: it may only be called from process (task) context, never from interrupt or atomic context. And exactly like the blocking variant, whichever thread successfully acquires the lock must be the one to release it with mutex_unlock().

Letting User-Space Signals Interrupt a Waiting Thread

A plain mutex_lock() puts the thread into an uninterruptible sleep — no user-space signal can wake it early. That is dangerous for any code path reachable from a system call, because a user pressing Ctrl+C on a hung process, or a process being killed, has no way to abort a driver that is stuck waiting on that mutex. The kernel solves this with the interruptible variant:

int mutex_lock_interruptible(struct mutex *lock);

This puts the calling thread into an interruptible sleep. If a signal arrives while the thread is waiting, the call returns early with a non-zero value (conventionally checked as != 0) instead of acquiring the lock, and the driver is expected to propagate -ERESTARTSYS or -EINTR back up through the system call layer so user space sees the interruption. If the call returns 0, the lock was acquired normally, exactly like mutex_lock().

Fatal-Signal-Only Wait: mutex_lock_killable

Sometimes you want a driver to remain unresponsive to ordinary signals (so routine signal delivery does not disturb an in-progress operation) but still remain abortable if the process is being terminated. The killable variant serves that purpose:

int mutex_lock_killable(struct mutex *lock);

Its signature is identical to the interruptible variant, but only signals that would kill the process (such as SIGKILL) can wake the sleeping thread early. Ordinary signals are ignored while the thread is waiting for the lock.

Original Demo: Combining trylock, interruptible and killable in One Driver

The following example driver is written for this course to demonstrate all three variants together in realistic driver entry points, targeting kernel 6.x. It is not copied from any book; it is an original illustration.

#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/fs.h>
#include <linux/errno.h>

static DEFINE_MUTEX(ep_lock_demo_mutex);

/* Non-blocking probe path: never stall this caller */
static int ep_try_reserve(void)
{
    if (!mutex_trylock(&ep_lock_demo_mutex)) {
        pr_info("ep_lock_demo: resource busy, not blocking\n");
        return -EBUSY;
    }

    pr_info("ep_lock_demo: reserved resource without blocking\n");
    /* ... critical section ... */
    mutex_unlock(&ep_lock_demo_mutex);
    return 0;
}

/* open(): user-abortable wait, used by a typical file operation */
static int ep_lock_demo_open(struct inode *inode, struct file *filp)
{
    if (mutex_lock_interruptible(&ep_lock_demo_mutex))
        return -ERESTARTSYS;

    pr_info("ep_lock_demo: open() acquired the lock\n");
    /* ... critical section ... */
    mutex_unlock(&ep_lock_demo_mutex);
    return 0;
}

/* Background worker: ignore ordinary signals, but allow SIGKILL to abort */
static int ep_lock_demo_worker_step(void)
{
    if (mutex_lock_killable(&ep_lock_demo_mutex))
        return -EINTR;

    pr_info("ep_lock_demo: worker step acquired the lock\n");
    /* ... critical section ... */
    mutex_unlock(&ep_lock_demo_mutex);
    return 0;
}

Notice the pattern: three different entry points, three different waiting behaviours, all protecting the same single mutex. Choosing the right variant per code path — rather than defaulting to plain mutex_lock() everywhere — is what makes a driver responsive and well-behaved under kernel 6.x.

The I/O-Accounted Variant: mutex_lock_io

The kernel also provides mutex_lock_io(), whose calling syntax is identical to plain mutex_lock():

void mutex_lock_io(struct mutex *lock);

Functionally it behaves the same as mutex_lock() — an uninterruptible sleep until the lock is free — but it tells the kernel’s scheduler accounting that the waiting time should be counted the same way as time spent waiting for I/O, rather than ordinary lock-contention wait. This distinction matters for system load and I/O-wait statistics reported by tools that read scheduler accounting data; it does not change the locking semantics themselves.

Variant Blocks? Interruptible by Typical use
mutex_lock() Yes Nothing General-purpose locking
mutex_trylock() No n/a Deadlock avoidance, non-blocking paths
mutex_lock_interruptible() Yes Any signal System-call-reachable driver paths
mutex_lock_killable() Yes Fatal signals only Long operations that must ignore routine signals
mutex_lock_io() Yes Nothing Same as mutex_lock but counted as I/O wait

A Note on Nested Mutex APIs

The kernel source also defines nested-locking variants such as mutex_lock_nested() and mutex_lock_interruptible_nested(). These only get compiled in when the kernel’s lock-validator debug option is enabled, and they exist to tell the validator about intentional nesting between instances of the same lock type — for example, locking two objects of an identical struct type in a defined order. As a general rule, the Linux kernel discourages nested or recursive locking in ordinary driver code, so these APIs are reserved for special, well-documented situations rather than everyday use.

mutex_trylock mutex_lock_interruptible mutex_lock_killable mutex_lock_io Linux kernel synchronization free Linux kernel development course

Frequently Asked Questions

1. Can I use mutex_trylock() to check if a mutex is locked right now?

No. The result reflects only the instant the call was made; another thread can change the lock state immediately after. Use it only to attempt an actual acquisition, never as a status check.

2. What does mutex_trylock() return on success versus failure?

It returns 1 when the lock was free and has now been acquired, and 0 when the lock is already held by someone else and the caller did not block.

3. Why would I choose mutex_lock_interruptible() over mutex_lock()?

Because it lets a waiting thread respond to a user-space signal instead of sleeping uninterruptibly. This is essential for any driver code reachable from a system call, so a stuck process can still be interrupted or killed by the user.

4. How is mutex_lock_killable() different from mutex_lock_interruptible()?

The killable variant only wakes early for signals that terminate the process, such as SIGKILL. Routine signals are ignored while waiting, whereas the interruptible variant wakes for any signal.

5. Does mutex_lock_io() change how the lock behaves?

No. Its locking behaviour is identical to plain mutex_lock(). The only difference is that the waiting time is accounted by the scheduler as I/O wait rather than ordinary lock-contention wait.

6. Can mutex_trylock() be called from interrupt context?

No. All mutex APIs, including the trylock variant, are restricted to process context. Use spinlocks for atomic or interrupt context instead.

7. Who is responsible for calling mutex_unlock() after mutex_trylock() succeeds?

The same thread that successfully acquired the lock. Ownership rules for a mutex are identical regardless of which acquisition API was used.

8. When should I use the nested mutex APIs like mutex_lock_nested()?

Only in special, well-documented cases where you must lock two instances of the same lock type in a defined order, and only when the kernel’s lock validator debug option is compiled in. They are not intended for everyday driver code.

« Previous Lecture  |  Next Lecture »

Continue the Free Linux Kernel Programming Course

Next we compare mutexes with semaphores and look at priority inversion in kernel locking.

2 Comments

Leave a Reply

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