Character devices

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:

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.