std::os
std::os 是 Rust 标准库中与操作系统相关的入口, 它本身没有太多直接可用的 API, 真正的功能都在平台相关的子模块里。跨平台写代码时, 需要自己处理系统差异, 这正是本章要讲的内容
介绍
Rust 的 std::os 按平台分为两大阵营, 需要用到平台专属功能时才需要 use 引入:
| 子模块 | 适用平台 | 典型功能 |
|---|---|---|
| std::os::unix | Linux / macOS / BSD 等 | 文件权限、进程信号、socket 等 |
| std::os::windows | Windows | 进程、控制台句柄、注册表相关操作 |
| std::os::raw | 所有平台 | 与 C 语言对应的原始类型 (c_char 等) |
注意, 平台专属 API 在别的平台上不存在, 直接使用会编译失败, 必须配合 #[cfg] 条件编译才能写出跨平台的代码。后面两节就是讲怎么判断系统和条件编译
判断当前操作系统
在运行时判断当前系统最常用的方式是 cfg! 宏, 它返回 bool, 两个分支都会参与 编译, 只是运行时选择走哪条分支。另一种方式是直接读取 std::env::consts::OS 常量, 我们在文末还会详细讲
| 写法 | 作用 | 注意点 |
|---|---|---|
| cfg! | 运行时判断编译目标平台 | 所有分支都会被编译, 只是运行时选择 |
| std::env::consts::OS | 直接拿到操作系统名字符串 | 编译期常量, 不能用于动态判断, 但更简洁 |
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 表示 "满足其一即可" |
// 同一个函数名写三份, 编译时只保留当前平台的那一份
#[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] 也可以用来修饰整个模块, 适合把平台相关的代码集中到独立的文件或模块里:
// 平台相关模块: 只在对应系统上编译
#[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 负责与 "运行环境" 打交道: 环境变量、命令行参数、当前工作目录、 临时目录等。写命令行工具、读取配置、拼接路径时几乎都会用到它
读取环境变量
读取环境变量最常用的是 var 和 vars, 它们返回的都是 Result 或迭代器, 因为变量可能不存在、也可能是非法 UTF-8
| 函数 | 作用 | 注意点 |
|---|---|---|
| var | 读取单个环境变量 | 变量不存在或内容不是合法 UTF-8 时返回 Err |
| var_os | 读取单个环境变量 | 返回 Option<OsString>, 不要求是合法 UTF-8 |
| vars | 遍历所有环境变量 | 遍历到非法 UTF-8 的值时会 panic |
| vars_os | 遍历所有环境变量 | 返回 OsString, 更安全, 但处理起来稍麻烦 |
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_var 和 remove_var 可以修改当前进程的环境变量, 设置后本进程内所有 env::var 都能读到
| 函数 | 作用 | 注意点 |
|---|---|---|
| set_var | 设置环境变量 | 只影响当前进程; 多线程下不安全; Rust 2024 edition 中是 unsafe |
| remove_var | 删除环境变量 | 同上 |
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 已被删除"), // 走到这里
}
}注意点
set_var/remove_var只对当前进程生效, 不会影响系统中的其他程序, 进程退出后修改也就消失了- 多线程程序里修改环境变量不是线程安全的 (读写可能不同步), 一般只建议在 程序启动阶段、单线程时使用
- 从 Rust 2024 edition 开始,
set_var和remove_var被标记为unsafe, 用--edition 2021编译本示例无需unsafe块, 但新版项目里需要
命令行参数
args 返回一个迭代器, 第 0 个元素是程序本身的路径, 后面依次是传入的参数。 经典场景: 写一个模拟命令行工具, 根据参数个数和内容给出不同行为
| 函数 | 作用 | 注意点 |
|---|---|---|
| args | 获取命令行参数迭代器 | 参数不是合法 UTF-8 时会 panic |
| args_os | 获取命令行参数迭代器 | 返回 OsString, 不要求 UTF-8, 更安全 |
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, 某些平台可能失败 |
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
// 封装跨平台的 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:
// 封装跨平台的 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 | 直接终止进程 (信号中止) | 更粗暴, 一般不用, 适合严重错误 |
// 用一个带析构函数的结构体, 演示 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 | 每次运行值不同 |
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 |
| ARCH | CPU 架构 | 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 |
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
}经典场景: 程序要加载一个动态库文件时, 需要按平台拼出正确的文件名
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 则一行搞定:
// 手动实现: 自己处理不同平台的差异
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!("找不到家目录"),
}
}// Cargo.toml 添加依赖:
// [dependencies]
// dirs = "6"
fn main() {
// dirs 自动处理了 Windows/Linux/macOS 的差异
match dirs::home_dir() {
Some(home) => println!("家目录: {}", home.display()),
None => println!("找不到家目录"),
}
}再比如, 想拿到系统名称和版本, 用 os_info 比自己去查每个平台方便得多:
// Cargo.toml 添加依赖:
// [dependencies]
// os_info = "3"
fn main() {
// 获取操作系统信息
let info = os_info::get();
println!("系统类型: {}", info.os_type());
println!("系统版本: {}", info.version());
}