直接访问环境变量
环境变量,其实就是一个字符串指针数组,指向的每个字符串都是形如 NAME=VALUE
这样的形式。他们在内存中是这样子存储,数组最后一项必须确保是 NULL
,即必须以一个空指针结尾。

环境变量指针数组
有两种方式可以直接操作这个指针数组。
1. envp
第一种是通过 main
函数的第三个参数,函数原型中通常命名为 envp
。
int main(int argc, char *argv[], char **envp);
envp
是一个二级指针,它指向一个指针数组的起始位置,通过这个起始位置,可以依次读取出全部环境变量字符串。如下图所示:

envp
遍历读它也很容易,
int main(int argc, char *argv[], char **envp) {
char **ptr = envp;
while(*ptr) {
printf("%s\n", *ptr);
ptr++;
}
}

1
但这种方式只在 main
方法里能用,而且它还有一个缺陷(后文会谈),所以更推荐的其实是 environ
。
2. environ
extern char **environ;
environ
是全局变量,在内存中的位置首先就跟 envp
不一样;envp
通过栈传输(或者 X86-64 架构下可以通过寄存器),而 environ
存储在数据区(具体来说是 bss section),他们在内存中示例如下:

envp 和 environ
上图呈现的是初始情况,此时 environ
和 envp
都指向栈内保存的字符串指针数组(即环境变量)。要通过 environ
读取环境变量字符串,方式跟 envp
一样;通过 environ
更方便的是在任何一个函数内都能访问到这个全局变量。
void show_env() {
char **ptr = environ;
while(*ptr) {
printf("%s\n", *ptr);
ptr++;
}
}
环境变量相关函数库
但是,并不推荐直接操作环境变量,而是通过函数库提供的如下几个函数来操作:
#include <stdlib.h>
// 设置
int setenv(const char *name, const char *value, int overwrite);
int putenv(const char *combined);
// 删除
int unsetenv(const char *name);
// 查询
char *getenv(const char *name);
接下来就到关键的地方了:上述函数是通过 environ
指针来获取并操作环境变量的;而且经过某些修改之后,通过 envp
和 environ
访问到的环境变量内容就可能不一样了,此时要以 environ
为准。接下来以 setenv
为例进行说明。
setenv 及其操作
调用 setenv
需要传入 name
、value
以及 overwrite
,可以改变或者增加环境变量。如果现有的环境变量中没有 name,则新增一个环境变量 ${name}=${value}
(JS 语法,你懂就好);如果已经存在了且 overwrite
为 1,则将指针数组中相应的指针替换为字符串 ${name}=${value}
的指针。
替换现有变量
先说替换的情况,setenv
会分配一块内存存储拼接的字符串 ${name}=${value}
,将新内存地址替换掉旧的。

setenv 1
新增环境变量
首先也是需要分配内存来存储拼接的字符串 ${name}=${value}
,然后将字符串的地址添加到指针数组后;但问题是,指针数组后不一定有空间,而且还需要保证数组必须以 NULL
结尾。这时就需要找一块更大的连续空间存储更大的指针数组,通常使用 realloc
实现。
需要申请多大的空间,不同的实现有不同的策略;FreeBSD 和 NetBSD 的策略是空间不够时就翻倍,截取相关的实现代码如下:
// NetBSD implementation
// https://github.com/NetBSD/src/blob/trunk/lib/libc/stdlib/_env.c#L295
new_size = ENV_ARRAY_SIZE_MIN;
while (new_size <= required_size)
new_size <<= 1;
// ...
new_environ = environ;
errno = reallocarr(&new_environ,
new_size, sizeof(*new_environ));
// FreeBSD implementation
// https://cgit.freebsd.org/src/tree/lib/libc/stdlib/getenv.c#n310
if (envVarsTotal > envVarsSize) {
newEnvVarsSize = envVarsTotal * 2;
tmpEnvVars = reallocarray(envVars, newEnvVarsSize,
sizeof(*envVars));
// ...
}
而 glibc(Linux)和 MacOS 的扩张策略则是 +1,新申请的内容刚好放下已有的指针数组和新增的字符串指针。个人感觉这是个比较低效的策略,因为这意味着每次新增环境变量都需要 realloc
一次,并不知道为何要这么做。截取相关实现如下:
// MacOS implementation
// https://opensource.apple.com/source/Libc/Libc-498/stdlib/setenv-fbsd.c.auto.html#:~:text=cnt
p = (char **)realloc((char *)*environp,
(size_t)(sizeof(char *) * (cnt + 2)));
if (!p)
return (-1);
*environp = p;
// glibc implementation
// https://sourceware.org/git/?p=glibc.git;a=blob;f=stdlib/setenv.c;h=2176cbac319478b68b49834500954e637ecf6df0;hb=HEAD#l157
new_environ = (char **) realloc (last_environ, (size + 2) * sizeof (char *));
glibc 和 MacOS 的实现代码中,新的空间大小是原来的大小 + 2;新增的两个内存位置,一个是放置新增的字符串指针,一个是存储作为结尾的
NULL
指针。
重新申请的整块内存空间地址会存储到 environ
变量中,这个地址可能跟原来一样(realloc 特性),也可能不一样;如果不一样,envp
和 environ
就不同步了。下图表现了这样的过程。

setenv 2
- 一开始,
environ
和envp
指向同一个内存地址,通过他们访问到的字符串数组是一样的; - 新增了一个环境变量
"FOO=bar"
,原来的数组没有多余的空间存储新增的字符串指针,于是需要重新申请一块空间,整个数组整体搬迁到新空间中,并将新增字符串指针添加到末尾的NULL
之前。新地址也会存储到environ
变量;而envp
变量是不修改的。此时如果继续通过envp
访问,得到的结果不是最新的,这就是envp
的问题。 - 原来环境变量中的字符串内容本身(如图中的
"SHELL=/bin/bash"
)并不需要拷贝,只是将一系列指针移动到另一块地方。
setenv 与 putenv
setenv
与 putenv
的操作很类似,当我们需要设置一个环境变量 NAME=VALUE
时,NAME
和 VALUE
字符串是 setenv
的前两个参数,如果用 putenv
需要将 ${NAME}=${VALUE}
字符串整体作为参数。
setenv("FOO1", "bar1", 1);
putenv("FOO2=bar2");
另一个重要的区别是:setenv
内部会拼接${NAME}=${VALUE}
,并将拼接的地址存储的指针数组中,而 putenv
直接将参数指针存储到指针数组中。这带来的一个限制是,如果用 putenv
,参数不能是自动存储变量,如下图中:
void bad_putenv() {
char env[] = "FOO3=bar3";
putenv(env);
}
上面的例子中,env
是栈内的数组;putenv
执行后,保存到指针列表的是一个当前函数栈内的地址,当函数退出后,再试图访问该地址,行为是不可预料的。
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
void show_last_env() {
char **ptr = environ;
while(*ptr) {
if (*(ptr + 1) == NULL) {
printf("\t> last env is: %s\n", *ptr);
break;
}
ptr++;
}
}
void do_putenv() {
char env[] = "FOO=bar";
putenv(env);
printf("inside do_putenv after putenv\n");
show_last_env();
}
int main() {
printf("inside main\n");
show_last_env();
do_putenv();
printf("do_putenv exited\n");
show_last_env();
return 0;
}

putenv
总结
- 如果环境变量没有修改过,通过
environ
和envp
都可以读到环境变量字符串; - 系统内的函数库如
setenv
、putenv
都是通过environ
来操作环境变量,一旦修改之后,envp
和environ
就可能不同步了。此时通过envp
读到的就不一定是最新的。 - 如果通过
putenv
设置环境变量,字符串参数不能是自动存储期变量。