Major and Minor Numbers

Char devices are accessed through names (or ``nodes'') in the filesystem, usually located in the /dev directory. Device files are special files and are identified by a ``c'' in the first column of the output of ls -l, indicating that they are char nodes. Block devices appear in /dev as well, but they are identified by a ``b''; even if some of the following information applies also to block devices, I am now focusing on char drivers.

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

crw-rw-rw-   1 root     root      10,   3 Nov 30  1993 bmouseatixl
crw-rw-rw-   1 root     sys        1,   3 Nov 30  1993 null
crw-rw-rw-   1 root     root       4, 128 Apr 30 13:02 ptyp0
crw-rw-rw-   1 root     root       4, 129 Apr 30 13:02 ptyp1
crw-rw-rw-   1 rubini   staff      4,   0 Jan 30  1995 tty0
crw-rw-rw-   1 root     tty        4,  64 Jan 25  1995 ttyS0
crw-rw-rw-   1 root     root       4,  65 May  1 00:04 ttyS1
crw-rw-rw-   1 root     sys        1,   5 Nov 30  1993 zero

The major number identifies the driver associated with the device. For example, /dev/null and /dev/zero are both managed by driver 1, while all the tty’s and pty’s are managed by driver 4. The kernel uses the major number to associate the appropriate driver with its device.

The minor number is only used by the device driver; other parts of the kernel don’t use it, and merely pass it it along to the driver. It isn’t unusual for a driver to control several devices (as in the example above)--the minor number provides a way to differentiate among them.

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 is the error code. A negative return code indicates an error; a zero or positive return code means 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 a jump table used to invoke the driver functions, 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. In 1.2.13 and early 2.x kernels, the array holds 64 elements, while 2.0.26 and 2.1.11 raised the number to 128. Minor numbers aren’t passed to register_chrdev, because only the driver cares about them.

Once the driver has been registered in the kernel table, whenever an operation is performed on a character file whose major number matches your driver’s major number, the kernel invokes the correct function in the driver’s code by indexing into the fops jump table.

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, and you must be the superuser to create devices. The command takes three arguments in addition to the name of the node being created. For example, the command:

mknod /dev/scull0 c 127 0

creates a char device (c) whose major number is 127 and whose minor number is 0. Minor numbers should be in the range 0-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 8-bit limit is still in force.

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 more custom drivers than available major numbers.

Fortunately (or rather, thanks to someone’s ingenuity), you can request dynamic assignment of a major number. If the argument major is set to zero when you call register_chrdev, the function selects a free number and returns it. The major number is always positive and thus can’t be mistaken for an error code.

I 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.

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 is hardly a problem, however, because once the number has been assigned, you can read it from /proc/devices. To load a driver, the invocation of insmod can be replaced by a simple script that reads /proc/devices to get the newly assigned major number in order to create the node(s).

A typical /proc/devices looks like the following:

Character devices:
 1 mem
 2 pty
 4 ttyp
 7 vcs
10 misc
63 scull

Block devices:
 3 ide0
22 ide1

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 /etc/rc.d/rc.local or call it manually whenever the module is needed. There is also a third option: using kerneld. This and other advanced features of modularization are covered in Chapter 11.

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

# invoke insmod with all arguments we got
/sbin/insmod -f $module $* || exit 1

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

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

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
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 shown above creates four devices because four is the default in the scull sources.

The last two lines of the script may seem obscure: why change the group and mode of a device? The reason is that the node is created by root and is thus 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 policy, and some change is needed. Access to a device is usually granted to a group of users, but the details depend on the device and on the system administrator. Security is a huge problem, beyond the scope of this book. The chmod and chgrp lines in scull_load are there only as a hint about handling permissions. Later, in the section 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.

If repeatedly creating and destroying /dev nodes sounds like overkill, there is a useful workaround. If you look at the kernel source, in fs/devices.c, you can see that dynamic numbers are assigned starting from 127 (or 63) and moving down, so you can create long-living nodes with 127 as the major number and avoid calling the script every time the associated device is loaded. This trick won’t work if you use several dynamic drivers, or if a new kernel release changes the behavior of dynamic allocation. (Code that you’ve written based on having peeked at the internals of the kernel isn’t guaranteed to keep working if the kernel changes.) Nonetheless, you might find this technique useful during development, when the module is constantly being loaded and unloaded.

The best way to assign major numbers, in my 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 I 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 zero, which means ``select 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.

Here’s the code I use in scull/main.c 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
",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 should be released. This is accomplished with the following function, which is called from cleanup_module:

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 they differ, -EINVAL is returned. The kernel also returns -EINVAL if the major number is out of the allowed range for majors or is not assigned to a driver.

Failing to unregister the resource from cleanup_module 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.[9]

When you unload the driver without unregistering the major number, the situation is unrecoverable, even from a ``rescue'' module written for that purpose, because the strcmp call in unregister_chrdev will use the unmapped name string and will oops when trying to release the number. Needless to say, any attempt to open a device node associated with the phantom major number will oops as well.

In addition to unloading the module, you’ll often need to remove the device nodes for the driver being unloaded. If the device nodes were created at load time, a simple script can be written to remove them at unload time. The script scull_unload does the job for our sample device. If dynamic nodes 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 used dynamic assignment to get a major number. ``No such file or directory'' is a friendlier response to opening /dev/framegrabber than the new device 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 the minor number to differentiate among devices.

Every time the kernel calls a device driver, it tells the driver what device is being acted upon. The major and minor numbers are paired in a single data type that is then used 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 is introduced later. Every driver function receives a pointer to struct inode as the first argument. The pointer is usually called inode as well, and 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 (and the C library itself) that know the internals of dev_t and would break if the structure were to change. The dev_t type, therefore, hasn’t been changed; it is still a 16-bit integer, and the minor numbers are restricted to the range 0-255.

Within the Linux kernel, however, a new type, kdev_t, is used. This new type is designed to be a black box for every kernel function. The idea is that user programs won’t even know about 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 philosophy 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 kdev_t type, unfortunately, is a ``modern'' idea and was missing from kernel version 1.2. In recent kernels, all the kernel variables and structure fields referring to devices are kdev_t items, but in 1.2.13 the same variables are in dev_t. This is not a problem if your driver uses only structure fields it receives, without declaring its own variables. If you need to declare your own device-type variable, you should add the following lines to your headers in order to ensure portability:

#if LINUX_VERSION_CODE < VERSION_CODE(1,3,28)
#  define kdev_t dev_t
   /* two conversion functions are defined in newer kernels */
#  define kdev_t_to_nr(dev) (dev)
#  define to_kdev_t(dev)    (dev)
#endif

This code is part of the sysdep.h header in the sample source files. I won’t refer to dev_t any more in the code, but will assume the previous conditional statement has been executed.

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);

Return 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.

The headers associated to Linux 1.2 defined the same functions to act on dev_t quantities, with the exception of the two conversion functions. That’s why the conditional code shown earlier defines them simply to return their argument.



[9] The word ``Oops'' is used as both a noun and a verb by Linux freaks.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.30.77