Skip to content

std::os

std::os 是 Rust 标准库中与操作系统相关的入口, 它本身没有太多直接可用的 API, 真正的功能都在平台相关的子模块里。跨平台写代码时, 需要自己处理系统差异, 这正是本章要讲的内容

介绍

Rust 的 std::os 按平台分为两大阵营, 需要用到平台专属功能时才需要 use 引入:

子模块适用平台典型功能
std::os::unixLinux / macOS / BSD 等文件权限、进程信号、socket 等
std::os::windowsWindows进程、控制台句柄、注册表相关操作
std::os::raw所有平台与 C 语言对应的原始类型 (c_char 等)

注意, 平台专属 API 在别的平台上不存在, 直接使用会编译失败, 必须配合 #[cfg] 条件编译才能写出跨平台的代码。后面两节就是讲怎么判断系统和条件编译

判断当前操作系统

在运行时判断当前系统最常用的方式是 cfg! 宏, 它返回 bool, 两个分支都会参与 编译, 只是运行时选择走哪条分支。另一种方式是直接读取 std::env::consts::OS 常量, 我们在文末还会详细讲

写法作用注意点
cfg!运行时判断编译目标平台所有分支都会被编译, 只是运行时选择
std::env::consts::OS直接拿到操作系统名字符串编译期常量, 不能用于动态判断, 但更简洁
rust
fn main() {
    // cfg! 在运行时判断当前操作系统, 所有分支都会被编译
    if cfg!(target_os = "windows") {
        println!("当前系统: Windows");
    } else if cfg!(target_os = "linux") {
        println!("当前系统: Linux");
    } else if cfg!(target_os = "macos") {
        println!("当前系统: macOS");
    } else {
        println!("当前系统: 其他系统");
    }

    // 经典场景: 根据系统选择路径分隔符
    // Windows 习惯用反斜杠 \, Linux/macOS 习惯用正斜杠 /
    let sep = if cfg!(target_os = "windows") {
        '\\'
    } else {
        '/'
    };
    println!("路径分隔符: {sep}"); // macOS 上输出 /
}

注意点

cfg! 判断的是编译目标平台, 不是运行平台。交叉编译时, 它按照编译目标来 选择分支, 这通常是想要的 (程序运行在哪个平台, 就用哪个平台的编译目标)

条件编译属性

#[cfg] 属性是编译期的: 不满足条件的代码在编译时直接被丢弃, 不会出现在 最终的程序里。同一个函数名可以写多个不同平台的版本, 编译器只保留匹配的那个

属性作用注意点
#[cfg(target_os = "...")]只有目标系统匹配时才编译不匹配的代码直接消失, 不会报错
#[cfg(target_family = "...")]按系统家族分组 (unix / windows)比 target_os 更粗粒度
#[cfg(not(...))] / #[cfg(any(...))]逻辑组合not 取反, any 表示 "满足其一即可"
rust
// 同一个函数名写三份, 编译时只保留当前平台的那一份
#[cfg(target_os = "windows")]
fn os_name() -> &'static str {
    "Windows"
}

#[cfg(target_os = "linux")]
fn os_name() -> &'static str {
    "Linux"
}

#[cfg(target_os = "macos")]
fn os_name() -> &'static str {
    "macOS"
}

fn main() {
    // 调用哪个实现, 在编译时就已经确定, 没有任何运行时开销
    println!("当前系统: {}", os_name()); // macOS 上输出 macOS
}

#[cfg] 也可以用来修饰整个模块, 适合把平台相关的代码集中到独立的文件或模块里:

rust
// 平台相关模块: 只在对应系统上编译
#[cfg(target_os = "windows")]
mod path_util {
    // Windows 路径分隔符是反斜杠
    pub fn join(a: &str, b: &str) -> String {
        format!("{a}\\{b}")
    }
}

// 非 Windows 系统 (包括 Linux/macOS) 统一使用正斜杠
#[cfg(not(target_os = "windows"))]
mod path_util {
    pub fn join(a: &str, b: &str) -> String {
        format!("{a}/{b}")
    }
}

fn main() {
    // 在 macOS/Linux 上运行, 输出 usr/bin
    // 在 Windows 上运行, 输出 usr\bin
    println!("{}", path_util::join("usr", "bin"));
}

std::env

std::env 负责与 "运行环境" 打交道: 环境变量、命令行参数、当前工作目录、 临时目录等。写命令行工具、读取配置、拼接路径时几乎都会用到它

读取环境变量

读取环境变量最常用的是 varvars, 它们返回的都是 Result 或迭代器, 因为变量可能不存在、也可能是非法 UTF-8

函数作用注意点
var读取单个环境变量变量不存在或内容不是合法 UTF-8 时返回 Err
var_os读取单个环境变量返回 Option<OsString>, 不要求是合法 UTF-8
vars遍历所有环境变量遍历到非法 UTF-8 的值时会 panic
vars_os遍历所有环境变量返回 OsString, 更安全, 但处理起来稍麻烦
rust
fn main() {
    // 1. env::var: 读取单个环境变量, 返回 Result<String, VarError>
    match std::env::var("HOME") {
        Ok(path) => println!("HOME = {path}"),
        // Windows 上没有 HOME, 用的是 USERPROFILE
        Err(_) => println!("当前系统没有 HOME 变量 (Windows 请用 USERPROFILE)"),
    }

    // 2. env::var_os: 返回 Option<OsString>, 值不要求是合法 UTF-8
    // 系统用户名在 Windows 上是 USERNAME, Linux/macOS 上是 USER
    let key = if cfg!(target_os = "windows") { "USERNAME" } else { "USER" };
    if let Some(name) = std::env::var_os(key) {
        // OsString 可能不是合法 UTF-8, 用 to_string_lossy 安全转换
        println!("用户名: {}", name.to_string_lossy());
    } else {
        println!("没有找到 {key} 变量");
    }

    // 3. env::vars: 遍历当前进程的所有环境变量
    // 变量通常很多, 这里只打印前 3 个
    for (i, (k, v)) in std::env::vars().enumerate() {
        println!("{k} = {v}");
        if i >= 2 {
            break;
        }
    }
}

经典场景: 很多程序把配置放在环境变量里, 比如读取 DATABASE_URL 来决定连哪个 数据库, 读取 RUST_LOG 来控制日志级别

设置与删除环境变量

set_varremove_var 可以修改当前进程的环境变量, 设置后本进程内所有 env::var 都能读到

函数作用注意点
set_var设置环境变量只影响当前进程; 多线程下不安全; Rust 2024 edition 中是 unsafe
remove_var删除环境变量同上
rust
fn main() {
    // 1. 设置环境变量: 只影响当前进程 (以及之后启动的子进程)
    std::env::set_var("MY_APP_MODE", "debug");
    std::env::set_var("MY_APP_PORT", "8080");

    // 2. 设置完马上读取, 自包含验证 (不需要其他进程配合)
    println!("MY_APP_MODE = {}", std::env::var("MY_APP_MODE").unwrap());
    println!("MY_APP_PORT = {}", std::env::var("MY_APP_PORT").unwrap());

    // 3. 删除环境变量
    std::env::remove_var("MY_APP_PORT");

    // 4. 删除后再读取, 会得到 Err
    match std::env::var("MY_APP_PORT") {
        Ok(v) => println!("MY_APP_PORT = {v}"),
        Err(_) => println!("MY_APP_PORT 已被删除"), // 走到这里
    }
}

注意点

  1. set_var / remove_var 只对当前进程生效, 不会影响系统中的其他程序, 进程退出后修改也就消失了
  2. 多线程程序里修改环境变量不是线程安全的 (读写可能不同步), 一般只建议在 程序启动阶段、单线程时使用
  3. 从 Rust 2024 edition 开始, set_varremove_var 被标记为 unsafe, 用 --edition 2021 编译本示例无需 unsafe 块, 但新版项目里需要

命令行参数

args 返回一个迭代器, 第 0 个元素是程序本身的路径, 后面依次是传入的参数。 经典场景: 写一个模拟命令行工具, 根据参数个数和内容给出不同行为

函数作用注意点
args获取命令行参数迭代器参数不是合法 UTF-8 时会 panic
args_os获取命令行参数迭代器返回 OsString, 不要求 UTF-8, 更安全
rust
fn main() {
    // args() 返回一个迭代器, 第 0 个元素是程序本身的路径
    let args: Vec<String> = std::env::args().collect();
    println!("程序路径: {}", args[0]);

    // 经典场景: 模拟命令行工具
    // 编译后这样运行: ./main 张三 18
    if args.len() < 3 {
        // 参数不够时打印用法提示
        println!("用法: {} <名字> <年龄>", args[0]);
        return;
    }

    let name = &args[1];
    let age = &args[2];
    println!("你好 {name}, 你 {age} 岁!");
}

其他常用

std::env 还提供几个和目录、文件路径相关的函数, 写工具时很常用

函数作用注意点
current_dir当前工作目录返回 Result, 目录被删除等情况会失败
temp_dir系统临时目录各平台位置不同, 见下方代码注释
current_exe当前可执行文件的完整路径返回 Result, 某些平台可能失败
rust
fn main() {
    // 1. 当前工作目录 (运行程序时所在的目录)
    match std::env::current_dir() {
        Ok(dir) => println!("当前工作目录: {}", dir.display()),
        Err(e) => println!("获取失败: {e}"),
    }

    // 2. 系统临时目录
    // Windows: C:\Users\xxx\AppData\Local\Temp
    // Linux/macOS: /tmp
    let tmp = std::env::temp_dir();
    println!("临时目录: {}", tmp.display());

    // 3. 当前可执行文件的完整路径
    match std::env::current_exe() {
        Ok(exe) => println!("可执行文件: {}", exe.display()),
        Err(e) => println!("获取失败: {e}"),
    }
}

std::process

std::process 负责进程相关的操作: 启动外部命令、退出当前进程、获取进程 ID 等。 Rust 用它来调用系统中的其他程序, 类似其他语言里的 system() 调用, 但更安全、 更好用

执行外部命令

Command 可以启动一个外部程序, 并选择是否捕获它的输出。基本流程是: Command::new(程序名).arg(参数).output()/status()/spawn()

方法作用注意点
new创建一个命令传入的是可执行文件的路径或名字
arg / args添加一个/多个参数args 接收一个数组, 一次传多个
output运行并捕获输出会阻塞等待子进程结束, 输出存在内存里
status运行, 只关心退出状态子进程的输出直接继承终端
spawn启动子进程后不等待需要配合 wait 使用, 适合后台任务
wait等待子进程结束返回 ExitStatus, 里面有退出码

注意点

echo 在 Windows 上是 cmd.exe 的内置命令, 不是独立的可执行文件, 直接 Command::new("echo") 会启动失败。所以下面的示例封装了一个跨平台的 echo_command(): Windows 走 cmd /C echo, 其他平台直接调用 echo

rust
// 封装跨平台的 echo 命令
fn echo_command() -> std::process::Command {
    #[cfg(target_os = "windows")]
    {
        let mut cmd = std::process::Command::new("cmd");
        cmd.args(["/C", "echo"]);
        cmd
    }

    #[cfg(not(target_os = "windows"))]
    {
        std::process::Command::new("echo")
    }
}

fn main() {
    // 1. output: 运行命令并捕获全部输出
    let output = echo_command()
        .arg("你好, 来自 Rust 的子进程!")
        .output()
        .expect("执行命令失败");

    // stdout 是 Vec<u8>, 用 from_utf8_lossy 转成字符串
    let stdout = String::from_utf8_lossy(&output.stdout);
    println!("stdout: {stdout}"); // stdout: 你好, 来自 Rust 的子进程!

    // 2. status: 只关心退出码, 子进程输出直接打到终端
    let status = echo_command()
        .arg("这条消息直接显示在终端上")
        .status()
        .expect("执行命令失败");
    println!("退出码: {}", status.code().unwrap_or(-1)); // 退出码: 0
}

如果想让子进程在后台运行, 同时父进程继续干别的事, 用 spawn + wait:

rust
// 封装跨平台的 echo 命令
fn echo_command() -> std::process::Command {
    #[cfg(target_os = "windows")]
    {
        let mut cmd = std::process::Command::new("cmd");
        cmd.args(["/C", "echo"]);
        cmd
    }

    #[cfg(not(target_os = "windows"))]
    {
        std::process::Command::new("echo")
    }
}

fn main() {
    // spawn: 启动子进程后不等待, 父进程可以继续干别的
    let mut child = echo_command()
        .arg("子进程输出")
        .spawn()
        .expect("启动子进程失败");

    // 父进程继续执行自己的代码
    println!("子进程已启动, PID: {:?}", child.id());

    // 需要子进程结果时, 再调用 wait 等待它结束
    let status = child.wait().expect("等待子进程失败");
    println!("子进程退出码: {}", status.code().unwrap_or(-1));
}

退出进程

std::process::exit 会立即终止当前进程并设置退出码。注意它的行为和我们平时 return 不一样, 后面会用一个例子演示

函数作用注意点
exit立即终止进程并设置退出码不执行析构函数, 不刷新缓冲区
abort直接终止进程 (信号中止)更粗暴, 一般不用, 适合严重错误
rust
// 用一个带析构函数的结构体, 演示 exit 不会执行析构
struct Guard {
    name: String,
}

impl Drop for Guard {
    fn drop(&mut self) {
        println!("析构函数执行: {}", self.name);
    }
}

fn main() {
    let _g = Guard {
        name: String::from("这个 Guard 不会被释放"),
    };

    println!("程序即将退出...");

    // process::exit: 立即终止进程, 退出码 0
    // exit 之后的所有代码都不会执行
    std::process::exit(0);
    // 下面的代码永远不会执行
    // println!("这行不会打印");
}

注意点

process::exit跳过所有析构函数 (包括全局变量的 Drop、文件的 flush 等), 所以上面代码里的 Guard 永远不会执行 drop。如果退出前需要清理资源 (比如 写日志、保存配置), 一定要在调用 exit 之前手动完成

当前进程信息

std::process::id 返回当前进程的 PID, 可以用来做进程相关操作, 比如写日志时 带上 PID 方便排查

函数作用注意点
id获取当前进程的 PID每次运行值不同
rust
fn main() {
    // 获取当前进程的 PID (进程 ID)
    let pid = std::process::id();
    println!("当前进程 PID: {pid}"); // 每次运行都不一样
}

std::env::consts

原骨架最后一个标题 ## std:: 不完整, 这里补全为 ## std::env::consts。 该模块提供一组编译时确定的平台常量, 写跨平台代码时可以直接当字符串用, 比 cfg! 更省事

平台常量

这些常量在编译时就确定了, 直接 use std::env::consts; 或用全路径访问即可

常量作用常见值
OS操作系统名称linux / macos / windows
ARCHCPU 架构x86_64 / aarch64
FAMILY系统家族unix / windows
EXE_SUFFIX可执行文件后缀.exe (Windows) / 空字符串 (其他)
EXE_EXTENSION可执行文件扩展名exe (Windows) / 空字符串 (其他)
DLL_PREFIX动态库前缀lib (Linux/macOS) / 空字符串 (Windows)
DLL_SUFFIX动态库后缀.so (Linux) / .dylib (macOS) / .dll (Windows)
DLL_EXTENSION动态库扩展名so / dylib / dll
rust
fn main() {
    // 这些常量在编译时就已经确定, 直接打印当前系统的信息
    println!("操作系统: {}", std::env::consts::OS);
    // 例如: linux / macos / windows
    println!("CPU 架构: {}", std::env::consts::ARCH);
    // 例如: x86_64 / aarch64
    println!("系统家族: {}", std::env::consts::FAMILY);
    // unix 或 windows
    println!("可执行文件后缀: {}", std::env::consts::EXE_SUFFIX);
    // Windows 是 .exe, 其他平台是空字符串
    println!("动态库前缀: {}", std::env::consts::DLL_PREFIX);
    // Linux/macOS 是 lib, Windows 是空字符串
    println!("动态库后缀: {}", std::env::consts::DLL_SUFFIX);
    // Windows 是 .dll, Linux 是 .so, macOS 是 .dylib
}

经典场景: 程序要加载一个动态库文件时, 需要按平台拼出正确的文件名

rust
fn main() {
    // 拼出平台对应的动态库文件名
    // Linux:   libmylib.so
    // macOS:   libmylib.dylib
    // Windows: mylib.dll
    let lib_name = format!(
        "{}{}{}",
        std::env::consts::DLL_PREFIX,
        "mylib",
        std::env::consts::DLL_SUFFIX
    );
    println!("动态库文件名: {lib_name}");
}

相关开源库

标准库的 std::env 只提供了基础能力, 想更省心地处理 "家目录在哪、当前用户是谁、 系统是什么版本" 这类问题, 可以用下面的库:

  • dirs: 跨平台获取家目录、配置目录、缓存目录等标准目录
  • whoami: 获取当前用户名、主机名、平台等信息
  • os_info: 获取操作系统名称和版本号
  • sysinfo: 获取 CPU、内存、磁盘、进程等系统信息

以 "获取家目录" 为例, 手动实现要自己处理各平台的差异, 用 dirs 则一行搞定:

rust
// 手动实现: 自己处理不同平台的差异
fn get_home() -> Option<std::path::PathBuf> {
    // Windows 用 USERPROFILE, Linux/macOS 用 HOME
    #[cfg(target_os = "windows")]
    let key = "USERPROFILE";

    #[cfg(not(target_os = "windows"))]
    let key = "HOME";

    std::env::var_os(key).map(std::path::PathBuf::from)
}

fn main() {
    match get_home() {
        Some(home) => println!("家目录: {}", home.display()),
        None => println!("找不到家目录"),
    }
}
rust
// Cargo.toml 添加依赖:
// [dependencies]
// dirs = "6"

fn main() {
    // dirs 自动处理了 Windows/Linux/macOS 的差异
    match dirs::home_dir() {
        Some(home) => println!("家目录: {}", home.display()),
        None => println!("找不到家目录"),
    }
}

再比如, 想拿到系统名称和版本, 用 os_info 比自己去查每个平台方便得多:

rust
// Cargo.toml 添加依赖:
// [dependencies]
// os_info = "3"

fn main() {
    // 获取操作系统信息
    let info = os_info::get();
    println!("系统类型: {}", info.os_type());
    println!("系统版本: {}", info.version());
}

Released under the MIT License.