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() 读这段内存。

所以:

顺带:/proc/self 里的 self 是执行 open() 的那个进程。所以在程序内部读它是自己,在 shell 里 cat /proc/self/environ 读的却是 cat 自己的(继承了 shell 的环境),要测目标进程得用它真实的 pid 或在其内部读。

环境变量存在哪、会不会 realloc

C 层就是全局 char **environ:一个 NULL 结尾的指针数组,每个元素指向一个 KEY=VALUE\0。

指针数组的增长看 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

结论:

  1. 会重新分配,但现代 glibc 是 calloc 新数组 + 拷贝再改指向(老版本才用 realloc);增长是近似翻倍,不是每加一个搬一次。
  2. 旧数组不释放,挂在 __environ_array_list 上直到进程退出(应用可能还持有旧 environ)。
  3. 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 完全适用。

实践