The plain mutex_lock() call is enough for most driver code, but the kernel also offers several specialized variants that solve real problems: letting a user cancel a stuck syscall with Ctrl-C, avoiding a lock wait altogether, or reacting properly to a fatal signal like SIGKILL. This lecture of our free linux kernel development course walks through every mutex lock variant available in modern kernels and gives you a clear rule for when to reach for each one — an essential topic for anyone serious about a free linux device drivers course.
What You Will Learn
Prerequisites
- Comfort with the basic
mutex_lock()/mutex_unlock()pair (covered in the previous lecture) - Basic understanding of Linux signals (SIGINT, SIGKILL) and how syscalls can be interrupted
- A kernel 6.1+ test environment for trying the examples in a simple driver
Quick Recap: The Standard mutex_lock()
The plain call is uninterruptible and unconditional — the calling task sleeps until the lock becomes available, full stop. It cannot be woken by a signal, so if the lock is held for a long time (which it should never be), the calling process becomes unresponsive to Ctrl-C until the lock is released. That single limitation is the reason the other variants exist.
mutex_lock(&ctx->lock);
/* critical section */
mutex_unlock(&ctx->lock);
The Interruptible Variant: mutex_lock_interruptible()
This variant sleeps waiting for the lock just like the plain call, but it can be woken early by a signal. It returns 0 on success and -EINTR if a signal interrupted the wait, so the caller must always check the return value and bail out cleanly:
static long my_dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
struct my_dev_ctx *ctx = filp->private_data;
if (mutex_lock_interruptible(&ctx->lock))
return -ERESTARTSYS;
/* critical section */
mutex_unlock(&ctx->lock);
return 0;
}
Returning -ERESTARTSYS lets the kernel automatically restart the syscall once the signal has been handled, which is the standard pattern in driver ioctl and read/write handlers that can block for a noticeable amount of time.
The Non-Blocking Trylock Variant: mutex_trylock()
Sometimes you do not want to wait at all — you want to check whether the lock is free, grab it if it is, and otherwise go do something else and retry later. This is the classic busy-wait pattern, and the kernel API is:
int mutex_trylock(struct mutex *lock);
retry later
run critical section,
then unlock
Unlike the other variants, mutex_trylock() returns 1 on success (lock acquired) and 0 if the lock was already held — the opposite convention from a typical error code, so read the return value carefully:
if (mutex_trylock(&ctx->lock)) {
/* lock acquired, run critical section */
ctx->poll_count++;
mutex_unlock(&ctx->lock);
} else {
/* lock busy, skip this round rather than block */
return -EBUSY;
}
O_NONBLOCK), where a caller has explicitly asked never to be put to sleep.
The Killable Variant: mutex_lock_killable()
This sits between the plain and interruptible variants: the wait can only be interrupted by a fatal signal (such as SIGKILL), not by every signal the process receives. It is the right choice when you want the process to remain uninterruptible for ordinary signals but still be killable if something has truly gone wrong:
static ssize_t my_dev_write(struct file *filp, const char __user *ubuf,
size_t count, loff_t *off)
{
struct my_dev_ctx *ctx = filp->private_data;
if (mutex_lock_killable(&ctx->lock))
return -EINTR;
ctx->bytes_written += count;
mutex_unlock(&ctx->lock);
return count;
}
This is commonly used for driver paths that may block for a long time waiting on hardware, where you still want a hung process to be killable with kill -9 rather than becoming permanently stuck.
The I/O Variant: mutex_lock_io()
This variant behaves identically to mutex_lock() in terms of locking semantics, but it hints to the scheduler’s I/O accounting that the task is waiting specifically on I/O rather than a plain CPU-bound sleep. This affects load-average calculation and I/O wait statistics reported by tools like top:
mutex_lock_io(&ctx->lock);
/* critical section that is effectively part of an I/O operation */
mutex_unlock(&ctx->lock);
Use it when the critical section the lock guards is conceptually part of a storage or block I/O path, so system monitoring tools correctly attribute the wait time.
Comparison Table of All Mutex Lock Variants
| Function | Blocks? | Interruptible by | Return value |
|---|---|---|---|
| mutex_lock() | Yes | Nothing | void |
| mutex_lock_interruptible() | Yes | Any signal | 0 or -EINTR |
| mutex_lock_killable() | Yes | Fatal signals only | 0 or -EINTR |
| mutex_trylock() | No | N/A | 1 (got it) or 0 (busy) |
| mutex_lock_io() | Yes | Nothing (I/O-accounted sleep) | void |
Choosing the Right Variant for Your Driver
- Use mutex_lock() when the critical section is guaranteed to be very short and the caller should never be interrupted
- Use mutex_lock_interruptible() in any syscall path (read/write/ioctl) that could block noticeably, so Ctrl-C keeps working for the user
- Use mutex_trylock() when the caller has requested non-blocking behavior (O_NONBLOCK) or inside a polling loop
- Use mutex_lock_killable() when you want the wait uninterruptible by ordinary signals but still killable in an emergency
- Use mutex_lock_io() when the wait is conceptually part of a block or storage I/O path and you want accurate I/O-wait accounting
Real-World Use Cases
- Storage drivers using
mutex_lock_io()soiostatand load average correctly reflect disk waits - Character device ioctl handlers using
mutex_lock_interruptible()so long-running configuration calls stay cancellable - Non-blocking file descriptor support in custom drivers using
mutex_trylock()to honorO_NONBLOCK - Long-running firmware update paths using
mutex_lock_killable()so a stuck update can still be killed without a full reboot
Common Mistakes and Troubleshooting Tips
- Ignoring the return value of mutex_lock_interruptible() or mutex_lock_killable(). Skipping the check means a signal-interrupted wait silently proceeds without the lock.
- Misreading mutex_trylock()’s return convention. It returns 1 for success, not 0 — the reverse of a typical error-style function.
- Using mutex_lock_io() as a general substitute for mutex_lock(). Only use it when the wait genuinely represents I/O; otherwise it skews system I/O-wait statistics.
- Busy-looping tightly on mutex_trylock() without a backoff. This wastes CPU cycles; add a small delay or use a wait queue instead for anything beyond a quick check.
- Returning the wrong error code after an interrupted lock. Prefer
-ERESTARTSYSfrom ioctl/read/write paths so the syscall can be safely restarted by the kernel.
Best Practices
- Default to
mutex_lock_interruptible()for any syscall-facing driver path that might wait - Reserve plain
mutex_lock()for tiny, guaranteed-fast critical sections - Always check and handle the return value of every non-void mutex variant
- Pair
mutex_trylock()with a sensible retry or backoff strategy, never a tight spin loop - Test signal-interruption paths deliberately (send SIGINT/SIGKILL mid-operation) during driver development
Performance Considerations
All blocking variants share the same underlying fast-path cost for the uncontended case — a single atomic operation. The differences only show up under contention, where the interruptible and killable variants have marginally more bookkeeping to check for pending signals during the sleep. This overhead is negligible compared to the cost of the sleep and reschedule itself, so choose the variant based on correctness and user experience, not on a difference in raw speed.
Security Considerations
Choosing the wrong variant can create a denial-of-service condition rather than a memory-safety bug: a driver that only ever uses plain mutex_lock() in a syscall path can leave a user process completely unkillable if the lock is ever held for an unexpectedly long time, for example due to a hung piece of hardware. Using mutex_lock_killable() or mutex_lock_interruptible() in blocking driver paths ensures a misbehaving device cannot hang unrelated user processes beyond recovery.
Summary / Key Takeaways
mutex_lock_interruptible()lets any signal wake a waiting task — use it in syscall pathsmutex_lock_killable()only wakes on fatal signals, ideal for long, semi-critical waitsmutex_trylock()never blocks and returns 1 on success, 0 if busymutex_lock_io()behaves likemutex_lock()but improves I/O-wait accounting- Always check return values on every variant except the plain, void-returning
mutex_lock()
Conclusion
Knowing all five mutex lock variants — and, more importantly, knowing which one fits a given driver code path — is what separates a driver that behaves well under real-world conditions from one that quietly hangs a user’s terminal. This wraps up our two-part look at mutex locking as part of this free linux device drivers course; from here, the natural next step in this free embedded systems course is to explore reader/writer locks and RCU for read-heavy driver data structures.
Frequently Asked Questions
For most syscall-facing driver code (read, write, ioctl), mutex_lock_interruptible() is the safest default because it keeps the process responsive to Ctrl-C.
No. It is entirely non-blocking — it checks the lock state and returns immediately either way.
The conventional pattern in driver code is to return -ERESTARTSYS, which allows the kernel to automatically restart the syscall once signal handling is complete.
No, the locking behavior is identical. The only difference is that the scheduler accounts the sleep time as I/O wait rather than a regular sleep, which affects tools like top and load average.
Yes, the same mutex can be locked with different variants depending on the calling context — for example, mutex_lock_interruptible() in read/write and mutex_trylock() in a non-blocking poll handler.
Nothing — the wait ignores non-fatal signals entirely. Only a fatal signal, such as SIGKILL or an unhandled terminating signal, wakes the task early.

2 Comments