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.
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
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.
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 */
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.
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.