Now that we’ve taken a quick look at the fields, we’ll start using them in real scull functions.
The open method is provided for a driver to do any initialization in preparation for later operations. In addition, open usually increments the usage count for the device so that the module won’t be unloaded before the file is closed. The count, described in Section 2.4.2 in Chapter 2, is then decremented by the release method.
In most drivers, open should perform the following tasks:
Increment the usage count
Check for device-specific errors (such as device-not-ready or similar hardware problems)
Initialize the device, if it is being opened for the first time
Identify the minor number and update the
f_op
pointer, if necessaryAllocate and fill any data structure to be put in
filp->private_data
In scull, most of the preceding tasks
depend on the minor number of the device being opened. Therefore, the
first thing to do is identify which device is involved. We can do that
by looking at inode->i_rdev
.
We’ve already talked about how the kernel doesn’t use the minor number
of the device, so the driver is free to use it at will. In practice,
different minor numbers are used to access different devices or to
open the same device in a different way. For example,
/dev/st0
(minor number 0) and
/dev/st1
(minor 1) refer to different SCSI tape
drives, whereas /dev/nst0
(minor 128) is the same
physical device as /dev/st0
, but it acts
differently (it doesn’t rewind the tape when it is closed). All of the
tape device files have different minor numbers, so that the driver can
tell them apart.
A driver never actually knows the name of the device being opened, just the device number—and users can play on this indifference to names by aliasing new names to a single device for their own convenience. If you create two special files with the same major/minor pair, the devices are one and the same, and there is no way to differentiate between them. The same effect can be obtained using a symbolic or hard link, and the preferred way to implement aliasing is creating a symbolic link.
The scull driver uses the minor number like
this: the most significant nibble (upper four bits) identifies the
type (personality) of the device, and the least significant nibble
(lower four bits) lets you distinguish between individual devices if
the type supports more than one device instance. Thus,
scull0 is different from
scullpipe0 in the top nibble, while
scull0 and scull1 differ in
the bottom nibble.[19] Two macros (TYPE
and
NUM
) are defined in the source to extract the bits
from a device number, as shown here:
#define TYPE(dev) (MINOR(dev) >> 4) /* high nibble */ #define NUM(dev) (MINOR(dev) & 0xf) /* low nibble */
For each device type, scull defines a
specific file_operations
structure, which is placed
in filp->f_op
at open time. The following code
shows how multiple fops
are implemented:
struct file_operations *scull_fop_array[]={ &scull_fops, /* type 0 */ &scull_priv_fops, /* type 1 */ &scull_pipe_fops, /* type 2 */ &scull_sngl_fops, /* type 3 */ &scull_user_fops, /* type 4 */ &scull_wusr_fops /* type 5 */ }; #define SCULL_MAX_TYPE 5 /* In scull_open, the fop_array is used according to TYPE(dev) */ int type = TYPE(inode->i_rdev); if (type > SCULL_MAX_TYPE) return -ENODEV; filp->f_op = scull_fop_array[type];
The kernel invokes open according to the major
number; scull uses the minor number in the
macros just shown. TYPE
is used to index into
scull_fop_array
in order to extract the right set
of methods for the device type being opened.
In scull, filp->f_op
is assigned to the correct file_operations
structure as determined by the device type, found in the minor
number. The open method declared in the new
fops
is then invoked. Usually, a driver doesn’t
invoke its own fops
, because they are used by the
kernel to dispatch the right driver method. But when your
open method has to deal with different device
types, you might want to call fops->open
after
modifying the fops
pointer according to the minor
number being opened.
The actual code for scull_open follows. It uses
the TYPE
and NUM
macros defined
in the previous code snapshot to split the minor number:
int scull_open(struct inode *inode, struct file *filp) { Scull_Dev *dev; /* device information */ int num = NUM(inode->i_rdev); int type = TYPE(inode->i_rdev); /* * If private data is not valid, we are not using devfs * so use the type (from minor nr.) to select a new f_op */ if (!filp->private_data && type) { if (type > SCULL_MAX_TYPE) return -ENODEV; filp->f_op = scull_fop_array[type]; return filp->f_op->open(inode, filp); /* dispatch to specific open */ } /* type 0, check the device number (unless private_data valid) */ dev = (Scull_Dev *)filp->private_data; if (!dev) { if (num >= scull_nr_devs) return -ENODEV; dev = &scull_devices[num]; filp->private_data = dev; /* for other methods */ } MOD_INC_USE_COUNT; /* Before we maybe sleep */ /* now trim to 0 the length of the device if open was write-only */ if ( (filp->f_flags & O_ACCMODE) == O_WRONLY) { if (down_interruptible(&dev->sem)) { MOD_DEC_USE_COUNT; return -ERESTARTSYS; } scull_trim(dev); /* ignore errors */ up(&dev->sem); } return 0; /* success */ }
A few explanations are due here. The data structure used to hold the
region of memory is Scull_Dev
, which will be
introduced shortly. The global variables
scull_nr_devs
and
scull_devices[]
(all lowercase) are the number of
available devices and the actual array of pointers to
Scull_Dev
.
The calls to down_interruptible and up can be ignored for now; we will get to them shortly.
The code looks pretty sparse because it doesn’t do any particular device handling when open is called. It doesn’t need to, because the scull0-3 device is global and persistent by design. Specifically, there’s no action like “initializing the device on first open” because we don’t keep an open count for sculls, just the module usage count.
Given that the kernel can maintain the usage count of the module via
the owner
field in the
file_operations
structure, you may be wondering why
we increment that count manually here. The answer is that older
kernels required modules to do all of the work of maintaining their
usage count—the owner
mechanism did not
exist. To be portable to older kernels,
scull increments its own usage count. This
behavior will cause the usage count to be too high on 2.4 systems, but
that is not a problem because it will still drop to zero when the
module is not being used.
The only real operation performed on the device is truncating it to a length of zero when the device is opened for writing. This is performed because, by design, overwriting a pscull device with a shorter file results in a shorter device data area. This is similar to the way opening a regular file for writing truncates it to zero length. The operation does nothing if the device is opened for reading.
We’ll see later how a real initialization works when we look at the code for the other scull personalities.
The role of the release method is the reverse of
open. Sometimes you’ll find that the method
implementation is called
device
_close
instead of
device
_release
. Either
way, the device method should perform the following tasks:
Deallocate anything that open allocated in
filp->private_data
Shut down the device on last close
Decrement the usage count
The basic form of scull has no hardware to shut down, so the code required is minimal:[20]
int scull_release(struct inode *inode, struct file *filp) { MOD_DEC_USE_COUNT; return 0; }
It is important to decrement the usage count if you incremented it at open time, because the kernel will never be able to unload the module if the counter doesn’t drop to zero.
How can the counter remain consistent if sometimes a file is closed without having been opened? After all, the dup and fork system calls will create copies of open files without calling open; each of those copies is then closed at program termination. For example, most programs don’t open their stdin file (or device), but all of them end up closing it.
The answer is simple: not every close system call
causes the release method to be invoked. Only the
ones that actually release the device data structure invoke the
method—hence its name. The kernel keeps a counter of how many
times a file
structure is being used. Neither
fork nor dup creates a new
file
structure (only open does
that); they just increment the counter in the existing structure.
The close system call executes the
release method only when the counter for the
file
structure drops to zero, which happens when
the structure is destroyed. This relationship between the
release method and the close
system call guarantees that the usage count for modules is always
consistent.
Note that the flush method is called every time an application calls close. However, very few drivers implement flush, because usually there’s nothing to perform at close time unless release is involved.
As you may imagine, the previous discussion applies even when the application terminates without explicitly closing its open files: the kernel automatically closes any file at process exit time by internally using the close system call.
[19] Bit splitting is a typical way to use minor numbers. The IDE driver, for example, uses the top two bits for the disk number, and the bottom six bits for the partition number.
[20] The other
flavors of the device are closed by different functions, because
scull_open substituted a different
filp->f_op
for each device. We’ll see those
later.
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.