open and release

Now that we’ve taken a quick look at the fields, we’ll start using them in real scull functions.

The open Method

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 necessary

  • Allocate 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 release Method

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.