系统环境变量与内存

直接访问环境变量

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

环境变量指针数组

环境变量指针数组

有两种方式可以直接操作这个指针数组。

1. envp

第一种是通过 main 函数的第三个参数,函数原型中通常命名为 envp

int main(int argc, char *argv[], char **envp);

envp是一个二级指针,它指向一个指针数组的起始位置,通过这个起始位置,可以依次读取出全部环境变量字符串。如下图所示:

envp

envp

遍历读它也很容易,

int main(int argc, char *argv[], char **envp) {
    char **ptr = envp;
    while(*ptr) {
    	printf("%s\n", *ptr);
        ptr++;
    }
}
1

1

但这种方式只在 main 方法里能用,而且它还有一个缺陷(后文会谈),所以更推荐的其实是 environ

2. environ

extern char **environ;

environ是全局变量,在内存中的位置首先就跟 envp 不一样;envp 通过栈传输(或者 X86-64 架构下可以通过寄存器),而 environ 存储在数据区(具体来说是 bss section),他们在内存中示例如下:

envp 和 environ

envp 和 environ

上图呈现的是初始情况,此时 environenvp 都指向栈内保存的字符串指针数组(即环境变量)。要通过 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 指针来获取并操作环境变量的;而且经过某些修改之后,通过 envpenviron 访问到的环境变量内容就可能不一样了,此时要以 environ 为准。接下来以 setenv 为例进行说明。

setenv 及其操作

调用 setenv 需要传入 namevalue 以及 overwrite,可以改变或者增加环境变量。如果现有的环境变量中没有 name,则新增一个环境变量 ${name}=${value}(JS 语法,你懂就好);如果已经存在了且 overwrite 为 1,则将指针数组中相应的指针替换为字符串 ${name}=${value}的指针。

替换现有变量

先说替换的情况,setenv 会分配一块内存存储拼接的字符串 ${name}=${value},将新内存地址替换掉旧的。

setenv 1

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 特性),也可能不一样;如果不一样,envpenviron 就不同步了。下图表现了这样的过程。

setenv 2

setenv 2

  • 一开始,environenvp 指向同一个内存地址,通过他们访问到的字符串数组是一样的;
  • 新增了一个环境变量 "FOO=bar",原来的数组没有多余的空间存储新增的字符串指针,于是需要重新申请一块空间,整个数组整体搬迁到新空间中,并将新增字符串指针添加到末尾的 NULL 之前。新地址也会存储到 environ 变量;而 envp 变量是不修改的。此时如果继续通过 envp 访问,得到的结果不是最新的,这就是 envp 的问题。
  • 原来环境变量中的字符串内容本身(如图中的 "SHELL=/bin/bash" )并不需要拷贝,只是将一系列指针移动到另一块地方。

setenv 与 putenv

setenvputenv 的操作很类似,当我们需要设置一个环境变量 NAME=VALUE 时,NAMEVALUE 字符串是 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

putenv

总结

  • 如果环境变量没有修改过,通过 environenvp 都可以读到环境变量字符串;
  • 系统内的函数库如 setenvputenv 都是通过 environ 来操作环境变量,一旦修改之后,envpenviron 就可能不同步了。此时通过 envp 读到的就不一定是最新的。
  • 如果通过 putenv 设置环境变量,字符串参数不能是自动存储期变量。
Built with Hugo
Theme Stack designed by Jimmy