putenv()和setenv()的问题

42

我对环境变量思考了一些,并有几个问题/观察。

  • putenv(char *string);

    这个调用似乎存在致命缺陷。因为它不会复制传递的字符串,所以你不能使用局部变量调用它,也无法保证堆分配的字符串不会被覆盖或意外删除。此外(虽然我没有测试过),由于环境变量的一种用途是将值传递给子进程的环境,如果子进程调用其中一个exec*()函数,则此调用似乎没有用。我的理解是否正确?

  • Linux man页面指出,glibc 2.0-2.1.1放弃了上述行为并开始复制该字符串,但这导致了一个内存泄漏,在glibc 2.1.2中得到了修复。我不清楚这个内存泄漏是什么以及如何修复。

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

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

5个回答

46
  • putenv(char *string);调用似乎存在致命缺陷。

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

因为它不会复制传递的字符串,所以不能使用局部变量调用它,并且无法保证堆分配的字符串不会被覆盖或意外删除。

你说得对,将局部变量传递给putenv()几乎总是不明智的选择——异常情况几乎不存在。如果字符串在堆上分配(使用malloc()等),必须确保您的代码不会修改它。如果它被修改了,同时也更改了环境变量。
此外(虽然我没有测试过),由于环境变量的一个用途是将值传递给子进程的环境,因此如果子进程调用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错误,但你必须非常努力才能这样做。如果将环境增大过多,则可能无法正确执行其他程序 - 环境将被截断或执行操作将失败。
考虑到上述问题,有没有理由更喜欢putenv()而不是setenv()?
- 在新代码中使用setenv()。 - 更新旧代码以使用setenv(),但不要把它作为最重要的任务。 - 不要在新代码中使用putenv()

提醒一下:如果使用本地变量调用 putenv(),则应优先将其替换为 setenv() - Yeow_Meng
@Yeow_Meng:嗯,有点吧……之前代码就有问题,所以很少有人这样做,因为它是有问题的。 - Jonathan Leffler
@Yeow_Meng:除非在相同的作用域中存在exec()(或者不太可能的情况下,存在exit())。例如,https://github.com/apk/c-utils/blob/dffc690ffec471087059bbdb7f6af453690aa835/aenv.c#L185 是可以的,并且在这里使用putenv是最好的选择,因为你已经有了NAME=value字符串。 - Andreas Krey
@JonathanLeffler 只有更糟的是... setenv()后跟unsetenv()会造成内存泄漏。使用相同键的setenv()后跟setenv()也可能会泄漏,具体取决于实现的质量。在重写之前手动free()putenv()可在不泄漏的情况下使用,并在unsetenv()时进行,在这种情况下,您需要跟踪哪些键值对实际上是堆分配的,使用外部数据结构,否则就会面临未定义行为等问题。-这很混乱。 :-) - Arne Vogel
@ArneVogel — setenv()unsetenv()函数不应该引起内存泄漏。在* env()函数包的内部,有许多不愉快的细节需要管理,正如您所暗示的那样,管理它们需要比仅通过environ公开的指针数组更多的信息。该公开变量打开了一个后门(尽管修改它会引发未定义的行为)。它成为了QoI(实现质量)问题。简单但质量低劣的实现容易出现内存泄漏。高质量的实现避免大多数泄漏,但可能被不良程序迫使泄漏。 - Jonathan Leffler
毫无疑问,从道德角度来看,这是不应该泄漏的,但请注意POSIX 2008putenv()的基本原理:“标准开发人员指出,putenv()是唯一可用于添加到环境中而不允许内存泄漏的函数。”;虽然自2013年版以来已经没有了这些含糊其辞的话语-请参见ERN 1086,但ERN根本没有解决setenv()可能会泄漏的原始声明。 - Arne Vogel

5

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

putenvsetenv 都应该符合 POSIX 标准。如果您的代码中有 putenv,并且代码运行良好,请不要更改它。如果您正在开发新代码,则可以考虑使用 setenv

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


5
没有特殊的“环境”空间 - setenv只是像通常一样动态分配字符串的空间(例如使用malloc)。由于环境中不包含每个字符串来自哪里的任何指示,因此setenvunsetenv无法释放可能已被先前调用setenv动态分配的任何空间。
“因为它不会复制传递的字符串,所以您不能使用本地字符串调用它,并且不能保证堆分配的字符串不会被覆盖或意外删除。” putenv的目的是确保如果您有一个堆分配的字符串,则可以有意地将其删除。这就是理性文本所说的“仅可用于向环境添加而不允许内存泄漏的函数”。是的,您可以使用本地调用它,只需在从函数返回之前从环境中删除字符串(putenv(“FOO =”)或unsetenv)即可。
重点是使用putenv使从环境中删除字符串的过程完全确定。而setenv将在某些现有实现中修改环境中的现有字符串,如果新值较短(为避免始终泄漏内存),并且由于在调用setenv时进行了复制,因此您无法控制最初动态分配的字符串,因此无法在删除时释放它。
同时,setenv本身(或unsetenv)无法释放先前的字符串,因为即使忽略putenv,该字符串也可能来自原始环境,而不是由先前调用setenv分配的。
(这整个答案假定正确实现了putenv,即您提到的glibc 2.0-2.1.1中没有。)

4
此外(虽然我没有测试过),由于环境变量的一个用途是将值传递给子进程的环境,如果子进程调用exec()函数之一,则这似乎是无用的。我理解正确吗?
那不是传递环境变量给子进程的方式。所有不同版本的exec()函数(你可以在手册的第3节找到它们,因为它们是库函数)最终都会调用系统调用execve()(你可以在手册的第2节找到它)。参数如下:
   int execve(const char *filename, char *const argv[], char *const envp[]);

环境变量向量是明确传递的(有可能部分由您的putenv()setenv()调用结果构建)。内核会将这些变量拷贝到新进程的地址空间中。历史上,由于可用空间限制(类似于参数限制),您的环境派生自拷贝的大小有一定限制,但我不了解现代Linux内核的限制。


3
我强烈建议不要使用这两个函数中的任何一个。只要你小心并且只有你代码的一部分负责修改环境,那么可以安全地使用它们而不会出现泄漏问题,但是如果任何代码可能使用线程并且可能读取环境(例如时区、区域设置、DNS配置等),那么这将很危险,并且很难正确处理。
我能想到只有两种目的可以修改环境:在运行时更改时区,或传递修改后的环境给子进程。对于前者,您可能必须使用这些函数之一(setenv/putenv),或手动遍历environ来更改它(如果您担心其他线程同时尝试读取环境,则此操作可能更安全)。对于后一种用途(子进程),请使用让您指定自己的环境数组的exec-系列函数之一,或者简单地覆盖environ(全局变量)或在fork之后但在exec之前在子进程中使用setenv/putenv。在这种情况下,您不必担心内存泄漏或线程安全性,因为没有其他线程,而且您即将销毁自己的地址空间并替换为新的进程映像。

这可能有点离题,但如果使用vfork()来fork一个子进程,然后修改环境变量,再执行exec(),那么父进程的环境变量会被修改吗? - user339222
3
这是未定义的。在最坏的情况下,您不仅修改它,还会可怕地破坏父进程的状态。例如,如果您在vfork子进程中使用setenvsetenv调用malloc,就可能会发生这种情况。根本不要考虑做这样的事情。请注意,在调用exec之前没有必要修改环境变量;只需使用其中一种形式的exec,使您能够传递一个新的环境指针即可。 - R.. GitHub STOP HELPING ICE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接