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.
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 */
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.
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
:
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.