活久见!记一次 fdisk 磁盘大小显示 bug

2022年9月17日 3243点热度 0人点赞 2条评论

在日常操作中,惊奇地发现老牌磁盘工具 util-linux fdisk 的一个容量显示 bug。笔者根据该值配置线上环境,最终导致了磁盘越界问题,好在最终影响范围不大。另外,也追踪源码,探究一下 fdisk 中的磁盘容量的计算方式。

TL;DR

是 fdisk 转换成 TiB 显示时的 bug。 该问题在 2.36 版本解决。写这篇文章时,ubuntu 20 的 apt 仓库仍在 2.34。问题仍然存在。

应对方案:若遇到容量显示不一致,优先使用 byte 原始值。

1 问题产生

使用 fdisk -l 查看所有磁盘的容量,得到上述结果。版本为 fdisk from util-linux 2.34

1.9TiB 和 1199638052864bytes 明显不对应。

笔者在一开始注意到了上述的差异,但是凭借对老牌工具的信赖,没有深入怀疑。以为可能是某些 raid 设置或者冗余容量导致的差异。

2 源码追踪

问询了几位老司机后无果,自己快速追一下代码。

util-linux 中字节格式转换全部由函数 size_to_human_string 负责。

有问题的代码在 19 年 2 月由 commit 07b94c9f 引入,旨在支持保留2位小数的功能:support two decimal places in size_to_human_string() output

问题代码位于 lib/strutils.c 如下 (release-2.33):

char *size_to_human_string(int options, uint64_t bytes)
{
    char buf[32];
    int dec, exp;
    uint64_t frac;
    const char *letters = "BKMGTPE";
    char suffix[sizeof(" KiB")], *psuf = suffix;
    char c;

    if (options & SIZE_SUFFIX_SPACE)
        *psuf++ = ' ';

    // 求字节等级和余数
    exp  = get_exp(bytes);
    c    = *(letters + (exp ? exp / 10 : 0));
    dec  = exp ? bytes / (1ULL << exp) : bytes;
    frac = exp ? bytes % (1ULL << exp) : 0;

    *psuf++ = c;

    if ((options & SIZE_SUFFIX_3LETTER) && (c != 'B')) {
        *psuf++ = 'i';
        *psuf++ = 'B';
    }

    *psuf = '\0';

    /* fprintf(stderr, "exp: %d, unit: %c, dec: %d, frac: %jd\n",
     *                 exp, suffix[0], dec, frac);
     */

    /* round */
    if (frac) {
        if (options & SIZE_DECIMAL_2DIGITS) {
            // !!! 问题代码
            frac = (frac / (1ULL << (exp - 10)) + 5) / 10;
            if (frac % 10 == 0)
                frac /= 10; /* convert N.90 to N.9 */
        } else {
            frac = (frac / (1ULL << (exp - 10)) + 50) / 100;
            if (frac == 10)
                dec++, frac = 0;
        }
    }

    if (frac) {
        struct lconv const *l = localeconv();
        char *dp = l ? l->decimal_point : NULL;

        if (!dp || !*dp)
            dp = ".";
        snprintf(buf, sizeof(buf), "%d%s%" PRIu64 "%s", dec, dp, frac, suffix);
    } else
        snprintf(buf, sizeof(buf), "%d%s", dec, suffix);

    return strdup(buf);
}

其中, 先求得容量等级 exp 用于展示 KB/MB/GB...,后求得下一级的字节余数 frac,用于渲染小数点后字符串。

问题出在 SIZE_DECIMAL_2DIGITS 时,如果 frac 在 10~95 范围内就会翻车。

    if (frac) {
        if (options & SIZE_DECIMAL_2DIGITS) {
            // 笔者添加了一个中间变量用于调试
            auto frac_temp = frac / (1ULL << (exp - 10)) + 5;
            // 如果余数 frac=90,得到的小数点后字符串为 "9",期望值为 "09"
            frac = (frac / (1ULL << (exp - 10)) + 5) / 10;
            if (frac % 10 == 0)
                frac /= 10; /* convert N.90 to N.9 */

该问题后续于 2020 年在 2.36 版本修复。参考:issue#998

3 Extra: fdisk 容量的计算方式

fdisk 使用 libfdisk 获取磁盘底层信息。

磁盘的总 byte 数由 sector 数量 * sector 大小得来。

    uint64_t bytes = fdisk_get_nsectors(cxt) * fdisk_get_sector_size(cxt);
    char *strsz = size_to_human_string(SIZE_DECIMAL_2DIGITS
                       | SIZE_SUFFIX_SPACE
                       | SIZE_SUFFIX_3LETTER, bytes);

    color_scheme_enable("header", UL_COLOR_BOLD);
    fdisk_info(cxt, _("Disk %s: %s, %ju bytes, %ju sectors"),
            fdisk_get_devname(cxt), strsz,
            bytes, (uintmax_t) fdisk_get_nsectors(cxt));

4 小结

  1. fdisk 转换成 TiB 显示时的 bug。 该问题在 2.36 版本解决。写这篇文章时,ubuntu 20 的 apt 仓库仍在 2.34。问题仍然存在。
  2. fdisk -l 中的 human 可读容量和后面的 byte 容量来源一致。
  3. fdisk 使用 libfdisk 获取磁盘底层信息。磁盘的总 byte 数由 sector 数量 * sector 大小得来。

另外,老司机的代码也可能翻车,时隔1年2个月才修复。作为开发者的我们如果能多写一些单元测试,此类问题就可能提前暴露避免。

SPtuan

团子最大的愿望是度过平静的时光。 当前从事分布式存储研发工作。

5 1 vote
文章评分
Subscribe
提醒
guest

2 评论
最新
最旧 得票最多
Inline Feedbacks
View all comments
wangtc
wangtc
3 年 之前

最近虾皮大规模毕业,站长有受到影响吗?