简介面向需要在Linux与Windows下获取硬件信息的C语言开发者这份小工具包提供了一套跨平台实现方案覆盖CPU序列号、硬盘序列号、网卡IP、MAC地址及网线插入状态等常见需求适用于系统监控、设备识别和故障排查场景。压缩包内含2个文件分别为头文件与C源文件整体仅7KB代码精简集中便于阅读和直接集成到现有工程。资源已获得3100人学习与下载实用价值得到较多认可。代码针对Windows侧WMI、GetAdaptersAddresses等接口和Linux侧/proc、sysfs、ioctl等读取方式分别做了封装并预置条件编译分支方便读者对比两套API差异快速移植到自研的硬件信息采集或网络管理工具中是学习系统底层调用与跨平台编程的良好参考。1. 用 C 语言采集硬件信息这套代码为什么值得你下下来做软件授权、资产盘点或者网络诊断小工具的人大概率都被一个问题卡过怎么用 C 语言在 Linux 和 Windows 两套平台上把 CPU 序列号、硬盘序列号、网卡 IP、MAC 地址以及“网线是否插入”这一堆硬件信息稳定地抠出来。标题里这套代码包就是把四类信息采集做成了可以直接编译运行的 C 源码覆盖了 Linux 和 Windows 双平台。适合做 License 绑定、机房资产采集、网络状态检测的开发者也适合想拿系统编程练手的初学者。它能解决的核心问题是不用引入一堆重型依赖纯 C 就能把机器身份“钉住”。2. CPU 序列号与硬盘序列号Linux 和 Windows 各走一条路2.1 Linux/proc/cpuinfo、/sys/block 与 udevadm 三条读取路径先说个容易翻车的常识x86 处理器上/proc/cpuinfo 里绝大多数情况根本没有 Serial 字段。老奔腾 III 时代有过 CPUID 指令读取序列号的玩法后来因为隐私问题被厂商弃用。现在能在 /proc/cpuinfo 里看到 Serial 的多半是 ARM 开发板或者某些国产平台。所以 CPU 序列号采集要先做“读得到就用读不到就兜底”不能一上来就断言失败。#include stdio.h #include string.h static int read_cpu_serial(char *buf, size_t size) { FILE *fp fopen(/proc/cpuinfo, r); if (!fp) return -1; char line[256]; while (fgets(line, sizeof(line), fp)) { // ARM 板子和部分平台才有 Serial 字段x86 通常没有 if (strncmp(line, Serial, 6) 0) { char *colon strchr(line, :); if (colon) { snprintf(buf, size, %s, colon 2); buf[strcspn(buf, \n)] 0; fclose(fp); return 0; } } } fclose(fp); return -1; // 读不到返回 -1调用方要自己兜底 }这段代码的逻辑很直接逐行读 /proc/cpuinfo用 strncmp 匹配行首的 “Serial” 字段再用 strchr 定位冒号把后面的值抠出来。snprintf 里传入 size 是为了防止缓冲区溢出这是 C 语言采集类代码的基本功。返回值 0 表示读到-1 表示当前平台没有这个字段。注意 strcspn 那行是为了去掉结尾换行符很多新手会漏掉这一步导致序列号里多一个\n。硬盘序列号在 Linux 下相对好办主流做法是直接读 sysfs 属性节点。SATA 和 NVMe 盘通常都会暴露 device/serial普通用户就能读不需要 root。我在实际项目里一般先读 /sys/block/sda/device/serial读不到再用 udevadm 兜底。#include stdio.h static int read_disk_serial(const char *dev, char *buf, size_t size) { char path[256]; snprintf(path, sizeof(path), /sys/block/%s/device/serial, dev); FILE *fp fopen(path, r); if (!fp) return -1; if (fgets(buf, size, fp) ! NULL) { buf[strcspn(buf, \n)] 0; fclose(fp); return 0; } fclose(fp); return -1; }这个函数把设备名作为参数传进来调用时传 “sda”“sdb” 这类块设备名函数内部拼出完整路径。有些主板或者硬盘固件会屏蔽序列号fopen 直接失败这时候就要走下面这招。# 后备方案用 udev 导出属性比直接读 sysfs 更稳 udevadm info --queryproperty --name/dev/sda | grep -E ID_SERIAL_SHORT|ID_SERIAL|ID_MODELudevadm 输出的 ID_SERIAL_SHORT 通常就是硬盘厂商固件里写的那串序列号比 sysfs 里的更干净。但注意 ID_MODEL 是型号字符串不是序列号拿它当机器指纹会出问题后面避坑章节专门说这个。注意Linux 下不存在“统一的 CPU 序列号”这个概念。采集前先确认目标平台到底有没有 Serial 字段没有就返回 NOT_AVAILABLE别硬编。2.2 WindowsWMI 查询 CPU 序列号与物理硬盘序列号Windows 上最符合直觉的做法是 WMI。CPU 的 ProcessorId 字段、硬盘的 SerialNumber 字段一条 WQL 就能查回来。但 WMI 有个知名坑首次查询极慢慢到像玄学。所以代码结构上要把 WMI 初始化、查询、释放分成清晰的步骤并且最好不要放在程序启动的主线程里。// Windows 下用 WMI 查询 CPU 序列号关键片段 #include windows.h #include wbemidl.h HRESULT hr CoInitializeEx(NULL, COINIT_MULTITHREADED); if (FAILED(hr)) return -1; IWbemLocator *pLoc NULL; CoCreateInstance(CLSID_WbemLocator, NULL, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)pLoc); IWbemServices *pSvc NULL; pLoc-ConnectServer(Lroot\\cimv2, NULL, NULL, NULL, 0, 0, NULL, pSvc); // 第一次调用有可能卡几秒建议放到后台线程 IEnumWbemClassObject *pEnum NULL; pSvc-ExecQuery(LWQL, LSELECT ProcessorId FROM Win32_Processor, WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, pEnum); // 循环调用 pEnum-Next() 取结果用完后逐个 Release这一段里有三个容易踩的地方。第一CoInitializeEx 不先调用后面 ConnectServer 直接失败错误码还不直观。第二ExecQuery 的 WBEM_FLAG_RETURN_IMMEDIATELY 是半同步模式配合 WBEM_FLAG_FORWARD_ONLY 避免结果集被缓存这是 WMI 查询的常见姿势。第三所有 COM 接口用完后都要 Release否则进程退出时可能崩溃。硬盘序列号换一张表SELECT SerialNumber FROM Win32_DiskDrive。这个 SerialNumber 对应的是物理硬盘的序列号和 C 盘右键属性里看到的“卷序列号”完全是两码事。很多人在这里栽跟头把卷序列号当成硬盘身份拿去绑 License结果用户一格式化授权就丢了。物理序列号才是稳定的硬件标识。2.3 返回值里的门道卷序列号、物理序列号和厂商字符串把两套平台的读取结果放在一起对比会发现字段名一样含义却可能完全不同。我在做跨平台采集时会把每个字段的“可信度”标出来防止上层逻辑误用。字段来源实际含义坑点Linux /proc/cpuinfo 的 Serial部分 ARM 平台的 CPU IDx86 下不存在Windows Win32_Processor.ProcessorId厂商填充的 CPU 标识有些机器是“Not Supported”Linux /sys/block/sda/device/serial硬盘物理序列号部分固件屏蔽udevadm 的 ID_SERIAL_SHORT硬盘物理序列号比 sysfs 更可靠Windows Win32_DiskDrive.SerialNumber硬盘物理序列号稳定但查询慢GetVolumeInformation 的卷序列号分区格式化时生成的随机数格式化就变不能当硬件指纹卷序列号那个坑值得单独说。卷序列号是文件系统格式化时随机生成的同一块硬盘重装系统或者格式化分区后就会变。如果拿它做机器绑定用户重装一次系统就得找你重新激活售后成本直接拉满。物理硬盘序列号则不会因为重装系统改变这才是做授权校验该用的字段。3. 网卡 IP、MAC 与网线插拔从枚举到状态轮询3.1 Linuxgetifaddrs 读 IPioctl 读 MACcarrier 读网线Linux 下枚举网卡我一般用 getifaddrs一次调用能把 IP、MAC、接口名全拿回来。它返回一个链表遍历时根据 sa_family 区分地址类型AF_INET 是 IPv4AF_PACKET 是以太网帧地址也就是 MAC。#include ifaddrs.h #include net/if.h #include arpa/inet.h #include stdio.h void dump_net_info(void) { struct ifaddrs *ifap, *ifa; getifaddrs(ifap); for (ifa ifap; ifa; ifa ifa-ifa_next) { if (!ifa-ifa_addr) continue; int fam ifa-ifa_addr-sa_family; if (fam AF_INET) { struct sockaddr_in *sin (struct sockaddr_in *)ifa-ifa_addr; char ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, sin-sin_addr, ip, sizeof(ip)); printf(%-8s IPv4 %s\n, ifa-ifa_name, ip); } else if (fam AF_PACKET) { struct sockaddr_ll *ll (struct sockaddr_ll *)ifa-ifa_addr; printf(%-8s MAC , ifa-ifa_name); for (int i 0; i 6; i) printf(%02x%s, ll-sll_addr[i], i 5 ? : : \n); } } freeifaddrs(ifap); }这段代码里inet_ntop 把二进制地址转成点分十进制字符串INET_ADDRSTRLEN 是 IPv4 地址字符串的最大长度。AF_PACKET 是 Linux 专有的BSD 和 macOS 上拿 MAC 要用 AF_LINK跨平台时这一层要做条件编译。MAC 地址是 6 字节sll_addr 里存的顺序就是网卡硬件顺序直接按 %02x 打印即可。网线插拔在 Linux 下有一个特别干净的判断方式读 /sys/class/net/eth0/carrier。这个文件只有 0 或 11 表示链路通0 表示网线断开或对端设备关机。注意 WiFi 网卡没有 carrier 文件无线网卡要看 /proc/net/wireless 或者用 NetworkManager 的 API。#include stdio.h // 返回值0 成功-1 失败网卡不存在或权限不足 int read_carrier(const char *iface, int *link_up) { char path[256]; snprintf(path, sizeof(path), /sys/class/net/%s/carrier, iface); FILE *fp fopen(path, r); if (!fp) return -1; char v (char)fgetc(fp); fclose(fp); *link_up (v 1); return 0; }这个函数的调用频率取决于你的业务需求。做资产盘点的话开机采一次就够了做网络监控的话每 5 到 10 秒读一次也不会有性能问题。fopen 失败要区分两种情况网卡名字拼错了或者内核没暴露这个节点。还有一种情况是权限不足后面避坑章节会细说。提示判断网线状态时carrier 为 1 只代表物理链路通了不代表能上网。DHCP 没拿到地址、DNS 不通carrier 照样是 1。3.2 WindowsGetAdaptersAddresses 一把抓回 IP、MAC、OperStatusWindows 下不推荐用 GetAdaptersInfo那是老 APIIPv6 支持不完整。用 GetAdaptersAddresses 一次调用就能拿回网卡名、MAC、IPv4、IPv6、DNS 和链路状态。这个 API 的标准用法是两次调用第一次传 NULL 拿缓冲区大小第二次真正取数据。我第一次写的时候偷懒只调了一次结果返回 ERROR_BUFFER_OVERFLOW血泪教训。// Windows 下枚举网卡的核心骨架伪代码去掉错误处理后只留主流程 ULONG bufLen 0; GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, NULL, bufLen); IP_ADAPTER_ADDRESSES *pAddrs (IP_ADAPTER_ADDRESSES*)malloc(bufLen); GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, NULL, pAddrs, bufLen); for (IP_ADAPTER_ADDRESSES *p pAddrs; p; p p-Next) { // p-FriendlyName - 以太网 / WLAN // p-PhysicalAddress - 6 字节 MAC // p-OperStatus - IfOperStatusUp 表示链路通 // p-FirstUnicastAddress - IP 地址链表 // p-IfType - IF_TYPE_ETHERNET_CSMACD 是物理以太网 } free(pAddrs);接口里的关键字段OperStatus 对应 Linux 的 carrier。网线拔掉后OperStatus 会从 IfOperStatusUp 变成 IfOperStatusDown这个状态变化可以用于实时监测。IfType 用于区分物理网卡和虚拟网卡IF_TYPE_ETHERNET_CSMACD 是以太网IF_TYPE_IEEE80211 是无线网卡。FirstUnicastAddress 是单播 IP 链表遍历时把 SOCKADDR_INET 里的 IP 转出来就行。3.3 多网卡、虚拟网卡和网线拔插的过滤策略真实机器上不止一块网卡。装了虚拟机软件的机器光虚拟网卡就有好几块如果不过滤拿到的 MAC 可能就是 VMware 或 VirtualBox 的虚拟 MAC。我做资产采集时过滤策略是这样的场景保留还是过滤判断依据物理以太网卡优先保留IfType 为以太网OperStatus UpWLAN 无线网卡保留Windows 下看 IfTypeLinux 下名字带 wlLoopback 回环过滤IFF_LOOPBACK 标志IPv4 地址 127.0.0.1虚拟机虚拟网卡过滤名字含 VMware/VirtualBox/loLinux 的 virbr0、docker0链路 Down 的网卡降级OperStatus Down 或 carrier 为 0Linux 下还要多注意一个点docker0、virbr0 这类虚拟网卡在 getifaddrs 里也会出现名字不以 eth 或 en 开头。过滤时不能只看 IFF_LOOPBACK还得维护一个虚拟网卡名字前缀表或者按 IFF_RUNNING 和是否有 carrier 文件双条件判断。4. 避坑指南权限、WMI 慢、序列号对不上的真实案例4.1 WMI 查询太慢线程里卡了 8 秒现象程序在 Windows 启动时调用一次 WMI 查询 CPU 序列号结果界面卡住任务管理器显示进程无响应耗时 8 到 10 秒才返回。原因WMI 服务是懒启动的第一次查询要拉起 WinMgmt 服务同时 COM 初始化、连接 root\cimv2 命名空间都需要时间。这个延迟和机器配置、杀毒软件都有关系属于典型的“第一次慢、后续快”。解决把 WMI 查询放到独立线程或者做成异步初始化。程序启动时先显示主界面后台线程去查查完再更新结果。如果对实时性要求高可以改用直接读注册表 HKLM\HARDWARE\DESCRIPTION\System\CentralProcessor那个键下虽然没有 CPU 序列号但有 ProcessorNameString 和 Identifier可以作为替代。4.2 硬盘序列号在 Linux 下读出来一堆厂商字符串现象同一块 SATA 盘Windows 下 Win32_DiskDrive.SerialNumber 返回 TX5A2K1AFLinux 下 /sys/block/sda/device/serial 返回的却是 VBOX HARDDISK 或者类似厂商型号的字符串。原因部分硬盘固件没有把真正的序列号写进 ATA Identify 信息sysfs 里的 serial 字段直接取了厂商字符串或者型号。还有一种情况是虚拟机环境虚拟磁盘的 serial 由虚拟机软件伪造读出来是一串固定常量。解决先用 udevadm info --queryproperty --name/dev/sda 看完整属性列表优先取 ID_SERIAL_SHORT。如果 ID_SERIAL_SHORT 也不对就对比 ID_MODEL 和 ID_SERIAL两者相同的说明固件没提供真实序列号这时候直接标记 NOT_AVAILABLE不要硬用。硬件采集的原则是“读不准的字段宁可空着”否则后续 License 校验会误杀。4.3 MAC 地址对不上拿到的是“当前活动网卡”还是“物理网卡”现象一台笔记本有板载网卡、USB 网卡和虚拟网卡程序取到的 MAC 和网卡标签上的不一致而且每次插拔 USB 网卡后结果还会变。原因没做网卡优先级过滤直接取了枚举结果里的第一块。Windows 的 GetAdaptersAddresses 返回顺序不保证是物理网卡优先Linux 的 getifaddrs 顺序更不保证——某些驱动注册顺序靠前虚拟网卡就被排到了前面。解决遍历所有网卡后按“物理以太网 WLAN 其它”的优先级打分OperStatus Up 的加权最后取分数最高的一块。判断物理网卡看 IfTypeWindows和接口名是否以 en/eth 开头Linux。另外注意Windows 下如果启用了网络连接随机 MAC 功能每次连网都会换 MAC这种情况要提示用户关闭。4.4 权限不足时 /sys/class/net/eth0/carrier 直接报错现象普通用户编译运行读 carrier 文件 fopen 返回 NULLerrno 是 EACCESsudo 运行就正常。网线明明插着读不到状态。原因某些发行版对 /sys/class/net/*/carrier 的默认权限是 0400只有 root 能读。不只是 carrier/sys/block/sda/device/serial 在某些系统上也被限制为 root 可见。解决Linux 下采集硬件信息统一采用“先普通读、失败提示提权”的策略。程序检测到 fopen 失败且 errno 为 EACCES 时提示用户用 root 或 systemd service 运行。如果在受限环境里无法提权可以退回用 getifaddrs 看 IFF_RUNNING 标志但注意 IFF_RUNNING 在有些驱动下不实时更新不如 carrier 准确。5. 统一成一套接口四类信息合成一个 hardware_info 结构体5.1 结构体设计与填充顺序跨平台代码最怕散落各处的宏定义和条件编译。我一般会定义一个统一的结构体各平台的采集函数只往里面填字段上层逻辑完全不知道底层是 Windows 还是 Linux。typedef struct { char cpu_serial[256]; // CPU 序列号读不到填 NOT_AVAILABLE char disk_serial[256]; // 硬盘物理序列号同上 char mac[18]; // 形如 00:11:22:33:44:55 char ip[64]; // 当前活动网卡的 IPv4 地址 int link_up; // 1 表示网线已插入0 表示断开 } hardware_info; int collect_hardware_info(hardware_info *info) { memset(info, 0, sizeof(*info)); // 各函数内部自行处理失败失败时把字段置为 NOT_AVAILABLE read_cpu_serial(info-cpu_serial, sizeof(info-cpu_serial)); read_disk_serial(sda, info-disk_serial, sizeof(info-disk_serial)); find_active_net_iface(info-mac, sizeof(info-mac), info-ip, sizeof(info-ip)); read_carrier(eth0, info-link_up); return 0; }这个设计的核心是“单字段失败不影响整体”。read_cpu_serial 在 x86 下返回 -1上层看到的只是 cpu_serial 为 “NOT_AVAILABLE”不影响硬盘和网卡字段。实际项目中各采集函数内部都做了边界检查snprintf 的 size 参数全部从 sizeof(info-xxx) 传入避免任何一处缓冲区溢出。5.2 编译验证与预期输出Linux 下用 GCC 直接编译不需要额外链接库getifaddrs 需要 _GNU_SOURCE 宏。# 编译全部源码注意要开 _GNU_SOURCE gcc -D_GNU_SOURCE -Wall -o hw_info hw_info.c ./hw_infoWindows 下用 MinGW 编译时需要链接 WMI 相关的 COM 库gcc -o hw_info.exe hw_info.c -lole32 -lwbemuuid -luuid程序跑起来的输出大概是这个格式字段缺了就显示 NOT_AVAILABLECPU Serial : 10000000abcd1234 Disk Serial : SATA-ABCDEF012345 MAC Address : 00:11:22:33:44:55 IP Address : 192.168.1.100 Link Status : UP5.3 这套结构还能往哪扩展硬件采集的代码包不会只跑一次。我后来在项目里做设备指纹时就是在 hardware_info 基础上拼了一个字符串再算 SHA-1 哈希当成机器唯一标识。如果你要做网络监控把 read_carrier 放进一个 10 秒间隔的定时线程检测到链路从 UP 变 DOWN 时上报事件整个模块就从一个“查一次”的工具变成了“持续监控”的服务。扩展方向通常还有三个一是加内存总量、显卡型号字段二是把采集结果序列化成 JSON 方便上报三是加跨平台编译脚本。每次加字段都记得先在结构体里补空间再按“读不到就置空”的规则处理。某次交付时我因为偷懒直接取第一块网卡的 MAC 当授权指纹结果客户一换 USB 网卡License 全部失效。从那以后我每次做硬件采集不管多急都强制走一遍“字段过滤、读不到置空、接口统一”的三步流程。希望帮到你。本文还有配套的精品资源点击获取