40

我一直在思考环境变量,并有一些问题/意见。

  • putenv(char *string);

    这个电话似乎有致命的缺陷。因为它不复制传递的字符串,所以你不能用本地调用它,并且不能保证堆分配的字符串不会被覆盖或意外删除。此外(虽然我没有测试过),因为环境变量的一种用途是将值传递给孩子的环境,如果孩子调用其中一个exec*()函数,这似乎没用。我错了吗?

  • Linux 手册页表明 glibc 2.0-2.1.1 放弃了上述行为并开始复制字符串,但这导致了 glibc 2.1.2 中修复的内存泄漏。我不清楚这个内存泄漏是什么或如何修复的。

  • setenv()复制字符串,但我不知道它是如何工作的。进程加载时为环境分配空间,但它是固定的。这里有一些(任意的?)约定吗?例如,在 env 字符串指针数组中分配比当前使用更多的插槽,并根据需要向下移动空终止指针?新的(复制的)字符串的内存是否分配在环境本身的地址空间中,如果它太大而无法容纳您只需获得 ENOMEM?

  • 考虑到上述问题,是否有任何理由putenv()偏爱setenv()

4

5 回答 5

44
  • [The] putenv(char *string);[...] 调用似乎存在致命缺陷。

是的,这是致命的缺陷。 它被保存在 POSIX (1988) 中,因为那是现有技术。setenv()机制后来到了。 更正: POSIX 1990 标准在 §B.4.6.1 中说“附加函数 putenv ()clearenv()被考虑但被拒绝”。1997 年的单一 Unix 规范(SUS) 版本 2 列出putenv()但未列出setenv()unsetenv(). 下一个修订版(2004 年)确实定义了这setenv()两者unsetenv()

因为它不复制传递的字符串,所以你不能用本地调用它,并且不能保证堆分配的字符串不会被覆盖或意外删除。

你说得对,局部变量几乎总是一个不好的选择putenv()——异常模糊到几乎不存在的地步。如果字符串是在堆上分配的(with malloc()et al),你必须确保你的代码不会修改它。如果是这样,它同时也在修改环境。

此外(虽然我没有测试过),因为环境变量的一种用途是将值传递给孩子的环境,如果孩子调用其中一个exec*()函数,这似乎没用。我错了吗?

这些exec*()函数制作环境的副本并将其传递给执行的进程。那里没有问题。

Linux 手册页表明 glibc 2.0-2.1.1 放弃了上述行为并开始复制字符串,但这导致了 glibc 2.1.2 中修复的内存泄漏。我不清楚这个内存泄漏是什么或如何修复的。

内存泄漏的出现是因为一旦你调用putenv()了一个字符串,你就不能再出于任何目的使用该字符串,因为你无法判断它是否仍在使用中,尽管你可以通过覆盖它来修改该值(如果你将名称更改为在环境中另一个位置找到的环境变量的名称)。因此,如果您已经分配了空间,putenv()那么如果您再次更改变量,经典版就会泄漏它。当putenv()开始复制数据时,分配的变量变为未引用,因为putenv()不再保留对参数的引用,但用户期望环境会引用它,因此内存泄漏。我不确定修复是什么——我 3/4 预计它会恢复到旧的行为。

setenv()复制字符串,但我不知道它是如何工作的。进程加载时为环境分配空间,但它是固定的。

原有的环境空间是固定的;当你开始修改它时,规则就会改变。即使使用putenv(),原始环境也会因添加新变量或更改现有变量以具有更长的值而被修改并且可能会增长。

这里有一些(任意的?)约定吗?例如,在 env 字符串指针数组中分配比当前使用更多的插槽,并根据需要向下移动空终止指针?

这就是该setenv()机制可能会做的事情。(全局)变量environ指向环境变量指针数组的开头。如果它一次指向一个内存块,而在不同的时间指向另一个块,那么环境就被切换了,就像那样。

新的(复制的)字符串的内存是否分配在环境本身的地址空间中,如果它太大而无法容纳您只需获得 ENOMEM?

嗯,是的,你可以得到 ENOMEM,但你必须非常努力。而且,如果您将环境增长得太大,您可能无法正确执行其他程序 - 环境将被截断或 exec 操作将失败。

考虑到上述问题,是否有任何理由更喜欢 putenv() 而不是 setenv()?

  • setenv()在新代码中使用。
  • 更新旧代码以使用setenv(),但不要将其作为首要任务。
  • 不要putenv()在新代码中使用。
于 2011-05-03T23:16:46.237 回答
5

阅读The Open Group Base Specifications Issue 6 手册页的RATIONALE部分。setenv

putenv并且setenv都应该符合 POSIX 标准。如果您有代码putenv,并且代码运行良好,请不要理会它。如果您正在开发新代码,您可能需要考虑setenv.

如果您想查看( ) 或( ) 的实现示例,请查看glibc 源代码setenvstdlib/setenv.cputenvstdlib/putenv.c

于 2011-05-03T17:56:32.740 回答
5

没有特殊的“环境”空间 - setenv 只是malloc像您通常所做的那样为字符串(例如)动态分配空间。因为环境不包含其中每个字符串来自何处的任何指示,所以不可能释放setenvunsetenv释放任何可能由先前调用 setenv 动态分配的空间。

“因为它不会复制传递的字符串,所以你不能用本地调用它,并且不能保证堆分配的字符串不会被覆盖或意外删除。” putenv 的目的是确保如果您有一个堆分配的字符串,则可以故意将其删除。这就是基本原理文本所指的“唯一可用于添加到环境而不允许内存泄漏的功能”。是的,您可以使用本地调用它,只需在putenv("FOO=")从函数返回之前从环境(或 unsetenv)中删除字符串。

关键是使用 putenv 使得从环境中删除字符串的过程完全具有确定性。而 setenv 将在某些现有实现上修改环境中的现有字符串,如果新值较短(以避免总是泄漏内存),并且由于它在您调用 setenv 时创建了副本,因此您无法控制最初动态分配的字符串所以当它被删除时你不能释放它。

同时,setenv本身(或 unsetenv)不能释放先前的字符串,因为 - 即使忽略 putenv - 字符串可能来自原始环境,而不是由先前的 setenv 调用分配。

(整个答案假设一个正确实现的 putenv,即不是你提到的 glibc 2.0-2.1.1 中的那个。)

于 2011-05-04T03:58:17.540 回答
4

此外(虽然我没有测试过),因为环境变量的一种用途是将值传递给孩子的环境,如果孩子调用 exec() 函数之一,这似乎没用。我错了吗?

这不是环境传递给孩子的方式。所有各种风格exec()(您可以在手册的第 3 节中找到,因为它们是库函数)最终都会调用系统调用execve()(您可以在手册的第 2 节中找到)。论据是:

   int execve(const char *filename, char *const argv[], char *const envp[]);

环境变量的向量是显式传递的(并且可能部分由您的putenv()andsetenv()调用的结果构造)。内核将这些复制到新进程的地址空间中。从历史上看,这个副本的可用空间对您的环境大小有限制(类似于参数限制),但我不熟悉现代 Linux 内核的限制。

于 2011-05-03T21:26:50.150 回答
3

我强烈建议不要使用这些功能中的任何一个。只要您小心,并且只有一部分代码负责修改环境,任何一种可以安全且无泄漏地使用,但是如果任何代码可能正在使用线程并可能读取环境,则很难正确且危险(例如,用于时区、语言环境、dns 配置等目的)。

我能想到的修改环境的唯一两个目的是在运行时更改时区,或者将修改后的环境传递给子进程。对于前者,您可能必须使用其中一个功能(setenv/ putenv),或者您可以environ手动更改它(如果您担心其他线程可能会尝试同时读取环境,这可能会更安全)。对于后一种用途(子进程),请使用exec-family 函数之一,该函数可让您指定自己的环境数组,或者简单地破坏environ(全局)或在子进程之后但之前使用setenv/putenvforkexec,在这种情况下,您不必关心内存泄漏或线程安全,因为没有其他线程并且您即将破坏地址空间并用新的进程映像替换它。

于 2011-05-03T18:25:33.947 回答