O'Reilly logo

Linux Device Drivers, Second Edition by Alessandro Rubini, Jonathan Corbet

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Backward Compatibility

This chapter, so far, has described the kernel programming interface for version 2.4 of the Linux kernel. Unfortunately, this interface has changed significantly over the course of kernel development. These changes represent improvements in how things are done, but, once again, they also pose a challenge for those who wish to write drivers that are compatible across multiple versions of the kernel.

Insofar as this chapter is concerned, there are few noticeable differences between versions 2.4 and 2.2. Version 2.2, however, changed many of the prototypes of the file_operations methods from what 2.0 had; access to user space was greatly modified (and simplified) as well. The semaphore mechanism was not as well developed in Linux 2.0. And, finally, the 2.1 development series introduced the directory entry (dentry) cache.

Changes in the File Operations Structure

A number of factors drove the changes in the file_operations methods. The longstanding 2 GB file-size limit caused problems even in the Linux 2.0 days. As a result, the 2.1 development series started using the loff_t type, a 64-bit value, to represent file positions and lengths. Large file support was not completely integrated until version 2.4 of the kernel, but much of the groundwork was done earlier and had to be accommodated by driver writers.

Another change introduced during 2.1 development was the addition of the f_pos pointer argument to the read and write methods. This change was made to support the POSIX pread and pwrite system calls, which explicitly set the file offset where data is to be read or written. Without these system calls, threaded programs can run into race conditions when moving around in files.

Almost all methods in Linux 2.0 received an explicit inode pointer argument. The 2.1 development series removed this parameter from several of the methods, since it was rarely needed. If you need the inode pointer, you can still retrieve it from the filp argument.

The end result is that the prototypes of the commonly used file_operations methods looked like this in 2.0:

int (*lseek) (struct inode *, struct file *, off_t, int);

Note that this method is called lseek in Linux 2.0, instead of llseek. The name change was made to recognize that seeks could now happen with 64-bit offset values.

int (*read) (struct inode *, struct file *, char *, int); , int (*write) (struct inode *, struct file *, const char *, int);

As mentioned, these functions in Linux 2.0 had the inode pointer as an argument, and lacked the position argument.

void (*release) (struct inode *, struct file *);

In the 2.0 kernel, the release method could not fail, and thus returned void.

There have been many other changes to the file_operations structure; we will cover them in the following chapters as we get to them. Meanwhile, it is worth a moment to look at how portable code can be written that accounts for the changes we have seen so far. The changes in these methods are large, and there is no simple, elegant way to cover them over.

The way the sample code handles these changes is to define a set of small wrapper functions that “translate” from the old API to the new. These wrappers are only used when compiling under 2.0 headers, and must be substituted for the “real” device methods within the file_operations structure. This is the code implementing the wrappers for the scull driver:

/*
 * The following wrappers are meant to make things work with 2.0 kernels
 */
#ifdef LINUX_20
int scull_lseek_20(struct inode *ino, struct file *f,
    off_t offset, int whence)
{
 return (int)scull_llseek(f, offset, whence);
}

int scull_read_20(struct inode *ino, struct file *f, char *buf, 
   int count)
{
 return (int)scull_read(f, buf, count, &f->f_pos);
}

int scull_write_20(struct inode *ino, struct file *f, const char *b, 
   int c)
{
 return (int)scull_write(f, b, c, &f->f_pos);
}

void scull_release_20(struct inode *ino, struct file *f)
{
 scull_release(ino, f);
}

/* Redefine "real" names to the 2.0 ones */
#define scull_llseek scull_lseek_20
#define scull_read scull_read_20
#define scull_write scull_write_20
#define scull_release scull_release_20
#define llseek lseek
#endif /* LINUX_20 */

Redefining names in this manner can also account for structure members whose names have changed over time (such as the change from lseek to llseek).

Needless to say, this sort of redefinition of the names should be done with care; these lines should appear before the definition of the file_operations structure, but after any other use of those names.

Two other incompatibilities are related to the file_operations structure. One is that the flush method was added during the 2.1 development cycle. Driver writers almost never need to worry about this method, but its presence in the middle of the structure can still create problems. The best way to avoid dealing with the flush method is to use the tagged initialization syntax, as we did in all the sample source files.

The other difference is in the way an inode pointer is retrieved from a filp pointer. Whereas modern kernels use a dentry (directory entry) data structure, version 2.0 had no such structure. Therefore, sysdep.h defines a macro that should be used to portably access an inode from a filp:

#ifdef LINUX_20
# define INODE_FROM_F(filp) ((filp)->f_inode)
#else
# define INODE_FROM_F(filp) ((filp)->f_dentry->d_inode)
#endif

The Module Usage Count

In 2.2 and earlier kernels, the Linux kernel did not offer any assistance to modules in maintaining the usage count. Modules had to do that work themselves. This approach was error prone and required the duplication of a lot of work. It also encouraged race conditions. The new method is thus a definite improvement.

Code that is written to be portable, however, must be prepared to deal with the older way of doing things. That means that the usage count must still be incremented when a new reference is made to the module, and decremented when that reference goes away. Portable code must also work around the fact that the owner field did not exist in the file_operations structure in earlier kernels. The easiest way to handle that is to use SET_MODULE_OWNER, rather than working with the owner field directly. In sysdep.h, we provide a null SET_FILE_OWNER for kernels that do not have this facility.

Changes in Semaphore Support

Semaphore support was less developed in the 2.0 kernel; support for SMP systems in general was primitive at that time. Drivers written for only that kernel version may not need to use semaphores at all, since only one CPU was allowed to be running kernel code at that time. Nonetheless, there may still be a need for semaphores, and it does not hurt to have the full protection needed by later kernel versions.

Most of the semaphore functions covered in this chapter existed in the 2.0 kernel. The one exception is sema_init; in version 2.0, programmers had to initialize semaphores manually. The sysdep.h header file handles this problem by defining a version of sema_init when compiled under the 2.0 kernel:

#ifdef LINUX_20
# ifdef MUTEX_LOCKED /* Only if semaphore.h included */
  extern inline void sema_init (struct semaphore *sem, int val)
  {
   sem->count = val;
   sem->waking = sem->lock = 0;
   sem->wait = NULL;
  }
# endif
#endif /* LINUX_20 */

Changes in Access to User Space

Finally, access to user space changed completely at the beginning of the 2.1 development series. The new interface has a better design and makes much better use of the hardware in ensuring safe access to user-space memory. But, of course, the interface is different. The 2.0 memory-access functions were as follows:

 void memcpy_fromfs(void *to, const void *from, unsigned long count);
 void memcpy_tofs(void *to, const void *from, unsigned long count);

The names of these functions come from the historical use of the FS segment register on the i386. Note that there is no return value from these functions; if the user supplies an invalid address, the data copy will silently fail. sysdep.h hides the renaming and allows you to portably call copy_to_user and copy_from_user.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required