Backward Compatibility

The Linux kernel is a moving target—many things change over time as new features are developed. The interface that we have described in this chapter is that provided by the 2.4 kernel; if your code needs to work on older releases, you will need to take various steps to make that happen.

This is the first of many “backward compatibility” sections in this book. At the end of each chapter we’ll cover the things that have changed since version 2.0 of the kernel, and what needs to be done to make your code portable.

For starters, the KERNEL_VERSION macro was introduced in kernel 2.1.90. The sysdep.h header file contains a replacement for kernels that need it.

Changes in Resource Management

The new resource management scheme brings in a few portability problems if you want to write a driver that can run with kernel versions older than 2.4. This section discusses the portability problems you’ll encounter and how the sysdep.h header tries to hide them.

The most apparent change brought about by the new resource management code is the addition of request_mem_region and related functions. Their role is limited to accessing the I/O memory database, without performing specific operations on any hardware. What you can do with earlier kernels, thus, is to simply not call the functions. The sysdep.h header easily accomplishes that by defining the functions as macros that return 0 for kernels earlier than 2.4.

Another difference between 2.4 and earlier kernel versions is in the actual prototypes of request_region and related functions.

Kernels earlier than 2.4 declared both request_region and release_region as functions returning void (thus forcing the use of check_region beforehand). The new implementation, more correctly, has functions that return a pointer value so that an error condition can be signaled (thus making check_region pretty useless). The actual pointer value will not generally be useful to driver code for anything other than a test for NULL, which means that the request failed.

If you want to save a few lines of code in your drivers and are not concerned about backward portability, you could exploit the new function calls and avoid using check_region in your code. Actually, check_region is now implemented on top of request_region, releasing the I/O region and returning success if the request is fulfilled; the overhead is negligible because none of these functions is ever called from a time-critical code section.

If you prefer to be portable, you can stick to the call sequence we suggested earlier in this chapter and ignore the return values of request_region and release_region. Anyway, sysdep.h declares both functions as macros returning 0 (success), so you can both be portable and check the return value of every function you call.

The last difference in the I/O registry between version 2.4 and earlier versions of the kernel is in the data types used for the start and len arguments. Whereas new kernels always use unsigned long, older kernels used shorter types. This change has no effect on driver portability, though.

Compiling for Multiprocessor Systems

Version 2.0 of the kernel didn’t use the CONFIG_SMP configuration option to build for SMP systems; instead, choice was made a global assignment in the main kernel makefile. Note that modules compiled for an SMP machine will not work in a uniprocessor kernel, and vice versa, so it is important to get this one right.

The sample code accompanying this book automatically deals with SMP in the makefiles, so the code shown earlier need not be copied in each module. However, we do not support SMP under version 2.0 of the kernel. This should not be a problem because multiprocessor support was not very robust in Linux 2.0, and everyone running SMP systems should be using 2.2 or 2.4. Version 2.0 is covered by this book because it’s still the platform of choice for small embedded systems (especially in its no-MMU implementation), but no such system has multiple processors.

Exporting Symbols in Linux 2.0

The Linux 2.0 symbol export mechanism was built around a function called register_symtab. A Linux 2.0 module would build a table describing all of the symbols to be exported, then would call register_symtab from its initialization function. Only symbols that were listed in the explicit symbol table were exported to the kernel. If, instead, the function was not called at all, all global symbols were exported.

If your module doesn’t need to export any symbols, and you don’t want to declare everything as static, just hide global symbols by adding the following line to init_module. This call to register_symtab simply overwrites the module’s default symbol table with an empty one:

 register_symtab(NULL);

This is exactly how sysdep.h defines EXPORT_NO_SYMBOLS when compiling for version 2.0. This is also why EXPORT_NO_SYMBOLS must appear within init_module to work properly under Linux 2.0.

If you do need to export symbols from your module, you will need to create a symbol table structure describing these symbols. Filling a Linux 2.0 symbol table structure is a tricky task, but kernel developers have provided header files to simplify things. The following lines of code show how a symbol table is declared and exported using the facilities offered by the headers of Linux 2.0:

static struct symbol_table skull_syms = {

#include <linux/symtab_begin.h>
  X(skull_fn1),
  X(skull_fn2),
  X(skull_variable),
#include <linux/symtab_end.h>
	};

 register_symtab(&skull_syms);

Writing portable code that controls symbol visibility takes an explicit effort from the device driver programmer. This is a case where it is not sufficient to define a few compatibility macros; instead, portability requires a fair amount of conditional preprocessor code, but the concepts are simple. The first step is to identify the kernel version in use and to define some symbols accordingly. What we chose to do in sysdep.h is define a macro REGISTER_SYMTAB() that expands to nothing on version 2.2 and later and expands to register_symtab on version 2.0. Also, __USE_OLD_SYMTAB__ is defined if the old code must be used.

By making use of this code, a module that exports symbols may now do so portably. In the sample code is a module, called misc-modules/export.c, that does nothing except export one symbol. The module, covered in more detail in Section 11.3 in Chapter 11, includes the following lines to export the symbol portably:

#ifdef __USE_OLD_SYMTAB__
 static struct symbol_table export_syms = {
  #include <linux/symtab_begin.h>
  X(export_function),
  #include <linux/symtab_end.h>
 };
#else
 EXPORT_SYMBOL(export_function);
#endif

int export_init(void)
{
 REGISTER_SYMTAB(&export_syms);
 return 0;
}

If __USE_OLD_SYMTAB__ is set (meaning that you are dealing with a 2.0 kernel), the symbol_table structure is defined as needed; otherwise, EXPORT_SYMBOL is used to export the symbol directly. Then, in init_module, REGISTER_SYMTAB is called; on anything but a 2.0 kernel, it will expand to nothing.

Module Configuration Parameters

MODULE_PARM was introduced in kernel version 2.1.18. With the 2.0 kernel, no parameters were declared explicitly; instead, insmod was able to change the value of any variable within the module. This method had the disadvantage of providing user access to variables for which this mode of access had not been intended; there was also no type checking of parameters. MODULE_PARM makes module parameters much cleaner and safer, but also makes Linux 2.2 modules incompatible with 2.0 kernels.

If 2.0 compatibility is a concern, a simple preprocessor test can be used to define the various MODULE_ macros to do nothing. The header file sysdep.h in the sample code defines these macros when needed.

Get Linux Device Drivers, Second Edition now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.