setenv 后 /proc/self/environ 为何不变:环境变量存哪、会不会 realloc
十月 09, 2026 [Linux 与系统] #Linux #Rust #memory #proc现象
Rust 里 std::env::set_var 之后,getenv 能看到,但 /proc/self/environ 里没有:
unsafe { std::env::set_var("MY_TEST_FOO", "hello-from-rust-set_var") };
println!("{:?}", std::env::var("MY_TEST_FOO")); // Ok("hello-from-rust-set_var")
// /proc/self/environ 里搜 MY_TEST_FOO -> 无
C 的 setenv 同样。反过来,如果原地改写原始环境内存里的字节(把 PATH= 首字节改成 X),/proc/self/environ 反而会变成 XATH=。
原因:它是内核记录的初始内存区间
execve(2) 时内核把环境字符串放在用户栈上,记下区间 mm->env_start .. mm->env_end。读 /proc/pid/environ 时,内核直接 access_remote_vm() 读这段内存。
所以:
setenv/set_var只在用户态改environ指针数组,从不碰这段区间 → 文件不变;- 有人往这段区间原地写字节 → 文件跟着变;
- 文件只反映
execve那一刻的环境,setenv的改动永远看不见(prctl(PR_SET_MM_ENV_START/END)可移动该区间)。
顺带:
/proc/self里的self是执行open()的那个进程。所以在程序内部读它是自己,在 shell 里cat /proc/self/environ读的却是cat自己的(继承了 shell 的环境),要测目标进程得用它真实的 pid 或在其内部读。
环境变量存在哪、会不会 realloc
C 层就是全局 char **environ:一个 NULL 结尾的指针数组,每个元素指向一个 KEY=VALUE\0。
- 初始:指针数组和字符串都在内核建好的栈上;
- 第一次
setenv:指针数组被拷到堆,environ改指堆;没动过的字符串仍留在栈上; - 新变量:每次对
KEY=VALUE做一次malloc,放堆上。
指针数组的增长看 glibc 2.41 的 stdlib/setenv.c:
if (__environ_array_list == NULL
|| __environ_array_list->allocated * 2 < required_size)
new_size = required_size + 16; // 缺口大:按需 +16
else
new_size = __environ_array_list->allocated * 2; // 否则翻倍
char **new_array = calloc (new_size, sizeof (*new_array)); // 不是 realloc
结论:
- 会重新分配,但现代 glibc 是
calloc新数组 + 拷贝再改指向(老版本才用realloc);增长是近似翻倍,不是每加一个搬一次。 - 旧数组不释放,挂在
__environ_array_list上直到进程退出(应用可能还持有旧environ)。 - glibc 用一棵
tsearch树缓存见过的KEY=VALUE:反复设为相同值会复用旧串、释放临时串,不会无限涨;反复设不同值则旧串有意保留、内存持续增长。
实测(env -i PATH=... TERM=xterm 启动):第一次 setenv 时数组地址从栈 0x7fff... 跳到堆 0x558b...,加到第 17 个变量时再次翻倍搬移,与源码里的翻倍公式吻合。
Rust 侧只是转发
Rust std(sys/pal/unix/os.rs)直接调 libc,自己只加了一把锁:
pub unsafe fn setenv(k: &OsStr, v: &OsStr) -> io::Result<()> {
let _guard = ENV_LOCK.write();
cvt(libc::setenv(k.as_ptr(), v.as_ptr(), 1)).map(drop) // overwrite = 1
}
所以上面关于栈/堆、realloc、/proc 的结论对 Rust 完全适用。
实践
- 想把环境传给子进程,用
Command::env;set_var只影响本进程的getenv和之后exec的继承。 std::env::set_var在较新 Rust 中是unsafe(跨线程调用有数据竞争),且非 async-signal-safe。- 运行中大量设置不同的环境变量,字符串会持续
malloc且旧串不回收,内存会涨。 - 想刷新
/proc/self/environ,只能重新execve。