进一步阅读

进一步阅读

我一直在读这本书:https://lwn.net/Kernel/LDD3/。在这里,作者区分了 3 种类型的设备文件,即字符设备、块设备和网络设备。在第一章的第六页,我发现了这一点:

字符设备

字符 (char) 设备是一种可以作为字节流(如文件)进行访问的设备;字符驱动程序负责实现此行为。这样的驱动程序通常至少实现openclosereadwrite系统调用。文本控制台 ( /dev/console) 和串行端口(/dev/ttyS0以及类似的端口)是字符设备的示例,因为它们可以通过流抽象很好地表示。 Char 设备通过文件系统节点进行访问,例如/dev/tty1/dev/lp0

那么 char 设备和这个到底有什么区别文件系统节点?更让我困惑的是ls -la /dev/显示了这些文件系统节点也可作为 char 设备(描述以 ac 开头)。

我的猜测是,这本书将字符设备称为硬件之间的一一对应关系,而这些文件系统节点作为软件抽象。任何与此相关的好的资源都将受到赞赏。

答案1

那么 char 设备和这个到底有什么区别文件系统节点

我将这个问题解释为“字符设备驱动程序和字符设备文件有什么区别?”

字符设备司机是对字节流进行操作的内核软件,通常用于与也对字节流进行操作的某些硬件进行通信。

字符设备文件是文件系统上的一个文件。设备文件具有与其关联的元数据,内核使用这些元数据来了解与该文件关联的字符设备驱动程序。字符设备文件(实际上是所有设备文件)有两个元数据:主设备号和次设备号。当您查看 的输出时,您可以看到主要/次要数字ls -l。例如,考虑字符设备文件/dev/null

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

请注意1, 3第二个根之后的内容——这是主设备号 (1) 和次设备号 (3)。当进程与设备文件交互时,内核使用主设备号来了解哪个内核设备驱动程序针对该文件处理 I/O。主设备号为1的字符设备与内存设备相关联;看major.h

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

单个设备驱动程序通常可以“驱动”多个设备;次设备号告诉内核用户正在操作的特定设备。例如,以下字符设备文件都具有相同的主设备号,但不同的次设备号:

# 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

以下源代码片段来自 Linux 5.4.32,文件drivers/char/mem.c.

ls上面的输出中,我们观察到所有这些文件的主设备号都是 1。从中我们知道,相同的内核设备驱动程序响应对打开/读取/写入这些文件的任何进程的 I/O 请求。从内核源代码中我们看到内存设备驱动程序负责处理所有这些文件的 I/O:

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

请注意,数组索引(括号中的数字)与关联文件上的次设备号相匹配。

现在,让我们考虑一个进程使用其中一个字符设备文件的示例。如果我们有一个 shell 脚本,其中包含:

echo "hello" > /dev/null

然后脚本open()是字符设备文件/dev/null。内核知道这/dev/null是一个字符设备并检查与该文件关联的主设备号和次设备号。它看到主设备号 1,因此它将请求路由open()到处理主设备号 1(内存设备)上的操作的字符设备驱动程序。最终出现在内存设备驱动程序中处理打开调用的函数中:

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

然后该memory_open()函数使用次设备号来索引devlist我们之前看到的数组。如果该设备有特殊open()功能,则调用该功能,否则仅返回 0;该null设备没有特殊open()功能。

最终,该进程将调用write()将“hello”写入与打开的文件关联的文件描述符。同样,内核知道打开的文件与主设备号为 1 和次设备号为 3 的字符设备关联,因此它将该文件路由write()到主设备类型 1(内存设备)的驱动程序。次设备号为 3 的设备注册了一组用于处理 I/O 的函数(此处为null_fops):

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

null_fops结构体包含以下函数指针:

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

因此,write()对主设备号为 1、次设备号为 3 的字符设备文件的调用将导致对write_null().该函数的实现是:

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

write_null()函数不执行任何操作,并返回count以指示count字节已成功写入(我们期望写入的行为/dev/null)。

总结一下,字符设备文件包含元数据:主设备号和次设备号。当进程在字符设备文件上执行 I/O 时,内核会使用该元数据来查找正确的字符设备司机在内核中处理对文件发出的 I/O 请求。

答案2

一般来说,节点可以只是任何图的顶点的通用名称。在文件系统的上下文中,文件和目录自然是节点(文件名可能是图的边)。保存文件数据的数据结构也称为“i节点”。

我们很少将文件称为不带“i”的普通“节点”,除非在调用上下文中mknod(),用于为设备创建这些特殊文件。

设备文件只是一种特殊的文件,不包含任何数据,而是有一些数字来标识哪个内核驱动程序负责处理对这些设备文件的任何访问。

驱动程序是内核中的一些软件,可以做一些有用的事情。它可能提供对实际硬件的访问(例如/dev/ttyS0串行端口),也可能不提供(例如/dev/zero)。

实际设备当然是物理硬件、连接器和芯片。

当然,为了混淆问题,我们将这些“设备文件”简称为“设备”。

因此,/dev/ttyS0是一个文件系统节点,它是一个字符特殊文件,其主要:次要设备编号为 64:0,它将其标识为由串行驱动程序处理的设备,该设备访问某些物理设备,可能类似于 16550 UART芯片。

答案3

您可以说设备是主设备号和次设备号的组合。这个组合是固定的,不能改变。该组合是设备文件的一部分。应用程序打开设备文件,并通过相应的数字组合定向到设备。

与主编号和次编号相反,设备文件的名称理论上是随机的。您可以创建一个/dev/sda指向内核设备名称的文件sdb

答案4

尽管道尔顿先生的回答很长,但这本书仍然让你感到困惑。这本书误导了你。这是书上的内容应该说:

内核虚拟终端、并行端口和串行端口是字符设备的示例,因为它们可以通过流抽象很好地表示。字符设备通过文件系统节点的方式打开,例如/dev/tty1(打开第一内核虚拟终端)、/dev/lp0(打开第一并口)、/dev/ttyS0(打开第一串口)。

其实根本不应该讨论/dev/console。这 和/dev/tty都太复杂,无法用作基本示例。 (请注意,M. Dalton 明智地没有使用它们。)它们也用于打开字符设备,但是到底是哪个字符设备比前面提到的 KVT、串行端口和并行端口更复杂。特别是控制台引入了第二组名称,它们看起来像文件系统节点名称,但实际上并非如此。

进一步阅读

相关内容