15、系统信息获取:操作系统版本、CPU信息、内存信息的跨平台获取

说实话,系统信息获取这事儿,看着简单,写起来全是坑。

我早年做嵌入式监控软件时,就吃过这个亏。在Windows上跑得好好的代码,一放到Linux服务器上,直接编译不过。后来我学乖了——跨平台获取系统信息,本质上就是跟三个不同的操作系统内核打交道。你得知道它们各自藏在哪里,怎么敲门,以及门开了之后拿到的数据长什么样。

15.1 为什么系统信息获取不能写死?

你想想看,一个程序如果连自己跑在什么系统上都不知道,那它怎么自适应?怎么动态调整内存池大小?怎么根据CPU核数开线程?

我见过不少新手,直接在代码里写死内存大小,结果部署到不同配置的机器上就崩。说白了,系统信息获取是跨平台程序的第一道防线。

核心原则:运行时检测,而非编译时假设。编译时用宏判断平台,运行时用API获取具体数值。

15.2 操作系统版本获取

每个操作系统都有自己的版本号体系。Windows喜欢用大版本.小版本.构建号,Linux用内核版本+发行版名称,macOS则是达尔文内核版本那一套。

15.2.1 Windows 平台

Windows上获取版本信息,我建议用 RtlGetVersion 而不是 GetVersionEx。为什么?因为 GetVersionEx 在Windows 8.1之后就被微软“阉割”了——它会返回虚假的版本号,除非你专门配置清单文件。我曾经被这个坑过,排查了半天才发现是API本身的问题。

#ifdef _WIN32
#include <windows.h>

typedef LONG (WINAPI *RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);

void get_windows_version() {
    HMODULE hMod = GetModuleHandleW(L"ntdll.dll");
    if (!hMod) return;
    
    RtlGetVersionPtr RtlGetVersion = (RtlGetVersionPtr)GetProcAddress(hMod, "RtlGetVersion");
    if (!RtlGetVersion) return;
    
    RTL_OSVERSIONINFOW osvi = {0};
    osvi.dwOSVersionInfoSize = sizeof(osvi);
    if (RtlGetVersion(&osvi) == 0) {
        printf("Windows %lu.%lu (Build %lu)\n",
               osvi.dwMajorVersion, osvi.dwMinorVersion, osvi.dwBuildNumber);
    }
}
#endif

15.2.2 Linux 平台

Linux下获取系统版本,最靠谱的方式是读取 /etc/os-release 文件。别去解析 /proc/version,那个只告诉你内核版本,不告诉你发行版信息。

#ifdef __linux__
#include <stdio.h>
#include <string.h>

void get_linux_version() {
    FILE *fp = fopen("/etc/os-release", "r");
    if (!fp) {
        // 回退方案:读取 /etc/issue
        fp = fopen("/etc/issue", "r");
        if (!fp) return;
    }
    
    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        if (strncmp(line, "PRETTY_NAME=", 12) == 0) {
            // 去掉引号和换行符
            char *start = strchr(line, '"');
            char *end = strrchr(line, '"');
            if (start && end && end > start) {
                *end = '\0';
                printf("OS: %s\n", start + 1);
            }
            break;
        }
    }
    fclose(fp);
}
#endif

我的习惯:在Linux上总是准备两套方案。主方案读 /etc/os-release,备方案读 /etc/issueuname -a。有些精简版容器镜像可能没有 /etc/os-release

15.3 CPU 信息获取

CPU信息包括:架构(x86、ARM、RISC-V)、核心数、频率、缓存大小。不同平台获取方式天差地别。

15.3.1 跨平台获取CPU核心数

这个需求最常见——开线程池时要用。好消息是,C++11之后有标准方法,但咱们讲C语言,还是得用平台API。

平台 API / 方法 头文件
Windows GetSystemInfo()dwNumberOfProcessors <windows.h>
Linux sysconf(_SC_NPROCESSORS_ONLN) <unistd.h>
macOS sysctl()HW_NCPU <sys/sysctl.h>
#include <stdio.h>

#ifdef _WIN32
#include <windows.h>
#elif defined(__linux__)
#include <unistd.h>
#elif defined(__APPLE__)
#include <sys/sysctl.h>
#endif

int get_cpu_cores() {
#ifdef _WIN32
    SYSTEM_INFO sysinfo;
    GetSystemInfo(&sysinfo);
    return sysinfo.dwNumberOfProcessors;
#elif defined(__linux__)
    return (int)sysconf(_SC_NPROCESSORS_ONLN);
#elif defined(__APPLE__)
    int count;
    size_t size = sizeof(count);
    if (sysctlbyname("hw.ncpu", &count, &size, NULL, 0) == 0)
        return count;
    return 1;
#else
    return 1; // 未知平台,保守返回1
#endif
}

注意:_SC_NPROCESSORS_ONLN 返回的是当前在线的核心数,不是物理核心数。如果你需要物理核心数(比如做CPU亲和性绑定),Linux下得解析 /proc/cpuinfo 中的 core id 字段。

15.3.2 获取CPU架构名称

这个更多是编译时判断,但运行时也有用——比如你要动态加载不同架构的优化库。

const char* get_cpu_arch() {
#if defined(__x86_64__) || defined(_M_X64)
    return "x86_64";
#elif defined(__i386__) || defined(_M_IX86)
    return "x86";
#elif defined(__aarch64__) || defined(_M_ARM64)
    return "ARM64";
#elif defined(__arm__) || defined(_M_ARM)
    return "ARM";
#elif defined(__riscv)
    return "RISC-V";
#else
    return "Unknown";
#endif
}

15.4 内存信息获取

内存信息包括总物理内存、可用内存、已用内存。这个在实现内存池、缓存淘汰策略时特别重要。

15.4.1 Windows 内存信息

#ifdef _WIN32
#include <windows.h>

void get_windows_memory() {
    MEMORYSTATUSEX statex;
    statex.dwLength = sizeof(statex);
    GlobalMemoryStatusEx(&statex);
    
    printf("Total RAM: %llu MB\n", statex.ullTotalPhys / (1024 * 1024));
    printf("Available RAM: %llu MB\n", statex.ullAvailPhys / (1024 * 1024));
    printf("Usage: %.1f%%\n", 
           100.0 * (1.0 - (double)statex.ullAvailPhys / statex.ullTotalPhys));
}
#endif

15.4.2 Linux 内存信息

Linux下内存信息在 /proc/meminfo 里。别用 sysinfo(),那个结构体里的字段单位在不同内核版本上不一致,我踩过这个坑。

#ifdef __linux__
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void get_linux_memory() {
    FILE *fp = fopen("/proc/meminfo", "r");
    if (!fp) return;
    
    long total = 0, available = 0;
    char line[128];
    
    while (fgets(line, sizeof(line), fp)) {
        if (sscanf(line, "MemTotal: %ld kB", &total) == 1) continue;
        if (sscanf(line, "MemAvailable: %ld kB", &available) == 1) break;
    }
    fclose(fp);
    
    printf("Total RAM: %ld MB\n", total / 1024);
    printf("Available RAM: %ld MB\n", available / 1024);
}
#endif

避坑指南:我曾经在解析 /proc/meminfo 时,直接用 fscanf 按顺序读字段。结果某个内核版本新增了一行,顺序全乱了。后来我改成逐行匹配字段名,再也没出过问题。

15.5 跨平台封装:统一接口设计

说了这么多平台差异,最终我们要封装成一个统一的接口。我个人习惯用结构体+函数指针的方式,这样调用方完全不用关心底层实现。

typedef struct {
    char os_name[64];
    char os_version[64];
    char cpu_arch[16];
    int  cpu_cores;
    long long total_memory;   // 字节
    long long available_memory; // 字节
} SystemInfo;

int get_system_info(SystemInfo *info); // 跨平台实现

这个 get_system_info 函数内部根据平台宏调用不同的实现。调用方只需要包含头文件,链接对应的平台源文件即可。

15.6 知识体系总览

下面这张图总结了系统信息获取的跨平台架构。你可以看到,应用层通过统一接口调用,底层按平台分流,各自实现细节被完全隔离。

系统信息获取跨平台架构 应用层:get_system_info() 统一接口层(平台宏分流) Windows 实现 Linux 实现 macOS 实现 Windows API RtlGetVersion GetSystemInfo GlobalMemoryStatusEx 注册表查询 Linux 文件/系统调用 /etc/os-release /proc/cpuinfo /proc/meminfo sysconf() macOS 系统调用 sysctl() / sysctlbyname() host_statistics() NSProcessInfo sw_vers 命令 统一接口 + 平台分流 = 一次编写,到处运行

15.7 实战建议

最后说几点我在项目中积累的经验:

  • 缓存结果:系统信息在程序运行期间基本不变。我习惯在初始化时一次性获取,存到全局结构体里,避免反复调用系统API。
  • 降级策略:如果某个API调用失败(比如 /proc/meminfo 被容器限制),要有合理的默认值。我一般用编译时宏定义的保守值作为兜底。
  • 日志输出:程序启动时打印系统信息到日志,方便后期排查问题。这个习惯帮我省了不少调试时间。
  • 单元测试:写一个测试程序,在不同平台上运行,验证返回值是否合理。比如内存不能为0,CPU核心数不能超过256等。

核心思想:跨平台不是把代码写死成多份,而是设计一个优雅的抽象层,让平台差异被隔离在底层。上层代码只关心“我要什么”,不关心“从哪里拿”。

嗯,系统信息获取就讲到这里。这部分代码量不大,但设计思路很重要。你写跨平台程序时,多想想“如果换一个操作系统,这段代码还能跑吗?”——有这个意识,你就已经超过大多数开发者了。


公众号:蓝海资料掘金营,微信deep3321