These devices are identified in user space by a filename: if you want to read from a UART, you open the device node, for example, the first serial port on the ARM Versatile Express would be /dev/ttyAMA0
. The driver is identified differently in the kernel, using the major number which, in the example given, is 204
. Since the UART driver can handle more than one UART, there is a second number, called the minor number, which identifies a specific interface, 64, in this case:
# ls -l /dev/ttyAMA* crw-rw---- 1 root root 204, 64 Jan 1 1970 /dev/ttyAMA0 crw-rw---- 1 root root 204, 65 Jan 1 1970 /dev/ttyAMA1 crw-rw---- 1 root root 204, 66 Jan 1 1970 /dev/ttyAMA2 crw-rw---- 1 root root 204, 67 Jan 1 1970 /dev/ttyAMA3
The list of standard major and minor numbers can be found in the kernel documentation, in Documentation/devices.txt
. The list does not get updated very often and does not include the ttyAMA
device described in the preceding paragraph. Nevertheless, if you look at the source code in drivers/tty/serial/amba-pl011.c
, you will see that the major and minor numbers are declared:
#define SERIAL_AMBA_MAJOR 204 #define SERIAL_AMBA_MINOR 64
Where there is more than one instance of a device, the naming convention for the device nodes is <base name><interface number>
, for example, ttyAMA0
, ttyAMA1
, and so on.
As I mentioned in Chapter 5, Building a Root Filesystem, the device nodes can be created in several ways:
devtmpfs
: The node that is created when the device driver registers a new device interface using a base name supplied by the driver (ttyAMA
) and an instance number.udev
or mdev
(without devtmpfs
): Essentially the same as with devtmpfs
, except that a user-space daemon program has to extract the device name from sysfs
and create the node. I will talk about sysfs
later.mknod
: If you are using static device nodes, they are created manually using mknod
.You may have the impression from the numbers I have used above that both major and minor numbers are 8-bit numbers in the range 0 to 255. In fact, from Linux 2.6 onwards, the major number is 12 bits long, which gives valid numbers from 1 to 4,095, and the minor number is 20 bits, from 0 to 1,048,575.
When you open a device node, the kernel checks to see whether the major and minor numbers fall into a range registered by a device driver of that type (a character or block). If so, it passes the call to the driver, otherwise the open call fails. The device driver can extract the minor number to find out which hardware interface to use. If the minor number is out of range, it returns an error.
To write a program that accesses a device driver, you have to have some knowledge of how it works. In other words, a device driver is not the same as a file: the things you do with it change the state of the device. A simple example is the pseudo random number generator, urandom
, which returns bytes of random data every time you read it. Here is a program that does just that:
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> int main(void) { int f; unsigned int rnd; int n; f = open("/dev/urandom", O_RDONLY); if (f < 0) { perror("Failed to open urandom"); return 1; } n = read(f, &rnd, sizeof(rnd)); if (n != sizeof(rnd)) { perror("Problem reading urandom"); return 1; } printf("Random number = 0x%x\n", rnd); close(f); return 0; }
The nice thing about the Unix driver model is that, once we know that there is a device named urandom
and that every time we read from it, it returns a fresh set of pseudo random data, we don't need to know anything else about it. We can just use normal functions such as open(2)
, read(2)
, and close(2)
.
We could use the stream I/O functions fopen(3)
, fread(3)
, and fclose(3)
instead, but the buffering implicit in these functions often causes unexpected behavior. For example, fwrite(3)
usually only writes to the user-space buffer, not to the device. We would need to call fflush(3)
to force the buffer to be written out.