Linux 如何查找文件/目录对应的块设备?(含代码)
2025-04-30 19:07:22
如何通过文件/目录路径找到对应的块设备文件
咱们在 Linux 系统上操作文件时,有时候会想知道某个文件或者目录到底存在哪个物理设备上。比如说,你想知道 /tmp
目录具体是在 /dev/sda1
还是 /dev/nvme0n1p2
上。命令行里用 df /tmp/
能轻松看到这个信息:
$ df /tmp/
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sdc 1055762868 173695480 828363916 18% /
上面的输出告诉我们,/tmp/
所在的挂载点 /
是由块设备 /dev/sdc
提供的。问题是,怎么在程序里(比如用 C 语言)获取这个 /dev/sdc
呢?
为啥需要这么做?
理解文件、文件系统和块设备的关系是关键。文件和目录并不是直接存储在硬盘上的某个扇区,它们是文件系统这个抽象层的一部分。文件系统(比如 ext4, XFS, Btrfs)负责组织数据、管理元数据(文件名、权限、时间戳等)。
而文件系统本身,需要被“挂载”到某个目录(挂载点)上才能访问。这个文件系统的数据,最终是存储在某个块设备上的。这个块设备可以是一个物理硬盘分区(/dev/sda1
)、一个 LVM 卷、一个 RAID 阵列,甚至是网络存储设备。
所以,从文件路径找到块设备的过程,实际上是:
- 找到文件所在的文件系统。
- 找到该文件系统的挂载点。
- 找到挂载该文件系统的块设备源。
df
命令就是干这个的。那程序里该怎么实现呢?
解决方案
有几种办法可以实现这个需求,各有优劣。
方法一:使用 stat
系列系统调用和 /sys
或 /proc
文件系统
这是最底层、最接近系统内核工作方式的方法。它不依赖外部命令,比较健壮。
原理和作用
- 获取设备 ID (
st_dev
) : 对于给定的文件或目录路径,可以使用stat()
或者lstat()
系统调用(如果是符号链接本身,用lstat
;如果是链接指向的目标,用stat
)。这两个函数会填充一个struct stat
结构体。其中,st_dev
成员包含了文件所在设备的唯一标识符(设备号)。这个设备号是一个整数,由主设备号(major number)和次设备号(minor number)组成。主设备号通常标识设备驱动类型(比如 SCSI 硬盘、NVMe 硬盘),次设备号则用来区分同一类型的多个设备。 - 查找设备名称 : 获取
st_dev
后,咱们需要找到哪个块设备文件(比如/dev/sda1
)也拥有相同的设备号。这些块设备文件的信息通常可以在/sys/block/
目录下或者/proc/partitions
文件里找到。/sys/block/
: 这是比较现代的方式。可以遍历/sys/block/
下的每个设备目录(如/sys/block/sda/
),再检查里面的分区目录(如/sys/block/sda/sda1/
)。每个分区目录下都有一个dev
文件,里面存的就是该分区的主次设备号(格式通常是major:minor
)。/proc/partitions
: 这是一个文本文件,列出了内核识别的所有分区及其主次设备号。解析这个文件也可以达到目的。格式一般是major minor #blocks name
。
通过比较 stat()
获取的 st_dev
(分解成主次设备号)和 /sys
或 /proc
中找到的主次设备号,就能匹配到具体的设备名称。
C 代码示例
#include <stdio.h>
#include <stdlib.h>
#include <sys/sysmacros.h> // For major() and minor()
#include <sys/stat.h>
#include <string.h>
#include <dirent.h> // For opendir, readdir, closedir
// 函数:根据设备号(dev_t)查找块设备名称
char* find_device_name(dev_t target_dev) {
DIR *block_dir;
struct dirent *entry;
char path[1024];
static char device_name[256]; // Use static buffer for simplicity, be careful in multi-threaded env
// 尝试 /sys/block/ 方式
block_dir = opendir("/sys/block");
if (block_dir) {
while ((entry = readdir(block_dir)) != NULL) {
if (entry->d_name[0] == '.') continue; // Skip "." and ".."
snprintf(path, sizeof(path), "/sys/block/%s", entry->d_name);
DIR *device_dir = opendir(path);
if (device_dir) {
struct dirent *part_entry;
while ((part_entry = readdir(device_dir)) != NULL) {
if (part_entry->d_name[0] == '.') continue;
// 检查设备本身(如 sda)和分区(如 sda1)
// 分区目录名通常包含设备名
if (strstr(part_entry->d_name, entry->d_name) != NULL) {
snprintf(path, sizeof(path), "/sys/block/%s/%s/dev", entry->d_name, part_entry->d_name);
FILE *dev_file = fopen(path, "r");
if (dev_file) {
unsigned int maj, min;
if (fscanf(dev_file, "%u:%u", &maj, &min) == 2) {
fclose(dev_file);
if (makedev(maj, min) == target_dev) {
snprintf(device_name, sizeof(device_name), "/dev/%s", part_entry->d_name);
closedir(device_dir);
closedir(block_dir);
return device_name;
}
} else {
fclose(dev_file);
}
}
// 同时检查设备自身,例如整个磁盘 sda, nvme0n1
snprintf(path, sizeof(path), "/sys/block/%s/dev", entry->d_name);
dev_file = fopen(path, "r");
if (dev_file) {
unsigned int maj, min;
if (fscanf(dev_file, "%u:%u", &maj, &min) == 2) {
fclose(dev_file);
if (makedev(maj, min) == target_dev) {
snprintf(device_name, sizeof(device_name), "/dev/%s", entry->d_name);
closedir(device_dir);
closedir(block_dir);
return device_name;
}
} else {
fclose(dev_file);
}
}
}
}
closedir(device_dir);
}
}
closedir(block_dir);
}
// 如果 /sys 没找到,可以尝试 /proc/partitions (这里省略代码,原理类似,解析文件内容)
// ...
return NULL; // 未找到
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <file_or_directory_path>\n", argv[0]);
return 1;
}
const char *path = argv[1];
struct stat file_stat;
// 使用 stat 获取文件信息
if (stat(path, &file_stat) == -1) {
perror("stat failed");
return 1;
}
printf("Path: %s\n", path);
printf("Device ID (st_dev): %lu (major: %u, minor: %u)\n",
(unsigned long)file_stat.st_dev,
major(file_stat.st_dev),
minor(file_stat.st_dev));
// 查找对应的块设备名
char *device_path = find_device_name(file_stat.st_dev);
if (device_path) {
printf("Mapped Block Device: %s\n", device_path);
} else {
printf("Could not find matching block device in /sys/block/.\n");
// 这里可以加上 /proc/partitions 的查找逻辑
}
return 0;
}
编译和运行:
gcc find_dev.c -o find_dev
./find_dev /tmp
# 可能的输出:
# Path: /tmp
# Device ID (st_dev): 64769 (major: 253, minor: 1)
# Mapped Block Device: /dev/dm-1 # 注意:可能是 LVM 或其他设备映射
# 或者
# Path: /home/user
# Device ID (st_dev): 66305 (major: 259, minor: 1)
# Mapped Block Device: /dev/nvme0n1p1
命令行等效操作
不写 C 代码,用 shell 命令也能模拟这个过程:
-
获取文件的设备号(十六进制格式):
stat -c '%D' /tmp/ # 输出类似 fd01 (这是十六进制的设备号 major=253, minor=1)
或者获取十进制主次设备号:
stat -c 'Maj:%t Min:%T' /tmp/ # 输出类似 Maj:fd Min:1 (这里用 %t 和 %T 显示的是十六进制) # 要获取十进制可以用 C 代码里的 major() minor() 或者间接通过 /proc/mounts
一个更直接的方式可能是结合
df
和stat
:# 获取文件所在的文件系统挂载点 mount_point=$(df --output=target "$path" | tail -n 1) # 获取挂载点的信息,包含设备号 stat -c '%n: DeviceID=%d (Maj=%t, Min=%T)' "$mount_point"
-
查找匹配的设备:
# 假设上面 stat 得到主设备号 253, 次设备号 1 (fd:1) # 在 /sys/block 中查找 find /sys/block/ -name dev -exec sh -c ' dev_info=$(cat "$1"); maj=$(echo "$dev_info" | cut -d: -f1); min=$(echo "$dev_info" | cut -d: -f2); # 目标是 253:1 if [ "$maj" -eq 253 ] && [ "$min" -eq 1 ]; then dev_path=$(dirname "$1"); dev_name=$(basename "$dev_path"); # 处理整个设备和分区的情况 parent_dir=$(dirname "$dev_path") if [ "$(basename $parent_dir)" = "block" ]; then # 如 /sys/block/sda/dev echo "/dev/$dev_name" else # 如 /sys/block/sda/sda1/dev echo "/dev/$dev_name" fi fi ' sh {} \; # 或者解析 /proc/partitions awk '$1 == 253 && $2 == 1 {print "/dev/"$4}' /proc/partitions
这些命令比较繁琐,不如 C 代码灵活,但在脚本中临时用用还行。
安全建议
- 权限:读取
/sys/block
或/proc/partitions
通常需要普通用户权限即可。但访问某些特殊设备信息可能需要 root 权限。stat()
系统调用本身对能访问的文件/目录都有效。 - 路径处理:代码需要健壮地处理用户提供的路径,防止路径遍历等安全问题(虽然这里主要是读取信息)。对 C 代码,注意缓冲区溢出,使用
snprintf
等安全函数。 - 错误处理:上面的 C 示例比较基础,实际使用中应添加更完善的错误检查(比如
opendir
、fopen
、fscanf
的返回值检查)。
进阶使用技巧
statvfs()
: 如果你更关心文件系统层面的信息(比如文件系统类型、总空间、可用空间),而不是非得拿到块设备名,statvfs()
是个好选择。它操作的是挂载的文件系统,其返回的struct statvfs
中没有直接的设备号st_dev
,但提供了文件系统 IDf_fsid
。这在某些场景下(如 NFS)可能更有用,但在本地块设备映射上不如stat
直接。- 符号链接 : 注意
stat()
和lstat()
的区别。如果给定的路径是个符号链接,stat()
会解析链接,获取目标文件的信息(包括st_dev
),而lstat()
获取的是链接文件本身的信息。链接文件通常和它所在的目录共享st_dev
。你需要根据具体需求选择用哪个。 - 复杂存储 : LVM(逻辑卷管理)、Device Mapper(如
dm-crypt
加密卷)、RAID 等技术会创建虚拟块设备(比如/dev/mapper/vg-lv
或/dev/md0
)。stat().st_dev
通常会指向这个虚拟设备的主次设备号。find_device_name
函数也需要能正确处理/sys/block/dm-*/
或/sys/block/md*/
等目录下的dev
文件。上面示例代码中的查找逻辑已经考虑了遍历/sys/block/
下的所有设备,理论上能覆盖这些情况。
方法二:调用并解析 df
或 findmnt
命令的输出
这种方法更简单直接,特别是在脚本语言(如 Shell, Python)中,但有其局限性。
原理和作用
利用现成的工具,比如 df
或 findmnt
,它们内部已经实现了从路径到块设备的查找逻辑。咱们只需要执行这些命令,然后用文本处理工具(如 awk
, grep
, sed
)或者编程语言的字符串处理功能,从输出中提取所需的信息(通常是第一列的设备名)。
df <path>
: 显示指定路径所在文件系统的磁盘使用情况,第一列就是设备名(或源)。findmnt -n -o SOURCE --target <path>
或findmnt -n -o SOURCE <path>
:findmnt
是一个更现代、功能更强的工具,专门用来查找挂载点信息。-n
去掉表头,-o SOURCE
只输出源设备,--target <path>
查找包含该路径的最内层挂载点,或者直接用findmnt <path>
让它自动查找。
命令行示例
# 使用 df
df --output=source /tmp/ | tail -n 1
# 输出: /dev/sdc (或者对应的设备名)
# 使用 findmnt (推荐)
# 查找包含 /tmp 的挂载点的源设备
findmnt -n -o SOURCE --target /tmp/
# 输出: /dev/sdc
# 或者直接让 findmnt 找出路径对应的设备
findmnt -n -o SOURCE /tmp/some/deep/file.txt
# 输出: /dev/sdc (或其他设备)
Shell 脚本示例
#!/bin/bash
target_path="$1"
if [ -z "$target_path" ]; then
echo "Usage: $0 <file_or_directory_path>"
exit 1
fi
# 检查路径是否存在
if [ ! -e "$target_path" ]; then
echo "Error: Path '$target_path' does not exist."
exit 1
fi
# 使用 findmnt 获取设备名
device_name=$(findmnt -n -o SOURCE --target "$target_path")
# 备选:使用 df (如果findmnt不可用或行为不符合预期)
# device_name=$(df --output=source "$target_path" | tail -n 1)
if [ -n "$device_name" ]; then
echo "Path: $target_path"
echo "Mapped Block Device: $device_name"
else
echo "Could not determine the block device for '$target_path'."
# 可能是路径无效、权限问题,或者是非常规挂载(如 FUSE)
exit 1
fi
exit 0
优缺点
- 优点 : 实现简单快速,代码量少,尤其适合脚本。
- 缺点 :
- 依赖外部命令 (
df
,findmnt
) 必须存在且行为符合预期。命令输出格式如果发生变化(虽然不太可能大幅变动),脚本就可能失效。 - 性能开销比直接调用系统调用大(需要创建进程、执行命令、捕获输出)。
- 错误处理相对麻烦,需要解析命令的退出码和标准错误输出。
- 不够底层,无法获取更详细的设备信息(比如主次设备号)。
- 依赖外部命令 (
安全建议
- 路径注入:如果脚本接受外部输入的路径,务必进行校验,防止恶意构造的路径(比如包含命令注入字符)被传递给
df
或findmnt
。虽然这些工具本身相对安全,但好的编程习惯是校验所有外部输入。 - 确保
df
和findmnt
来自可信来源,防止被替换成恶意版本。
其他考虑因素
- 网络文件系统 (NFS, CIFS等) : 对于挂载的网络文件系统上的文件,
stat().st_dev
可能是一个伪设备号,或者df
/findmnt
会显示服务器端的路径(如server:/share
),而不是本地块设备。这符合预期,因为数据确实不在本地块设备上。 - 绑定挂载 (Bind Mounts) :
mount --bind olddir newdir
会让newdir
看起来和olddir
内容一样。对于newdir
下的文件,stat().st_dev
和df
/findmnt
通常会正确地指向olddir
原始文件系统所在的块设备。 - 容器/命名空间 : 在 Docker 等容器环境中,文件系统和设备的视图可能与宿主机不同。在容器内执行上述方法,通常会得到容器内看到的设备名或挂载源,这可能是一个虚拟设备,也可能通过命名空间映射到宿主机的某个设备,具体取决于容器的配置。
选择哪种方法取决于你的具体需求:
- 如果是在 C/C++ 程序中需要高可靠性、高性能,并且可能需要处理复杂情况(如特定错误处理、获取更底层信息),优先选择方法一:使用
stat
和/sys
。 - 如果只是在 Shell 脚本中快速实现功能,或者对性能要求不高,方法二:调用
df
或findmnt
通常更方便。findmnt
比df
在解析上可能更可靠。