☰
C语言跨平台硬件信息采集:不依赖命令行的序列号方案
2026/10/16 3:31:15 网站建设 项目流程

简介:一套面向系统管理、网络运维与嵌入式开发场景的C语言跨平台硬件信息获取资源包,解决Windows与Linux下统一读取CPU序列号、硬盘序列号、网卡名称、IP地址、MAC地址及网络连接状态等常见需求,适合初学者通过实际代码理解操作系统接口,也适合开发者抽取核心逻辑用于监控告警、资产盘点或设备识别系统。资源共包含2个文件,其中1个为接口头文件,1个为具体实现源码,压缩包整体仅7KB,体量轻巧、无额外依赖,便于快速阅读和二次修改。实现上,Windows分支通过系统管理接口查询CPU与硬盘序列号,并利用网络适配器接口获取网卡名称、地址及连接状态;Linux分支则读取虚拟文件系统下的处理器信息、块设备信息与网络接口状态文件来完成对应采集。两条路线用条件编译清晰隔离,方便对比两个平台在设备信息获取路径上的差异。已有3100人学习下载,对希望从实战中理解操作系统底层机制、提升跨平台开发能力的读者而言,是一份轻量可运行、值得收藏的入门参考;头文件与实现文件分离的结构,也方便按需抽取复用。

1. 用C语言读硬件信息:不依赖命令行的序列号采集方案

排查物理机问题时,最怕的就是台账和实际机器对不上。某公司凌晨报故障,运维按资产管理表找到对应机柜,结果序列号对不上,重启权限又不在手里,排查直接卡住——这种时候最缺的不是远程工具,而是一个能快速拿到真实硬件指纹的本地程序。这份源码包做的就是这件事:用C语言在Linux和Windows下分别读取CPU序列号、硬盘序列号、网卡IP、MAC地址,以及网线是否插入,最终输出一份可直接入库的硬件信息文本。它不依赖dmidecode、wmic这类系统命令,适合做软件授权绑定、资产盘点、机房巡检工具,也适合想学系统编程的开发者拿来做跨平台移植练习。

2. 工程结构与技术选型:Linux走sysfs,Windows走WMI,两条腿独立编译

2.1 目录拆成五个文件,平台差异被挡在统一接口后面

源码包解开之后,不是把逻辑全塞进一个main.c,而是按“平台无关壳 + 平台具体实现”来分:

  • hw_info.h:对外统一头文件,声明硬件信息结构体和获取函数
  • hw_info.c:入口模块,负责结构体初始化、结果格式化、错误码处理
  • linux_hw.c:Linux实现,读sysfs、procfs,走getifaddrs
  • win_hw.c:Windows实现,走WMI查询和IP Helper API
  • main.c:命令行演示程序,把结果逐行打印到stdout

这种拆法的好处在二次开发时才体现得出来:后续要加macOS支持,只需要补充一个darwin_hw.c,把hw_info.h里声明的函数逐一实现,上层调用代码一行不用改。

2.2 为什么选公开系统接口而不是直接操作硬件

我见过一些同类工具,直接调用物理端口或者加载内核模块,读取结果确实更底层,但编译、分发、权限配置都是坎,换一台机器就可能跑不起来。这个源码包走的是系统公开接口:Linux的sysfs与netlink,Windows的WMI与IP Helper API。这类接口的性能不算极致,胜在稳定,跨内核版本和系统补丁的兼容性都有保障,在真实生产环境里反而比花哨方案靠谱。

设计上还做了一层状态码约定:每个获取函数返回0表示成功,负数表示失败。这样上层不用关心某个字段是在Linux还是Windows下读到的,只要判断返回值,再决定是填充结构体还是报错跳过。

3. 核心源码拆解:CPU序列号、硬盘序列号、网卡信息与链路状态

3.1 CPU序列号:CPUID指令读取,sysfs做兜底

x86平台读取CPU信息的正统做法是CPUID指令,源码包里封装了一个跨平台的cpuid调用,既兼容gcc环境下的内联汇编,也兼容MSVC的intrin.h:

// 跨平台封装 CPUID 指令 #include <stdint.h> static void read_cpuid(uint32_t leaf, uint32_t *eax, uint32_t *ebx, uint32_t *ecx, uint32_t *edx) { #if defined(_MSC_VER) int regs[4]; __cpuid(regs, (int)leaf); *eax = regs[0]; *ebx = regs[1]; *ecx = regs[2]; *edx = regs[3]; #elif defined(__i386__) || defined(__x86_64__) __asm__ __volatile__( "cpuid" : "=a"(*eax), "=b"(*ebx), "=c"(*ecx), "=d"(*edx) : "a"(leaf), "c"(0) ); #else (void)leaf; *eax = *ebx = *ecx = *edx = 0; #endif }

说句实在话,现代CPU不像当年的Pentium III那样直接给出可打印序列号,所以源码包里不是死磕一个字段,而是把CPUID拿到的Family、Model、Stepping以及ProcessorId组合成一个字符串作为指纹。Windows下再通过WMI查询Win32_Processor的ProcessorId字段做交叉验证。Linux环境下如果CPUID结果不理想,代码会退回读/sys/class/dmi/id/product_serial,这块在工控机和服务器上通常有值。参数leaf传1时返回特征信息,传0时返回供应商字符串,改造时可以根据需要自行扩展。

3.2 硬盘序列号:Linux读sysfs,Windows查物理卷

Linux下硬盘序列号在sysfs里现成就有,不需要做IOCTL:

/** * 读取指定块设备的序列号 * @disk 设备名,例如 "sda" 或 "nvme0n1" */ int get_disk_serial(const char *disk, char *out, size_t out_size) { char path[160]; FILE *fp; snprintf(path, sizeof(path), "/sys/block/%s/device/serial", disk); fp = fopen(path, "r"); if (!fp) return -1; if (fgets(out, (int)out_size, fp) == NULL) { fclose(fp); return -2; } fclose(fp); // 清理尾部换行和空格 out[strcspn(out, "\r\n")] = '\0'; return 0; }

这里有个关键点:SATA盘走/device/serial,NVMe盘通常也兼容,但如果遇到老版本内核或者驱动未导出序列号的情况,就要退回去用HDIO_GET_IDENTITY做ioctl查询。Windows端绕不开WMI,核心步骤是CoInitializeEx初始化COM、连接ROOT\CIMV2命名空间、执行SELECT SerialNumber FROM Win32_PhysicalMedia。整个链路源码包里已经封装好,输出格式和Linux端统一。注意磁盘序列号通常不是“C盘序列号”,后者指的是文件系统卷序列号,两者用途完全不同,资产登记时别写混。

3.3 网卡IP与MAC:getifaddrs和GetAdaptersAddresses对位

Linux下遍历网卡最简洁的方式是getifaddrs,这个函数一次调用就能把IP和MAC都拿出来:

#include <ifaddrs.h> #include <netpacket/packet.h> #include <netinet/in.h> void dump_net_info(void) { struct ifaddrs *ifap = NULL, *ifa; if (getifaddrs(&ifap) != 0) return; for (ifa = ifap; ifa; ifa = ifa->ifa_next) { if (ifa->ifa_addr == NULL) continue; if (ifa->ifa_addr->sa_family == AF_PACKET) { // 这一层拿到的是物理地址,即 MAC struct sockaddr_ll *sll = (struct sockaddr_ll *)ifa->ifa_addr; printf("%s MAC: ", ifa->ifa_name); for (int i = 0; i < 6; i++) { printf("%02X%s", sll->sll_addr[i], i < 5 ? ":" : "\n"); } } else if (ifa->ifa_addr->sa_family == AF_INET) { // 这一层拿到的是 IPv4 地址 struct sockaddr_in *sin = (struct sockaddr_in *)ifa->ifa_addr; printf("%s IP: %s\n", ifa->ifa_name, inet_ntoa(sin->sin_addr)); } } freeifaddrs(ifap); }

逻辑上先用AF_PACKET分支过滤MAC地址,再用AF_INET分支拿IPv4,顺序不会乱。Windows端走GetAdaptersAddresses,这个方法在压缩包里封装得比较完整,会过滤掉虚拟网卡和禁用状态网卡,这点比Linux端的原始遍历更友好,因为Linux下docker0、virbr0这类虚拟网卡全都会冒出来,采集结果里不带过滤规则的话,入库数据会多出一堆噪音。我在改造时通常会在输出层增加白名单前缀匹配,只保留eth、ens、enp开头的物理网卡。

3.4 网线是否插入:carrier文件与OperStatus字段

判断网线是否插入,绕不开链路状态探测。Linux下的判断依据是/sys/class/net/接口名/carrier文件:

int get_link_status(const char *ifname, int *status) { char path[128]; FILE *fp; snprintf(path, sizeof(path), "/sys/class/net/%s/carrier", ifname); fp = fopen(path, "r"); if (!fp) return -1; fscanf(fp, "%d", status); fclose(fp); // 1 表示物理链路已建立,0 表示没有载波 return 0; }

carrier为1代表对端设备已通电且链路协商完成,为0则说明网线没插或对端设备关机。Windows下对应的字段是IP_ADAPTER_ADDRESSES结构体里的OperStatus,当它等于IfOperStatusUp时表示链路正常,等于IfOperStatusDown时就是网线拔出或驱动禁用。这里有个容易误用的点:链路正常不等于能上网,后面还隔着DHCP、网关、DNS三层,源码包只是在采集物理状态这一层,业务层连通性检测需要另外实现。

4. 编译运行:Makefile与MSVC的踩踏实录

4.1 Linux下直接make,注意链接库顺序

Linux端编译依赖较少,源码目录里给了一份Makefile,核心逻辑简洁直接:

CC ?= gcc CFLAGS := -Wall -O2 -std=c11 TARGET := hw_info SRCS := main.c hw_info.c linux_hw.c OBJS := $(SRCS:.c=.o) $(TARGET): $(OBJS) $(CC) $(CFLAGS) -o $@ $^ clean: rm -f $(TARGET) $(OBJS)

需要留意的是,编译命令里没有额外加-lpthread,因为采集流程目前是单线程顺序执行。如果你二次开发时加入并发采集,记得把线程库补到链接行最后面,否则会出现符号未定义的链接错误。另外建议在x32和x64环境各编一次,CPUID在不同位数下的寄存器使用没有差异,但结构体对齐会影响输出字节流,特别是后续要做二进制序列化时。

4.2 Windows下用MSVC或MinGW都能编

Windows端源码在Visual Studio里可以直接建空控制台工程,把win_hw.c、hw_info.c、main.c拖进去即可。依赖的库有两个,一个是ole32.lib负责COM初始化,一个是iphlpapi.lib负责GetAdaptersAddresses,缺了会在链接阶段报LNK2019。

用MinGW的话命令行方式更直接:

gcc main.c hw_info.c win_hw.c -o hw_info.exe -lole32 -liphlpapi

编译通过只是第一步,运行终端如果发现中文乱码,多半不是代码问题,而是控制台代码页不对,执行chcp 65001切换到UTF-8再看。这块在下一章避坑记录里单独说。

5. 避坑记录:权限、乱码和读不出来的序列号

5.1 现象:Linux下读取product_serial返回Permission denied

原因:这个文件在多数发行版上默认权限是600,只有root能读,普通用户直接fopen就会失败。

解决:运行二进制时加sudo,或者把执行用户加入对应组。如果是在系统初始化阶段使用,可以在udev规则里补充ACL授权,避免程序提权。

5.2 现象:Windows控制台输出的中文全部变成乱码

原因:源码内部使用UTF-8存储字符串,而Windows默认控制台代码页是GBK,两者不一致导致输出错乱。

解决:程序启动时调用SetConsoleOutputCP(CP_UTF8),或者在运行前手动执行chcp 65001。如果采集结果要写文件,建议统一以UTF-8落盘,数据库侧读取更省事。

5.3 现象:VMware虚拟机的硬盘序列号全是一串零

原因:虚拟磁盘默认没有真实序列号,部分虚拟化平台把这块数据留空或者填了占位符。

解决:这种情况不是程序缺陷,采集端要做的不是强行读一个假值,而是在结构体里增加valid字段标为false,由上层判断是否纳入授权绑定。真实物理机上同样的代码能读到正常序列号,拿虚拟机做自测时要有这个预期。

5.4 现象:macOS下编译不通过,因为ifaddrs结构体定义不同

原因:这份源码包明确只覆盖Linux和Windows,macOS虽然有同名函数getifaddrs,但头文件路径和部分字段类型与Linux有差异,直接跨系统编译会报错。

解决:macOS用户需要单独适配,最简单的方式是在原生头文件定义变化的地方用宏隔离。或者干脆只在目标系统上编译,别指望一套代码两头通吃。

6. 二次开发:把单机采集工具改成客户端上报模块

如果要把这个源码包集成进公司自己的资产管理系统,第一件事就是把main.c里的printf全部去掉,改成结构化输出。常见做法是把硬件信息结构体序列化为JSON:

void print_report(const hw_info_t *info) { printf("{\"cpu_sn\":\"%s\",\"disk_sn\":\"%s\"," "\"mac\":\"%s\",\"ip\":\"%s\",\"link\":%d}\n", info->cpu_sn, info->disk_sn, info->mac, info->ip, info->link_status); }

第二步是改造采样时机。现在的流程是一次性顺序采集,适合命令行调用;改造成常驻进程时,建议加定时扫描线程,只对MAC和链路状态做增量更新,CPU和硬盘序列号开机后不会变,没有必要每轮重读。网络采集部分还要加超时保护,避免在网卡异常退出的机器上卡死——我在自己的改造中会给每个采集步骤加timeout,线程池回收后再做数据合并。

验证方法也不难,启动改造后的工具,把输出和BIOS界面逐项对照一遍,再配合拔插网线观察链路状态字段是否在1和0之间翻转。开发机上做三次连续运行,对比输出是否稳定,稳定性通过后再放到三台不同品牌的主机上做兼容性测试。从那以后我每次提交采集模块,都会强制走一遍多主机对比,确认输出的序列号没有重复或缺失才交付,这个习惯帮我挡掉了不少客户现场的数据质量问题。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询