« 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.
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.
Frequently Asked Questions
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.
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.
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.
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.
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.
No. All mutex APIs, including the trylock variant, are restricted to process context. Use spinlocks for atomic or interrupt context instead.
The same thread that successfully acquired the lock. Ownership rules for a mutex are identical regardless of which acquisition API was used.
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 »
Next we compare mutexes with semaphores and look at priority inversion in kernel locking.

2 Comments