本文将介绍 Linux 文件系统中的文件的设备 ID 相关内容,基于的内核版本为 6.18。

为了介绍设备 ID 的概念,我们借助 stat 工具来对打印几个不同类型文件的元数据信息。

首先是一个位于磁盘文件系统下的普通文件 file.txt

1
2
3
4
5
$ stat file.txt
File: file.txt
Size: 392 Blocks: 8 IO Block: 4096 regular file
Device: 8,80 Inode: 16473 Links: 1
...

可以看到,其 Device 编号为 8,80,具体含义我们后面再解释。

接下来要查看的是伪文件系统(如 ramfs、procfs 等没有直接关联的磁盘设备的文件系统)下的文件元数据信息,由于我的本地 Linux 环境是 WSL2,因此这里我选取了挂载在 Linux 下的 Windows 磁盘:/mnt/d,顾名思义,它就是我 Windows 系统下的 D 盘。WSL2 通过 9p 协议将 Windows 磁盘挂载到 Linux 下,其并不属于磁盘文件系统,打印的元数据信息如下:

由于 9p 走的网络协议栈,因此 WSL2 下读写 Windows 磁盘文件的性能很差。

1
2
3
4
5
$ stat /mnt/d/
File: /mnt/d/
Size: 4096 Blocks: 0 IO Block: 4096 directory
Device: 0,116 Inode: 1407374883553287 Links: 1
...

可以看到,其 Device 编号为 0,116

接下来是一个实际的物理磁盘文件(块设备文件),即我的 WSL2 下的根目录的设备文件:

1
2
3
4
$ stat /dev/sdf
File: /dev/sdf
Size: 0 Blocks: 0 IO Block: 4096 block special file
Device: 0,5 Inode: 169 Links: 1 Device type: 8,80

对于设备文件,其既包含 Device 编号(0,5),还包括 Device Type(8,80,注意刚好与前面的第一个实例相匹配),同时明确表明了这是一个块设备文件(block special file)。

最后一个实例也是设备文件,只不过是一个字符设备文件,这里我选取了 KVM 文件,其打印如下:

1
2
3
4
$ stat /dev/kvm
File: /dev/kvm
Size: 0 Blocks: 0 IO Block: 4096 character special file
Device: 0,5 Inode: 401 Links: 1 Device type: 10,232

可以看到,其 Device 编号与 /dev/sdf 一样。

设备 ID 构成

上面提到了两个设备 ID 信息——Device Type 和 Device,它们分别对应 fstat 系统调用返回的 struct stat 结构中的 st_rdev 字段和 st_dev 字段。它们的值都满足 Linux 设备 ID 的规范:编号用一个 32 位无符号整型表示,低 20 位表示次设备号(minor),剩余的高位表示主设备号(major)。相关代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
/* include/linux/types.h:18,21 */
typedef u32 __kernel_dev_t;
typedef __kernel_dev_t dev_t;

/* include/linux/kdev_t.h:7-12 */
#define MINORBITS 20
#define MINORMASK ((1U << MINORBITS) - 1)

#define MAJOR(dev) ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev) ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi) (((ma) << MINORBITS) | (mi))

设备 ID 获取

Device Type (st_rdev) 表示一个设备文件(块设备或字符设备)的设备 ID,对于普通文件、目录等其他文件类型来说,该字段是不存在(没有意义)的。Device Type 存储在文件的内存 inode 结构中的 i_rdev 字段,因此 Device Type 是文件粒度的,其在块设备驱动初始化的时候进行设置(具体设置的流程不在本文讨论的范围之内)。

设备 ID 的主设备号表示设备的类型(如磁盘设备、内存设备、串口设备等),次设备号表示本设备在主设备类型中的编号。以下是一些设备号分配的示例:

设备 主设备号 次设备号 说明
/dev/sda 8 0 第一个 SCSI 磁盘
/dev/sdb 8 16 第二个 SCSI 磁盘(16 的倍数)
/dev/ram0 1 0 第一个 RAM 设备
/dev/loop0 7 0 第一个回环设备

Device(st_dev) 表示 一个文件(包括设备文件和非设备文件)所位于的文件系统的设备 ID 。可以分为两种情况来考虑,即:

  1. 文件位于磁盘文件系统下,那么 st_dev 即该磁盘对应的设备 ID(即上述提到的 st_rdev)。
  2. 文件位于伪文件系统下,那么 st_dev 为该伪文件系统对应的设备 ID。

前面在介绍设备 ID 构成时有一个没有提到的约定是: 对于主设备号来说,0 值表示该设备是一个伪文件系统。

Device 值存储在 super_block 结构的 st_dev 字段中,由于存储在超级块中,因此 Device 是文件系统粒度的。对于伪文件系统而言,它在文件系统的挂载阶段使用 Linux 内核中的 IDA(ID Allocator)进行动态分配;对于磁盘文件系统而言,其从块设备中获取,具体来说,根据挂载参数解析出对应磁盘文件,然后获取该磁盘文件的 Device Type,即为整个磁盘文件系统的 Device 值。

有一个细节需要注意,IDA 在进行次设备 ID 分配时,最小是从 1 开始的,因为许多的用户态程序通常认为设备 ID 为 0(主、次设备 ID 都为 0)的设备是无效的,对此内核代码中有明确的说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* fs/super.c:1253 */
int get_anon_bdev(dev_t *p)
{
int dev;

/*
* Many userspace utilities consider an FSID of 0 invalid.
* Always return at least 1 from get_anon_bdev.
*/
dev = ida_alloc_range(&unnamed_dev_ida, 1, (1 << MINORBITS) - 1,
GFP_ATOMIC);
if (dev == -ENOSPC)
dev = -EMFILE;
if (dev < 0)
return dev;

*p = MKDEV(0, dev);
return 0;
}

最后,在理解了上述内容后,回看文章开头的四个例子。

第一个 file.txt 文件的 Device 为 8,80,实际上表示的是磁盘 /dev/sdf,即我的根目录挂载点的磁盘设备。

第二个 /mnt/d/ 文件的 Device 为 0,116,主设备 ID 为 0,满足伪文件系统的定义。

第三个 /dev/sdf 文件的 Device 为 0,5,可能初看会有些令人困惑,但仔细想想,设备文件 /dev/sdf 是挂载在 /dev 目录下的一个文件,其属于伪文件系统 devtmpfs 的范畴,那么主设备号为 0 也就不奇怪了,实际上 /dev 目录下的所有文件的 Device 值都为 0,5。而 Device Type 就比较明确了,正好和前面的 file.txt 相对应。

第四个 /dev/kvm 文件的 Device 也为 0,5,和刚刚所说的一致。而 Device Type 值为 10,232(10 通常表示一个杂项的字符设备),也符合预期。