Many parts of the device driver API covered in this chapter have changed between the major kernel releases. For those of you needing to make your driver work with Linux 2.0 or 2.2, here is a quick rundown of the differences you will encounter.
A relatively small amount of the material in this chapter changed in the 2.3 development cycle. The one significant change is in the area of wait queues. The 2.2 kernel had a different and simpler implementation of wait queues, but it lacked some important features, such as exclusive sleeps. The new implementation of wait queues was introduced in kernel version 2.3.1.
The 2.2 wait queue implementation used variables of the type
struct wait_queue *
instead of
wait_queue_head_t
. This pointer had to be
initialized to NULL
prior to its first use. A
typical declaration and initialization of a wait queue looked like
this:
struct wait_queue *my_queue = NULL;
The various functions for sleeping and waking up looked the same, with
the exception of the variable type for the queue itself. As a result,
writing code that works for all 2.x kernels is
easily done with a bit of code like the following, which is part of
the sysdep.h
header we use to compile our sample
code.
# define DECLARE_WAIT_QUEUE_HEAD(head) struct wait_queue *head = NULL typedef struct wait_queue *wait_queue_head_t; # define init_waitqueue_head(head) (*(head)) = NULL
The synchronous versions of wake_up were added in
2.3.29, and sysdep.h
provides macros with the same
names so that you can use the feature in your code while maintaining
portability. The replacement macros expand to normal
wake_up, since the underlying mechanisms were
missing from earlier kernels. The timeout versions of
sleep_on were added in kernel 2.1.127. The rest
of the wait queue interface has remained relatively unchanged. The
sysdep.h
header defines the needed macros in
order to compile and run your modules with Linux 2.2 and Linux 2.0
without cluttering the code with lots of #ifdef
s.
The wait_event macro did not exist in the 2.0
kernel. For those who need it, we have provided an implementation in
sysdep.h
Some small changes have been made in how asynchronous notification works for both the 2.2 and 2.4 releases.
In Linux 2.3.21, kill_fasync got its third argument. Prior to this release, kill_fasync was called as
kill_fasync(struct fasync_struct *queue, int signal);
Fortunately, sysdep.h
takes care of the issue.
In the 2.2 release, the type of the first argument to the
fasync method changed. In the 2.0 kernel, a
pointer to the inode
structure for the device was
passed, instead of the integer file descriptor:
int (*fasync) (struct inode *inode, struct file *filp, int on);
To solve this incompatibility, we use the same approach taken for read and write: use of a wrapper function when the module is compiled under 2.0 headers.
The inode
argument to the
fasync method was also passed in when called from
the release method, rather than the
-1
value used with later kernels.
The third argument to the fsync
file_operations
method (the integer
datasync
value) was added in the 2.3 development
series, meaning that portable code will generally need to include a
wrapper function for older kernels. There is a trap, however, for
people trying to write portable fsync methods: at
least one distributor, which will remain nameless, patched the 2.4
fsync API into its 2.2 kernel. The kernel
developers usually (usually...) try to avoid
making API changes within a stable series, but they have little
control over what the distributors do.
Memory access was handled differently in the 2.0 kernels. The Linux virtual memory system was less well developed at that time, and memory access was handled a little differently. The new system was the key change that opened 2.1 development, and it brought significant improvements in performance; unfortunately, it was accompanied by yet another set of compatibility headaches for driver writers.
The functions used to access memory under Linux 2.0 were as follows:
-
verify_area(int mode, const void *ptr, unsigned long size);
This function worked similarly to access_ok, but performed more extensive checking and was slower. The function returned 0 in case of success and
-EFAULT
in case of errors. Recent kernel headers still define the function, but it’s now just a wrapper around access_ok. When using version 2.0 of the kernel, calling verify_area is never optional; no access to user space can safely be performed without a prior, explicit verification.-
put_user(datum, ptr)
The put_user macro looks much like its modern-day equivalent. It differed, however, in that no verification was done, and there was no return value.
-
get_user(ptr)
This macro fetched the value at the given address, and returned it as its return value. Once again, no verification was done by the execution of the macro.
verify_area had to be called explicitly because no user-area copy function performed the check. The great news introduced by Linux 2.1, which forced the incompatible change in the get_user and put_user functions, was that the task of verifying user addresses was left to the hardware, because the kernel was now able to trap and handle processor exceptions generated during data copies to user space.
As an example of how the older calls are used, consider scull one more time. A version of scull using the 2.0 API would call verify_area in this way:
int err = 0, tmp; /* * extract the type and number bitfields, and don't decode * wrong cmds: return ENOTTY before verify_area() */ if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY; if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY; /* * the direction is a bit mask, and VERIFY_WRITE catches R/W * transfers. `Type' is user oriented, while * verify_area is kernel oriented, so the concept of "read" and * "write" is reversed */ if (_IOC_DIR(cmd) & _IOC_READ) err = verify_area(VERIFY_WRITE, (void *)arg, _IOC_SIZE(cmd)); else if (_IOC_DIR(cmd) & _IOC_WRITE) err = verify_area(VERIFY_READ, (void *)arg, _IOC_SIZE(cmd)); if (err) return err;
Then get_user and put_user can be used as follows:
case SCULL_IOCXQUANTUM: /* eXchange: use arg as pointer */ tmp = scull_quantum; scull_quantum = get_user((int *)arg); put_user(tmp, (int *)arg); break; default: /* redundant, as cmd was checked against MAXNR */ return -ENOTTY; } return 0;
Only a small portion of the ioctl switch code has been shown, since it is little different from the version for 2.2 and beyond.
Life would be relatively easy for the compatibility-conscious driver writer if it weren’t for the fact that put_user and get_user are implemented as macros in all Linux versions, and their interfaces changed. As a result, a straightforward fix using macros cannot be done.
One possible solution is to define a new set of version-independent
macros. The path taken by sysdep.h consists in
defining upper-case macros: GET_USER
,
__GET_USER
, and so on. The arguments are
the same as with the kernel macros of Linux 2.4, but the caller must
be sure that verify_area has been called first
(because that call is needed when compiling for 2.0).
The 2.0 kernel did not support the capabilities abstraction at all. All permissions checks simply looked to see if the calling process was running as the superuser; if so, the operation would be allowed. The function suser was used for this purpose; it takes no arguments and returns a nonzero value if the process has superuser privileges.
suser still exists in later kernels, but its use
is strongly discouraged. It is better to define a version of
capable for 2.0, as is done in
sysdep.h
:
# define capable(anything) suser()
In this way, code can be written that is portable but which works with modern, capability-oriented systems.
The 2.0 kernel did not support the poll system call; only the BSD-style select call was available. The corresponding device driver method was thus called select, and operated in a slightly different way, though the actions to be performed are almost identical.
The select method is passed a pointer to a
select_table
, and must pass that pointer to
select_wait only if the calling process should
wait for the requested condition (one of SEL_IN
,
SEL_OUT
, or SEL_EX
).
The scull driver deals with the incompatibility by declaring a specific select method to be used when it is compiled for version 2.0 of the kernel:
#ifdef __USE_OLD_SELECT__ int scull_p_poll(struct inode *inode, struct file *filp, int mode, select_table *table) { Scull_Pipe *dev = filp->private_data; if (mode == SEL_IN) { if (dev->rp != dev->wp) return 1; /* readable */ PDEBUG("Waiting to read\n"); select_wait(&dev->inq, table); /* wait for data */ return 0; } if (mode == SEL_OUT) { /* * The buffer is circular; it is considered full * if "wp" is right behind "rp". "left" is 0 if the * buffer is empty, and it is "1" if it is completely full. */ int left = (dev->rp + dev->buffersize - dev->wp) % dev->buffersize; if (left != 1) return 1; /* writable */ PDEBUG("Waiting to write\n"); select_wait(&dev->outq, table); /* wait for free space */ return 0; } return 0; /* never exception-able */ } #else /* Use poll instead, already shown */
The __USE_OLD_SELECT__
preprocessor
symbol used here is set by the sysdep.h
include
file according to kernel version.
Prior to Linux 2.1, the llseek device method was called lseek instead, and it received different parameters from the current implementation. For that reason, under Linux 2.0 you were not allowed to seek a file, or a device, past the 2 GB limit, even though the llseek system call was already supported.
The prototype of the file operation in the 2.0 kernel was the following:
int (*lseek) (struct inode *inode, struct file *filp , off_t off, int whence);
Those working to write drivers compatible with 2.0 and 2.2 usually end up defining separate implementations of the seek method for the two interfaces.
Because Linux 2.0 only minimally supported SMP systems, race conditions of the type mentioned in this chapter did not normally come about. The 2.0 kernel did have a spinlock implementation, but, since only one processor could be running kernel code at a time, there was less need for locking.
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.