Featured image of post Linux Device Driver Deep Dive

Linux Device Driver Deep Dive

Presentation-Walk through of how Device Drivers work in Linux

=====================================

Ariticle History- About this Blog Post
Below is my best attempt to re-format a series of PPTs I built in the past so I can publish on this blog

For much of my career, I have been the driving person behind “Lunch and Learns” - where every other friday myself and other team members would present something they learned to the others at lunch.

Many times- Me or another would get a book- and then convert the book into a series of Powerpoints for a Lunch and learn

For this presentation, I was reading

“Linux Device Drivers, Third Edition”
Written by Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman.

The first time I delivered this presentation was around 2007. And I have pulled it out, for lunch and learns, from time to time since then.

Its important to also know- that the Linux version around this time was v2.6.20, and its now around v6.

Undoubtedly, the Linux kernel is not the same as it was then, but I still think these old presentations are useful.

So enjoy my re-constitution \ conversion of material.

=====================================

Understanding Linux Device Drivers

Presentation Based on

“Linux Device Drivers, Third Edition”
Written by Jonathan Corbet, Alessandro Rubini, and Greg Kroah-Hartman.

Linux Kernel v2.6.20

Why Bother with Device Drivers?

Imagine buying a shiny new gadget—let’s call it the “Turbo Encabulator 3000.” You plug it into your Linux machine, and…
nothing.
Crickets.
Your system doesn’t know what to do with this alien contraption.
That’s where device drivers come in.

The Kernel’s Modular Magic

The Linux kernel is like a well-organized toolbox.

Instead of being a monolithic beast, it’s designed to be modular.

This means you can add or remove pieces—like device drivers—without having to rebuild the whole darn thing.

It’s like adding a new app to your phone without reinstalling the OS.

Mechanism vs. Policy: The Great Divide

In the Unix world, there’s a golden rule: separate mechanism (the “how”) from policy (the “what”).

Think of it this way:

the mechanism is the engine under the hood, while the policy is the person behind the wheel deciding where to go.

Classes of Devices: A Zoo of Gadgets

  • Character Devices: These are like the scribes of the system, handling data one character at a time.
  • Block Devices: The heavy lifters, managing data in chunks or blocks.

Security: Keeping the Bad Guys at Bay

With great power comes great responsibility.
(So sayeth the Spiderman)

Version Numbering: Keeping Up with the Times

The Linux kernel is a living, breathing entity, evolving faster than a caffeinated cheetah.

Keeping your drivers compatible means staying on top of version changes.

License Terms: Sharing is Caring

Linux is all about open-source goodness.

Joining the Kernel Development Community

Writing device drivers isn’t a solitary endeavor.

The Linux kernel community is a bustling bazaar of developers, testers, and the occasional opinionated penguin.

Setting Up Your Playground

Before we unleash our coding prowess, we need a proper playground.

  1. Get the Kernel Source: First things first, grab the latest and greatest kernel source code from kernel.org.

Why? Because working with the mainline kernel ensures we’re on the same page as the cool kids.

  1. Build and Install the Kernel: Once you’ve got the source, it’s time to configure, compile, and install it.

This isn’t as scary as it sounds. Just follow the steps, and soon you’ll have a shiny new kernel running.

  1. Designate a Test Machine: Kernel development is like playing with fire.

It’s exciting, but you don’t want to burn down your house.

Use a test machine or a virtual environment.

This way, if things go south, your main system remains unscathed.

Hello, Kernel!

Time to write our first kernel module.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("Dual BSD/GPL");

static int __init hello_init(void)
{
    printk(KERN_ALERT "Hello, world\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_ALERT "Goodbye, cruel world\n");
}

module_init(hello_init);
module_exit(hello_exit);
  • hello_init: This function runs when our module loads.
  • hello_exit: This function runs when our module unloads.

Compiling the Module

  1. Create a Makefile: The kernel build system loves Makefiles. Here’s a simple one:

    1
    2
    3
    4
    5
    6
    7
    
    obj-m += hello.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
    
  2. Build It: Run make, and if the stars align, you’ll get a hello.ko file. That’s your module, ready to make its debut.

Loading and Unloading the Module

  • Insert the Module: Use sudo insmod hello.ko to load your module.
  • Remove the Module: When you’re done, sudo rmmod hello bids farewell.

Safety First!

  • Backup Important Data: Kernel mishaps can lead to data loss.
  • Use Version Control: Track your changes.
  • Stay Calm: Mistakes happen.

Char Drivers: The Not-So-Scary World of Linux Device Drivers

Imagine your computer’s memory is a bustling city.

In this metropolis, char drivers are like the friendly neighborhood shops—you interact with them by reading from or writing to them, one byte at a time.

Meet SCULL: Our Training Wheels

Enter SCULL (Simple Character Utility for Loading Localities).

Think of SCULL as a virtual playground—a char driver that doesn’t rely on any pesky hardware.

Instead, it uses your computer’s memory as its sandbox.

This means you can experiment without worrying about frying your motherboard.

  • scull0 to scull3: Four devices sharing a communal memory space.
    It’s like a public park—everyone can play, and the data persists even after you leave.
  • scullpipe0 to scullpipe3: Think of these as message tubes between processes.
    One process writes, another reads.
  • scullsingle, scullpriv, sculluid, scullwuid: These devices come with exclusive access rules.

Major and Minor Numbers: The Device Address Book

In the grand Linux filesystem, device files reside in the /dev directory.

Rolling Up Our Sleeves: Writing a Char Driver

  1. Initialization: Set up the driver’s internal structures and register it with the kernel. It’s like opening your shop and putting up the “Open” sign.

  2. File Operations: Define how your driver handles operations like open, read, write, and release. These are the services your shop offers to its patrons.

  3. Registration: Assign those all-important major and minor numbers and create device files in /dev. Now, customers know how to find you.

  4. Cleanup: When it’s time to close up shop, ensure you unregister your driver and free up resources. A tidy shopkeeper leaves no mess behind.

Testing Our Creation

Here’s how you can test your newly created TTY driver using echo and cat. Let’s assume your TTY driver has registered a device at /dev/mytty.

Step 1: Load Your Driver

Before testing, ensure your TTY driver module is loaded:

1
sudo insmod my_tty_driver.ko

Now, check if the device node exists:

1
ls -l /dev/mytty

If the device file isn’t there, create it manually using mknod (replace major_number with your driver’s assigned major number):

1
sudo mknod /dev/mytty c <major_number> 0

Step 2: Sending Data to the TTY Device

You can write data to the device using echo:

1
echo "Hello, TTY!" > /dev/mytty

If your driver is implemented correctly, it should capture this input and process it accordingly.

Step 3: Reading Data from the TTY Device

To check if your device is returning any output, use cat:

1
cat /dev/mytty

If your driver is working as expected, you should see the data echoed back.

Step 4: Checking System Logs

If something goes wrong, check the system logs for any error messages or debugging output:

1
dmesg | tail -50

This will show recent kernel messages, which might include debugging info from your driver.

Step 5: Interactive Testing with minicom

For a more interactive approach, use minicom:

1
sudo minicom -D /dev/mytty

If your driver correctly implements TTY functionality, you should be able to type in minicom and see the input processed.


Troubleshooting

If things don’t work as expected:

  • Check the permissions on /dev/mytty:

    1
    
    ls -l /dev/mytty
    

    If needed, adjust them with:

    1
    
    sudo chmod 666 /dev/mytty
    
  • Ensure your driver is correctly registered in /proc/tty/drivers:

    1
    
    cat /proc/tty/drivers
    
  • Look for kernel errors with:

    1
    
    dmesg | grep mytty
    

Debugging the Kernel: When printf Just Isn’t Enough

The Kernel’s Built-in Debugging Goodies

Before you start scattering printk statements like confetti, it’s worth noting that the Linux kernel comes equipped with its own set of debugging features.
However, these aren’t always enabled in the stock kernels provided by distributions.

  • CONFIG_DEBUG_KERNEL: Unlocks a treasure trove of debugging options.

  • CONFIG_DEBUG_SLAB: Ever wondered if your memory allocations are misbehaving?
    This option adds checks to the kernel’s memory allocator, helping you catch those pesky overflows and use-after-free bugs.
    Plus, it fills allocated memory with the charming pattern 0xa5 and freed memory with 0x6b.

  • CONFIG_DEBUG_PAGEALLOC: Removes pages from the kernel’s address space when they’re freed.
    It’s like hiring a bouncer to kick out rogue memory accesses.

  • CONFIG_DEBUG_SPINLOCK & CONFIG_DEBUG_SPINLOCK_SLEEP: These options are the kernel’s way of saying, “Don’t you dare mess up with spinlocks!” They catch uninitialized spinlocks and complain if you try to sleep while holding one.

  • CONFIG_INIT_DEBUG: Ensures that code meant to run only during initialization doesn’t overstay its welcome.

  • CONFIG_DEBUG_INFO & CONFIG_FRAME_POINTER: Include these if you fancy debugging the kernel with gdb.
    They provide the necessary debugging symbols and frame pointers.

  • CONFIG_MAGIC_SYSRQ: Enables the “magic SysRq” key—a mystical key combination that lets you perform various low-level commands, even when your system is on the brink of meltdown.

  • CONFIG_DEBUG_STACKOVERFLOW & CONFIG_DEBUG_STACK_USAGE: These keep an eye on your kernel’s stack, ensuring it doesn’t overflow like a poorly managed buffer.

  • CONFIG_KALLSYMS: Builds symbol information into the kernel, turning cryptic oops messages into something a tad more decipherable.

printk: The Kernel Developer’s BFF

When it comes to kernel debugging, printk is your go-to function for printing messages.

It’s like printf’s older, wiser sibling, designed to play nice in the kernel’s unique environment.

  • Log Levels: printk messages come with log levels, indicating their importance.

    1
    
    printk(KERN_INFO "The foo variable is: %d\n", foo);
    

This prints an informational message about the current state of foo.

  • Viewing Messages: To see your printk masterpieces, check the system log (usually /var/log/syslog or /var/log/messages) or use the dmesg command.

Deciphering Oops Messages

An “oops” in kernel land is more than just a minor slip-up.

  • EIP Value: The instruction pointer at the time of the crash.

  • Call Trace: A breadcrumb trail of function calls leading up to the oops.

When the System Hangs: Summoning the Magic SysRq

Sometimes, your system doesn’t just oops—it freezes entirely.
In such dire moments, the magic SysRq key combo is your best friend.

  1. Enable SysRq: Ensure it’s enabled by echoing 1 to /proc/sys/kernel/sysrq:

    1
    
    echo 1 > /proc/sys/kernel/sysrq
    
  2. Invoke Commands: Press Alt + SysRq (that’s the “Print Screen” key) and then a command key. Some handy commands include:

Remember, with great power comes great responsibility. (Again with the Spiderman…)

Debugging Tools: More Than Just printf

  • gdb: The GNU Debugger can attach to a running kernel, but it’s not as straightforward as user-space debugging.
    You’ll need to set up a serial or network connection to another machine.

  • kgdb: A built-in kernel debugger that works with gdb.
    It allows you to set breakpoints and step through code.

  • ftrace: A tracing framework that lets you track function calls and more.

Concurrency and Race Conditions: The Untamed Wilds of Kernel Programming

The Scull Driver: A Cautionary Tale

Let’s go back to the scull driver we discussed earlier….
Imagine two processes, A and B, both trying to write to the same spot in the scull device.

1
2
3
4
5
if (!dptr->data[s_pos]) {
    dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
    if (!dptr->data[s_pos])
        goto out;
}

Both see that dptr->data[s_pos] is as empty as a developer’s coffee cup on a Monday morning.

So, they both decide to allocate memory.

Process A finishes first, but before it can bask in its success, Process B swoops in and overwrites the pointer.

The memory allocated by A?

Lost to the abyss.

Taming the Beast: Synchronization to the Rescue

To prevent your driver from descending into anarchy, synchronization is key.

The Chaos of Concurrency

Imagine two hungry programmers, Alice and Bob, trying to grab the last coffee cup in the break room. Both reach for it at the same time.

Who gets it?

Maybe Alice.

Maybe Bob.

Maybe the cup falls and shatters into a million pieces (kernel panic, anyone?).

This is exactly what happens in kernel drivers when multiple processes try to access a shared resource without synchronization.

The Kernel’s Wild West

In a Linux driver, concurrency problems arise from multiple sources:

  • Multiple processes can access your driver simultaneously.
  • SMP systems (multiple CPUs) mean your driver code can run on different processors at the same time.
  • Interrupts can strike while your driver is in the middle of an operation.
  • Preemptive scheduling allows another process to hijack execution at the worst possible moment.

Without synchronization, one process might be reading while another is writing, leading to unpredictable behavior—just like two people trying to type on the same keyboard.


Locking It Down: Kernel Synchronization Tools

To keep things under control, the Linux kernel provides synchronization mechanisms. Here are the main ones:

1. Spinlocks: The “Busy” Solution

Think of a spinlock as a “WAIT YOUR TURN” sign. If one CPU is using a resource, any other CPU trying to access it will just keep spinning (waiting) until it’s free.

Example:

1
2
3
4
5
spinlock_t my_lock;

spin_lock(&my_lock);   // Lock the resource
// Critical section: Do something important
spin_unlock(&my_lock); // Release the resource

✅ Best for short, quick operations
❌ Bad if you have long operations (because the waiting CPU is just wasting cycles)


2. Mutexes: The “Chill Out and Wait” Approach

Mutexes are like the spinlocks’ more patient cousin. Instead of spinning and wasting CPU, a mutex puts the waiting process to sleep until the resource is free.

Example:

1
2
3
4
5
struct mutex my_mutex;

mutex_lock(&my_mutex);   // Lock the resource
// Critical section: Access shared data
mutex_unlock(&my_mutex); // Release the resource

✅ Great for long operations
❌ Can’t be used in interrupt context (interrupt handlers can’t sleep!)


3. Atomic Operations: The “One Shot” Fix

Sometimes, all you need is a quick single-step update to a variable, without worrying about locks.

Example:

1
2
3
4
atomic_t counter = ATOMIC_INIT(0);

atomic_inc(&counter);  // Safely increment counter
atomic_dec(&counter);  // Safely decrement counter

✅ Super fast
❌ Only works for simple operations (no complex logic)


4. Read-Copy-Update (RCU): The “Ninja Stealth” Method

RCU lets readers access data without locking, while writers update it in a way that ensures consistency.

1
2
3
4
5
6
7
rcu_read_lock();
ptr = rcu_dereference(shared_data);  // Read safely
rcu_read_unlock();

// Updating:
rcu_assign_pointer(shared_data, new_value);
synchronize_rcu();

✅ Amazing for performance
❌ Tricky to use correctly


Real-World Example: Protecting a Shared Buffer

Let’s say your driver has a shared buffer that multiple processes can read and write. You could use a mutex to prevent corruption:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
struct mutex buffer_lock;
char my_buffer[256];

ssize_t my_driver_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) {
    mutex_lock(&buffer_lock);   // Lock before writing

    if (count > sizeof(my_buffer))
        count = sizeof(my_buffer);

    if (copy_from_user(my_buffer, buf, count))
        return -EFAULT;

    mutex_unlock(&buffer_lock); // Unlock after writing
    return count;
}

Without this lock, if two processes write at the same time, the buffer could end up with random, corrupted data.


Advanced Char Driver Operations: Beyond the Basics

The Mysterious World of ioctl

Imagine you’re at a fancy restaurant.

Reading and writing are like ordering from the menu.

But what if you want your steak cooked exactly 3.14159 minutes on each side?

1
int ioctl(int fd, unsigned long cmd, ...);

It’s the Swiss Army knife of system calls, allowing you to perform device-specific operations beyond the standard read/write.

But beware!
With great power comes great responsibility.
(Yes.. I am toally into Spiderman…)

The unstructured nature of ioctl can lead to chaos if not managed properly.

Blocking I/O: Hurry Up and Wait

Blocking I/O is like waiting in line for your morning coffee.
Your process makes a request and then…
waits.

  1. Checking Conditions: Before sleeping, always check if the data is already available. No need to nap if your coffee’s ready.
  2. Sleeping: If the data isn’t ready, use functions like wait_event_interruptible() to catch some Z’s.
  3. Waking Up: Once the data is ready (the coffee’s brewed), wake up the process.

Nonblocking I/O: I Want It Now!

Nonblocking I/O is for the impatient ones.
It’s like walking into the café, seeing the long line, and deciding to come back later.

  • Check in the Driver: In your driver, check if this flag is set using filp->f_flags & O_NONBLOCK.

Polling: Are We There Yet?

  1. Set Up a poll_table: This keeps track of processes interested in the device’s status.
  2. Check Status: Determine if the device is ready for reading or writing.
  3. Return Events: Return a bitmask indicating the device’s status (e.g., POLLIN for readable, POLLOUT for writable).

Access Control: Who Goes There?

Not everyone should have access to your precious device.

Tick-Tock: Mastering Time, Delays, and Deferred Work in the Linux Kernel

Jiffies: Not the Breakfast Spread

In kernel land, jiffies is the ever-incrementing counter that ticks away with each timer interrupt.
Think of it as the kernel’s heartbeat, marking the passage of time since the system booted up.

How Fast Does It Tick?

The frequency of these ticks is defined by the magical constant HZ.

This means if HZ is set to 1000, the kernel experiences 1000 ticks per second.

Reading the Jiffies

Accessing the current value of jiffies is as simple as checking your watch—if your watch were a volatile unsigned long variable.

1
2
3
4
5
6
#include <linux/jiffies.h>

unsigned long start_time = jiffies;
/* Do some fancy processing here */
unsigned long end_time = jiffies;
unsigned long elapsed = end_time - start_time;

Just remember, jiffies is a read-only variable.

Delays: The Art of Napping

Sometimes, your driver needs to take a breather.
Maybe it’s waiting for hardware to catch up or just needs a moment to ponder life’s mysteries.

Busy Waiting: The Impatient Approach

1
2
3
4
#include <linux/delay.h>

udelay(10); // Sleep for 10 microseconds
ndelay(500); // Sleep for 500 nanoseconds

Sleeping: The Relaxed Approach

1
2
3
#include <linux/delay.h>

msleep(50); // Sleep for 50 milliseconds

Deferred Work: Procrastination Done Right

Sometimes, you need to schedule work to be done later, because immediate gratification isn’t always possible.

Workqueues: The Delegates

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <linux/workqueue.h>

static void my_work_function(struct work_struct *work) {
    /* Do some work here */
}

DECLARE_WORK(my_work, my_work_function);

/* Schedule the work */
schedule_work(&my_work);

Tasklets: The Lightweights

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <linux/interrupt.h>

static void my_tasklet_function(unsigned long data) {
    /* Quick, non-sleeping work here */
}

DECLARE_TASKLET(my_tasklet, my_tasklet_function, 0);

/* Schedule the tasklet */
tasklet_schedule(&my_tasklet);

Memory Allocation in the Kernel

kmalloc: The Kernel’s Own malloc

First up, we have kmalloc, the kernel’s version of malloc.

It’s like the kernel’s personal shopper for memory: fast, efficient, and doesn’t bother clearing out the old data (because who has time for that?).

But remember, with great power comes great responsibility.
(!!!!)

The Flags: GFP What?

When calling kmalloc, you need to specify how you want your memory served.

  • GFP_KERNEL: The standard flag for code running in process context.

  • GFP_ATOMIC: For those moments when you’re in a hurry (like interrupt handlers).

  • GFP_USER and GFP_HIGHUSER: Used for allocating memory that will eventually make its way to user space.

The Slab Allocator: Custom Memory Carving

If kmalloc is like buying pre-packaged snacks, the slab allocator is your personal chef.

It prepares memory objects of specific sizes, so you don’t have to repeatedly slice and dice your memory.

vmalloc: When You Need the Big Guns

Sometimes, kmalloc just won’t cut it, especially when you need a large, contiguous chunk of memory.
Enter vmalloc.

It’s like renting a storage unit: it gives you a contiguous virtual address space, but under the hood, it might be piecing together non-contiguous physical pages.

The downside?

Per-CPU Variables: Keeping Things Local

In the world of SMP (Symmetric Multiprocessing), sharing is not caring.

To avoid the chaos of multiple processors stepping on each other’s toes, we have per-CPU variables.

Each processor gets its own copy of a variable, ensuring they don’t brawl over a single memory spot.

Conclusion: Handle with Care

Memory allocation in the kernel isn’t just about grabbing what you need; it’s about being a good citizen.

Always free what you allocate, choose the right tool for the job, and remember: with great power comes great responsibility.

Communicating with Hardware: When Your Code Needs to Chat with Gadgets

From Scull to the Real Deal

Remember our old friend, the scull driver?
It was fun while it lasted, but let’s face it—pretending to interact with hardware isn’t as thrilling as the real thing.

I/O Ports and I/O Memory: The Hardware Hangouts

  • I/O Ports: Think of these as exclusive clubs where only the cool devices get their own special addresses.

  • I/O Memory: This is the general playground where devices map their registers into the system’s memory space.

The Parallel Port: Your Hardware BFF

To keep things simple, let’s cozy up to the parallel port.

It’s like the “Hello, World!” of hardware interaction.

By writing data to this port, you can control external devices—like lighting up LEDs or sending data to a printer.

Mind the Gap: Differences Between Memory and I/O

  • Memory: What you write is what you get.

  • I/O Registers: These guys have side effects.
    Writing to them can trigger actions, and reading from them might give you fresh data each time.

Playing Nice: Ensuring Smooth Communication

  • Avoid Assumptions: Hardware can be unpredictable.

  • Respect Timing: Some devices need time to process data.

Interrupt Handling: When Hardware Just Can’t Wait to Interrupt Your Day

The Basics: Why Interrupts?

Imagine your CPU as a chef in a bustling kitchen.

It’s got a million tasks to juggle, from sautéing code to flambéing processes.
Now, if the chef had to constantly check the oven to see if the roast (i.e., your hardware device) was done, nothing else would get cooked.

Instead, wouldn’t it be better if the oven could just ding when the roast is ready?

The Parallel Port: A Blast from the Past

To dive into the world of interrupts, we’ll take a nostalgic trip to the land of parallel ports.

Remember those?

They’re like the rotary phones of computer interfaces.

Setting Up the Parallel Port for Interrupts

Before our parallel port can start waving its hands frantically to get the CPU’s attention, we need to enable its interrupt capabilities.

This involves setting bit 4 of port 2 (addresses like 0x37A or 0x27A).

Once that’s done, the port will generate an interrupt whenever pin 10 (the ACK signal) goes from low to high.

The easiest way to make this happen?

Connect pin 9 (the data strobe) to pin 10.

A tiny wire bridging these pins will do the trick.

Writing the Interrupt Handler: The Unsung Hero

Now that our parallel port is all set to interrupt at the drop of a bit, we need someone (or something) to handle these interruptions gracefully.

Registering the Handler

To get our handler into the game, we register it with the kernel using the request_irq function.

It’s like signing up your handler for interrupt duty.

Handling the Interrupt

  1. Acknowledge the Interrupt: Let the hardware know, “Got it! I’m on it.”
  2. Do the Necessary Processing: Whatever needs to be done in response to the interrupt—read data, clear flags, etc.
  3. Exit Swiftly: Interrupt handlers should be like ninjas—come in, do the job, and disappear without a trace. Lingering around can hold up the system, and nobody wants that.

Cleaning Up: Don’t Forget to Say Goodbye

When your driver is done and it’s time to pack up, it’s crucial to release the interrupt line using free_irq.

Think of it as cleaning up your workspace before leaving.

Kernel Data Types: The Wild World of Bits and Bytes

The Standard C Types: A Cautionary Tale

Once upon a time, you might have thought that an int is an int is an int.
But in the kernel’s realm, that’s a fairy tale.

Depending on the architecture, your beloved int could be 16, 32, or even 64 bits.
Imagine the shock when your 32-bit long on an x86 system morphs into a 64-bit behemoth on an Alpha machine.

1
2
3
arch Size: char short int long ptr long-long u8 u16 u32 u64
i386 1    2     4   4    4   8        1  2   4   8
alpha 1   2     4   8    8   8        1  2   4   8

See?
On an Alpha system, your long and pointers are living large at 8 bytes, while on i386, they’re modest 4-byte citizens.

The moral of the story?

Enter Explicitly Sized Types: The Heroes We Need

To tame this wild west of data sizes, the kernel offers explicitly sized types.
Meet the u8, u16, u32, and u64 family for unsigned integers, and their signed cousins s8, s16, s32, and s64.

So, when you’re dealing with hardware registers or data structures that demand precision, ditch the ambiguous int and long types.

Instead, embrace the clarity of u32 and friends.

Special Kernel Types: Because Why Not?

The kernel, in its infinite wisdom, also defines types for specific objects.

Need to handle process IDs?

Say hello to pid_t.

Working with user IDs?

uid_t is at your service.

These types not only make your code more readable but also shield you from the chaos of underlying implementation changes.

Portability Pitfalls: The Architectures Strike Back

Porting code across architectures isn’t just about data sizes.
Oh no, the kernel has more tricks up its sleeve.

For instance, some architectures are picky about data alignment.
Misaligned access can lead to performance hits or, worse, catastrophic crashes.

Linked Lists: The Kernel’s Favorite Data Structure

In user space, you might reach for arrays or fancy data structures from your favorite library.

But in kernel land, simplicity and efficiency reign supreme.

The kernel offers a robust linked list implementation with macros and helper functions to make your life easier.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct my_struct {
    int data;
    struct list_head list;
};

struct list_head my_list = LIST_HEAD_INIT(my_list);

// Adding an element
struct my_struct *new_entry = kmalloc(sizeof(*new_entry), GFP_KERNEL);
new_entry->data = 42;
list_add(&new_entry->list, &my_list);

With this setup, you can traverse, add, and remove elements without reinventing the wheel.

PCI Drivers: Wrangling the Wild Stallions of the Hardware Frontier

The PCI Rodeo: More Than Just Wires and Slots

When most folks think of PCI (Peripheral Component Interconnect), they picture a mess of wires and slots.

But hold your horses!

PCI is more than just a tangled tumbleweed; it’s a full-fledged set of rules dictating how your computer’s parts play nice together.

Why PCI? Because ISA Was Slower Than a Three-Legged Mule

Back in the day, we had the ISA bus, which was about as fast as molasses in January.

But the real kicker?
PCI devices are jumperless.
No more fiddling with tiny pins; these bad boys auto-configure at boot time.

Finding Your Steed: PCI Addressing

In Linux, you don’t have to track these manually.

The Lay of the Land: PCI System Layout

Picture this: a sprawling ranch with multiple corrals (buses), all linked by gates (bridges).
Each bus can host up to 32 devices, and each device can juggle up to 8 functions.

To see the lay of your land, tools like lspci (from the pciutils package) can list all the devices in your system.

Mounting Up: Writing a PCI Driver

Ready to ride?

  1. Include the Right Gear: Start with the necessary headers:
    1
    2
    3
    
    #include <linux/pci.h>
    #include <linux/init.h>
    #include <linux/module.h>
    
  2. Identify Your Steed: Define the PCI devices your driver will handle using pci_device_id structures.
  3. Register with the Sheriff: Use pci_register_driver to let the kernel know you’re in town and ready to manage your devices.
  4. Tame the Beast: In your probe function, set up the device (allocate resources, map memory, etc.).
  5. Clean Up After the Rodeo: Implement a remove function to release resources when the device is no longer needed.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static int __init my_pci_driver_init(void)
{
    return pci_register_driver(&my_pci_driver);
}

static void __exit my_pci_driver_exit(void)
{
    pci_unregister_driver(&my_pci_driver);
}

module_init(my_pci_driver_init);
module_exit(my_pci_driver_exit);

Remember, this is just the tip of the iceberg.

USB Drivers: Taming the Wild World of Plug-and-Play

Ah, USB—the Universal Serial Bus.

It’s the magical port that lets us connect everything from keyboards and mice to dancing hula girl desk ornaments.

But have you ever wondered what sorcery happens behind the scenes to make all these gadgets work seamlessly?

A Brief History of USB: From Chaos to Harmony

Once upon a time, connecting devices to your computer was like trying to herd cats.
Each gadget had its own special port, cable, and secret handshake.

Enter USB, the superhero of connectivity, swooping in to unify these unruly peripherals under a single standard.

The USB Topology: A High-Tech Family Tree

USB Classes: Sorting Devices into Neat Little Boxes

Writing a USB Driver: Channeling Your Inner Wizard

Ready to conjure up a USB driver?

  1. Include the Magical Incantations:

    1
    2
    
    #include <linux/usb.h>
    #include <linux/module.h>
    
  2. Define Your Device’s True Name:

    1
    2
    
    #define VENDOR_ID  0x1234
    #define PRODUCT_ID 0x5678
    

Replace these placeholders with your device’s actual Vendor ID and Product ID.
No, you can’t just make them up.

  1. Create a Table of Legends:

    1
    2
    3
    4
    5
    
    static struct usb_device_id my_usb_table[] = {
        { USB_DEVICE(VENDOR_ID, PRODUCT_ID) },
        { }
    };
    MODULE_DEVICE_TABLE(usb, my_usb_table);
    
  2. Write the Enchanted Functions:

  • Probe Function: Called when your device plugs in.

  • Disconnect Function: Called when your device unplugs.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    static int my_usb_probe(struct usb_interface *interface, const struct usb_device_id *id) {
        printk(KERN_INFO "My USB device (%04X:%04X) plugged in\n", id->idVendor, id->idProduct);
        // Initialize your device here
        return 0;
    }
    
    static void my_usb_disconnect(struct usb_interface *interface) {
        printk(KERN_INFO "My USB device removed\n");
        // Cleanup your device here
    }
    
  1. Register Your Driver with the Council:
    1
    2
    3
    4
    5
    6
    7
    8
    
    static struct usb_driver my_usb_driver = {
        .name = "my_usb_driver",
        .id_table = my_usb_table,
        .probe = my_usb_probe,
        .disconnect = my_usb_disconnect,
    };
    
    module_usb_driver(my_usb_driver);
    

This macro handles the registration and deregistration of your driver.

The Linux Device Model: Herding Cats in the Kernel

Ever tried organizing a group of hyperactive kittens?

That’s pretty much what managing devices in the Linux kernel was like before the 2.6 release.

Each device did its own thing, and the kernel had to play catch-up.

Why Bother with a Device Model?

Imagine trying to power down your computer, but your USB blender is still running.
Not ideal, right?

The Grand Blueprint: How It All Fits Together

Think of the device model as a family tree for your hardware.
At the root, you have the core system buses.

Branching out are devices connected to these buses, and further still are the functions these devices perform.

For example, your USB mouse isn’t just a mouse.

It’s a USB device connected to a USB bus, recognized by the system as an input device.

sysfs: The Gossip Hub of Devices

Ever wanted to peek into the inner workings of your devices?

Enter sysfs, the virtual filesystem that spills all the juicy details.

Mounted at /sys, sysfs lays out the device model hierarchy for all to see.

Want to know what devices are connected to your PCI bus?

Just navigate to /sys/bus/pci/devices/.

Curious about the drivers handling your USB gadgets?

Check out /sys/bus/usb/drivers/.

Kobjects: The Unsung Heroes

Behind the scenes of the device model are kobjects—the kernel’s way of keeping track of objects and their lifecycles.

Wrangling Devices: A Day in the Life

  1. Detection: The kernel spots the new device and assigns it a kobject.
  2. Registration: The device is registered within the device hierarchy, finding its place in the family tree.
  3. sysfs Integration: A new entry pops up in sysfs, letting you (and the rest of the system) know about the new arrival.
  4. Driver Binding: The kernel searches for a suitable driver to handle the device. If it finds one, they shake hands and get to work.

Memory Mapping and DMA: The Dynamic Duo of Linux Kernel Adventures

Ah, memory mapping and DMA—two peas in the Linux kernel pod.

If you’ve ever wanted to peek under the hood of your operating system and see how it juggles memory like a circus performer, you’re in the right place.

The Grand Illusion: Virtual Memory

Imagine you’re at an all-you-can-eat buffet.

To you, it seems like there’s an endless supply of food, but behind the scenes, the kitchen is frantically refilling trays.

That’s virtual memory for you.

mmap: Rolling Out the Red Carpet for User Space

Sometimes, user programs need to peek into the kernel’s world—maybe to interact with hardware or share memory with other processes.

  1. Set Up the mmap File Operation: Add an mmap function to your driver’s file operations.
  2. Handle the Mapping: In your mmap function, use remap_pfn_range to map device memory to user space.
  3. Manage Permissions: Ensure only the right folks get access. You don’t want just anyone wandering into the VIP lounge.
1
2
3
4
5
6
7
8
9
static int my_device_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long pfn = /* the physical frame number */;
    unsigned long size = vma->vm_end - vma->vm_start;

    if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
        return -EAGAIN;
    return 0;
}

DMA: Direct Memory Access (or Don’t Mess Around)

DMA is like having a personal assistant who handles data transfers for you.

  1. Allocating a DMA-Capable Buffer: Use dma_alloc_coherent to get a buffer both the device and CPU can agree on.
  2. Mapping the Buffer: Ensure the device knows where to find the buffer in its own address space.
  3. Synchronizing: Make sure the CPU and device don’t step on each other’s toes. Use memory barriers if necessary.
1
2
3
4
5
6
7
dma_addr_t dma_handle;
void *cpu_addr;

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
if (!cpu_addr) {
    /* Handle allocation failure */
}

Remember, with great power (and direct memory access) comes great responsibility.
(!!!!!!!!!!!!!!)

Platform Devices: Homegrown Hardware Without the Glamour

Welcome to the quirky world of platform devices!

If you’ve ever been puzzled by those mysterious devices that just don’t show up on your system’s “Plug and Play” radar, you’re in the right place.

What Exactly Are Platform Devices?

Imagine you’ve got a custom gadget built for your secret spy mission, but it doesn’t come with a label or instructions.

That’s pretty much a platform device.

These devices are often built into the hardware (like on embedded boards or custom-designed systems) and require you to tell the kernel about them manually.
No automatic detection?

The Lowdown on Registration

Unlike PCI or USB devices, platform devices don’t announce themselves.

Instead, they rely on platform data provided by board files or device trees.

This means the kernel is like, “Hey, I need a list of devices here!” and you provide the guest list.

The Main Players:

A Day in the Life of a Platform Driver

Picture this: you write a platform driver that gets registered with the kernel.

When the kernel boots up, it consults its list, finds your device, and says, “Ah, here you are!” Your driver then takes over, initializes the device, and makes sure it behaves nicely.

  1. Defining the Device: You create a platform_device structure with all the juicy details.
  2. Writing the Driver: Your platform_driver handles the nitty-gritty of operating the device.
  3. Matching Them Up: The kernel uses the provided data to pair the device with the driver.
  4. Initialization and Teardown: Your driver’s probe and remove functions ensure everything starts up and shuts down cleanly.

Why Bother with Platform Devices?

Well, not every device fits the mold of modern, auto-detectable hardware.
In many embedded systems, there’s no room for fancy discovery mechanisms.
Instead, simplicity and direct control rule the day.

Block Device Drivers: Unraveling the Mysteries of Data Storage

So, you’ve mastered character drivers and you’re feeling pretty good about yourself, huh?

Well, buckle up, buttercup, because it’s time to dive into the wild world of block device drivers.

What’s the Deal with Block Devices?

Imagine your data as a giant chocolate bar.

Block devices are like breaking that bar into bite-sized pieces (blocks) so you can savor each chunk.

These devices handle data in fixed-size blocks, making them perfect for storage mediums like hard drives and SSDs.

Unlike char devices that deal with data streams, block devices let you jump to any block you fancy.

Why Should You Care?

Well, unless you’re living in the Stone Age, your system relies on block devices for virtual memory, file storage, and all that jazz.

Meet sbull: Your New Best Friend

To get our hands dirty, we’ll tinker with sbull (Simple Block Utility for Loadable Linux).

Think of it as a RAM disk—a block device that uses your system’s memory for storage.

Sure, the kernel already has a fancy shmancy RAM disk, but where’s the fun in that?

Registering Your Block Device: Roll Call!

Before your block device can strut its stuff, it needs to register with the kernel.
It’s like signing the guestbook at a party.

1
int register_blkdev(unsigned int major, const char *name);

If you’re feeling adventurous and want the kernel to assign a major number, just pass 0 as the major.

1
int unregister_blkdev(unsigned int major, const char *name);

But here’s the kicker: in the 2.6 kernel, registering is like optional RSVP.

Gearing Up: The gendisk Structure

Your block device needs a gendisk structure—a backstage pass that holds all the juicy details about your device.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct gendisk {
    int major;                  // Major number
    int first_minor;            // First minor number
    int minors;                 // Number of minors
    char disk_name[32];         // Name (e.g., /dev/sda)
    struct hd_geometry *geo;    // Geometry (heads, sectors, cylinders)
    struct block_device_operations *fops; // File operations
    struct request_queue *queue;// Request queue
    void *private_data;         // Your driver's private data
};

Handling I/O Requests: The Bread and Butter

When the kernel has I/O operations for your device, it sends requests your way.
Your job is to handle them efficiently.

1
void (*request_fn)(struct request_queue *q);
1
blk_qc_t (*queue_rq)(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd);

Think of it as upgrading from a flip phone to a smartphone.

Network Drivers: Navigating the Wild Packets of the Kernel Frontier

Network Interfaces: The Unsung Heroes

Imagine your computer as a bustling frontier town.

The network interface is the trusty stagecoach, ferrying messages to and from the outside world.

Unlike block devices that have cozy little homes in /dev, network interfaces are free spirits—no special files for these adventurers!

Sockets and Interfaces: A Dynamic Duo

You might be thinking, “But I use read and write with sockets all the time!”

True, but sockets are like the town’s telegraph office, handling multiple messages over the same line.

Enter Snull: Your Training Steed

To get a feel for network drivers, we’ll introduce you to snull—a simple, memory-based network interface.

Think of it as your training steed before you ride the wild mustangs of real hardware.

The Asynchronous Dance

Unlike block drivers that wait for the kernel to say, “Hey, fetch me this block,” network drivers are always on their toes.

Packets arrive unannounced, and the driver must be ready to catch them like a seasoned rodeo clown dodging bulls.

Registration: Making Your Mark

Before your network interface can start mingling at the system’s hoedown, it needs to register with the kernel.

This involves setting up specific data structures and letting the kernel know, “Howdy!

Handling Packets: The Bucking Bronco

When a packet arrives, it’s like a bucking bronco bursting out of the gate.

Our driver needs to lasso that packet, process it, and hand it off to the kernel for further handling.

Administrative Tasks: More Than Just Riding

Being a network driver isn’t all about chasing packets.

Protocol Independence: Riding All Trails

The Linux network subsystem is designed to be protocol-agnostic.

Whether you’re dealing with IP, IPX, or some other trail, your network driver should handle packets one at a time, leaving the protocol specifics to the higher-ups.

TTY Drivers: The Unsung Heroes of Your Linux Box

TTY: More Than Just a Funny Acronym

“TTY” stands for teletypewriter.
Back in the day, these were actual physical devices.

The TTY Lineup: Who’s Who?

  • Serial Ports: Think /dev/ttyS0, /dev/ttyUSB0.

  • Virtual Consoles: These are the terminals you access with Ctrl+Alt+F1 to Ctrl+Alt+F7.

  • Pseudoterminals (PTYs): These are the dynamic duos behind terminal emulators like xterm or gnome-terminal.

The TTY Core: Traffic Control Central

At the heart of it all is the TTY core.
It’s like the air traffic controller for data flowing between user space (that’s you typing commands) and the hardware or virtual terminal.

  • TTY Line Disciplines: These are the interpreters, deciding how to process the data.

Writing a TTY Driver: DIY Edition

Feeling adventurous?

  1. Define the TTY Operations: Set up functions for operations like opening, closing, reading, and writing.
  2. Allocate the TTY Driver Structure: Register your driver with the TTY core.
  3. Handle Data Transmission: Implement the logic for sending and receiving data.
  4. Manage Line Discipline: Ensure your driver plays nice with different line disciplines.

Remember, with great power comes great responsibility.

Peek Under the Hood: Inspecting TTYs

Curious about the TTY devices on your system?
Check out /proc/tty/drivers for a list of registered TTY drivers.