Linux 物理块大小查询: statvfs 误区与正确方法
2025-04-29 09:58:04
好的,这是符合你要求的博客文章内容:
Linux 下怎么拿到文件或目录真正的物理块大小?
写代码或者鼓捣系统的时候,有时会想知道某个文件或者目录所在的存储设备,它底层的物理块大小(Physical Block Size)到底是多少。比如你用 df -h /tmp/
查到 /tmp
挂在 /dev/sdc
上,然后去 /sys/block/sdc/queue/physical_block_size
这个文件里瞅一眼,看到个 4096
。
这时候你可能想写段代码来获取这个值,比如下面这样用了 statvfs
函数:
#include <stdio.h>
#include <string.h>
#include <sys/statvfs.h>
int main() {
struct statvfs buf;
memset(&buf, 0, sizeof(buf));
// 注意:这里用 statvfs,不是 statvfs64,效果类似
const int res = statvfs("/tmp/", &buf);
if (res != 0) {
perror("statvfs failed");
return 1;
}
// 这里打印的是 buf.f_bsize
printf("statvfs buf.f_bsize: %lu\n", buf.f_bsize);
// 顺便也看下 f_frsize (Fundamental block size)
printf("statvfs buf.f_frsize: %lu\n", buf.f_frsize);
return 0;
}
跑了一下,嘿,输出的 buf.f_bsize
可能正好也是 4096
!跟 /sys
目录下的值一样。这时候就犯嘀咕了:statvfs
拿到的到底是不是那个物理块大小?我想改改 /sys/block/sdc/queue/physical_block_size
文件里的值来测试下代码,结果发现这文件是只读的,改不了。那咋办?statvfs
返回的 buf.f_bsize
真的是物理块大小吗?
为啥会搞混?先分清几个“块大小”
这事儿吧,容易混是因为有好几个不同的“块大小”概念凑一块儿了:
-
文件系统块大小 (Filesystem Block Size):
- 是啥? 这是文件系统(比如 ext4, XFS, btrfs 等)管理数据用的基本单位。创建文件系统时(比如用
mkfs.ext4
)指定的-b
参数就是它。 - 谁说了算? 文件系统自己。
- 在哪看?
statvfs
函数返回的buf.f_bsize
就是这个。或者用tune2fs -l /dev/sdXN | grep 'Block size'
也能看到 (对 ext 文件系统)。 - 影响啥? 文件系统内部的碎片情况、存储小文件的空间利用率、单次 I/O 操作的数据量(部分影响)。
- 是啥? 这是文件系统(比如 ext4, XFS, btrfs 等)管理数据用的基本单位。创建文件系统时(比如用
-
物理块大小 (Physical Block Size / Physical Sector Size):
- 是啥? 这是底层存储硬件(SSD、HDD)自己操作数据的最小物理单元。现代硬盘(特别是“先进格式化”Advanced Format, AF 的硬盘)通常是 4096 字节 (4K)。老硬盘可能是 512 字节。SSD 的物理页大小可能更大。
- 谁说了算? 硬盘/SSD 固件。操作系统只是检测并报告这个值。
- 在哪看?
/sys/block/sdX/queue/physical_block_size
这个文件里。 - 影响啥? 底层硬件的写入效率。如果操作系统或文件系统下发的写入请求大小不是物理块大小的整数倍,硬盘可能需要执行“读取-修改-写入”(Read-Modify-Write, RMW)操作,影响性能。
-
逻辑块大小 (Logical Block Size / Logical Sector Size):
- 是啥? 硬盘向操作系统报告的“可以”进行读写操作的最小单位。为了兼容性,很多 4K 物理块的硬盘会报告逻辑块大小是 512 字节(这种情况叫 512e, 512 emulation)。也有直接报告 4K 逻辑块的(叫 4K Native, 4Kn)。
- 谁说了算? 硬盘/SSD 固件,操作系统读出来。
- 在哪看?
/sys/block/sdX/queue/logical_block_size
这个文件里。用fdisk -l /dev/sdX
看到的 "Sector size (logical/physical)" 也能看到。 - 影响啥? 操作系统如何跟硬盘打交道。分区对齐等问题跟这个有关。
-
I/O 大小提示 (I/O Size Hint / Preferred I/O Size):
- 是啥? 文件系统或者设备驱动建议应用程序进行 I/O 操作时最好用的块大小,以获得最佳性能。
- 谁说了算? 文件系统或驱动。
- 在哪看?
stat
结构体(通过stat()
或fstat()
系统调用获取)里的st_blksize
字段。statvfs
结构体里的f_frsize
(fundamental filesystem block size) 有时也反映类似概念,但f_bsize
更常用作文件系统的块大小。 - 影响啥? 上层应用(比如数据库、备份软件)可能根据这个值来优化自己的 I/O 行为。
所以,回到最初的问题:statvfs
里的 buf.f_bsize
拿到的是 文件系统块大小,不是 物理块大小。 你在 /tmp
目录上运行代码,碰巧那个文件系统(可能是 ext4 或 xfs)在创建时,设置的块大小就是 4096 字节,正好等于底层 /dev/sdc
设备的物理块大小 4096 字节。这纯属巧合!如果当初用 mkfs.ext4 -b 1024 /dev/sdc1
来格式化,那 statvfs
在挂载了这个文件系统的目录下就会返回 1024,而 /sys/block/sdc/queue/physical_block_size
依然是 4096。
同理,stat()
函数获取的 st_blksize
也不是物理块大小, 它通常反映的是文件系统的块大小或者一个推荐的 I/O 大小,目的是为了提高读写效率,尽量避免跨文件系统块的操作。
明白了这一点,我们就知道不能依赖 statvfs
或 stat
来获取真实的物理块大小。那该用什么方法呢?
获取物理块大小的正确姿势
想拿到跟 /sys/block/sdX/queue/physical_block_size
一样的值,主要有以下几种靠谱的方法:
方法一:简单直接,用 lsblk
命令
这是最省事儿的办法,尤其是在命令行下或者脚本里用。lsblk
命令能显示块设备的信息,包括物理扇区大小。
-
找到路径对应的设备:
先用df <你的文件或目录路径>
找到它挂载在哪个设备上。# 比如找 /tmp 对应的设备 df /tmp/
输出里会有一行,第一列就是设备名,可能是
/dev/sdc1
,/dev/mapper/vg-lv
,/dev/nvme0n1p2
等等。注意,如果看到的是类似tmpfs
或overlay
这样的,说明它不在一个常规的物理块设备上,那物理块大小的概念就不直接适用了。我们需要的是一个指向真实硬盘分区的设备,比如/dev/sdc1
。如果挂载点直接是设备,比如/dev/sdc
本身(虽然不常见),那就用/dev/sdc
。 -
用
lsblk
查询物理块大小:
拿到设备名后(比如/dev/sdc1
),我们要找的是它所属的那个物理设备(通常是去掉分区号的名字,即/dev/sdc
)。然后用lsblk
带上-o
参数指定输出列,找到物理扇区大小 (PHY-SEC
)。# 假设 df 输出 /tmp/ 在 /dev/sdc1 上 # 我们要查 sdc 的信息 DEVICE_NAME="sdc" lsblk -o NAME,MOUNTPOINT,PHY-SEC,LOG-SEC /dev/${DEVICE_NAME}
或者更通用点,让
lsblk
自己显示整个树状结构,然后看对应设备那行的PHY-SEC
值:lsblk -o NAME,MOUNTPOINT,PHY-SEC,LOG-SEC
然后在输出里找到对应的设备名(比如
sdc
或nvme0n1
),看它那一行的PHY-SEC
列的值是多少。
示例:
$ df /tmp/
文件系统 1K-块 已用 可用 已用% 挂载点
/dev/sdc1 51580660 4896180 44047644 10% /tmp
$ lsblk -o NAME,MAJ:MIN,RM,SIZE,RO,TYPE,MOUNTPOINT,PHY-SEC,LOG-SEC /dev/sdc
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINT PHY-SEC LOG-SEC
sdc 8:0 0 238.5G 0 disk 4096 512
└─sdc1 8:1 0 100G 0 part /tmp 4096 512
看!sdc
和 sdc1
的 PHY-SEC
(物理扇区大小) 都是 4096
。LOG-SEC
(逻辑扇区大小) 是 512
,这是典型的 512e 硬盘。
优点: 简单、直观,大多数 Linux 发行版自带 lsblk
。
缺点: 需要执行外部命令,如果要在程序里用,得解析命令输出,稍微麻烦点。
方法二:直接读取 /sys
文件系统
既然我们知道物理块大小就在 /sys/block/<设备名>/queue/physical_block_size
这个文件里,那直接读取它不就行了?这种方法适合写脚本或者代码。
-
找到路径对应的块设备名:
这步跟方法一类似,但我们需要的是没有分区号的设备名(如sda
,nvme0n1
)。可以组合使用df
和一些文本处理工具。
或者,在 C 代码里,可以通过以下步骤(稍微复杂些):
a. 对给定的文件/目录路径调用stat()
,获取struct stat
结构体。
b. 从结构体中取出st_dev
字段,这是设备号(包含主设备号和次设备号)。
c. 遍历/sys/class/block/
下的所有目录(这些目录名就是块设备名,如sda
,sdb
,nvme0n1
等)。
d. 读取每个设备目录下的dev
文件,里面是 "主设备号:次设备号" 的文本。
e. 比较读取到的主次设备号是否和stat()
得到的st_dev
里的主设备号 相匹配(注意:st_dev
对应的是文件系统所在的设备的设备号,比如/dev/sdc1
的设备号;我们需要找到包含这个分区的物理设备/dev/sdc
的设备名)。更准确的做法是,拿到文件路径的st_dev
后,可以通过findmnt -n -o SOURCE --target <路径>
直接获得源设备路径(如/dev/sdc1
),然后提取出设备名(sdc1
),再去掉分区部分得到物理设备名(sdc
)。假设我们通过某种方式(比如先用
df
或findmnt
找到了设备路径/dev/sdc1
)确定了物理设备名是sdc
。 -
构建
/sys
文件路径:
路径就是/sys/block/sdc/queue/physical_block_size
。 -
读取文件内容:
用cat
命令,或者在 C 代码里用fopen
,fgets
/fscanf
来读取。
Shell 脚本示例:
#!/bin/bash
TARGET_PATH="/tmp/" # 你想查询的路径
# 使用 findmnt 找到源设备路径 (可能带分区)
DEVICE_PATH=$(findmnt -n -o SOURCE --target "$TARGET_PATH")
# 检查是否找到了设备
if [ -z "$DEVICE_PATH" ]; then
echo "找不到路径 '$TARGET_PATH' 对应的挂载设备。"
exit 1
fi
# 从设备路径中提取块设备名 (尝试去除分区号等)
# 这里用 basename 和 lsblk 来找到父设备名,更可靠些
DEVICE_FULLNAME=$(lsblk -no pkname "$DEVICE_PATH" 2>/dev/null)
if [ -z "$DEVICE_FULLNAME" ]; then
# 如果找不到父设备名(可能是整个设备被挂载),尝试直接用basename提取
DEVICE_FULLNAME=$(basename "$DEVICE_PATH")
fi
# 确保提取的设备名存在于 /sys/block
if [ ! -d "/sys/block/$DEVICE_FULLNAME" ]; then
echo "无法确定物理设备名,或者 '$DEVICE_FULLNAME' 不是一个有效的块设备。"
# 可能需要更复杂的逻辑来处理 LVM, RAID, NVMe 等情况
# 作为简化,这里我们先假设它是个简单设备名
# 可以尝试去掉结尾的数字看看 (简化处理分区)
DEVICE_NAME=$(echo "$DEVICE_FULLNAME" | sed 's/[0-9]*$//')
if [ ! -d "/sys/block/$DEVICE_NAME" ]; then
echo "尝试去掉数字后,设备名 '$DEVICE_NAME' 仍然无效。"
exit 1
fi
else
DEVICE_NAME=$DEVICE_FULLNAME
fi
SYS_PATH="/sys/block/${DEVICE_NAME}/queue/physical_block_size"
if [ -f "$SYS_PATH" ]; then
PHYSICAL_BLOCK_SIZE=$(cat "$SYS_PATH")
echo "路径 '$TARGET_PATH' 所在设备 '$DEVICE_NAME' 的物理块大小是: $PHYSICAL_BLOCK_SIZE"
else
echo "找不到文件: $SYS_PATH"
exit 1
fi
exit 0
C 代码示例 (只演示读取部分,假设已知设备名是 "sdc"):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
const char* device_name = "sdc"; // 假设我们已经通过其他方式得到是 sdc
char sys_path[256];
snprintf(sys_path, sizeof(sys_path), "/sys/block/%s/queue/physical_block_size", device_name);
FILE *fp = fopen(sys_path, "r");
if (fp == NULL) {
perror("无法打开 sysfs 文件");
fprintf(stderr, "路径: %s\n", sys_path);
return 1;
}
long physical_block_size = 0;
if (fscanf(fp, "%ld", &physical_block_size) != 1) {
fprintf(stderr, "无法从 %s 读取物理块大小\n", sys_path);
fclose(fp);
return 1;
}
fclose(fp);
printf("设备 '%s' 的物理块大小是: %ld\n", device_name, physical_block_size);
return 0;
}
优点: 直接从内核获取信息,不依赖 lsblk
等外部工具(如果设备名已知或用纯 C/系统调用找到的话),方便集成到代码里。
缺点: 需要自己处理找到设备名的逻辑,这个过程可能有点绕,特别是对于 LVM、RAID、NVMe 等复杂情况。读取 /sys
文件需要相应的权限。
方法三:终极武器,使用 ioctl
系统调用
这是一种更底层、更偏 C 语言的方式,直接通过 ioctl
和块设备进行交互。
-
找到块设备文件的路径:
还是得先确定文件/目录对应的块设备路径,比如/dev/sdc
。同样,可以用df
,findmnt
或者上面提到的 C 代码方法(stat()
+ 遍历/sys/dev/block/
或/sys/class/block
)。 -
打开块设备文件:
使用open()
系统调用打开这个设备路径,例如open("/dev/sdc", O_RDONLY)
。注意,这通常需要 root 权限或者用户属于特定的组(比如disk
组)。 -
调用
ioctl
:
对打开的文件符fd
,调用ioctl
,使用BLKSSZGET
获取逻辑块大小 (Sector Size),使用BLKPBSZGET
获取物理块大小 (Physical Block Size)。
C 代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h> // For O_RDONLY
#include <unistd.h> // For close()
#include <sys/ioctl.h> // For ioctl()
#include <linux/fs.h> // For BLKSSZGET, BLKPBSZGET definitions
int main(int argc, char *argv[]) {
const char* device_path = "/dev/sdc"; // 重要:需要替换成实际找到的设备路径!
if (argc > 1) {
// 可以从命令行参数传入设备路径
device_path = argv[1];
} else {
fprintf(stderr, "用法: %s <块设备路径, 例如 /dev/sda>\n", argv[0]);
fprintf(stderr, "正在使用默认值: %s\n", device_path);
// 这里应该添加代码来根据 文件/目录 路径动态找到 device_path
// 为了演示 ioctl,暂时写死或从参数获取
}
int fd = open(device_path, O_RDONLY);
if (fd < 0) {
perror("无法打开块设备");
fprintf(stderr, "请检查路径 '%s' 是否正确以及是否有读取权限。\n", device_path);
return 1;
}
// 获取物理块大小
unsigned int physical_block_size = 0;
if (ioctl(fd, BLKPBSZGET, &physical_block_size) == -1) {
// 如果 BLKPBSZGET 失败,物理块大小可能与逻辑块大小相同
// 或者设备不支持报告这个值。我们尝试获取逻辑块大小作为备选。
perror("ioctl(BLKPBSZGET) 失败");
int logical_block_size = 0;
if (ioctl(fd, BLKSSZGET, &logical_block_size) == -1) {
perror("ioctl(BLKSSZGET) 也失败了");
physical_block_size = 0; // 表示未知
} else {
printf("警告: 无法获取物理块大小, 使用逻辑块大小代替: %d\n", logical_block_size);
physical_block_size = logical_block_size; // 用逻辑大小作为估计
}
}
if (physical_block_size > 0) {
printf("设备 '%s' 的物理块大小是: %u\n", device_path, physical_block_size);
} else {
printf("未能确定设备 '%s' 的物理块大小。\n", device_path);
}
// (可选)获取逻辑块大小
int logical_block_size = 0;
if (ioctl(fd, BLKSSZGET, &logical_block_size) == -1) {
perror("ioctl(BLKSSZGET) 失败");
} else {
printf("设备 '%s' 的逻辑块大小是: %d\n", device_path, logical_block_size);
}
close(fd);
return 0;
}
编译运行:
gcc get_block_size.c -o get_block_size
# 需要 root 权限或者用户在 disk 组才能打开 /dev/sdX
sudo ./get_block_size /dev/sdc
输出可能像这样:
设备 '/dev/sdc' 的物理块大小是: 4096
设备 '/dev/sdc' 的逻辑块大小是: 512
优点: 是标准的 C 语言接口,直接和内核驱动交互,效率可能比解析 /sys
文件高一点点。能同时获取逻辑块大小。
缺点: 需要打开块设备文件,权限要求高。同样需要先解决如何从任意路径映射到块设备路径的问题。ioctl
的接口相对来说没那么直观。
安全建议: 用 ioctl
方法时,打开块设备要用 O_RDONLY
只读模式,除非你真的需要写操作。并且要确保程序只打开它应该访问的设备,防止误操作。检查所有系统调用的返回值是个好习惯。
小结一下
statvfs()
返回的f_bsize
是 文件系统块大小 ,由mkfs
决定。stat()
返回的st_blksize
是 I/O 操作的推荐大小 ,通常等于文件系统块大小。- 想获取 物理块大小 (硬件的最小读写单元),得用
lsblk
命令、读取/sys/block/<dev>/queue/physical_block_size
文件,或者对块设备文件/dev/<dev>
使用ioctl(BLKPBSZGET)
。 - 哪种方法最好?看情况。临时查一下用
lsblk
最方便。写脚本推荐读/sys
文件(可能需要配合findmnt
或lsblk
找设备名)。写 C/C++ 程序可以考虑ioctl
或封装读取/sys
文件的逻辑。
这下应该搞清楚了吧?别再把文件系统块大小当成硬盘的物理块大小啦!