In the next few sections, we’ll look at the various operations
a driver can perform on the devices it manages. The device
is identified internally by a file
structure, and the kernel
uses the file_operations
structure
to access the driver’s functions. This design is the first evidence
we’ve seen of the object-oriented design of the Linux kernel. We’ll
see more evidence of object-oriented design later. The
structure file_operations
is a table of function pointers,
defined in <linux/fs.h>
. The structure struct file
is
going to be described next.
The fops
pointer, which we’ve already seen as an argument
to the register_chrdev call, points to a table of operations
(open, read, and so on). Each entry in the table points
to the function defined by the driver to handle the requested
operation. The table can contain NULL
pointers for the operations
you don’t support. The exact behavior of the kernel when a NULL
pointer is specified is different for each function, as the list in
the next section shows.
The file_operations
structure has been slowly getting bigger as
new functionality is added to the kernel (although no new fields were
added between 1.2.0 and 2.0.x). There should be no side effects
from this increase, because the C compiler takes care of any size mismatch
by zero-filling uninitialized fields in global or static
struct
variables.
New items are added at the end of the structure,[10]
so a NULL
value inserted at compile time will
select the default behavior (remember that the module needs to be
recompiled in any case for each new kernel version it will be loaded into).
A few of the function prototypes associated to fops
fields,
actually changed slightly during 2.1 development. These differences
are covered in Section 17.2, in Chapter 17.
The following list introduces all the operations that an application can invoke on a device. These operations are often called ``methods,'' using object-oriented programming terminology to denote actions declared by an object to act on itself.
I’ve tried to keep the list brief so it can be used as a reference,
merely summarizing each operation and the default
kernel behavior when a NULL
pointer is used. You
can skip over this list on your first reading and return to it later.
The rest of the chapter, after describing another important data
structure (the file
), explains the role of the most important
operations and offers hints, caveats, and real code examples. We’ll defer
discussion of the more complex operations to a later chapter, because
we aren’t yet ready to dig into memory management and
asynchronous notification.
The operations appear in struct file_operations
in this
order, and their return value is 0 for success or a negative error
code to signal an error, unless otherwise noted:
int (*lseek) (struct inode *, struct file *, off_t, int);
The lseek method is used to change the current
read/write position in a file, and the new position
is returned as a (positive) return value. Errors are signalled
by a negative return value. If the function is not
specified for the driver, a seek relative to end-of-file
fails, while other seeks succeed by modifying the position
counter in the file
structure (described in
Section 3.4). The prototype of this function
changed in
2.1.0, as explained in Section 17.2.1 in
Chapter 17.
int (*read) (struct inode *, struct file *, char *, int);
Used to retrieve data from the device. A null pointer in
this position causes the read system call to fail with
-EINVAL
(``Invalid argument''). A non-negative return value
represents the number of bytes successfully read.
int (*write) (struct inode *, struct file *, const char *,
,
int);
Sends data to the device. If missing, -EINVAL
is returned to the program calling the write system call.
Note that the
const
specifier was missing in 1.2 headers. If you include
const
in your own write method, a warning is
generated when compiling against older Linux headers. If you
don’t include const
, a warning is generated for newer
versions; you can safely ignore the warning in either case.
The return value, if non-negative, represents the number of
bytes successfully written.
int (*readdir) (struct inode *, struct file *, void *,
,
filldir_t);
This field should be NULL
for device nodes; it
is used only for directories.
int (*select) (struct inode *, struct file *, int,
,
select_table *);
select is used by programs to ask if the device is
readable or writable, or if an ``exception'' condition has
happened. If the pointer is NULL
, the device is
assumed to be both readable and writable, with no
exceptions pending. The meaning of ``exception'' is
device-dependent. The implementation of select is
completely different in the current 2.1 development kernels.
(See Section 17.2.2, in Chapter 17.)
The return value tells whether condition
is met (1) or not (0).
int (*ioctl) (struct inode *, struct file *, unsigned int,
,
unsigned long);
The ioctl system call offers a way to issue
device-specific commands (like formatting a track of a floppy
disk, which is neither reading nor writing). Additionally, a
few ioctl commands are recognized by the kernel without
referring to the fops
table. If the device doesn’t offer
an ioctl entry point, the system call returns
-EINVAL
for any request that isn’t predefined. A
non-negative return value is passed back to the calling program
to indicate a successful completion.
int (*mmap) (struct inode *, struct file *,
,
struct vm_area_struct *);
mmap is used to request a mapping of device memory
to a process’s memory. If the device doesn’t provide this
method, the mmap system call returns -ENODEV
.
int (*open) (struct inode *, struct file *);
Though this is always the first operation
performed on the device node, the driver is not required
to declare a corresponding method. If this entry is
NULL
, opening the device always succeeds, but
your driver isn’t notified.
void (*release) (struct inode *, struct file *);
This operation is invoked when the node is closed. Like open, release can be missing. In 2.0 and earlier kernel versions, the close system call never fails; this changed in 2.1.31 (see Chapter 17).
int (*fsync) (struct inode *, struct file *);
Flush the device. If not supported, the fsync
system call returns -EINVAL
.
int (*fasync) (struct inode *, struct file *, int);
This operation is used to notify the device of a
change in its FASYNC
flag. Asynchronous notification
is an advanced topic and will be described in
Section 5.4 in Chapter 5. The field
can be NULL
if the driver won’t support asynchronous
notification.
int (*check_media_change) (kdev_t dev);
check_media_change is only used with block devices, especially removable media like floppies. The method is called by the kernel to determine if the physical medium (e.g., the floppy disk) in the device has changed since the last operation (return value is 1) or not (0). This function doesn’t need to be declared for char devices.
int (*revalidate) (kdev_t dev);
This is the last entry, which is meaningful only for block drivers. revalidate is related to buffer cache management, as is the previous method. We’ll discuss revalidate in Section 12.6 in Chapter 12.
The file_operations
structure used in the scull
driver is the following:
struct file_operations scull_fops = { scull_lseek, scull_read, scull_write, NULL, /* scull_readdir */ NULL, /* scull_select */ scull_ioctl, NULL, /* scull_mmap */ scull_open, scull_release, /* nothing more, fill with NULLs */ };
A few of the prototypes above have changed slightly in the latest kernel development. The list was extracted from a 2.0.x header, and the prototypes as shown are correct for a wide range of kernels. The differences introduced in the 2.1 kernel (and the fix needed to make our modules portable) are detailed in the section pertaining to each specific operation and in Section 17.2 in Chapter 17.
18.216.239.46