2

I've been reading this book: https://lwn.net/Kernel/LDD3/. Here, the authors distinguish between 3 types of device files, namely char, block, and network devices. In page 6 of the first chapter, I find this:

Character devices

A character (char) device is one that can be accessed as a stream of bytes (like a file); a char driver is in charge of implementing this behaviour. Such a driver usually implements at least the open, close, read, and write system calls. The text console (/dev/console) and the serial ports (/dev/ttyS0 and friends) are examples of char devices, as they are well represented by the stream abstraction. Char devices are accessed by means of filesystem nodes, such as /dev/tty1 and /dev/lp0.

So what exactly is the difference between a char device and this filesystem node? What confuses me more is that an ls -la /dev/ shows these filesystem nodes also as char devices (the description starts with a c).

My guess is that the book refers to char devices as a one-one correspondence between hardware, and these filesystem nodes as a software abstraction. Any good resources regarding this are appreciated.

Andy Dalton
  • 13,993
  • So what exactly is the difference between a char device and this filesystem node?: Char devices are accessed by means of filesystem nodes Just as files are accessed by filename. – Eduardo Trápani Jun 21 '20 at 17:24
  • @EduardoTrápani /dev/ttyS0 and /dev/tty0 seem so similar. How is one a char device while the other is a filesystem node? What is the point of difference between them? So I can't open() a char device but can open() a filesystem node? I mean both of them seem to offer almost exactly the same interface to userspace applications – Suraaj K S Jun 21 '20 at 17:35

4 Answers4

2

So what exactly is the difference between a char device and this filesystem node?

I interpret this question as "what is the difference between a character device driver and the character device file?"

A character device driver is kernel software that operates on a stream of bytes, usually to communicate with some piece of hardware that also operates on a stream of bytes.

A character device file is a file on the filesystem. Device files have metadata associated with them that the kernel uses to know the character device driver with which the file is associated. Character device files (actually, all device files) have two pieces of metadata: the major and minor device numbers. You can see the major/minor numbers when you look the output of ls -l. Consider, for example, the character device file /dev/null:

$ ls -l /dev/null
crw-rw-rw- 1 root root 1, 3 Jun  6 14:30 /dev/null

Notice here the 1, 3 after the second root -- that's the major device number (1) and the minor device number (3). When a process interacts a device file, the kernel uses the major device number to know which kernel device driver handles I/O against the file. Character devices with major number 1 are associated with memory devices; see major.h:

./include/uapi/linux/major.h:#define MEM_MAJOR      1

A single device driver can often "drive" multiple devices; the minor device number tells the kernel the specific device on which the user is operating. For example, the following character device files all have the same major number, but different minor numbers:

# ls -l /dev/zero /dev/mem /dev/null /dev/full /dev/random /dev/urandom /dev/kmsg
crw-rw-rw- 1 root root 1,  7 Jun  6 14:30 /dev/full
crw-r--r-- 1 root root 1, 11 Jun  6 14:30 /dev/kmsg
crw-r----- 1 root kmem 1,  1 Jun  6 14:30 /dev/mem
crw-rw-rw- 1 root root 1,  3 Jun  6 14:30 /dev/null
crw-rw-rw- 1 root root 1,  8 Jun  6 14:30 /dev/random
crw-rw-rw- 1 root root 1,  9 Jun  6 14:30 /dev/urandom
crw-rw-rw- 1 root root 1,  5 Jun  6 14:30 /dev/zero

The following source snippets come from Linux 5.4.32, file drivers/char/mem.c.

From the ls output above, we observe that all of those files have major device number 1. From that we know that the same kernel device driver responds to I/O requests to any process opening/reading/writing those files. From the kernel sources we see that the memory device driver is responsible for handling I/O against all those files:

static const struct memdev {
        const char *name;
        umode_t mode;
        const struct file_operations *fops;
        fmode_t fmode;
} devlist[] = {
#ifdef CONFIG_DEVMEM
         [1] = { "mem", 0, &mem_fops, FMODE_UNSIGNED_OFFSET },
#endif
#ifdef CONFIG_DEVKMEM
         [2] = { "kmem", 0, &kmem_fops, FMODE_UNSIGNED_OFFSET },
#endif
         [3] = { "null", 0666, &null_fops, 0 },
#ifdef CONFIG_DEVPORT
         [4] = { "port", 0, &port_fops, 0 },
#endif
         [5] = { "zero", 0666, &zero_fops, 0 },
         [7] = { "full", 0666, &full_fops, 0 },
         [8] = { "random", 0666, &random_fops, 0 },
         [9] = { "urandom", 0666, &urandom_fops, 0 },
#ifdef CONFIG_PRINTK
        [11] = { "kmsg", 0644, &kmsg_fops, 0 },
#endif
};

Notice that the array indices --- the numbers in brackets --- match up with the minor device numbers on the associated files.

Now, let's consider an example where a process uses one of the character device files. If we have a shell script that contains:

echo "hello" > /dev/null

Then the script open()s the character device file /dev/null. The kernel knows that /dev/null is a character device and examines the major and minor device number associated with the file. It sees major number 1, so it routes the open() request to the character device driver that handles operations on major number 1 (the memory device). That ends up in the the function in the memory device driver that handles open calls:

static int memory_open(struct inode *inode, struct file *filp)
{
        int minor;
        const struct memdev *dev;
    minor = iminor(inode);
    if (minor >= ARRAY_SIZE(devlist))
            return -ENXIO;

    dev = &devlist[minor];
    if (!dev->fops)
            return -ENXIO;

    filp->f_op = dev->fops;
    filp->f_mode |= dev->fmode;

    if (dev->fops->open)
            return dev->fops->open(inode, filp);

    return 0;

}

The memory_open() function then uses the minor device number to index into the devlist array that we saw earlier. If that device has a special open() function, then it calls that, otherwise it just returns 0; for the null device there is no special open() function.

Eventually, the process will call write() to write "hello" to the file descriptor associated with the open file. Again, the kernel knows that the open file is associated with a character device with major number 1 and minor number 3, so it routes the write() to the driver for major device type 1 (the memory device). The device with minor number 3 has a set of functions registered to handle I/O (here, null_fops):

         [3] = { "null", 0666, &null_fops, 0 },

The null_fops struct contains the following pointers to functions:

static const struct file_operations null_fops = {
        ...
        .write          = write_null,
        ...
};

So a write() to a character device file with major number 1, minor number 3 will result in a call to write_null(). The implementation of that function is:

static ssize_t write_null(struct file *file, const char __user *buf,
                          size_t count, loff_t *ppos)
{
        return count;
}

The write_null() function does nothing, and returns count to indicate that count bytes were successfully written (the behavior that we expect from writing to /dev/null).

To summarize, character device files contain metadata: the major and minor device numbers. When processes perform I/O on character device files, the kernel uses that metadata to find the right character device driver in the kernel to handle the I/O requests made against the file.

Andy Dalton
  • 13,993
  • Well, my issue was with why the book distinguishes /dev/tty0 and /dev/ttyS0. The book says that device files like the latter are "accessed by" device files such as the former. That is, device files (/dev/ttyS0) are accessed by filesystem nodes (/dev/tty0) – Suraaj K S Jun 22 '20 at 11:36
  • @SuraajKS All of those are character device files ("filesystem nodes"). Do some happen to interact with the same hardware sometimes? Yes, but that's not a fundamental aspect of the concept of character device files/drivers. See, for instance, https://unix.stackexchange.com/a/60649/90691 – Andy Dalton Jun 22 '20 at 13:03
0

In general, a node could be just a generic name for the vertex of any graph. In the context of the filesystem thee, the files and directories would naturally be the nodes (filenames perhaps being the graph edges). The data structure holding the data of files is also called the "inode".

We seldom call files plain "nodes" without the "i", except in the context of the mknod() call, used to create those special files for devices.

The device files are just a special sort of file that contain no data, but instead have some numbers identifying which kernel driver is responsible for handling any accesses to those device files.

The driver is some piece of software in the kernel that does something useful. It might provide access to an actual piece of hardware (e.g. /dev/ttyS0 for a serial port), or not (e.g. /dev/zero).

The actual device is of course that piece of physical hardware, connectors and chips.

Of course, just to confuse issues, we call those "device files" just "devices" for short.

So, /dev/ttyS0 is a filesystem node that is a character special file, with the major:minor device number 64:0, which identifies it as a device handled by the serial driver, which accesses some physical device, probably something that acts like a 16550 UART chip.

ilkkachu
  • 138,973
0

You could say that the device is the combination of major and minor number. This combination is fixed and cannot be changed. This combination is part of the device file. An application opens the device file and is kind of directed to the device by the respective number combination.

In contrast to the major and minor numbers, the name of a device file is in theory random. You could create a file /dev/sda which points to the kernel device name sdb.

Hauke Laging
  • 90,279
0

Despite M. Dalton's long answer, the book is still confusing you. The book is misleading you. Here is what the book should say:

The kernel virtual terminals, the parallel ports, and the serial ports are examples of character devices, as they are well represented by the stream abstraction. Character devices are opened by means of filesystem nodes, such as /dev/tty1 (for opening the first kernel virtual terminal), /dev/lp0 (for opening the first parallel port), and /dev/ttyS0 (for opening the first serial port).

It shouldn't really discuss /dev/console at all. That and /dev/tty are too complex to use as basic examples. (Notice that M. Dalton wisely did not use them.) They too are used for opening character devices, but exactly which character devices is more complex than in the aforementioned cases of KVTs, serial ports, and parallel ports. And the console, in particular, introduces a whole second set of names that look like filesystem node names, but in fact are not.

Further reading

JdeBP
  • 68,745