1.
Introduction & Core Concepts
Device Drivers as a Bridge: The book emphasizes that device
drivers are crucial for allowing the Linux kernel to interact with
hardware. As stated in the Preface, "...somebody is going to have to
make all those new gadgets work with Linux."
Kernel Internals: Beyond just driver writing, the book aims to
illuminate how the kernel operates and how developers can tailor it
to their needs, promoting accessibility for a broader community of
developers.
Open System: Linux is positioned as an open system, and the
book's goal is to make it "...more open and accessible to a larger
community of developers."
Module Parameters: Modules can accept parameters, allowing
user customisation at load time. These parameters have types (e.g.
bool, int, charp) and default values. They are exposed via sysfs,
controlled by permissions (e.g. S_IRUGO for read-only, S_IRUGO|
S_IWUSR for read/write by root). However, the module is not actively
notified when sysfs changes the parameter, so writable parameters
should be handled carefully.
Block vs. Char Devices: Both are accessed via nodes in /dev. The
key distinction is how data is managed internally by the kernel.
Block devices typically handle I/O in fixed-size blocks (often 512
bytes or larger multiples), while char devices permit transfer of any
number of bytes at a time. Although Linux allows applications to
read/write block devices like character devices the core differences
mean they have completely different kernel interfaces.
Device Numbers: Internally, the kernel uses dev_t to hold device
numbers. As of kernel 2.6.0, this is a 32-bit number, with 12 bits for
the major number and 20 for the minor number. Macros are
provided in <linux/kdev_t.h> (MAJOR(), MINOR(), MKDEV()) to
access major/minor numbers reliably.
2. Module Development
Hello World Example: A basic "Hello World" module is used as an
introduction to kernel module creation, demonstrating the
differences between kernel modules and user space applications.
printk function: This function (analogous to printf) is the standard
way to output messages from kernel code. It supports different
levels of logging (e.g., KERN_DEBUG, KERN_WARNING).
Dynamic Device Numbers: Modules can request dynamic major
numbers. Tools like awk can then extract these numbers from
/proc/devices and be used to create the correct /dev entries.
Module Loading Script: The scull_load script illustrates how a
module can be loaded and the appropriate device nodes created, a
process usually performed at boot time or manually by an
administrator.
Module parameters: These are set using the module_param
macro, allowing configuration of behaviour during module load.
3. Character Driver Fundamentals (using the scull driver as an
example)
scull_open: This function acquires device information using
container_of. It uses the f_flags to check if it was opened in write-
only mode and, if so, will trim the memory to zero.
scull_read & scull_write: These functions are responsible for
transferring data between the kernel and user space, using
copy_to_user and copy_from_user respectively. They interact with a
device's underlying memory, moving data in quantum sized blocks.
scull_llseek: If a driver requires seek operations that map to a
physical action of the device a custom llseek method can be
supplied for custom logic.
4. Debugging Techniques
Print Debugging: The printk function is a primary tool for
debugging. The PDEBUG macro can conditionally enable/disable
debugging messages for both kernel and user space.
/proc Filesystem: Device drivers can expose information via the
/proc filesystem. Example code to dump scull device info is shown.
Kernel Oops: The book covers interpreting "oops" messages from
the kernel, caused by events like dereferencing a null pointer,
providing the example of a NULL pointer dereference using *(int*)0
= 0;.
Using gdb: The gdb debugger can be used to examine variables
within loadable modules. The book suggests a script (gdbline) to
ease the creation of necessary commands (add-symbol-file).
kdb Kernel Debugger: An example session using kdb is provided
for examination of in kernel data structures.
5. Concurrency and Locking
Race Conditions: The scull device is used to demonstrate potential
race conditions, emphasising that shared resources require careful
management of concurrent access.
Locking Mechanisms: The book outlines various locking
techniques:
Semaphores and Mutexes: For protecting critical sections of code
that might sleep.
Completions: A lightweight method for synchronisation.
Spinlocks: For short, critical sections where sleeping is not
permitted.
Atomic Variables: Used to implement atomic operations without
requiring locking for simple counter-like operations. Includes
functions like atomic_set, atomic_read, atomic_add, atomic_sub, etc.
Bit Operations: The text mentions bit-level operations
(test_and_set_bit, test_and_clear_bit) that can be used for creating
low-level locks.
Locking Traps: Discusses problems to avoid when implementing
locking, including lock ordering issues, which can lead to deadlocks.
6. Advanced Character Driver Operations
ioctl: This function provides a mechanism for sending control
commands and data to devices (beyond basic read/write). Example
code shows accessing it from user-space using ioctl system calls.
Blocking I/O: Covers using wait queues and the
wait_event_interruptible function to implement blocking I/O.
poll and select: These system calls allow monitoring of multiple
file descriptors for readability or writeability.
Asynchronous Notification: Discusses how drivers can notify user
space about events using signals.
7. Time, Delays, and Deferred Work
Time Measurement: Covers time measurements within the kernel
using "jiffies" and functions like get_cycles to access the timestamp
counter. Conversion macros like jiffies_to_msecs are mentioned.
Delays: Short delays can be achieved using functions like udelay,
ndelay, mdelay, and msleep. It notes that udelay and ndelay have
upper limits on values, and an unresolved symbol, __bad_udelay can
be a result of calling it with too high a number.
Kernel Timers: Timers enable functions to be scheduled to run
after a specific delay, and provides functions like add_timer,
mod_timer and del_timer_sync.
Tasklets: Tasklets are a mechanism for performing deferred work
(specifically, actions that can occur shortly, but not immediately),
within the kernel. They are serialized with respect to themselves and
always run on the same CPU that scheduled them.
Workqueues: Workqueues provide a general-purpose way to defer
work in the kernel, often to process data from interrupt handlers.
They run in a separate process context, allowing potentially blocking
calls.
8. Memory Allocation
kmalloc: Discusses the real story of kmalloc (as it is not always the
best way to allocate memory).
Lookaside Caches: Used for efficient allocation of frequently used
objects of a fixed size.
get_free_page and vmalloc: Methods for allocating page-sized
and larger memory blocks.
Per-CPU Variables: Used for storing data that is specific to a
particular CPU core.
9. Communicating with Hardware
I/O Ports and Memory: Hardware interaction is performed via I/O
ports or memory-mapped I/O.
request_region: Used to claim exclusive access to specific I/O port
ranges.
ioremap: Required to map physical memory addresses into the
kernel's virtual address space. Functions like iowrite8, ioread32, are
used to access memory-mapped I/O after it has been mapped with
ioremap.
10. Interrupt Handling
Interrupt Handler Installation: Drivers must install interrupt
handlers (using functions like request_irq) to respond to
asynchronous hardware events.
Top and Bottom Halves: Introduces the concept of separating
interrupt handling into fast "top halves" and slower "bottom halves"
(often tasklets or workqueues) to avoid delays.
11. Data Types in the Kernel
Explicit Sizing: It is important to use fixed size data types like u8,
u16, u32, and u64 instead of the more general types like int to
ensure portability across different architectures.
Endianness: Portability problems due to differences between
endian architectures are noted.
12. PCI Drivers
PCI Interface: Discusses the PCI bus and how to access hardware
on it. Mentions other PC buses such as ISA, PC/104, SBus and
NuBus.
13. USB Drivers
USB Basics: Introduces core USB concepts (endpoints, URBs, etc.)
USB Transfers: Covers different types of USB transfers, including
bulk and interrupt transfers.
USB Device Model: Discusses how USB devices are exposed via
sysfs.
14. The Linux Device Model
kobjects and ksets: Key components of the Linux device model.
Sysfs: Describes the role of sysfs in representing the device model.
Buses, Devices, and Drivers: Highlights the relationships
between these elements.
Hotplug: Discusses handling of device hotplug events via a
/sbin/hotplug utility.
15. Memory Mapping and DMA
Memory Management: Introduces concepts of how Linux
manages memory, including virtual address spaces.
mmap Device Operation: Allows mapping of device memory into
user-space process memory.
Direct I/O: How to perform I/O operations directly into user space
buffers.
Direct Memory Access (DMA): DMA mechanisms for hardware to
access system memory directly, without involving the main
processor. dma_alloc_coherent is mentioned.
16. Block Drivers
Block Concepts: Introduces the concepts of blocks and sectors,
and notes that the kernel uses 512-byte sectors internally.
gendisk Structure: How the kernel represents a block device.
Functions for creating (alloc_disk) and adding (add_disk) this
structure are covered.
Request Queues: Block drivers use request queues to handle I/O
operations.
Ioctl: Block devices may implement ioctl calls for control.
Request processing: Details how block requests are handled by a
device driver, and examples given.
Tagged command queueing: Mention of initialising tagged
command queueing.
17. Network Drivers
net_device Structure: The core structure for network device
representation. alloc_netdev is used to create the structure.
Snull Example: The snull driver is used as an example of a simple
virtual network driver, and covers how devices are registered.
Packet Transmission/Reception: The driver must handle the
transmission and reception of network packets, using dev_alloc_skb
to allocate socket buffers. netif_receive_skb delivers the buffer to
the networking layer.
Interrupts: How to handle network device interrupts, often by
queuing data to be processed in bottom halves.
Polling: Using the poll method to process queued network packets.
Socket Buffers (sk_buffs): Structure used to manage and pass
network data.
18. TTY Drivers
TTY Concepts: An overview of TTY devices (terminals) is provided.
tty_driver and tty_operations: Data structures that describe a
TTY device and its associated functions.
TTY Line Settings: How TTY devices are configured (e.g., baud
rate, parity)
ioctl: The ioctl function can be used to implement TTY specific
control features, and examples given.
Key Takeaways:
Abstraction: Linux device drivers are a layer of abstraction,
allowing a consistent interface for higher layers of the system to
interact with hardware.
Kernel Integration: Drivers must integrate tightly with the kernel
through specific interfaces.
Concurrency: Developers must understand and handle
concurrency issues that arise when multiple processes access a
device simultaneously.
Debugging: Solid debugging skills are essential, and knowledge of
kernel debugging techniques is crucial.
Portability: Careful coding practices and using the correct data
types are important for maintaining device driver portability.
This briefing document summarises the core concepts and important
details extracted from the provided material. It provides a foundation for
further exploration of the intricate world of Linux device driver
development.