关于putenv()和setenv()的问题

时间:2011-05-03 17:03:04

标签: c linux unix environment-variables setenv

我一直在考虑环境变量,并有一些问题/观察。

  • putenv(char *string);

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

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

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

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

5 个答案:

答案 0 :(得分:37)

  
      
  • [{] putenv(char *string); [...]电话似乎有致命缺陷。
  •   

是的,这是致命的缺陷。 它保存在POSIX(1988)中,因为那是现有技术。 setenv()机制稍后到达。 更正: POSIX 1990标准在§B.4.6.1中说“附加功能 putenv()和< em> 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()不再保留对参数的引用,但是用户期望环境引用它,因此内存被泄露。我不确定修复是什么 - 我希望它能够恢复原来的行为。

  

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

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

  

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

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

  

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

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

  

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

  • 在新代码中使用setenv()
  • 更新旧代码以使用setenv(),但不要将其作为最高优先级。
  • 请勿在新代码中使用putenv()

答案 1 :(得分:5)

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

putenvsetenv都应该符合POSIX标准。如果您的代码中包含putenv,并且代码运行良好,请不要管它。如果您正在开发新代码,可能需要考虑setenv

如果您想查看setenvglibc source code)或putenvstdlib/setenv.c)的实施示例,请查看stdlib/putenv.c

答案 2 :(得分:5)

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

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

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

与此同时,setenv 本身(或unsetenv)无法释放前一个字符串,因为 - 即使忽略putenv - 字符串可能来自原始环境,而不是之前调用的SETENV。

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

答案 3 :(得分:4)

  

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

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

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

环境变量的向量是明确传递的(可能部分是根据putenv()setenv()调用的结果构建的)。内核将这些复制到新进程的地址空间中。从历史上看,从可用于此副本的空间派生的环境大小有限(类似于参数限制),但我不熟悉对现代Linux内核的限制。

答案 4 :(得分:3)

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

我可以考虑修改环境的唯一两个目的是在运行时更改时区,或者将修改后的环境传递给子进程。对于前者,您可能必须使用其中一个函数(setenv / putenv),或者您可以手动走environ来更改它(此可能如果你担心其他线程可能会同时尝试读取环境,那么会更安全。对于后者使用(子进程),使用exec - 系列函数之一,可以指定自己的环境数组,或者只使用clobber environ(全局)或使用setenv /在putenv之后fork之前的子进程中exec,在这种情况下,您不必关心内存泄漏或线程安全,因为没有其他线程而且您'重新破坏你的地址空间并用新的过程映像替换它。