在日常操作中,惊奇地发现老牌磁盘工具 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 小结
- fdisk 转换成 TiB 显示时的 bug。 该问题在
2.36
版本解决。写这篇文章时,ubuntu 20 的 apt 仓库仍在2.34
。问题仍然存在。 - fdisk -l 中的 human 可读容量和后面的 byte 容量来源一致。
fdisk
使用libfdisk
获取磁盘底层信息。磁盘的总 byte 数由 sector 数量 * sector 大小得来。
另外,老司机的代码也可能翻车,时隔1年2个月才修复。作为开发者的我们如果能多写一些单元测试,此类问题就可能提前暴露避免。
最近虾皮大规模毕业,站长有受到影响吗?
@wangtc 暂时还没被影响到。不过资本的本质是无情的,俺永远做好准备