Major and Minor Numbers

Char devices are accessed through names in the filesystem. Those names are called special files or device files or simply nodes of the filesystem tree; they are conventionally located in the /dev directory. Special files for char drivers are identified by a “c” in the first column of the output of ls -l. Block devices appear in /dev as well, but they are identified by a “b.” The focus of this chapter is on char devices, but much of the following information applies to block devices as well.

If you issue the ls -l command, you’ll see two numbers (separated by a comma) in the device file entries before the date of last modification, where the file length normally appears. These numbers are the major device number and minor device number for the particular device. The following listing shows a few devices as they appear on a typical system. Their major numbers are 1, 4, 7, and 10, while the minors are 1, 3, 5, 64, 65, and 129.

 crw-rw-rw- 1 root   root    1, 3   Feb 23 1999  null
 crw------- 1 root   root   10, 1   Feb 23 1999  psaux
 crw------- 1 rubini tty     4, 1   Aug 16 22:22 tty1
 crw-rw-rw- 1 root   dialout 4, 64  Jun 30 11:19 ttyS0
 crw-rw-rw- 1 root   dialout 4, 65  Aug 16 00:00 ttyS1
 crw------- 1 root   sys     7, 1   Feb 23 1999  vcs1
 crw------- 1 root   sys     7, 129 Feb 23 1999  vcsa1
 crw-rw-rw- 1 root   root    1, 5   Feb 23 1999  zero

The major number identifies the driver associated with the device. For example, /dev/null and /dev/zero are both managed by driver 1, whereas virtual consoles and serial terminals are managed by driver 4; similarly, both vcs1 and vcsa1 devices are managed by driver 7. The kernel uses the major number at open time to dispatch execution to the appropriate driver.

The minor number is used only by the driver specified by the major number; other parts of the kernel don’t use it, and merely pass it along to the driver. It is common for a driver to control several devices (as shown in the listing); the minor number provides a way for the driver to differentiate among them.

Version 2.4 of the kernel, though, introduced a new (optional) feature, the device file system or devfs. If this file system is used, management of device files is simplified and quite different; on the other hand, the new filesystem brings several user-visible incompatibilities, and as we are writing it has not yet been chosen as a default feature by system distributors. The previous description and the following instructions about adding a new driver and special file assume that devfs is not present. The gap is filled later in this chapter, in Section 3.10.

When devfs is not being used, adding a new driver to the system means assigning a major number to it. The assignment should be made at driver (module) initialization by calling the following function, defined in <linux/fs.h>:

 int register_chrdev(unsigned int major, const char *name,
      struct file_operations *fops);

The return value indicates success or failure of the operation. A negative return code signals an error; a 0 or positive return code reports successful completion. The major argument is the major number being requested, name is the name of your device, which will appear in /proc/devices, and fops is the pointer to an array of function pointers, used to invoke your driver’s entry points, as explained in Section 3.3, later in this chapter.

The major number is a small integer that serves as the index into a static array of char drivers; Section 3.2.1 later in this chapter explains how to select a major number. The 2.0 kernel supported 128 devices; 2.2 and 2.4 increased that number to 256 (while reserving the values 0 and 255 for future uses). Minor numbers, too, are eight-bit quantities; they aren’t passed to register_chrdev because, as stated, they are only used by the driver itself. There is tremendous pressure from the developer community to increase the number of possible devices supported by the kernel; increasing device numbers to at least 16 bits is a stated goal for the 2.5 development series.

Once the driver has been registered in the kernel table, its operations are associated with the given major number. Whenever an operation is performed on a character device file associated with that major number, the kernel finds and invokes the proper function from the file_operations structure. For this reason, the pointer passed to register_chrdev should point to a global structure within the driver, not to one local to the module’s initialization function.

The next question is how to give programs a name by which they can request your driver. A name must be inserted into the /dev directory and associated with your driver’s major and minor numbers.

The command to create a device node on a filesystem is mknod; superuser privileges are required for this operation. The command takes three arguments in addition to the name of the file being created. For example, the command

 mknod /dev/scull0 c 254 0

creates a char device (c) whose major number is 254 and whose minor number is 0. Minor numbers should be in the range 0 to 255 because, for historical reasons, they are sometimes stored in a single byte. There are sound reasons to extend the range of available minor numbers, but for the time being, the eight-bit limit is still in force.

Please note that once created by mknod, the special device file remains unless it is explicitly deleted, like any information stored on disk. You may want to remove the device created in this example by issuing rm /dev/scull0.

Dynamic Allocation of Major Numbers

Some major device numbers are statically assigned to the most common devices. A list of those devices can be found in Documentation/devices.txt within the kernel source tree. Because many numbers are already assigned, choosing a unique number for a new driver can be difficult—there are far more custom drivers than available major numbers. You could use one of the major numbers reserved for “experimental or local use,”[14] but if you experiment with several “local” drivers or you publish your driver for third parties to use, you’ll again experience the problem of choosing a suitable number.

Fortunately (or rather, thanks to someone’s ingenuity), you can request dynamic assignment of a major number. If the argument major is set to 0 when you call register_chrdev, the function selects a free number and returns it. The major number returned is always positive, while negative return values are error codes. Please note the behavior is slightly different in the two cases: the function returns the allocated major number if the caller requests a dynamic number, but returns 0 (not the major number) when successfully registering a predefined major number.

For private drivers, we strongly suggest that you use dynamic allocation to obtain your major device number, rather than choosing a number randomly from the ones that are currently free. If, on the other hand, your driver is meant to be useful to the community at large and be included into the official kernel tree, you’ll need to apply to be assigned a major number for exclusive use.

The disadvantage of dynamic assignment is that you can’t create the device nodes in advance because the major number assigned to your module can’t be guaranteed to always be the same. This means that you won’t be able to use loading-on-demand of your driver, an advanced feature introduced in Chapter 11. For normal use of the driver, this is hardly a problem, because once the number has been assigned, you can read it from /proc/devices.

To load a driver using a dynamic major number, therefore, the invocation of insmod can be replaced by a simple script that after calling insmod reads /proc/devices in order to create the special file(s).

A typical /proc/devices file looks like the following:

Character devices:
 1 mem
 2 pty
 3 ttyp
 4 ttyS
 6 lp
 7 vcs
 10 misc
 13 input
 14 sound
 21 sg
180 usb

Block devices:
 2 fd
 8 sd
 11 sr
 65 sd
 66 sd

The script to load a module that has been assigned a dynamic number can thus be written using a tool such as awk to retrieve information from /proc/devices in order to create the files in /dev.

The following script, scull_load, is part of the scull distribution. The user of a driver that is distributed in the form of a module can invoke such a script from the system’s rc.local file or call it manually whenever the module is needed.

#!/bin/sh
module="scull"
device="scull"
mode="664"

# invoke insmod with all arguments we were passed
# and use a pathname, as newer modutils don't look in . by default
/sbin/insmod -f ./$module.o $* || exit 1

# remove stale nodes
rm -f /dev/${device}[0-3]

major=`awk "\\$2==\"$module\" {print \\$1}" /proc/devices`

mknod /dev/${device}0 c $major 0
mknod /dev/${device}1 c $major 1
mknod /dev/${device}2 c $major 2
mknod /dev/${device}3 c $major 3

# give appropriate group/permissions, and change the group.
# Not all distributions have staff; some have "wheel" instead.
group="staff"
grep '^staff:' /etc/group > /dev/null || group="wheel"

chgrp $group /dev/${device}[0-3]
chmod $mode /dev/${device}[0-3]

The script can be adapted for another driver by redefining the variables and adjusting the mknod lines. The script just shown creates four devices because four is the default in the scull sources.

The last few lines of the script may seem obscure: why change the group and mode of a device? The reason is that the script must be run by the superuser, so newly created special files are owned by root. The permission bits default so that only root has write access, while anyone can get read access. Normally, a device node requires a different access policy, so in some way or another access rights must be changed. The default in our script is to give access to a group of users, but your needs may vary. Later, in Section 5.6 in Chapter 5, the code for sculluid will demonstrate how the driver can enforce its own kind of authorization for device access. A scull_unload script is then available to clean up the /dev directory and remove the module.

As an alternative to using a pair of scripts for loading and unloading, you could write an init script, ready to be placed in the directory your distribution uses for these scripts.[15] As part of the scull source, we offer a fairly complete and configurable example of an init script, called scull.init; it accepts the conventional arguments—either “start” or “stop” or “restart”—and performs the role of both scull_load and scull_unload.

If repeatedly creating and destroying /dev nodes sounds like overkill, there is a useful workaround. If you are only loading and unloading a single driver, you can just use rmmod and insmod after the first time you create the special files with your script: dynamic numbers are not randomized, and you can count on the same number to be chosen if you don’t mess with other (dynamic) modules. Avoiding lengthy scripts is useful during development. But this trick, clearly, doesn’t scale to more than one driver at a time.

The best way to assign major numbers, in our opinion, is by defaulting to dynamic allocation while leaving yourself the option of specifying the major number at load time, or even at compile time. The code we suggest using is similar to the code introduced for autodetection of port numbers. The scull implementation uses a global variable, scull_major, to hold the chosen number. The variable is initialized to SCULL_MAJOR, defined in scull.h. The default value of SCULL_MAJOR in the distributed source is 0, which means “use dynamic assignment.” The user can accept the default or choose a particular major number, either by modifying the macro before compiling or by specifying a value for scull_major on the insmod command line. Finally, by using the scull_load script, the user can pass arguments to insmod on scull_load’s command line.[16]

Here’s the code we use in scull’s source to get a major number:

 result = register_chrdev(scull_major, "scull", &scull_fops);
 if (result < 0) {
  printk(KERN_WARNING "scull: can't get major %d\n",scull_major);
  return result;
 }
 if (scull_major == 0) scull_major = result; /* dynamic */

Removing a Driver from the System

When a module is unloaded from the system, the major number must be released. This is accomplished with the following function, which you call from the module’s cleanup function:

 int unregister_chrdev(unsigned int major, const char *name);

The arguments are the major number being released and the name of the associated device. The kernel compares the name to the registered name for that number, if any: if they differ, -EINVAL is returned. The kernel also returns -EINVAL if the major number is out of the allowed range.

Failing to unregister the resource in the cleanup function has unpleasant effects. /proc/devices will generate a fault the next time you try to read it, because one of the name strings still points to the module’s memory, which is no longer mapped. This kind of fault is called an oops because that’s the message the kernel prints when it tries to access invalid addresses.[17]

When you unload the driver without unregistering the major number, recovery will be difficult because the strcmp function in unregister_chrdev must dereference a pointer (name) to the original module. If you ever fail to unregister a major number, you must reload both the same module and another one built on purpose to unregister the major. The faulty module will, with luck, get the same address, and the name string will be in the same place, if you didn’t change the code. The safer alternative, of course, is to reboot the system.

In addition to unloading the module, you’ll often need to remove the device files for the removed driver. The task can be accomplished by a script that pairs to the one used at load time. The script scull_unload does the job for our sample device; as an alternative, you can invoke scull.init stop.

If dynamic device files are not removed from /dev, there’s a possibility of unexpected errors: a spare /dev/framegrabber on a developer’s computer might refer to a fire-alarm device one month later if both drivers used a dynamic major number. “No such file or directory” is a friendlier response to opening /dev/framegrabber than the new driver would produce.

dev_t and kdev_t

So far we’ve talked about the major number. Now it’s time to discuss the minor number and how the driver uses it to differentiate among devices.

Every time the kernel calls a device driver, it tells the driver which device is being acted upon. The major and minor numbers are paired in a single data type that the driver uses to identify a particular device. The combined device number (the major and minor numbers concatenated together) resides in the field i_rdev of the inode structure, which we introduce later. Some driver functions receive a pointer to struct inode as the first argument. So if you call the pointer inode (as most driver writers do), the function can extract the device number by looking at inode->i_rdev.

Historically, Unix declared dev_t (device type) to hold the device numbers. It used to be a 16-bit integer value defined in <sys/types.h>. Nowadays, more than 256 minor numbers are needed at times, but changing dev_t is difficult because there are applications that “know” the internals of dev_t and would break if the structure were to change. Thus, while much of the groundwork has been laid for larger device numbers, they are still treated as 16-bit integers for now.

Within the Linux kernel, however, a different type, kdev_t, is used. This data type is designed to be a black box for every kernel function. User programs do not know about kdev_t at all, and kernel functions are unaware of what is inside a kdev_t. If kdev_t remains hidden, it can change from one kernel version to the next as needed, without requiring changes to everyone’s device drivers.

The information about kdev_t is confined in <linux/kdev_t.h>, which is mostly comments. The header makes instructive reading if you’re interested in the reasoning behind the code. There’s no need to include the header explicitly in the drivers, however, because <linux/fs.h> does it for you.

The following macros and functions are the operations you can perform on kdev_t:

MAJOR(kdev_t dev);

Extract the major number from a kdev_t structure.

MINOR(kdev_t dev);

Extract the minor number.

MKDEV(int ma, int mi);

Create a kdev_t built from major and minor numbers.

kdev_t_to_nr(kdev_t dev);

Convert a kdev_t type to a number (a dev_t).

to_kdev_t(int dev);

Convert a number to kdev_t. Note that dev_t is not defined in kernel mode, and therefore int is used.

As long as your code uses these operations to manipulate device numbers, it should continue to work even as the internal data structures change.



[14] Major numbers in the ranges 60 to 63, 120 to 127, and 240 to 254 are reserved for local and experimental use: no real device will be assigned such major numbers.

[15] Distributions vary widely on the location of init scripts; the most common directories used are /etc/init.d, /etc/rc.d/init.d, and /sbin/init.d. In addition, if your script is to be run at boot time, you will need to make a link to it from the appropriate run-level directory (i.e., .../rc3.d).

[16] The init script scull.init doesn’t accept driver options on the command line, but it supports a configuration file because it’s designed for automatic use at boot and shutdown time.

[17] The word oops is used as both a noun and a verb by Linux enthusiasts.

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.