☰
C/C++ 编程获取 Linux 分区卷标、UUID 与文件系统类型
2026/10/27 22:22:32 网站建设 项目流程

1. 为什么要在 Linux 下折腾卷标、UUID 和文件系统类型

做系统运维或者嵌入式开发的朋友,大概率都遇到过这样的场景:一台机器上挂了好几块盘,分区数量多到/dev/sda1、/dev/nvme0n1p2这种名字根本记不住,重启之后盘符还可能漂移。这时候如果程序里硬编码了设备路径,轻则读不到数据,重则把数据写到错误的盘上,后果不堪设想。卷标(Label)、UUID 和文件系统类型这三样东西,恰恰就是解决"盘符漂移"问题的关键抓手。

卷标是人为给分区起的一个可读名字,比如DATA、BACKUP,方便人看;UUID 是文件系统创建时生成的一串唯一标识,几乎不可能重复,适合程序做精确匹配;文件系统类型(ext4、xfs、btrfs、vfat、ntfs 等)则决定了你该怎么挂载、能不能读写、支持哪些特性。把这三者拿全,你就能在 C/C++ 程序里实现"按 UUID 找盘""按卷标定位数据分区""挂载前先校验文件系统类型"这类实用功能。

这篇内容面向的是有一定 Linux 系统编程基础、需要在 C/C++ 里以编程方式枚举块设备信息的开发者。我会从整体设计思路讲起,把底层原理、核心 API、实操代码、参数取舍和踩坑经验全部摊开讲。哪怕你之前只用过lsblk这种命令行工具,看完也能自己动手写一个等价的小工具。全文围绕"如何用 C/C++ 稳定、可移植地拿到所有硬盘分区的卷标、UUID 和文件系统类型"这个核心目标展开,不跑题、不注水。

2. 整体设计与技术路线拆解

2.1 三条可选路线:sysfs、libblkid、直接读超级块

要在 Linux 上拿到分区信息,摆在面前的路其实有三条,选哪条直接决定了代码的复杂度、可移植性和稳定性。

第一条是直接解析/sys和/proc下的虚拟文件系统。/proc/partitions能列出所有块设备,/sys/class/block/<dev>/下面有dev、size、removable等属性。这条路的好处是零依赖、纯文本解析,坏处是 sysfs 里根本没有卷标、UUID 和文件系统类型——这些信息属于文件系统层面的元数据,内核的块设备层不负责暴露。所以纯 sysfs 路线走不通,只能用来枚举设备列表。

第二条是直接打开块设备读超级块(superblock),自己按 ext4、xfs、vfat 各自的格式去解析偏移量。这条路最硬核,能让你彻底理解文件系统布局,但工作量巨大:光 ext4 的超级块就有几十个字段,xfs 又是完全不同的结构,vfat 的卷标还藏在引导扇区的特定偏移里。更麻烦的是权限——读块设备通常需要 root,普通用户跑不起来。这条路适合做教学或者极端定制场景,工程上不推荐。

第三条是用libblkid库。这是 util-linux 项目提供的官方库,blkid命令背后就是它。它内部封装了各种文件系统的探测逻辑,能直接返回TYPE、UUID、LABEL等标签,还自带缓存机制(/run/blkid/blkid.tab)加速查询。这条路兼顾了稳定性和开发效率,是绝大多数项目的首选。

我的结论很明确:用 libblkid 做主力,用 sysfs 做设备枚举的补充。下面所有的设计和代码都基于这个组合。选 libblkid 的核心理由是它把"探测文件系统类型"这件脏活累活标准化了——你不需要关心 ext4 的 magic number 是0xEF53还是 xfs 的XFSB,库会帮你判断。而且它对权限友好,普通用户只要有读设备的权限(或者缓存里有记录)就能查。

2.2 设备枚举与信息查询为什么要分开

一个容易犯的错误是把"枚举设备"和"查询信息"混在一起写。正确的做法是分两步:先用 sysfs 或/proc/partitions拿到一份块设备清单,再对清单里的每个设备调用 libblkid 查询详情。这样分层的好处是:枚举逻辑可以独立测试,查询失败也不影响整体流程(某个分区没格式化,查不到信息就跳过,不至于整个程序崩掉)。

枚举时要注意区分整盘和分区。/sys/class/block/下既有sda这样的整盘,也有sda1这样的分区。判断方法是看该目录下有没有partition文件——有就是分区,没有就是整盘。程序里通常只关心分区,因为卷标和 UUID 是挂在分区(文件系统)上的,整盘本身一般没有文件系统(除非是没分区的裸盘直接格式化)。

2.3 缓存机制带来的坑与应对

libblkid 默认会读缓存,这在交互式使用blkid命令时很方便,但在程序里可能带来"信息过期"的问题。比如你刚用mkfs重新格式化了分区,UUID 变了,但缓存里还是旧的。解决办法是在调用blkid_probe_lookup_value之前,用blkid_probe_enable_superblocks和blkid_probe_set_superblocks_flags控制探测行为,或者干脆用blkid_new_probe_from_filename走"直接探测"路径,绕过缓存。这个细节后面在实操部分会给出具体代码。

3. 核心 API 与关键细节解析

3.1 libblkid 的核心对象模型

libblkid 的 API 围绕两个核心对象展开:blkid_probe和blkid_dev。前者是"探测器",你给它一个设备路径,它去读数据、识别文件系统;后者是"设备记录",偏向缓存查询。做实时探测时用blkid_probe系列函数更直接。

典型调用链是这样的:blkid_new_probe_from_filename()创建一个探测器并打开设备,blkid_probe_enable_superblocks()开启超级块探测,blkid_probe_set_superblocks_flags()设置要探测哪些标签(BLKID_SUBLKS_LABEL | BLKID_SUBLKS_UUID | BLKID_SUBLKS_TYPE),然后blkid_do_safeprobe()执行探测,最后用blkid_probe_lookup_value()按名字取值。取完值记得blkid_free_probe()释放。

这里有个关键点:blkid_do_safeprobe()比blkid_do_probe()更安全,它会做多重校验,避免误判。代价是稍慢一点,但对于枚举几十个分区的场景完全无感。我实测过,一块盘上 10 个分区,safeprobe 全跑一遍也就几十毫秒。

3.2 编译链接与头文件依赖

用 libblkid 需要安装开发包。Debian/Ubuntu 系是libblkid-dev,RHEL/CentOS 系是libblkid-devel。头文件是<blkid/blkid.h>,链接时加-lblkid。如果你的发行版把库放在非标准路径,可能还需要-I/usr/include/blkid。

编译命令大致长这样:

gcc -o diskinfo diskinfo.c -lblkid

如果是 C++ 项目,把gcc换成g++即可,libblkid 是 C 库,C++ 里直接包含头文件就能用,注意用extern "C"包裹(多数发行版的头文件已经自带,但保险起见可以手动加)。

3.3 卷标、UUID、类型三者的取值差异

这三个标签的"可靠性"并不一样,理解差异能帮你写出更健壮的代码。

文件系统类型(TYPE)最可靠,只要分区被格式化过,libblkid 基本都能识别出来,因为它是靠 magic number 判断的,不依赖任何人为设置。

UUID次之,绝大多数现代文件系统(ext2/3/4、xfs、btrfs、vfat、ntfs)都会生成 UUID,但某些老旧的或者特殊格式可能没有。程序里必须处理"取不到 UUID"的情况,不能假设它一定存在。

卷标(LABEL)最不可靠,因为它是可选的——用户格式化时完全可以不设卷标。而且卷标允许重复,两个分区都叫DATA是合法的。所以绝对不能用卷标做唯一标识,它只适合做展示或者辅助匹配。

下面这张表总结了常见文件系统对三者的支持情况,供你设计逻辑时参考:

文件系统TYPE 识别UUIDLABEL备注
ext2/3/4稳定有可选最常用,支持完善
xfs稳定有可选大文件场景常见
btrfs稳定有可选支持子卷,注意区分
vfat/FAT32稳定有(较短)可选U 盘常见,卷标有长度限制
ntfs稳定有可选需内核或用户态驱动支持
swap稳定有可选不是常规文件系统,但也能查

注意:vfat 的 UUID 格式和 ext4 不同,通常是XXXX-XXXX这种 8 位短格式,程序里如果对 UUID 长度有假设,务必先做兼容处理。

3.4 权限与错误处理的设计原则

查询块设备信息时,普通用户可能因为权限不足而失败。设计程序时要把"查询失败"当成正常分支处理,而不是异常。具体做法是:对每个设备独立 try,失败就记录一条日志并继续下一个,绝不让单个设备的失败中断整个枚举流程。

另外,blkid_new_probe_from_filename()在设备不存在或无法打开时返回 NULL,必须判空。blkid_probe_lookup_value()在标签不存在时也返回 NULL,同样要判空。这两个判空是新手最容易漏的地方,漏了就是段错误。

4. 完整实操:从枚举到输出的全流程实现

4.1 第一步:用 sysfs 枚举所有分区

先写枚举部分。思路是遍历/sys/class/block/目录,对每个条目检查是否存在partition文件,存在则说明是分区,把设备名拼成/dev/xxx存进列表。

#include <stdio.h> #include <stdlib.h> #include <string.h> #include <dirent.h> #include <sys/stat.h> #define SYS_BLOCK_PATH "/sys/class/block" #define MAX_PARTITIONS 256 static int is_partition(const char *name) { char path[512]; struct stat st; snprintf(path, sizeof(path), SYS_BLOCK_PATH "/%s/partition", name); return stat(path, &st) == 0; } int enumerate_partitions(char devs[][64], int max) { DIR *dir = opendir(SYS_BLOCK_PATH); if (!dir) { perror("opendir " SYS_BLOCK_PATH); return -1; } int count = 0; struct dirent *ent; while ((ent = readdir(dir)) != NULL && count < max) { if (ent->d_name[0] == '.') continue; if (!is_partition(ent->d_name)) continue; snprintf(devs[count], 64, "/dev/%s", ent->d_name); count++; } closedir(dir); return count; }

这段代码有几个细节值得说。readdir返回的条目里包含.和..,必须跳过,否则会去检查/sys/class/block/./partition这种无意义路径。is_partition用stat判断文件是否存在,比access更通用。设备名缓冲区给 64 字节足够,因为 Linux 设备名最长也就nvme0n1p128这种级别。

实操心得:有些环境(比如容器里)/sys/class/block可能是只读挂载或者被裁剪过,枚举结果可能为空。程序里要对"枚举到 0 个分区"做兜底提示,而不是静默返回。

4.2 第二步:用 libblkid 查询单个分区的三项信息

枚举拿到设备路径后,逐个查询。核心函数封装如下:

#include <blkid/blkid.h> typedef struct { char label[256]; char uuid[128]; char fstype[64]; int has_label; int has_uuid; int has_type; } part_info_t; int query_partition(const char *dev, part_info_t *info) { memset(info, 0, sizeof(*info)); blkid_probe pr = blkid_new_probe_from_filename(dev); if (!pr) { return -1; /* 设备打不开,可能权限不足或不存在 */ } blkid_probe_enable_superblocks(pr, 1); blkid_probe_set_superblocks_flags(pr, BLKID_SUBLKS_LABEL | BLKID_SUBLKS_UUID | BLKID_SUBLKS_TYPE); int rc = blkid_do_safeprobe(pr); if (rc == 0) { const char *val = NULL; if (blkid_probe_lookup_value(pr, "LABEL", &val, NULL) == 0) { strncpy(info->label, val, sizeof(info->label) - 1); info->has_label = 1; } if (blkid_probe_lookup_value(pr, "UUID", &val, NULL) == 0) { strncpy(info->uuid, val, sizeof(info->uuid) - 1); info->has_uuid = 1; } if (blkid_probe_lookup_value(pr, "TYPE", &val, NULL) == 0) { strncpy(info->fstype, val, sizeof(info->fstype) - 1); info->has_type = 1; } } blkid_free_probe(pr); return rc == 0 ? 0 : -1; }

blkid_do_safeprobe返回 0 表示探测成功,返回 1 表示没探测到已知文件系统(比如分区是空的),返回负数表示出错。这三种情况要区别对待:返回 1 时不是错误,只是这个分区没格式化,程序应该跳过而不是报错。

blkid_probe_lookup_value的第三个参数是值指针,第四个参数是长度(可传 NULL)。注意这个指针指向探测器内部缓冲区,blkid_free_probe之后就失效了,所以必须立刻strncpy拷贝出来,不能只存指针。这是 libblkid 使用中最容易踩的坑之一。

4.3 第三步:组装主流程与输出格式

把枚举和查询串起来,主函数逻辑就很清晰了:

int main(void) { char devs[MAX_PARTITIONS][64]; int n = enumerate_partitions(devs, MAX_PARTITIONS); if (n <= 0) { fprintf(stderr, "未枚举到任何分区\n"); return 1; } printf("%-20s %-36s %-10s %s\n", "DEVICE", "UUID", "FSTYPE", "LABEL"); printf("------------------------------------------------------------\n"); for (int i = 0; i < n; i++) { part_info_t info; if (query_partition(devs[i], &info) != 0) { printf("%-20s %-36s %-10s %s\n", devs[i], "-", "-", "(无法读取)"); continue; } printf("%-20s %-36s %-10s %s\n", devs[i], info.has_uuid ? info.uuid : "-", info.has_type ? info.fstype : "-", info.has_label ? info.label : "-"); } return 0; }

输出用固定宽度对齐,%-20s给设备名,%-36s给 UUID(标准 UUID 是 36 字符含连字符),%-10s给文件系统类型,最后是卷标。取不到的字段统一显示-,比留空更清晰。

编译运行:

gcc -O2 -Wall -o diskinfo diskinfo.c -lblkid sudo ./diskinfo

普通用户跑可能因为读不了块设备而全部显示"无法读取",加sudo或者把用户加入disk组即可。实测在一块装了 ext4 和 vfat 分区的机器上,输出大致是:

DEVICE UUID FSTYPE LABEL ------------------------------------------------------------ /dev/sda1 3f2a1b4c-...-9e8d ext4 root /dev/sda2 A1B2-C3D4 vfat EFI /dev/sdb1 7c8d9e0f-...-1a2b xfs data

4.4 参数与缓冲区大小的取舍依据

缓冲区大小不是随便定的。UUID 标准格式是 36 字符(8-4-4-4-12 加连字符),但某些文件系统的 UUID 可能更长或更短,给 128 字节留足余量。卷标在 ext4 里最长 16 字符,vfat 最长 11 字符,但为了兼容各种奇怪情况,给 256 字节。文件系统类型名最长的是f2fs、btrfs这种,64 字节绰绰有余。

MAX_PARTITIONS设 256 是经验值,普通机器几十个分区顶天了,服务器上挂几十块盘也就几百个分区。如果你的场景更极端,把这个值调大即可,栈上放不下就改成malloc。

5. 常见问题与排查技巧实录

5.1 查询结果为空或全是"-"

最常见的原因是权限不足。blkid_new_probe_from_filename需要打开设备文件读数据,普通用户对/dev/sda1通常没有读权限。排查方法:先用ls -l /dev/sda1看权限位,再用sudo blkid /dev/sda1确认命令行能查到。如果命令行能查而程序不能,基本就是权限问题。

第二个原因是分区没格式化。空分区没有超级块,blkid_do_safeprobe返回 1,程序里如果把这个当错误处理就会显示异常。正确做法是把返回 1 当作"无文件系统"正常跳过。

第三个原因是缓存干扰。如果你刚改过分区,缓存里可能是旧数据。可以在查询前调用blkid_probe_set_superblocks_flags时加上BLKID_SUBLKS_BADCSUM之类的标志强制重新探测,或者直接删掉/run/blkid/blkid.tab再试。

5.2 段错误:lookup_value 之后立刻 free 了 probe

这是新手最常犯的错。blkid_probe_lookup_value返回的val指针指向探测器内部内存,一旦blkid_free_probe被调用,这块内存就释放了,再去访问就是 use-after-free。正确顺序永远是:先 lookup,立刻拷贝到自己的缓冲区,最后才 free probe。我在代码里用strncpy就是为了这个。

5.3 设备名带特殊字符或路径过长

/dev/mapper/下的 LVM 逻辑卷名字可能很长,比如/dev/mapper/vg_data-lv_home。枚举时如果只从/sys/class/block取名字,LVM 设备会以dm-0这种形式出现,拼成/dev/dm-0也能用,但可读性差。如果需要友好名字,可以额外读/sys/class/block/dm-0/dm/name。这个属于进阶需求,基础版本用/dev/dm-0完全够用。

5.4 常见问题速查表

现象可能原因排查方法解决方式
全部显示"无法读取"权限不足ls -l /dev/sdX看权限用 root 或加入 disk 组
某分区三项全空分区未格式化sudo blkid /dev/sdX验证正常跳过,非错误
UUID 是旧值缓存未更新对比blkid命令输出清缓存或强制重探
程序段错误probe 提前释放检查 free 与 lookup 顺序先拷贝再 free
枚举不到分区sysfs 被裁剪ls /sys/class/block改用/proc/partitions兜底
vfat UUID 格式异常短格式 UUID打印实际值观察不做长度假设

避坑技巧:调试阶段可以在query_partition里把blkid_do_safeprobe的返回值打印出来,0/1/负数分别代表成功/无文件系统/出错,一眼就能定位问题出在哪一层。

5.5 性能优化:批量查询时避免重复打开设备

如果分区数量很多(比如上百个),每个都blkid_new_probe_from_filename会反复打开关闭设备文件,有一定开销。优化思路是复用同一个 probe 对象,用blkid_probe_set_device切换设备。不过实测下来,几十个分区的场景这点开销可以忽略,除非你的程序是高频调用的守护进程,否则没必要过早优化。我个人的原则是:先保证正确性和可读性,性能瓶颈真正出现时再动手。

6. 扩展思路与工程化建议

基础版本跑通之后,这个工具还能往几个方向扩展。一是输出成 JSON 或 CSV,方便被其他程序消费,比如给监控系统上报磁盘清单。二是结合挂载状态,读/proc/mounts判断某个分区当前挂载在哪个目录,把挂载点也纳入输出。三是做成库,把枚举和查询封装成list_partitions()接口,供更大的项目调用。

工程化方面,我建议把设备枚举的 sysfs 路径和 libblkid 的调用都做成可配置的,方便在容器、嵌入式等特殊环境下替换实现。另外,错误日志要带上设备名和 errno,出问题时能快速定位是哪个分区、什么原因失败。

最后分享一个我在实际项目里踩过的坑:早期版本我用卷标做数据分区的定位依据,结果用户手动改了个卷标,程序就找不到盘了。后来全部改成按 UUID 匹配,卷标只用于界面展示,稳定性立刻上了一个台阶。这个教训说明,唯一标识永远优先用 UUID,卷标只是给人看的装饰,这个原则值得写进任何涉及磁盘定位的代码规范里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询