返回

Linux 物理块大小查询: statvfs 误区与正确方法

Linux

好的,这是符合你要求的博客文章内容:


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 真的是物理块大小吗?

为啥会搞混?先分清几个“块大小”

这事儿吧,容易混是因为有好几个不同的“块大小”概念凑一块儿了:

  1. 文件系统块大小 (Filesystem Block Size):

    • 是啥? 这是文件系统(比如 ext4, XFS, btrfs 等)管理数据用的基本单位。创建文件系统时(比如用 mkfs.ext4)指定的 -b 参数就是它。
    • 谁说了算? 文件系统自己。
    • 在哪看? statvfs 函数返回的 buf.f_bsize 就是这个。或者用 tune2fs -l /dev/sdXN | grep 'Block size' 也能看到 (对 ext 文件系统)。
    • 影响啥? 文件系统内部的碎片情况、存储小文件的空间利用率、单次 I/O 操作的数据量(部分影响)。
  2. 物理块大小 (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)操作,影响性能。
  3. 逻辑块大小 (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)" 也能看到。
    • 影响啥? 操作系统如何跟硬盘打交道。分区对齐等问题跟这个有关。
  4. 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 大小,目的是为了提高读写效率,尽量避免跨文件系统块的操作。

明白了这一点,我们就知道不能依赖 statvfsstat 来获取真实的物理块大小。那该用什么方法呢?

获取物理块大小的正确姿势

想拿到跟 /sys/block/sdX/queue/physical_block_size 一样的值,主要有以下几种靠谱的方法:

方法一:简单直接,用 lsblk 命令

这是最省事儿的办法,尤其是在命令行下或者脚本里用。lsblk 命令能显示块设备的信息,包括物理扇区大小。

  1. 找到路径对应的设备:
    先用 df <你的文件或目录路径> 找到它挂载在哪个设备上。

    # 比如找 /tmp 对应的设备
    df /tmp/
    

    输出里会有一行,第一列就是设备名,可能是 /dev/sdc1, /dev/mapper/vg-lv, /dev/nvme0n1p2 等等。注意,如果看到的是类似 tmpfsoverlay 这样的,说明它不在一个常规的物理块设备上,那物理块大小的概念就不直接适用了。我们需要的是一个指向真实硬盘分区的设备,比如 /dev/sdc1。如果挂载点直接是设备,比如 /dev/sdc 本身(虽然不常见),那就用 /dev/sdc

  2. 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 
    

    然后在输出里找到对应的设备名(比如 sdcnvme0n1),看它那一行的 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 

看!sdcsdc1PHY-SEC (物理扇区大小) 都是 4096LOG-SEC (逻辑扇区大小) 是 512,这是典型的 512e 硬盘。

优点: 简单、直观,大多数 Linux 发行版自带 lsblk
缺点: 需要执行外部命令,如果要在程序里用,得解析命令输出,稍微麻烦点。

方法二:直接读取 /sys 文件系统

既然我们知道物理块大小就在 /sys/block/<设备名>/queue/physical_block_size 这个文件里,那直接读取它不就行了?这种方法适合写脚本或者代码。

  1. 找到路径对应的块设备名:
    这步跟方法一类似,但我们需要的是没有分区号的设备名(如 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)。

    假设我们通过某种方式(比如先用 dffindmnt 找到了设备路径 /dev/sdc1)确定了物理设备名是 sdc

  2. 构建 /sys 文件路径:
    路径就是 /sys/block/sdc/queue/physical_block_size

  3. 读取文件内容:
    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 和块设备进行交互。

  1. 找到块设备文件的路径:
    还是得先确定文件/目录对应的块设备路径,比如 /dev/sdc。同样,可以用 df, findmnt 或者上面提到的 C 代码方法(stat() + 遍历 /sys/dev/block//sys/class/block)。

  2. 打开块设备文件:
    使用 open() 系统调用打开这个设备路径,例如 open("/dev/sdc", O_RDONLY)。注意,这通常需要 root 权限或者用户属于特定的组(比如 disk 组)。

  3. 调用 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_blksizeI/O 操作的推荐大小 ,通常等于文件系统块大小。
  • 想获取 物理块大小 (硬件的最小读写单元),得用 lsblk 命令、读取 /sys/block/<dev>/queue/physical_block_size 文件,或者对块设备文件 /dev/<dev> 使用 ioctl(BLKPBSZGET)
  • 哪种方法最好?看情况。临时查一下用 lsblk 最方便。写脚本推荐读 /sys 文件(可能需要配合 findmntlsblk 找设备名)。写 C/C++ 程序可以考虑 ioctl 或封装读取 /sys 文件的逻辑。

这下应该搞清楚了吧?别再把文件系统块大小当成硬盘的物理块大小啦!