Linux文件系统的设备ID
本文将介绍 Linux 文件系统中的文件的设备 ID 相关内容,基于的内核版本为 6.18。
为了介绍设备 ID 的概念,我们借助 stat 工具来对打印几个不同类型文件的元数据信息。
首先是一个位于磁盘文件系统下的普通文件 file.txt:
1 | $ stat file.txt |
可以看到,其 Device 编号为 8,80,具体含义我们后面再解释。
接下来要查看的是伪文件系统(如 ramfs、procfs 等没有直接关联的磁盘设备的文件系统)下的文件元数据信息,由于我的本地 Linux 环境是 WSL2,因此这里我选取了挂载在 Linux 下的 Windows 磁盘:/mnt/d,顾名思义,它就是我 Windows 系统下的 D 盘。WSL2 通过 9p 协议将 Windows 磁盘挂载到 Linux 下,其并不属于磁盘文件系统,打印的元数据信息如下:
由于 9p 走的网络协议栈,因此 WSL2 下读写 Windows 磁盘文件的性能很差。
1 | $ stat /mnt/d/ |
可以看到,其 Device 编号为 0,116。
接下来是一个实际的物理磁盘文件(块设备文件),即我的 WSL2 下的根目录的设备文件:
1 | $ stat /dev/sdf |
对于设备文件,其既包含 Device 编号(0,5),还包括 Device Type(8,80,注意刚好与前面的第一个实例相匹配),同时明确表明了这是一个块设备文件(block special file)。
最后一个实例也是设备文件,只不过是一个字符设备文件,这里我选取了 KVM 文件,其打印如下:
1 | $ stat /dev/kvm |
可以看到,其 Device 编号与 /dev/sdf 一样。
设备 ID 构成
上面提到了两个设备 ID 信息——Device Type 和 Device,它们分别对应 fstat 系统调用返回的 struct stat 结构中的 st_rdev 字段和 st_dev 字段。它们的值都满足 Linux 设备 ID 的规范:编号用一个 32 位无符号整型表示,低 20 位表示次设备号(minor),剩余的高位表示主设备号(major)。相关代码如下所示:
1 | /* include/linux/types.h:18,21 */ |
设备 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 。可以分为两种情况来考虑,即:
- 文件位于磁盘文件系统下,那么
st_dev即该磁盘对应的设备 ID(即上述提到的st_rdev)。 - 文件位于伪文件系统下,那么
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 通常表示一个杂项的字符设备),也符合预期。











