关于 putenv() 和 setenv() 的问题
- 2024-11-07 08:55:00
- admin 原创
- 35
问题描述:
我对环境变量有一些思考,并且有一些疑问/观察。
putenv(char *string);
这个调用似乎存在致命缺陷。因为它不复制传递的字符串,所以您不能用本地调用它,并且不能保证堆分配的字符串不会被覆盖或意外删除。此外(虽然我还没有测试过),由于环境变量的一个用途是将值传递给子级环境,如果子级调用其中一个函数,这似乎毫无用处exec*()
。我错了吗?
Linux 手册页指出 glibc 2.0-2.1.1 放弃了上述行为并开始复制字符串,但这会导致内存泄漏,该问题已在 glibc 2.1.2 中修复。我不清楚这个内存泄漏是什么或如何修复。
setenv()
复制字符串,但我不知道它的具体工作原理。环境的空间是在进程加载时分配的,但它是固定的。这里是否有一些(任意的?)约定?例如,在 env 字符串指针数组中分配比当前使用的更多的插槽,并根据需要将空终止指针向下移动?新(复制的)字符串的内存是否分配在环境本身的地址空间中,如果它太大而无法容纳,您只会得到 ENOMEM?考虑到上述问题,是否有理由
putenv()
选择setenv()
?
解决方案 1:
[该]
putenv(char *string);
[...] 呼吁似乎存在致命缺陷。
是的,它存在致命缺陷。 它在 POSIX(1988 年)中被保留下来,因为那是现有技术。该 更正: POSIX 1990 标准在 §B.4.6.1 中说“考虑了附加函数putenv()和clearenv(),但被拒绝了”。1997年的单一 Unix 规范setenv()
机制后来才出现。putenv()
(SUS) 第 2 版列出了但没有列出setenv()
或unsetenv()
。下一个修订版(2004 年)确实定义了setenv()
和unsetenv()
。
因为它不会复制传递的字符串,所以您不能用本地调用它,并且不能保证堆分配的字符串不会被覆盖或意外删除。
您说得对,将局部变量传递给局部变量几乎总是一个糟糕的选择putenv()
——异常几乎不存在。如果字符串是在堆上分配的(使用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,但您必须非常努力。如果您将环境扩大得太大,您可能无法正确执行其他程序 - 环境将被截断或执行操作将失败。
考虑到上述问题,有什么理由选择 putenv() 而不是 setenv() 呢?
在新代码中使用
setenv()
。更新旧代码以供使用
setenv()
,但不要将其作为首要任务。不要
putenv()
在新代码中使用。
解决方案 2:
没有特殊的“环境”空间 - setenv 只是malloc
像平常一样动态地为字符串分配空间(例如)。由于环境不包含任何关于其中每个字符串来自何处的指示,因此不可能释放setenv
或unsetenv
释放任何可能由先前对 setenv 的调用动态分配的空间。
“因为它不复制传递的字符串,所以您不能用本地函数调用它,而且不能保证堆分配的字符串不会被覆盖或意外删除。” putenv 的目的是确保如果您有一个堆分配的字符串,则可以故意删除它。这就是理由文本中所说的“唯一可以添加到环境中而不允许内存泄漏的函数”。是的,您可以使用本地函数调用它,只需在从函数返回之前从环境中删除字符串(putenv("FOO=")
或 unsetenv)。
重点是使用 putenv 使得从环境中删除字符串的过程完全具有确定性。而 setenv 在某些现有实现中,如果新值较短(以避免始终泄漏内存),则会修改环境中的现有字符串,并且由于它在调用 setenv 时进行了复制,因此您无法控制最初动态分配的字符串,因此在删除时无法释放它。
同时,setenv本身(或 unsetenv)无法释放前一个字符串,因为 - 即使忽略 putenv - 该字符串可能来自原始环境,而不是由之前调用 setenv 分配的。
(整个答案假设正确实现了 putenv,即不是您提到的 glibc 2.0-2.1.1 中的那个。)
解决方案 3:
阅读The Open Group Base Specifications Issue 6 的手册页的RATIONALE部分。setenv
putenv
和setenv
都应该符合 POSIX 标准。如果您的代码putenv
中包含 ,并且代码运行良好,请保留它。如果您正在开发新代码,您可能需要考虑setenv
。
如果您想查看( ) 或( )的实现示例,请查看glibc 源代码。setenv
`stdlib/setenv.cputenv
stdlib/putenv.c`
解决方案 4:
此外(虽然我还没有测试过),由于环境变量的一个用途是将值传递给子环境,因此如果子调用 exec() 函数之一,这似乎毫无用处。我错了吗?
这不是将环境传递给子进程的方式。所有各种类型exec()
(您可以在手册的第 3 节中找到,因为它们是库函数)最终都会调用系统调用execve()
(您可以在手册的第 2 节中找到)。参数如下:
int execve(const char *filename, char *const argv[], char *const envp[]);
环境变量向量是显式传递的(可能部分由你的putenv()
andsetenv()
调用的结果构成)。内核将这些复制到新进程的地址空间中。从历史上看,环境的大小是有限制的,这个限制来自可用于此副本的空间(类似于参数限制),但我不熟悉现代 Linux 内核的限制。
解决方案 5:
我强烈建议不要使用这两个函数。只要你小心谨慎,并且只有一部分代码负责修改环境,这两个函数都可以安全使用,不会出现泄漏,但如果任何代码可能使用线程并可能读取环境(例如,出于时区、语言环境、dns 配置等目的),则很难正确使用,而且很危险。
我能想到的修改环境的唯一两个目的是在运行时更改时区,或者将修改后的环境传递给子进程。对于前者,您可能必须使用其中一个函数(setenv
/ putenv
),或者您可以environ
手动更改它(如果您担心其他线程可能同时尝试读取环境,这可能exec
更安全)。对于后一种用途(子进程),使用其中一个-family 函数,允许您指定自己的环境数组,或者简单地 clobber environ
(全局)或在子进程中在setenv
/之后但在 之前使用,在这种情况下,您不必关心内存泄漏或线程安全,因为没有其他线程,并且您即将破坏地址空间并将其替换为新的进程映像。putenv
`fork`exec
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件