/ —
Reading
0%
100% ⧉ New Tab ⬇ Save

Loading PDF…

📄
Cannot Display PDF Inline

Your browser or server settings are blocking the inline PDF viewer. Open it in a new tab or download it — it's the same guide!

🐧 Linux Device Drivers — Complete Notes

Linux Device Driver Development

From Linux fundamentals to kernel modules, character drivers, IOCTL, interrupts, workqueues and beyond.

Linux & Kernel Fundamentals
What is Linux? What is the Unix Philosophy? ▼

Linux is a free and open-source OS based on the Unix-like system. It powers PCs, servers, embedded systems, and IoT devices.

Unix Philosophy:

  • Do one thing and do it well — Each tool focuses on one task efficiently.
  • Work together — Output of one tool is input to another.
  • Plain text data — Easy to read, debug, and process.
  • Small and modular tools — Combine to perform complex tasks. e.g. ls | grep "error" | wc -l
  • Avoid clutter — Keep things simple, no unnecessary features.
  • Fail early and clearly — Programs fail quickly with clear error messages.
What is Linux Architecture? Explain User Space vs Kernel Space. ▼

Linux is divided into two primary layers:

AspectKernel SpaceUser Space
DescriptionPrivileged space where the kernel runsWhere user applications run
Hardware AccessDirect hardware interactionCannot directly access hardware
ModePrivileged (Ring 0)Unprivileged (Ring 3)
CommunicationManages process, memory, I/OUses system calls to request kernel services

The interface between user space and kernel space is the System Call Interface (syscall). Example: write(), read(), fork().

Example: When LibreOffice prints a document, it makes a write() system call → kernel invokes the printer device driver → driver translates to hardware-specific commands → printer executes.

What is a Linux Kernel Module (LKM)? How do you load and unload it? ▼
LKMCore Concept

An LKM is a piece of code that can be dynamically loaded/unloaded into the kernel at runtime without rebooting. LKMs extend kernel functionality — adding device drivers, new filesystems, or custom system calls.

Two methods to add LKMs:

  • Method 1: Add code to the kernel source tree and recompile (found in /driver directory).
  • Method 2 (preferred): Load/unload the module while kernel is running — called a Loadable Kernel Module.
# Load a module
sudo insmod mymodule.ko

# Check kernel logs
dmesg | tail

# Unload a module
sudo rmmod mymodule

# List loaded modules
lsmod

LKM vs Base Kernel: Base kernel is at /boot and loaded at boot time. LKMs are loaded after the base kernel and communicate with it to complete their functions.

Difference between Kernel Modules and User Programs ▼
FeatureKernel ModuleUser Program
Address SpaceKernel space (shared with kernel)User space (isolated)
Hardware AccessDirect (reads/writes I/O ports)Via system calls only
ExecutionEvent-driven (interrupt handling)Sequential, start to finish
Headers<linux/module.h><stdio.h>
Entry Pointmodule_init()main()
PrivilegesFull access to all hardware & kernel resourcesRestricted — must use system calls
What is Module Information? (LICENSE, AUTHOR, DESCRIPTION, VERSION) ▼

Module metadata is stored as macros from <linux/module.h> and is visible via the modinfo command.

#include <linux/module.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name <[email protected]>");
MODULE_DESCRIPTION("A sample Linux kernel module.");
MODULE_VERSION("1.0");

License types:

LicenseRuleUse Case
GPLMust share modificationsPreferred for Linux — kernel is GPL
Dual BSD/GPLNo need to share changesEmbedded systems, proprietary products
MITCompletely permissiveWidespread adoption
ProprietaryCannot see/modify/redistributeClosed-source (e.g. NVIDIA driver)
# View module info
modinfo my_module.ko

# Example output:
description:  A sample driver for learning purposes.
author:       Your Name <[email protected]>
version:      1.0
license:      GPL
What are Init and Exit Functions in a Kernel Module? ▼
Kernel Programming

Instead of main(), kernel modules use init and exit functions as entry/exit points.

#include <linux/module.h>
#include <linux/init.h>

/* Called when: sudo insmod module.ko */
static int __init hello_init(void)
{
    printk(KERN_INFO "Module loaded!\n");
    return 0;  // Return 0 = success
}

/* Called when: sudo rmmod module */
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Module removed!\n");
}

module_init(hello_init);  // Register init function
module_exit(hello_exit);  // Register exit function

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Hello World Kernel Module");

__init — marks the function as initialization code; freed after init to save memory.
__exit — marks the function as cleanup code.

What is printk()? How does it differ from printf()? ▼

printk() is the kernel-level logging function — like printf() but for kernel space. Messages are stored in the kernel log buffer and viewed with dmesg.

Featureprintk()printf()
SpaceKernel spaceUser space
OutputKernel ring buffer (dmesg)stdout (terminal)
Log levelsYes (KERN_INFO, KERN_ERR, etc.)No
// Log level macros (in order of severity)
KERN_EMERG    // 0 - System is unusable
KERN_ALERT    // 1 - Action must be taken immediately
KERN_CRIT     // 2 - Critical condition
KERN_ERR      // 3 - Error condition
KERN_WARNING  // 4 - Warning condition
KERN_NOTICE   // 5 - Normal but significant
KERN_INFO     // 6 - Informational
KERN_DEBUG    // 7 - Debug-level messages

// Usage
printk(KERN_INFO "Device initialized, major = %d\n", major);
pr_info("Shorthand for KERN_INFO\n");
pr_err("Shorthand for KERN_ERR\n");
How do you pass arguments to a Linux Kernel Module? (module_param) ▼
module_param

Use module_param() to accept parameters at load time via insmod. Parameters appear in /sys/module/<name>/parameters/.

// Syntax
module_param(name, type, permissions);

// Supported types: bool, int, uint, long, ulong, short, ushort, charp

// Example
static int my_value = 10;
module_param(my_value, int, S_IRUSR | S_IWUSR);
MODULE_PARM_DESC(my_value, "An integer parameter");

// Load with custom value
sudo insmod mymodule.ko my_value=42

// Read current value
cat /sys/module/mymodule/parameters/my_value

Pass an array:

// module_param_array(name, type, &count, permissions)
static int my_array[5];
static int array_size;
module_param_array(my_array, int, &array_size, 0444);

// Load with array values
sudo insmod mymodule.ko my_array=10,20,30

Callback on parameter change:

// module_param_cb() — triggers callback when parameter is updated
static int param_set_cb(const char *val, const struct kernel_param *kp)
{
    int res = kstrtoint(val, 10, &valueETX);
    pr_info("Parameter updated to: %d\n", valueETX);
    return res;
}

static const struct kernel_param_ops param_ops = {
    .set = param_set_cb,
    .get = param_get_standard_int,
};
module_param_cb(valueETX, ¶m_ops, &valueETX, 0644);
Device Drivers & Device Files
What is a Device Driver? What are the three types? ▼

A device driver is a translator between the OS/user applications and hardware. In Linux, everything is a file — even hardware is accessed through device files in /dev.

TypeData TransferExamples/dev entry
Character DeviceByte by byte (sequential stream)Keyboard, serial port, audio/dev/tty0, /dev/ttyS0
Block DeviceFixed-size blocks (random access)HDD, SSD, USB drive, CD-ROM/dev/sda, /dev/sdb
Network DevicePacketsEthernet card, Wi-Fi adapter, loopbackNot in /dev (use ip link)
# Identify device types using ls -l /dev
ls -l /dev

# Output example:
brw-rw---- 1 root disk 8, 0  /dev/sda    # b = block device
crw-rw---- 1 root tty  4, 0  /dev/tty0   # c = character device

# b = block device, c = character device
# The two numbers are Major and Minor numbers
What are Major and Minor Numbers? ▼

Major Number: Identifies the device driver (which driver handles this device).
Minor Number: Identifies the specific device instance managed by that driver.

ls -l /dev/ttyS0
# crw-rw---- 1 root dialout 4, 64 /dev/ttyS0
# Major: 4 → Serial driver
# Minor: 64 → Specific port ttyS0

# View all registered drivers
cat /proc/devices

Analogy: Major Number = Phone number of the driver. Device Node = Phone you pick up to call. User program = Caller.

Static vs Dynamic Allocation of Major/Minor Numbers ▼
FeatureStaticDynamic (Preferred)
Major numberManually set by developerKernel assigns automatically
ConflictsProne to conflictsNo conflicts
Functionregister_chrdev_region()alloc_chrdev_region()

Static Allocation:

dev_t dev = MKDEV(202, 0);  // Major=202, Minor=0
register_chrdev_region(dev, 1, "my_device");

// Retrieve numbers
printk("Major=%d Minor=%d\n", MAJOR(dev), MINOR(dev));

// Free on exit
unregister_chrdev_region(dev, 1);

Dynamic Allocation:

dev_t dev = 0;
alloc_chrdev_region(&dev, 0, 1, "my_device");

printk("Major=%d Minor=%d\n", MAJOR(dev), MINOR(dev));

// Free on exit
unregister_chrdev_region(dev, 1);
What is a Device Node? How to create one? ▼

A device node is a special file in /dev that acts as the interface between user-space applications and the kernel driver.

Manual creation using mknod:

# mknod -m <permissions> <path> <type> <major> <minor>
sudo mknod -m 666 /dev/my_device c 202 0
# c = character device, 202 = major, 0 = minor

# Set permissions separately
sudo chmod 666 /dev/my_device

# Verify
ls -l /dev/my_device
# crw-rw-rw- 1 root root 202, 0 /dev/my_device

# Remove when done
sudo rm /dev/my_device

Automatic creation using udev:

// In driver init function:
// 1. Allocate major/minor
alloc_chrdev_region(&dev, 0, 1, "etx_Dev");

// 2. Create a class (visible in /sys/class/)
dev_class = class_create(THIS_MODULE, "etx_class");

// 3. Create the device — udev creates /dev/etx_device automatically
device_create(dev_class, NULL, dev, NULL, "etx_device");

// In driver exit function — cleanup:
device_destroy(dev_class, dev);
class_destroy(dev_class);
Character Device Driver Implementation
What is the cdev Structure? How do you initialize and register it? ▼
Character Driver

struct cdev represents a character device in the kernel. It links the device number (dev_t) to the driver's file operations (file_operations).

// Static allocation (compile-time)
static struct cdev my_cdev;
cdev_init(&my_cdev, &my_fops);   // Link with file ops

// Dynamic allocation (runtime)
struct cdev *my_cdev = cdev_alloc();
my_cdev->ops = &my_fops;

// Register with the kernel (after init)
int cdev_add(struct cdev *cdev, dev_t dev, unsigned int count);

// Example
cdev_init(&etx_cdev, &fops);
if (cdev_add(&etx_cdev, dev, 1) < 0) {
    pr_err("Cannot add device to system\n");
}

// Unregister on exit
void cdev_del(struct cdev *cdev);
What are File Operations in a Character Driver? ▼

struct file_operations defines the functions the driver provides to handle system calls from user space.

#include <linux/fs.h>

// Define the file operations
static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = etx_open,
    .release = etx_release,
    .read    = etx_read,
    .write   = etx_write,
};

// Open: called when app opens /dev/etx_device
static int etx_open(struct inode *inode, struct file *file) {
    pr_info("Driver Open Called\n");
    return 0;
}

// Release: called when app closes the device
static int etx_release(struct inode *inode, struct file *file) {
    pr_info("Driver Release Called\n");
    return 0;
}

// Read: copies data from kernel to user space
static ssize_t etx_read(struct file *filp, char __user *buf,
                        size_t len, loff_t *off) {
    copy_to_user(buf, kernel_buffer, mem_size);
    return mem_size;
}

// Write: copies data from user space to kernel
static ssize_t etx_write(struct file *filp, const char __user *buf,
                         size_t len, loff_t *off) {
    copy_from_user(kernel_buffer, buf, len);
    return len;
}
Complete Character Driver: Full Module with Device Creation ▼
Full Example
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>        // kmalloc
#include <linux/uaccess.h>     // copy_to/from_user

#define MEM_SIZE 1024

dev_t dev = 0;
static struct class  *dev_class;
static struct cdev    etx_cdev;
static char          *kernel_buffer;

/* File Operations */
static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = etx_open,
    .release = etx_release,
    .read    = etx_read,
    .write   = etx_write,
};

static int __init etx_driver_init(void)
{
    // 1. Allocate major/minor numbers
    alloc_chrdev_region(&dev, 0, 1, "etx_Dev");
    pr_info("Major=%d Minor=%d\n", MAJOR(dev), MINOR(dev));

    // 2. Init and add cdev
    cdev_init(&etx_cdev, &fops);
    cdev_add(&etx_cdev, dev, 1);

    // 3. Create class and device node
    dev_class = class_create(THIS_MODULE, "etx_class");
    device_create(dev_class, NULL, dev, NULL, "etx_device");

    // 4. Allocate kernel buffer
    kernel_buffer = kmalloc(MEM_SIZE, GFP_KERNEL);
    strcpy(kernel_buffer, "Hello_World");

    pr_info("Driver Inserted Successfully\n");
    return 0;
}

static void __exit etx_driver_exit(void)
{
    kfree(kernel_buffer);
    device_destroy(dev_class, dev);
    class_destroy(dev_class);
    cdev_del(&etx_cdev);
    unregister_chrdev_region(dev, 1);
    pr_info("Driver Removed Successfully\n");
}

module_init(etx_driver_init);
module_exit(etx_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeTronicX");
MODULE_DESCRIPTION("Simple Linux Character Device Driver");

Testing:

sudo insmod driver.ko
ls /dev/etx_device
sudo cat /dev/etx_device          # Read
sudo echo "Hello" > /dev/etx_device  # Write
dmesg | tail                      # Check logs
sudo rmmod driver
What are kmalloc(), copy_from_user() and copy_to_user()? ▼

kmalloc() — Allocates physically contiguous memory in kernel space (like malloc for kernel).

#include <linux/slab.h>

// Syntax: kmalloc(size, flags)
char *buffer = kmalloc(1024, GFP_KERNEL);
if (!buffer) { pr_err("Memory allocation failed\n"); }

// Clear the memory (kmalloc does NOT zero it)
memset(buffer, 0, 1024);

// Free when done
kfree(buffer);

// Common GFP flags:
// GFP_KERNEL  — Normal allocation, may sleep (use in process context)
// GFP_ATOMIC  — No sleep (use in interrupt handlers)
// GFP_DMA     — DMA-compatible memory

copy_from_user() — Copy data from user space → kernel space.

// unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
// Returns: 0 on success, non-zero = bytes NOT copied

copy_from_user(kernel_buffer, user_buf, len);

copy_to_user() — Copy data from kernel space → user space.

// unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)

copy_to_user(user_buf, kernel_buffer, mem_size);
IOCTL, procfs & sysfs
What is IOCTL? How do you implement it? ▼
IOCTL

IOCTL (Input/Output Control) is a system call for device-specific operations that can't be handled by standard read()/write(). Used for: changing baud rate, ejecting CD, adjusting volume, toggling LEDs, controlling fan speed.

4 IOCTL command types:

MacroDirectionDescription
_IONoneNo data transfer
_IOWUser → KernelWrite data to driver (copy_from_user)
_IORKernel → UserRead data from driver (copy_to_user)
_IOWRBothRead and Write

Kernel driver side:

#include <linux/ioctl.h>

// Define IOCTL commands
#define WR_VALUE  _IOW('a', 'a', int32_t*)
#define RD_VALUE  _IOR('a', 'b', int32_t*)

int32_t value = 0;

// IOCTL handler function
static long etx_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    switch (cmd) {
    case WR_VALUE:
        copy_from_user(&value, (int32_t*)arg, sizeof(value));
        pr_info("Value Written: %d\n", value);
        break;
    case RD_VALUE:
        copy_to_user((int32_t*)arg, &value, sizeof(value));
        pr_info("Value Read: %d\n", value);
        break;
    default:
        pr_info("Invalid IOCTL command\n");
    }
    return 0;
}

// Register in file_operations
static struct file_operations fops = {
    .unlocked_ioctl = etx_ioctl,
    // ... other ops
};

User space application:

#include <sys/ioctl.h>
#define WR_VALUE  _IOW('a','a',int32_t*)
#define RD_VALUE  _IOR('a','b',int32_t*)

int fd = open("/dev/my_device", O_RDWR);
int32_t number = 42, value;

ioctl(fd, WR_VALUE, &number);  // Write 42 to driver
ioctl(fd, RD_VALUE, &value);   // Read back from driver
printf("Value from device: %d\n", value);
close(fd);
What is procfs (/proc)? How to create proc files? ▼

procfs is a virtual filesystem mounted at /proc. It provides a runtime interface to kernel data structures. Not stored on disk — created in memory at boot.

Useful /proc entries:

cat /proc/devices     # All registered char/block major numbers
cat /proc/iomem       # Physical RAM and bus addresses
cat /proc/ioports     # I/O port addresses (x86)
cat /proc/interrupts  # Registered IRQ numbers
cat /proc/softirqs    # Registered soft IRQs
cat /proc/kallsyms    # Running kernel symbols
lsmod                 # All loaded kernel modules

Creating a proc file:

#include <linux/proc_fs.h>

// Create proc entry
static struct proc_dir_entry *proc_entry;

static ssize_t read_proc(struct file *filp, char __user *buf,
                         size_t len, loff_t *off) {
    copy_to_user(buf, etx_array, len);
    return len;
}

static ssize_t write_proc(struct file *filp, const char *buf,
                          size_t len, loff_t *off) {
    copy_from_user(etx_array, buf, len);
    return len;
}

static struct proc_ops proc_fops = {
    .proc_read  = read_proc,
    .proc_write = write_proc,
};

// In init function
proc_entry = proc_create("my_proc_file", 0666, NULL, &proc_fops);

// In exit function
proc_remove(proc_entry);
What is sysfs (/sys)? How to create sysfs attributes? ▼

sysfs is a special filesystem mounted at /sys. It exposes kernel objects (devices, drivers, subsystems) as files for user-space interaction.

#include <linux/kobject.h>
#include <linux/sysfs.h>

int etx_value = 0;
struct kobject *kobj_ref;

// show: called when user reads the sysfs file
static ssize_t sysfs_show(struct kobject *kobj,
                          struct kobj_attribute *attr, char *buf) {
    return sprintf(buf, "%d\n", etx_value);
}

// store: called when user writes to the sysfs file
static ssize_t sysfs_store(struct kobject *kobj,
                           struct kobj_attribute *attr,
                           const char *buf, size_t count) {
    sscanf(buf, "%d", &etx_value);
    return count;
}

// Define attribute (file name = "etx_value", permissions = 0660)
struct kobj_attribute etx_attr = __ATTR(etx_value, 0660,
                                        sysfs_show, sysfs_store);

// In init function
kobj_ref = kobject_create_and_add("etx_sysfs", kernel_kobj);
// Creates: /sys/kernel/etx_sysfs/
sysfs_create_file(kobj_ref, &etx_attr.attr);

// In exit function
sysfs_remove_file(kernel_kobj, &etx_attr.attr);
kobject_put(kobj_ref);

// Usage from user space
cat /sys/kernel/etx_sysfs/etx_value        # Read
echo 42 > /sys/kernel/etx_sysfs/etx_value  # Write
Interrupts & Deferred Work
What are Interrupts? Interrupt vs Exception? Top Half vs Bottom Half? ▼
Interrupts

An interrupt is a signal to the processor to temporarily halt current execution and handle an event (keyboard press, network packet, timer overflow).

FeatureInterruptException
TimingAsynchronous — anytimeSynchronous — during instruction
SourceExternal hardwareProcessor itself
ExamplesKeyboard, NIC, timerPage fault, divide-by-zero

Top Half vs Bottom Half:

AspectTop Half (ISR)Bottom Half (Deferred)
TimingImmediate — runs when interrupt firesDelayed — runs later in process context
PurposeAcknowledge interrupt, minimal workHeavy/slow processing
ContextInterrupt context — cannot sleepProcess context — can sleep
Mechanismsrequest_irq()Softirq, Tasklet, Workqueue, Threaded IRQ
How to register an Interrupt Handler? (request_irq, free_irq) ▼
#include <linux/interrupt.h>

// Register ISR
// int request_irq(irq, handler, flags, name, dev_id)
int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev_id);

// Common flags
IRQF_SHARED     // Share IRQ with other devices
IRQF_DISABLED   // Disable all interrupts when handler runs

// Example ISR
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    pr_info("Interrupt received! IRQ = %d\n", irq);
    // Handle the interrupt...
    return IRQ_HANDLED;  // or IRQ_NONE if not ours
}

// Register in init
request_irq(IRQ_NUM, my_irq_handler, IRQF_SHARED, "my_device", &dev_id);

// Unregister in exit
free_irq(IRQ_NUM, &dev_id);

// Enable/Disable IRQ
enable_irq(IRQ_NUM);
disable_irq(IRQ_NUM);
disable_irq_nosync(IRQ_NUM);  // Non-blocking disable
What are Wait Queues? How to use them? ▼

Wait queues put a process to sleep until a condition becomes true. They prevent busy-waiting and free up the CPU for other tasks.

#include <linux/wait.h>

// 1. Declare and initialize (static)
DECLARE_WAIT_QUEUE_HEAD(my_wq);

// 1. Declare and initialize (dynamic)
wait_queue_head_t my_wq;
init_waitqueue_head(&my_wq);

// 2. Put process to sleep (various macros):

// Uninterruptible sleep until condition is true
wait_event(my_wq, condition);

// Interruptible sleep (can be woken by signals)
int ret = wait_event_interruptible(my_wq, condition);
if (ret == -ERESTARTSYS) pr_info("Interrupted by signal\n");

// Sleep with timeout (returns remaining jiffies or 0)
wait_event_timeout(my_wq, condition, HZ * 5);  // 5 second timeout

// Killable sleep (woken only by fatal signals)
wait_event_killable(my_wq, condition);

// 3. Wake up sleeping processes:
wake_up(&my_wq);                  // Wake one uninterruptible task
wake_up_all(&my_wq);              // Wake all uninterruptible tasks
wake_up_interruptible(&my_wq);    // Wake one interruptible task
What are Workqueues? How to schedule deferred work? ▼

A workqueue defers work from an interrupt handler to a kernel thread running in process context, allowing it to sleep and access user memory.

#include <linux/workqueue.h>

// 1. Define and declare work (static)
void my_work_fn(struct work_struct *work) {
    pr_info("Deferred work executing!\n");
}
DECLARE_WORK(my_work, my_work_fn);

// 2. Dynamic initialization
struct work_struct my_work;
INIT_WORK(&my_work, my_work_fn);

// 3. Schedule work (adds to global workqueue)
schedule_work(&my_work);

// 4. Delayed work (execute after delay)
DECLARE_DELAYED_WORK(my_delayed_work, my_work_fn);
schedule_delayed_work(&my_delayed_work, HZ * 2);  // 2 sec delay

// 5. Wait for work to complete
flush_work(&my_work);
flush_scheduled_work();

// 6. Cancel pending work
cancel_work_sync(&my_work);
cancel_delayed_work_sync(&my_delayed_work);

// 7. Check if work is pending
if (work_pending(&my_work))
    pr_info("Work still pending\n");

Bottom Half Comparison:

MechanismContextCan Sleep?Use Case
SoftirqInterruptNoHigh-priority (networking, block)
TaskletInterruptNoSimple deferred work, simpler than softirq
WorkqueueProcess (kernel thread)YesHeavy/blocking work after interrupt
Threaded IRQProcess (kernel thread)YesPreemptible IRQ handling
Process Context vs Interrupt Context — Key Differences ▼
FeatureProcess ContextInterrupt Context
DefinitionKernel code handling system callsKernel code handling hardware interrupts
PreemptibilityPreemptibleNon-preemptible
Can sleep?Yes (can block, use mutexes)No (must complete quickly)
User memory accessYesNo
LockingMutexes, semaphoresSpinlocks only
Exampleread(), write() system callsKeyboard interrupt, timer ISR
Complete Workflow: Building and Testing a Linux Kernel Driver ▼
Step by Step

1. Makefile for the module:

obj-m += driver.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

# Cross-compilation for ARM
# make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-

2. Build, Insert, Test, Remove:

# Build
make

# Insert module
sudo insmod driver.ko

# Check logs
dmesg | tail -20

# Verify device created
ls -l /dev/etx_device
cat /proc/devices | grep etx

# Test read/write
sudo cat /dev/etx_device
echo "test" | sudo tee /dev/etx_device

# List loaded modules
lsmod | grep driver

# Remove module
sudo rmmod driver

# Check cleanup logs
dmesg | tail -5
Kernel Module vs Driver & Device Tree
What is the difference between a Kernel Module and a Device Driver? ▼
Fundamentals

Kernel Module: A piece of code that can be dynamically loaded/unloaded into the Linux kernel at runtime without recompiling the entire kernel. It extends kernel functionality broadly.

Device Driver: A specific type of kernel module that manages communication between the OS and hardware devices.

AspectKernel ModuleDevice Driver
ScopeGeneral — filesystems, protocols, etc.Specifically manages hardware
RelationSupersetA subset of kernel modules
HardwareMay or may not interactAlways manages a hardware device
Device typesNot applicablechar, block, network
ModelGeneric module modelFollows Linux driver model & device tree

Key takeaway: All device drivers are kernel modules, but not all kernel modules are device drivers.

sudo insmod module_name.ko   # Load
sudo rmmod module_name       # Unload
lsmod                        # List all loaded modules
modinfo module_name.ko       # Show module metadata
What is the Device Tree? Why is it important for embedded Linux drivers? ▼
Device TreeARM / Embedded

The Device Tree is a data structure that describes the hardware platform to the Linux kernel — replacing hardcoded board-specific definitions in the kernel source.

Why it matters:

  • Hardware Abstraction — A single kernel binary can run on different hardware boards.
  • Flexibility — Change hardware configuration without recompiling the kernel.
  • Standardization — Consistent hardware description across all ARM/RISC-V architectures.
  • Embedded essential — Mandatory in modern embedded Linux development.

File extensions:

  • .dts — Device Tree Source (human-readable)
  • .dtb — Device Tree Blob (compiled binary loaded by bootloader)
  • .dtsi — Device Tree Source Include (shared/common definitions)

Example .dts file:

/ {
    compatible = "my-board";

    uart0: serial@e1000000 {
        compatible = "ns16550a";
        reg = <0xe1000000 0x1000>;
        clock-frequency = <115200>;
        interrupts = <10>;
    };

    gpio0: gpio@e1001000 {
        compatible = "gpio-controller";
        reg = <0xe1001000 0x1000>;
        #gpio-cells = <2>;
    };
};

Driver binding: When the kernel boots, it matches compatible strings in the device tree with drivers registered via of_match_table.

static const struct of_device_id my_driver_ids[] = {
    { .compatible = "ns16550a" },
    { }
};
MODULE_DEVICE_TABLE(of, my_driver_ids);

static struct platform_driver my_driver = {
    .driver = {
        .name           = "my_driver",
        .of_match_table = my_driver_ids,
    },
    .probe  = my_probe,
    .remove = my_remove,
};
file_operations — Complete Reference
Explain the file_operations structure and all its commonly used callbacks. ▼

The file_operations structure is the heart of a character driver — it contains function pointers that the kernel calls in response to system calls on the device file.

struct file_operations {
    int      (*open)   (struct inode *, struct file *);
    int      (*release)(struct inode *, struct file *);
    ssize_t  (*read)   (struct file *, char __user *,
                        size_t, loff_t *);
    ssize_t  (*write)  (struct file *, const char __user *,
                        size_t, loff_t *);
    long     (*unlocked_ioctl)(struct file *, unsigned int,
                               unsigned long);
    int      (*mmap)   (struct file *, struct vm_area_struct *);
    __poll_t (*poll)   (struct file *, struct poll_table_struct *);
    loff_t   (*llseek) (struct file *, loff_t, int);
    int      (*fsync)  (struct file *, loff_t, loff_t, int);
};
CallbackTriggered byPurpose
openopen()Device opened, initialize resources
releaseclose()Device closed, free resources
readread()Send data from kernel → user
writewrite()Receive data from user → kernel
unlocked_ioctlioctl()Device-specific control commands
mmapmmap()Map device memory into user space
pollselect()/poll()Non-blocking I/O readiness check
llseeklseek()Change file position (offset)
// Registration example
static struct file_operations fops = {
    .owner          = THIS_MODULE,
    .open           = my_open,
    .release        = my_release,
    .read           = my_read,
    .write          = my_write,
    .unlocked_ioctl = my_ioctl,
    .mmap           = my_mmap,
    .poll           = my_poll,
};

Any callback left as NULL uses the kernel's default behavior for that operation.

Kernel Synchronization
Explain Mutex, Semaphore, and Spinlock — when to use which? ▼
SynchronizationCritical Section

Mutex (Mutual Exclusion): Binary lock. Only one task holds it at a time. The holder sleeps if unavailable.

  • Only the task that locked it can unlock it
  • Can sleep while waiting (blocking)
  • Use for: long critical sections, I/O operations
  • Cannot use in interrupt context

Semaphore: Counter-based synchronization. Allows multiple concurrent holders.

  • Binary semaphore (count=1) ≈ mutex, but any task can unlock
  • Counting semaphore for resource pools
  • Use for: producer-consumer patterns, resource counting

Spinlock: Busy-waiting lock. Task spins in a loop until the lock is free.

  • Never sleeps — disables preemption
  • Must be released very quickly
  • Use for: very short critical sections, interrupt/ISR context
FeatureMutexSemaphoreSpinlock
BlockingYes (sleeps)Yes (sleeps)No (busy-waits)
SpeedFastMediumFastest
ISR Safe❌ No❌ No✅ Yes
Who unlocksSame task onlyAny taskSame task only
Best forLong critical sectionsResource countingShort CS, interrupt context
#include <linux/mutex.h>
#include <linux/spinlock.h>
#include <linux/semaphore.h>

/* ── Mutex ── */
DEFINE_MUTEX(my_mutex);
mutex_lock(&my_mutex);
    // critical section — can sleep here
mutex_unlock(&my_mutex);

/* ── Spinlock ── */
DEFINE_SPINLOCK(my_spinlock);
spin_lock(&my_spinlock);
    // critical section — must be fast, no sleeping
spin_unlock(&my_spinlock);

// ISR-safe variant (saves/restores IRQ state)
unsigned long flags;
spin_lock_irqsave(&my_spinlock, flags);
    // safe to use inside/outside interrupt context
spin_unlock_irqrestore(&my_spinlock, flags);

/* ── Semaphore ── */
struct semaphore my_sem;
sema_init(&my_sem, 1);   // binary semaphore (count = 1)
down(&my_sem);           // acquire (decrements count; sleeps if 0)
    // critical section
up(&my_sem);             // release (increments count)
What are Tasklets? How do they differ from Workqueues? ▼
Bottom Half

Tasklets are a softirq-based bottom-half mechanism for simple deferred work. They run in interrupt context and cannot sleep.

// Define a tasklet function
static void my_tasklet_func(unsigned long data)
{
    struct my_device *dev = (struct my_device *)data;
    pr_info("Tasklet running — processing interrupt data\n");
    // Cannot call sleep, mutex_lock, kmalloc(GFP_KERNEL), etc.
}

// Declare tasklet (static)
static DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);

// Schedule from ISR (top half)
static irqreturn_t my_isr(int irq, void *dev_id)
{
    // Quick: acknowledge hardware interrupt
    tasklet_schedule(&my_tasklet);  // Defer the rest
    return IRQ_HANDLED;
}

// Disable/Kill tasklet on exit
tasklet_disable(&my_tasklet);
tasklet_kill(&my_tasklet);
FeatureTaskletWorkqueue
ContextInterrupt (softirq)Process (kernel thread)
Can sleep?❌ No✅ Yes
Can block?❌ No✅ Yes (mutex, I/O)
ComplexitySimpleMore flexible
SchedulingASAP on same CPUQueued in kernel thread
Use whenFast non-blocking deferred workHeavy/blocking deferred work
Process Management
How do you manipulate process states in the kernel? ▼
Process Management

Process states are controlled using kernel macros and state constants defined in <linux/sched.h>:

StateMeaning
TASK_RUNNINGProcess is running or ready to run
TASK_INTERRUPTIBLESleeping — can be woken by a signal
TASK_UNINTERRUPTIBLESleeping — cannot be woken by signal (e.g. waiting for I/O)
TASK_STOPPEDProcess stopped by debugger or job control
TASK_TRACEDProcess being traced by a debugger
// Set state of current process
set_current_state(TASK_INTERRUPTIBLE);
schedule();  // Yield CPU — go to sleep

// Set another task's state
set_task_state(task, TASK_RUNNING);

// Direct set (no memory barriers — use carefully)
__set_task_state(task, TASK_INTERRUPTIBLE);
What is task_struct? What information does it contain? ▼

task_struct is the fundamental process descriptor in the Linux kernel — every process/thread has one. It stores everything the kernel needs to manage a process.

FieldTypePurpose
statevolatile longCurrent process state
pidpid_tProcess ID
tgidpid_tThread group ID
prio / static_priointDynamic / static priority
mmstruct mm_struct*Memory map (NULL for kernel threads)
filesstruct files_struct*Open file descriptors
signalstruct signal_struct*Signal handlers
se.vruntimeu64CFS virtual runtime
real_parentstruct task_struct*Parent process pointer
commchar[]Command name (executable)
// 'current' always points to the running process's task_struct
#include <linux/sched.h>

pr_info("PID: %d\n",    current->pid);
pr_info("Name: %s\n",   current->comm);
pr_info("State: %ld\n", current->state);

// Get thread_info (stack bottom)
current_thread_info();
What are kernel threads? How do they differ from user threads? ▼
FeatureKernel ThreadUser Thread
Address space (mm)NULL — no user address spaceHas user address space
Memory accessKernel memory onlyUser + kernel (via syscalls)
Creationkthread_create()pthread_create() / clone()
Exampleskswapd, kworker, ksoftirqdApp threads
Run modeKernel mode onlyUser + kernel mode
#include <linux/kthread.h>

static int my_thread_fn(void *data)
{
    while (!kthread_should_stop()) {
        pr_info("Kernel thread running\n");
        msleep(1000);
    }
    return 0;
}

// Create and start thread
struct task_struct *t = kthread_create(my_thread_fn, NULL, "my_kthread");
if (!IS_ERR(t))
    wake_up_process(t);

// Stop thread on exit
kthread_stop(t);
What is the difference between fork() and vfork()? ▼
Featurefork()vfork()
MemoryCopy-on-write — separate address spacesShares parent's address space
ParentContinues running immediatelyBlocked until child calls exec() or exit()
SpeedSlower (page table setup)Faster (no copying)
SafetySafe — changes isolatedDangerous — child must not modify shared memory
Use caseGeneral process creationWhen child immediately calls exec()
// fork() — parent and child run independently
pid_t pid = fork();
if (pid == 0) {
    // Child code — has own copy of memory (copy-on-write)
    exit(0);
} else if (pid > 0) {
    // Parent continues — pid = child's PID
    wait(NULL);  // Reap child to avoid zombie
}

// vfork() — parent blocks until child exec()/exit()
pid_t pid = vfork();
if (pid == 0) {
    // Child MUST call exec() or _exit() immediately
    execv("/bin/ls", args);
} else {
    // Parent resumes here after child calls exec/exit
}
What is a zombie process and how does the kernel handle it? ▼

A zombie process is a child that has exited but whose exit status hasn't been collected by the parent (via wait()). Its task_struct remains in the kernel occupying a process table slot.

How the kernel handles orphaned zombies:

  • If parent dies, child is reparented to init (PID 1) or nearest subreaper
  • init periodically calls wait() to reap all zombies
  • Kernel uses exit_notify() and forget_original_parent() internally
pid_t pid = fork();
if (pid == 0) {
    exit(5);              // Child exits immediately
} else {
    sleep(10);            // Child is ZOMBIE during this gap!
    wait(NULL);           // Parent reaps zombie — frees task_struct
}

// To detect zombies
// ps aux | grep 'Z'   or   cat /proc/<pid>/status | grep State
Process Scheduling
What is CFS (Completely Fair Scheduler) and how does it work? ▼
SchedulerCFS

CFS is the default Linux process scheduler since kernel 2.6.23. It aims to give every process a fair share of CPU time proportional to its weight (nice value).

Key Concepts:

ConceptDescription
vruntimeVirtual runtime — tracks how much CPU each process has used. Lower = more starved.
Red-Black TreeAll runnable tasks stored in an RB-tree ordered by vruntime. O(log n) insert/delete.
Next to runAlways the leftmost node (smallest vruntime = most CPU-starved process).
No fixed time sliceRun time is dynamic, based on number of runnable processes and their weights.

Scheduling loop:

  • Pick leftmost node in RB-tree (smallest vruntime)
  • Let it run for its allocated time
  • Update: vruntime += actual_runtime / weight
  • Reinsert into RB-tree at new position
  • Repeat

Advantages over the old O(1) scheduler: Better fairness, no starvation, improved interactive response, scales well on multi-CPU systems.

What is nice value and how does it affect CPU scheduling? ▼

Nice value is a priority hint ranging from -20 (highest priority) to +19 (lowest priority). Default is 0.

In CFS, nice maps to a weight. Lower nice → higher weight → vruntime grows slower → process gets more CPU time.

NiceWeightEffect
-20~88761Gets the most CPU — near real-time
01024Default — balanced share
+19~15Gets the least CPU — background tasks
nice -n 10 my_app          # Start app with nice +10 (lower priority)
renice -n -5 -p 1234       # Change running PID 1234 to nice -5
ps axo pid,ni,comm         # View nice values of all processes

# In kernel — set scheduling priority programmatically
set_user_nice(current, 10);
What is a context switch and what does the kernel save/restore? ▼

A context switch is the process of saving the running process's state and loading another process's state so it can execute on the CPU.

What gets saved/restored:

  • CPU general-purpose registers
  • Program Counter (PC / Instruction Pointer)
  • Stack Pointer (SP)
  • Memory management registers (page table base)
  • TLB flushed (unless ASID used)
  • FPU/SIMD state (lazy save)
// Kernel context_switch() flow (simplified)
context_switch(rq, prev, next) {
    struct mm_struct *mm = next->mm;
    struct mm_struct *oldmm = prev->active_mm;

    if (!mm)                        // Next is kernel thread
        next->active_mm = oldmm;    // Borrow prev's mm
    else
        switch_mm(oldmm, mm, next); // Switch page tables + TLB

    switch_to(prev, next, prev);    // Save/restore registers, stack
}

// Cost of context switch: typically 1–10 µs
// Minimise by: reducing lock contention, using per-CPU data
Memory Management
What are Linux memory zones? How is physical memory organised? ▼
Memory

Linux divides physical memory into zones based on hardware constraints:

ZoneRange (32-bit)Purpose
ZONE_DMA0 – 16 MBLegacy ISA DMA devices with limited address range
ZONE_NORMAL16 MB – 896 MBDirectly mapped kernel memory — most common
ZONE_HIGHMEM> 896 MBNot directly mapped (32-bit only) — requires ioremap
ZONE_MOVABLEVariesMovable pages for memory hot-plug

Memory allocation functions:

// Allocate pages directly (returns struct page *)
struct page *p = alloc_pages(GFP_KERNEL, order); // 2^order pages

// Allocate and get virtual address
unsigned long addr = __get_free_pages(GFP_KERNEL, 0); // 1 page

// Small allocations — slab allocator
struct mydata *d = kmalloc(sizeof(*d), GFP_KERNEL);

// Large non-contiguous virtual allocation
void *buf = vmalloc(1024 * 1024);  // 1 MB

// Zero-filled single page
unsigned long zp = get_zeroed_page(GFP_KERNEL);

// Free functions
kfree(d);
vfree(buf);
free_pages(addr, 0);
kmalloc() vs vmalloc() — differences and when to use each? ▼
Featurekmalloc()vmalloc()
Physical memoryContiguous (required)Scattered — not contiguous
Virtual memoryContiguous (identity mapped)Contiguous (via page tables)
SpeedFast (slab cache)Slower (page table setup)
DMA safe✅ Yes❌ No (physical addresses scattered)
Max size~128–256 KB typicallyMuch larger (limited by virtual space)
Best forSmall allocs, DMA buffers, ISRLarge buffers, module data
ISR safe flagGFP_ATOMIC (no sleep)Cannot use in ISR context
// kmalloc — physically contiguous, fast
char *buf = kmalloc(512, GFP_KERNEL);    // Process context — can sleep
char *isr_buf = kmalloc(64, GFP_ATOMIC); // Interrupt context — no sleep
kfree(buf);

// vmalloc — virtually contiguous, large
void *large = vmalloc(4 * 1024 * 1024);  // 4 MB
vfree(large);

// kzalloc — kmalloc + zero fill
struct mydev *dev = kzalloc(sizeof(*dev), GFP_KERNEL);

// kcalloc — kmalloc array + zero fill
int *arr = kcalloc(100, sizeof(int), GFP_KERNEL);
Advanced Kernel Synchronization
Spinlock deep dive — all variants and when to use each ▼
Spinlock

Spinlocks busy-wait (spin in a loop) rather than sleeping. They are the only lock safe in interrupt context. Keep the critical section as short as possible.

VariantWhat it doesUse when
spin_lock()Basic lock, disables preemptionNo interrupt contention
spin_lock_irq()Lock + disable local IRQsIRQ handler and process share the lock
spin_lock_irqsave()Lock + save/restore IRQ stateSafe even if IRQs already disabled
spin_lock_bh()Lock + disable bottom halvesTasklet/softirq and process share lock
DEFINE_SPINLOCK(my_lock);

/* Basic */
spin_lock(&my_lock);
    // critical section
spin_unlock(&my_lock);

/* IRQ-safe (preferred in drivers) */
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
    // safe from both process and interrupt context
spin_unlock_irqrestore(&my_lock, flags);

/* Bottom-half safe */
spin_lock_bh(&my_lock);
    // safe from tasklets/softirqs
spin_unlock_bh(&my_lock);

/* Nested locks — ALWAYS acquire in the same order to avoid deadlock */
spin_lock(&lock_a);
spin_lock(&lock_b);
    // work
spin_unlock(&lock_b);
spin_unlock(&lock_a);
What are atomic operations? Give examples of reference counting. ▼

Atomic operations complete as indivisible CPU instructions — no spinlock required. Fast and safe across CPUs for simple integer operations.

#include <linux/atomic.h>

atomic_t count;

atomic_set(&count, 5);            // Set to 5
int val = atomic_read(&count);    // Read value
atomic_inc(&count);               // Increment
atomic_dec(&count);               // Decrement
atomic_add(3, &count);            // Add 3
atomic_sub(2, &count);            // Subtract 2
bool z = atomic_inc_and_test(&count);  // Inc, returns true if now 0
bool z = atomic_dec_and_test(&count);  // Dec, returns true if now 0
atomic_xchg(&count, 10);              // Swap, returns old value
atomic_cmpxchg(&count, 5, 10);        // If val==5, set to 10

Reference counting pattern (common in drivers):

struct my_device {
    atomic_t refcount;
    // ... other fields
};

// Grab a reference
void get_device(struct my_device *dev) {
    atomic_inc(&dev->refcount);
}

// Release a reference — free when count hits 0
void put_device(struct my_device *dev) {
    if (atomic_dec_and_test(&dev->refcount))
        kfree(dev);
}

// Preferred modern API: kref
#include <linux/kref.h>
struct my_device { struct kref kref; };
kref_init(&dev->kref);
kref_get(&dev->kref);
kref_put(&dev->kref, my_release_fn);

When to use atomic vs spinlock: Use atomics for simple counters/flags. Use spinlocks for compound operations that must be atomic as a group.

Tasklets, Softirqs, and Workqueues — complete comparison ▼
FeatureSoftirqTaskletWorkqueue
ContextSoftirq (interrupt)Softirq (interrupt)Process (kernel thread)
Can sleep?❌ No❌ No✅ Yes
Can block?❌ No❌ No✅ Yes (mutex, I/O)
SpeedFastestFastSlower
ConcurrencyMultiple CPUs simultaneouslyOne instance at a timeMultiple worker threads
Limited countYes — only 10 vectorsNoNo
Best forCritical kernel subsystems (networking, block)Simple fast deferred workI/O, long-running, blocking work
/* ── Tasklet ── */
void my_tasklet_fn(unsigned long data) {
    pr_info("Tasklet running\n");
}
DECLARE_TASKLET(my_tasklet, my_tasklet_fn, 0);
tasklet_schedule(&my_tasklet);    // From ISR or anywhere
tasklet_kill(&my_tasklet);        // On driver exit

/* ── Workqueue ── */
void my_work_fn(struct work_struct *work) {
    pr_info("Work running in process context\n");
    msleep(100);       // Can sleep!
    mutex_lock(&my_mutex); // Can use mutexes!
}
DECLARE_WORK(my_work, my_work_fn);
schedule_work(&my_work);          // Add to global workqueue
flush_work(&my_work);             // Wait for completion
cancel_work_sync(&my_work);       // Cancel + wait
Device Files, /dev & Device Nodes
What are Device Files in /dev? How do you read them? ▼
Device Files

Device files in /dev are special files that serve as interfaces between user-space applications and device drivers. They are not real files on disk — they are virtual entry points into the kernel driver.

Each device file has two key identifiers:

NumberPurposeExample
MajorIdentifies which driver handles the device/dev/sda → major 8 (SCSI disk driver)
MinorIdentifies the specific device instance/dev/sda1 → minor 1 (first partition)
$ ls -l /dev/ttyS0
crw-rw---- 1 root dialout 4, 64 /dev/ttyS0
│                           │  └─ minor number (64)
│                           └──── major number (4)
└──────────────────────────────── 'c' = character device

$ ls -l /dev/sda
brw-rw---- 1 root disk 8, 0 /dev/sda
└─ 'b' = block device

# Device file types:
# c = character device  (serial port, keyboard, GPIO)
# b = block device      (HDD, SSD, SD card)
# l = symbolic link
# s = socket
# p = named pipe (FIFO)

Common /dev files:

  • /dev/null — discards all written data, reads return EOF
  • /dev/zero — infinite stream of zero bytes
  • /dev/random — cryptographically secure random bytes
  • /dev/mem — direct access to physical memory
  • /dev/ttyS0 — first serial (UART) port
Static vs Dynamic Device Number Allocation — Which is preferred? ▼
Device Numbers
AspectStatic AllocationDynamic Allocation
Functionregister_chrdev_region()alloc_chrdev_region()
Major numberYou choose (risk of conflict)Kernel assigns available one
PredictabilityAlways same numberChanges each load
Conflict riskHighNone
Recommended?Only for legacy/known numbers✅ Yes — always preferred
#include <linux/fs.h>

/* ── Static allocation ── */
dev_t dev = MKDEV(240, 0);
if (register_chrdev_region(dev, 1, "my_device") < 0) {
    pr_err("Static alloc failed\n");
    return -1;
}

/* ── Dynamic allocation (RECOMMENDED) ── */
dev_t dev;
if (alloc_chrdev_region(&dev, 0, 1, "my_device") < 0) {
    pr_err("Dynamic alloc failed\n");
    return -1;
}
int major = MAJOR(dev);
int minor = MINOR(dev);
pr_info("Got major=%d minor=%d\n", major, minor);

/* ── Always free in module exit ── */
static void __exit my_exit(void) {
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev, 1);   // MUST call this!
}

Check /proc/devices after loading to see your assigned major number.

How to automatically create /dev nodes? (class_create + device_create) ▼
udevclass_create

Modern Linux uses udev to automatically create /dev entries when a driver loads. You trigger this from the driver using class_create() and device_create().

#include <linux/device.h>
#include <linux/fs.h>

static dev_t dev;
static struct class  *dev_class;
static struct cdev    my_cdev;

static int __init my_init(void)
{
    /* 1. Allocate device number */
    alloc_chrdev_region(&dev, 0, 1, "my_device");

    /* 2. Initialize and register cdev */
    cdev_init(&my_cdev, &my_fops);
    my_cdev.owner = THIS_MODULE;
    cdev_add(&my_cdev, dev, 1);

    /* 3. Create device class → appears in /sys/class/ */
    dev_class = class_create(THIS_MODULE, "my_class");
    if (IS_ERR(dev_class)) {
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        return PTR_ERR(dev_class);
    }

    /* 4. Create device → udev creates /dev/my_device automatically */
    if (IS_ERR(device_create(dev_class, NULL, dev, NULL, "my_device"))) {
        class_destroy(dev_class);
        cdev_del(&my_cdev);
        unregister_chrdev_region(dev, 1);
        return -1;
    }

    pr_info("/dev/my_device created automatically!\n");
    return 0;
}

static void __exit my_exit(void)
{
    device_destroy(dev_class, dev);    /* Remove /dev node */
    class_destroy(dev_class);          /* Remove /sys/class entry */
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev, 1);
}

Without device_create(): must manually run sudo mknod /dev/my_device c 240 0 every time.
With device_create(): udev creates it automatically on insmod, removes on rmmod. ✅

Linked Lists & Kernel Data Structures
How are Linked Lists implemented in the Linux Kernel? ▼
list_headIntrusive List

The Linux kernel uses an intrusive doubly-linked list — the list_head node is embedded inside your data structure rather than wrapping it. This avoids extra allocation and gives O(1) insert/delete.

#include <linux/list.h>

/* Embed list_head inside your struct */
struct my_device {
    int  id;
    char name[32];
    struct list_head list;   /* ← kernel list node */
};

/* Declare and initialize the list head */
static LIST_HEAD(device_list);
static DEFINE_SPINLOCK(list_lock);

/* ── Add device to tail ── */
void add_device(struct my_device *dev) {
    spin_lock(&list_lock);
    list_add_tail(&dev->list, &device_list);
    spin_unlock(&list_lock);
}

/* ── Remove device ── */
void remove_device(struct my_device *dev) {
    spin_lock(&list_lock);
    list_del(&dev->list);
    spin_unlock(&list_lock);
}

/* ── Find by ID ── */
struct my_device *find_device(int id) {
    struct my_device *dev;
    spin_lock(&list_lock);
    list_for_each_entry(dev, &device_list, list) {
        if (dev->id == id) {
            spin_unlock(&list_lock);
            return dev;
        }
    }
    spin_unlock(&list_lock);
    return NULL;
}

/* ── Print all ── */
void print_all(void) {
    struct my_device *dev;
    list_for_each_entry(dev, &device_list, list)
        pr_info("Device: %s (id=%d)\n", dev->name, dev->id);
}
OperationFunctionDescription
Init headLIST_HEAD(name)Declare and initialize list head
Add frontlist_add(new, head)Add after head
Add taillist_add_tail(new, head)Add before head
Removelist_del(entry)Unlink from list
Iteratelist_for_each_entry(pos, head, member)Type-safe iteration
Empty checklist_empty(head)Returns true if empty
Kernel Timers & High-Resolution Timers
What are Kernel Timers? How do you implement a repeating timer? ▼
timer_listjiffies

Standard kernel timers schedule a callback function to run after a delay. They are based on jiffies (system ticks) and provide millisecond granularity.

Key concepts:

  • HZ — ticks per second (typically 100–1000 depending on kernel config)
  • jiffies — current tick count since boot
  • jiffies + HZ — 1 second from now
  • msecs_to_jiffies(ms) — convert ms to jiffies
#include <linux/timer.h>

static struct timer_list heartbeat_timer;
static int count = 0;

/* Timer callback — runs in softirq context, cannot sleep */
void timer_callback(struct timer_list *t)
{
    count++;
    pr_info("Heartbeat #%d\n", count);

    /* Reschedule every 5 seconds */
    mod_timer(&heartbeat_timer, jiffies + (5 * HZ));
}

static int __init driver_init(void)
{
    /* Initialize timer */
    timer_setup(&heartbeat_timer, timer_callback, 0);

    /* Start timer: fires in 1 second */
    mod_timer(&heartbeat_timer, jiffies + HZ);
    return 0;
}

static void __exit driver_exit(void)
{
    del_timer_sync(&heartbeat_timer);  /* Wait for any running callback */
}

/* ── Timer operations ── */
// mod_timer(&t, jiffies + HZ)    — start or reset timer
// del_timer(&t)                  — cancel timer (may already be running)
// del_timer_sync(&t)             — cancel and wait for completion ✅
// timer_pending(&t)              — returns 1 if timer is scheduled

Limitation: Jiffies granularity is ~1–10ms. For finer timing, use High-Resolution Timers (HRT).

What are High-Resolution Timers (HRT)? When and how to use them? ▼
hrtimernanoseconds

High-Resolution Timers (HRT) provide nanosecond precision using a hardware clock, unlike standard timers which are limited to jiffies (milliseconds).

FeatureStandard TimerHRT
ResolutionJiffies (~1–10ms)Nanosecond
Timestamp width32-bit jiffies64-bit ktime_t
Data structureSimple linked listRed-black tree (O(log n))
Best forPolling, watchdogsAudio, multimedia, real-time
#include <linux/hrtimer.h>
#include <linux/ktime.h>

static struct hrtimer my_hrtimer;

/* Callback — return value controls rescheduling */
enum hrtimer_restart hrt_callback(struct hrtimer *timer)
{
    pr_info("HRT fired — nanosecond precision!\n");

    /* Reschedule every 100ms */
    hrtimer_forward_now(timer, ktime_set(0, 100000000)); /* 100ms */
    return HRTIMER_RESTART;          /* Keep repeating */
    // return HRTIMER_NORESTART;     /* One-shot */
}

static int __init my_init(void)
{
    /* Initialize with monotonic clock */
    hrtimer_init(&my_hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
    my_hrtimer.function = hrt_callback;

    /* Start: fires in 1 second */
    hrtimer_start(&my_hrtimer, ktime_set(1, 0), HRTIMER_MODE_REL);
    return 0;
}

static void __exit my_exit(void)
{
    hrtimer_cancel(&my_hrtimer);   /* Cancel and wait */
}

/* Time creation helpers */
ktime_t t1 = ktime_set(1, 500000000);  /* 1.5 seconds */
ktime_t t2 = ktime_set(0,   1000000);  /* 1 millisecond */
ktime_t t3 = ktime_set(0,      1000);  /* 1 microsecond */
Read-Write Spinlocks & Advanced Sync
What are Read-Write Spinlocks? When to use them over regular spinlocks? ▼
rwlock_tReader-Writer

A read-write spinlock (rwlock) distinguishes between read access and write access. Multiple readers can hold the lock simultaneously; a writer requires exclusive access.

OperationConcurrent?Use when
Multiple readers✅ Yes — fully parallelRead-only access to shared data
Reader + Writer❌ No — blockedWriter must wait for all readers to exit
Multiple writers❌ No — exclusiveOnly one writer at a time
#include <linux/spinlock.h>

static DEFINE_RWLOCK(my_rwlock);
static int shared_data = 0;

/* ── Many readers can run concurrently ── */
int read_data(void)
{
    int val;
    read_lock(&my_rwlock);         /* Multiple readers OK */
    val = shared_data;
    read_unlock(&my_rwlock);
    return val;
}

/* ── Only one writer at a time ── */
void write_data(int new_val)
{
    write_lock(&my_rwlock);        /* Exclusive access */
    shared_data = new_val;
    write_unlock(&my_rwlock);
}

/* ── IRQ-safe variants ── */
unsigned long flags;
read_lock_irqsave(&my_rwlock, flags);
    // read
read_unlock_irqrestore(&my_rwlock, flags);

write_lock_irqsave(&my_rwlock, flags);
    // write
write_unlock_irqrestore(&my_rwlock, flags);

When to use rwlock over spinlock: When reads happen much more frequently than writes (e.g., routing tables, device registry, configuration data). If writes are as frequent as reads, a regular spinlock is simpler and has lower overhead.

Kernel Threads & GPIO Driver
How do you create and manage Kernel Threads? ▼
kthreadtask_struct

Kernel threads are processes that run entirely in kernel space — no user-space memory context. Used for background tasks like flushing buffers, polling hardware, and driver housekeeping.

FeatureKernel ThreadUser Thread
Memory spaceKernel only (no user VM)User + kernel
Context switchNo user/kernel transitionFull mode switch
Can sleep?✅ Yes✅ Yes
Visible in ps?✅ Yes (inside [brackets])Yes
Create withkthread_create()pthread_create()
#include <linux/kthread.h>
#include <linux/delay.h>

static struct task_struct *my_thread;

/* Thread function — runs until kthread_should_stop() returns true */
static int thread_fn(void *data)
{
    pr_info("Kernel thread started\n");

    while (!kthread_should_stop()) {
        pr_info("Thread tick\n");
        msleep(1000);              /* Sleep 1 second — OK to sleep! */
    }

    pr_info("Kernel thread stopping\n");
    return 0;
}

static int __init my_init(void)
{
    /* Create thread (does NOT start it yet) */
    my_thread = kthread_create(thread_fn, NULL, "my_kthread_%d", 0);
    if (IS_ERR(my_thread))
        return PTR_ERR(my_thread);

    /* Start the thread */
    wake_up_process(my_thread);
    return 0;
}

static void __exit my_exit(void)
{
    if (my_thread) {
        kthread_stop(my_thread);   /* Signal stop + wait for exit */
        my_thread = NULL;
    }
}

/* Bind thread to specific CPU */
// kthread_bind(my_thread, 0);    /* Run only on CPU 0 */
How do you control GPIO from a Linux kernel driver? ▼
GPIOgpio_request

The Linux GPIO subsystem provides a unified API for controlling digital I/O pins across all hardware platforms. The same API works on Raspberry Pi, BeagleBone, STM32MP1, etc.

#include <linux/gpio.h>
#include <linux/interrupt.h>

#define GPIO_OUTPUT_PIN   17    /* LED */
#define GPIO_INPUT_PIN    27    /* Button */

static int irq_number;

static irqreturn_t gpio_irq_handler(int irq, void *dev_id)
{
    pr_info("GPIO interrupt! Value=%d\n",
            gpio_get_value(GPIO_INPUT_PIN));
    return IRQ_HANDLED;
}

static int __init gpio_driver_init(void)
{
    /* ── Validate ── */
    if (!gpio_is_valid(GPIO_OUTPUT_PIN) || !gpio_is_valid(GPIO_INPUT_PIN)) {
        pr_err("Invalid GPIO number\n");
        return -EINVAL;
    }

    /* ── Request ownership ── */
    gpio_request(GPIO_OUTPUT_PIN, "led_gpio");
    gpio_request(GPIO_INPUT_PIN,  "btn_gpio");

    /* ── Configure direction ── */
    gpio_direction_output(GPIO_OUTPUT_PIN, 0);   /* Output, init LOW */
    gpio_direction_input(GPIO_INPUT_PIN);         /* Input */

    /* ── Drive and read ── */
    gpio_set_value(GPIO_OUTPUT_PIN, 1);           /* LED ON */
    int btn = gpio_get_value(GPIO_INPUT_PIN);     /* Read button */
    pr_info("Button state: %d\n", btn);

    /* ── GPIO as interrupt ── */
    irq_number = gpio_to_irq(GPIO_INPUT_PIN);
    request_irq(irq_number, gpio_irq_handler,
                IRQF_TRIGGER_RISING, "gpio_int", NULL);

    /* ── Export to user space via /sys/class/gpio/ ── */
    gpio_export(GPIO_OUTPUT_PIN, false);   /* false = no direction change */
    return 0;
}

static void __exit gpio_driver_exit(void)
{
    gpio_set_value(GPIO_OUTPUT_PIN, 0);
    gpio_unexport(GPIO_OUTPUT_PIN);
    free_irq(irq_number, NULL);
    gpio_free(GPIO_OUTPUT_PIN);
    gpio_free(GPIO_INPUT_PIN);
    pr_info("GPIO driver removed\n");
}

GPIO from user space (via sysfs):

# Export GPIO 17
echo 17 > /sys/class/gpio/export

# Set as output
echo "out" > /sys/class/gpio/gpio17/direction

# Drive high / low
echo 1 > /sys/class/gpio/gpio17/value
echo 0 > /sys/class/gpio/gpio17/value

# Set as input and read
echo "in" > /sys/class/gpio/gpio17/direction
cat /sys/class/gpio/gpio17/value

# Unexport when done
echo 17 > /sys/class/gpio/unexport
Signals & Complete Driver Patterns
What are Signals in Linux? How can a driver send signals to user-space? ▼
SignalsIPC

Signals are software interrupts sent to a process to notify it of an event. A kernel driver can send signals (like SIGIO) to user-space processes to notify them of hardware events — an alternative to polling or blocking reads.

SignalDefault ActionCommon Use
SIGIOIgnoredAsync I/O notification from driver
SIGKILLTerminateForce-kill process (uncatchable)
SIGTERMTerminateGraceful shutdown request
SIGUSR1/2TerminateUser-defined events
SIGSEGVCore dumpSegmentation fault (invalid memory)
/* ── Kernel driver: send SIGIO to user process ── */
#include <linux/signal.h>
#include <linux/sched/signal.h>

static struct fasync_struct *async_queue;

/* Handle fasync() system call from user space */
static int my_fasync(int fd, struct file *filp, int mode)
{
    return fasync_helper(fd, filp, mode, &async_queue);
}

/* In ISR or timer: notify user-space process */
void notify_user(void)
{
    if (async_queue)
        kill_fasync(&async_queue, SIGIO, POLL_IN);
}

/* Add to file_operations */
static struct file_operations my_fops = {
    .fasync  = my_fasync,
    /* ... other ops ... */
};

/* ── User space: receive async notification ── */
/*
#include <signal.h>
#include <fcntl.h>

void sigio_handler(int sig) {
    printf("Async data ready from driver!\n");
}

int main() {
    int fd = open("/dev/my_device", O_RDWR);
    signal(SIGIO, sigio_handler);        // Install handler
    fcntl(fd, F_SETOWN, getpid());       // This process owns the signal
    fcntl(fd, F_SETFL, O_ASYNC | O_NONBLOCK);  // Enable async
    pause();                              // Wait for signal
}
*/
Complete Real-World Driver: All pieces together ▼
Full DriverProduction Pattern

This shows the complete, production-quality skeleton of a character driver combining all the key pieces: dynamic allocation, udev, mutex, wait queue, and proper cleanup.

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/mutex.h>
#include <linux/wait.h>
#include <linux/uaccess.h>

#define DEVICE_NAME "mydev"
#define BUF_SIZE    1024

MODULE_LICENSE("GPL");
MODULE_AUTHOR("EmbeddedLinuxCareer");

static dev_t             dev_num;
static struct cdev       my_cdev;
static struct class     *my_class;
static DEFINE_MUTEX(dev_mutex);
static DECLARE_WAIT_QUEUE_HEAD(read_queue);

static char   kernel_buf[BUF_SIZE];
static int    data_ready = 0;

static int my_open(struct inode *i, struct file *f)
{
    pr_info("%s: opened\n", DEVICE_NAME);
    return 0;
}

static int my_release(struct inode *i, struct file *f)
{
    pr_info("%s: closed\n", DEVICE_NAME);
    return 0;
}

static ssize_t my_read(struct file *f, char __user *buf,
                       size_t count, loff_t *off)
{
    /* Block until data is available */
    if (wait_event_interruptible(read_queue, data_ready))
        return -ERESTARTSYS;

    mutex_lock(&dev_mutex);
    if (copy_to_user(buf, kernel_buf, min(count, (size_t)BUF_SIZE))) {
        mutex_unlock(&dev_mutex);
        return -EFAULT;
    }
    data_ready = 0;
    mutex_unlock(&dev_mutex);
    return min(count, (size_t)BUF_SIZE);
}

static ssize_t my_write(struct file *f, const char __user *buf,
                        size_t count, loff_t *off)
{
    mutex_lock(&dev_mutex);
    if (copy_from_user(kernel_buf, buf, min(count, (size_t)BUF_SIZE))) {
        mutex_unlock(&dev_mutex);
        return -EFAULT;
    }
    data_ready = 1;
    mutex_unlock(&dev_mutex);

    wake_up_interruptible(&read_queue);   /* Wake blocked readers */
    return count;
}

static struct file_operations fops = {
    .owner   = THIS_MODULE,
    .open    = my_open,
    .release = my_release,
    .read    = my_read,
    .write   = my_write,
};

static int __init my_init(void)
{
    alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
    cdev_init(&my_cdev, &fops);
    my_cdev.owner = THIS_MODULE;
    cdev_add(&my_cdev, dev_num, 1);
    my_class = class_create(THIS_MODULE, DEVICE_NAME);
    device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
    pr_info("/dev/%s ready (major=%d)\n", DEVICE_NAME, MAJOR(dev_num));
    return 0;
}

static void __exit my_exit(void)
{
    device_destroy(my_class, dev_num);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(dev_num, 1);
    pr_info("/dev/%s removed\n", DEVICE_NAME);
}

module_init(my_init);
module_exit(my_exit);
🔒 Premium Course

Unlock 400+ Interview
Questions

Deep-dive coverage of C programming, Operating Systems, Linux System Programming, Kernel Development, RTOS, and Advanced ARM — all in one place.

✦
Part 1: C & Data Structures50 questions — Pointers, memory, algorithms
✦
Part 2: Operating Systems50 questions — Scheduling, deadlocks, sync
✦
Part 3: Linux System Programming50 questions — Processes, IPC, sockets
✦
Part 4: Embedded & ARM50+ questions — Cortex, power, optimization
✦
Part 5: Kernel & Device Drivers50 questions — LKM, sync, interrupts
✦
Part 7: RTOS & Real-Time30 questions — Scheduling, memory, sync
₹199

One-time payment  ·  Lifetime access  ·  No subscription

Unlock Premium Course →