Linux Device Driver 3rd edition - CH2 recap
Building and Running Modules
Setting Up Your Test System
You can get latest mainline kernel from kernel.org to setup your test environment.
[Tip] Prepare a “sacrificial” environment to test your new code, so that it won’t ruin your machine. (The possibility of bugs is always present)
The Hello World Module
#include <linux/init.h>
#include <linux/module.h>
static int hello_init(void) {
printk(KERN_ALERT "Hello, world\n");
return 0;
}
static void hello_exit(void) {
printk(KERN_ALERT "Goodbye, cruel world\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("Dual BSD/GPL");
Kernel Modules Versus Applications
- Execution behavior
- Applications
- Usually performs a single task from begining to end.
- Kernel Modules
- Initialized to prepare for later invocation.
- Similar to event driven programming.
- Undo everything the init function built up when exit.
- Applications
User Space and Kernel Space
- Where to run
- A module runs in kernel space.
- An application runs in user space.
- Level
- Kernel executes in the highest level (also called supervisor mode), where everything is allowed.
- Applications execute in the lowest level (also called user mode), where the processor regulates direct access to hardware and unauthorized access to memory.
Concurrency in the Kernel
- Linux kernel code, including driver code, must be reentrant. (be capable of running in more than one context at the same time)
- Writing code that handles concurrency and avoids race conditions (situations in which an unfortunate order of execution causes undesirable behavior).
- Common mistake: concurrency is not a problem.
- If you do not write your code with concurrency in mind, it will be subject to catastrophic failures that can be exceedingly difficult to debug.
The Current Process
The need to reference to current happen frequently, which is global, and defined in <asm/current.h>
printk(KERN_INFO "The process is \"%s\" (pid %i)\n", current->comm, current->pid);
A Few Other Details
- Stack
- Applications are laid out in virtual memory with a very large stack area.
- Kernel has a very small stack. (e.g., 4096-byte page)
- The name of Kernel API/function started with a double underscore (__) are generally low-level component, should be used with caution. “If you call this function, be sure you know what you are doing.”
- Kernel code cannot do floating point arithmetic.
Compiling and Loading
Compiling Modules
# If KERNELRELEASE is defined, we've been invoked from the kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
# Otherwise we were called directly from the command line; invoke the kernel build system.
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
Loading and Unloading Modules
// Load module
$ sudo insmod hello.ko
// check printk message from system log (Ubuntu)
$ tail /var/log/syslog
// Unload module
$ sudo rmmod hello
- insmod doesn’t take symbol dependency into consideration. To load module comprehensively, use modprobe instead.
Version Dependency
- Bear in mind that your module’s code has to be recompiled for each version of the kernel that it is linked to.
- Kernel interfaces often change between releases, make use of macros and #ifdef constructs to make your code build properly.
- macros defined in <linux/version.h> :
- UTS_RELEASE (Unix Time-sharing System)
- the version of this kernel. (e.g., “2.6.10”)
- LINUX_VERSION_CODE
- Binary representation of the kernel version. (e.g., “2.6.10” = 0x02060a = 132618)
- KERNEL_VERSION(major,minor,release)
- used to build an integer version code from the individual numbers. (e.g., KERNEL_VERSION(2,6,10) = 13268)
- UTS_RELEASE (Unix Time-sharing System)
Platform Dependency
According to the peculiarities of each platform, kernel can be exploit to achieve better performance by designers freely.
The Kernel Symbol Table
- The table contains the addresses of global kernel items (functions and variables) that are needed to implement modularized drivers.
- A module usually implements its own functionality without the need to export any symbols at all.
- Module stacking
- New modules can use symbols exported by your module, and you can stack new modules on top of other modules.
- implemented in the mainstream kernel sources as well.
- To export symbols :
EXPORT_SYMBOL(name); EXPORT_SYMBOL_GPL(name);
Preliminaries
#include <linux/module.h>
#include <linux/init.h>
/*
* some coding...
*/
// not strictly necessary, but really should
MODULE_LICENSE("GPL v2");
// Other descriptive definitions
MODULE_AUTHOR
MODULE_DESCRIPTION
MODULE_VERSION
MODULE_ALIAS
MODULE_DEVICE_TABLE
- Put MODULE_ declarations at the end of the file.
Initialization and Shutdown
static int __init initialization_function(void) {
/* Initialization code here */
}
module_init(initialization_function);
- static
- not meant to be visible outside the specific file.
- __init
- the given function is used only at initialization time.
- Be sure not to use them for any function (or data structure) you will be using after initialization completes.
- module_init
- Mandatory, to state where the module’s initialization function is to be found.
The Cleanup Function
static void __exit cleanup_function(void) {
/* Cleanup code here */
}
module_exit(cleanup_function);
- __exit
- it marks the code as being for module unload only.
- module_exit
- Mandatory, used to find your cleanup function.
Error Handling During Initialization
- One thing you must always bear in mind when registering facilities with the kernel is that the registration could fail.
- Whenever possible, your module should press forward and provide what capabilities it can after things fail.
-
If module can’t be load after a particular type of failure, you must undo any registration activities performed before the failure.
int __init my_init_function(void) { int err; /* registration takes pointer and a name */ err = register_one(ptr1, "skull"); if (err) goto fail_one; err = register_two(ptr2, "sull"); if (err) goto fail_two; err = register_three(ptr3, "skull"); if (err) goto fail_three; return 0; /* success */ fail_three: unregister_two(ptr2, "skull"); fail_two: unregister_one(ptr1, "skull"); fail_one: return err; /* propagate the error */ }
- In the Linux kernel, error codes are negative numbers.
- <linux/errno.h>
- Cleanup function must undo any registration performed by the initialization function. (reverse order is not usually mandatory)
void __exit my_cleanup_function(void) { unregister_three(ptr3, "skull"); unregister_two(ptr2, "skull"); unregister_one(ptr1, "skull"); }
- If initialization and cleanup are more complex that the goto approach may become difficult to manage :
struct something *item1; struct somethingelse *item2; int stuff_ok; void my_cleanup(void) { if (item1) release_thing(item1); if (item2) release_thing2(item2); if (stuff_ok) release_all(); } int __init my_init(void) { int err = -ENOMEM; item1 = allocate_thing(args); item2 = allocate_thing2(args2); if (!item1 || !item2) goto fail; err = register_stuff(item1, item2); if (!err) stuff_ok = 1; else goto fail; return 0; /* success */ fail: my_cleanup(); return err; }
Module-Loading Races
- The first is that you should always remember that some other part of the kernel can make use of any facility you register immediately after that registration has completed.
- Do not register any facility until all of your internal initialization needed to support that facility has been completed.
Module Parameters
- The parameter values can be assigned at load time by insmod or modprobe.
insmod hello.ko howmany=10 whom="Mom"
- implementation
#include <linux/moduleparam.h> static char *whom = "world"; static int howmany = 1; // module_param(name, type, permissions mask) module_param(howmany, int, S_IRUGO); module_param(whom, charp, S_IRUGO);
- To declare an array parameter:
module_param_array(name, type, num, perm);
- All module parameters should be given a default value.
- about permission value, definitions are in <linux/stat.h>
Doing It in User Space
The advantages of user-space drivers are:
- The full C library can be linked in.
- The programmer can run a conventional debugger on the driver code.
- If a user-space driver hangs, you can simply kill it.
- User memory is swappable, unlike kernel memory.
- A well-designed driver program can still, like kernel-space drivers, allow concurrent access to a device.
- If you must write a closed-source driver, the user-space option makes it easier for you to avoid ambiguous licensing situations and problems with changing kernel interfaces.
The drawbacks of user-space drivers are:
- Interrupts are not available in user space.
- Direct access to memory is possible only by mmapping /dev/mem, and only a privileged user can do that.
- Access to I/O ports is available only after calling ioperm or iopl.
- Response time is slower. If the driver has been swapped to disk, response time is unacceptably long.
- The most important devices can’t be handled in user space. (e.g., network interfaces and block devices.)
One case in which working in user space might make sense is when you are beginning to deal with new and unusual hardware.