Вопросы о putenv () и setenv () - PullRequest
35 голосов
/ 03 мая 2011

Я немного подумал о переменных среды и у меня есть несколько вопросов / наблюдений.

  • putenv(char *string);

    Этот вызов кажется фатально ошибочным.Поскольку он не копирует переданную строку, вы не можете вызвать ее с локальным именем, и нет гарантии, что выделенная куча строка не будет перезаписана или случайно удалена.Кроме того (хотя я не проверял это), поскольку одно из применений переменных среды - передача значений в дочернюю среду, это кажется бесполезным, если дочерний процесс вызывает одну из exec*() функций.Я не прав в этом?

  • Страница руководства Linux указывает, что glibc 2.0-2.1.1 отказался от вышеуказанного поведения и начал копировать строку, но это привело к утечке памяти, которая была исправлена ​​вglibc 2.1.2.Мне не ясно, что это за утечка памяти или как она была исправлена.

  • setenv() копирует строку, но я не знаю точно, как это работает.Пространство для среды выделяется при загрузке процесса, но оно фиксировано.Есть ли здесь какое-то (произвольное?) Соглашение?Например, выделение большего количества слотов в массиве указателей строки env, чем используется в настоящее время, и перемещение нулевого завершающего указателя вниз по мере необходимости?Распределена ли память для новой (скопированной) строки в адресном пространстве самой среды, и если она слишком велика, чтобы соответствовать размеру, просто получите ENOMEM?

  • Учитывая вышеперечисленные проблемы, есть ли основания предпочитать putenv() над setenv()?

Ответы [ 5 ]

38 голосов
/ 04 мая 2011
  • [The] putenv(char *string); [...] вызов кажется фатальным.

Да, это смертельно опасно. Это было сохранено в POSIX (1988), потому что это был предшествующий уровень техники. Механизм setenv() появился позже. Исправление: Стандарт POSIX 1990 гласит в §B.4.6.1 «Дополнительные функции putenv () и clearenv () были рассмотрены, но отклонены ". В спецификации Single Unix (SUS) версии 2 1997 года перечислены putenv(), но не setenv() или unsetenv(). Следующая редакция (2004) также определила как setenv() и unsetenv().

Поскольку он не копирует переданную строку, вы не можете вызвать ее с локальным именем, и нет гарантии, что выделенная куча строка не будет перезаписана или случайно удалена.

Вы правы, что локальная переменная почти всегда является плохим выбором для передачи putenv() - исключения неясны до такой степени, что почти не существуют. Если строка выделена в куче (с malloc() и др.), Вы должны убедиться, что ваш код не изменяет ее. Если это так, он одновременно изменяет среду.

Более того (хотя я не проверял это), поскольку одно из применений переменных среды - передача значений в дочернюю среду, это кажется бесполезным, если дочерний процесс вызывает одну из exec*() функций. Я не прав в этом?

Функции exec*() делают копию среды и передают ее исполняемому процессу. Там нет никаких проблем.

Страница man 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() в новом коде.
5 голосов
/ 04 мая 2011

Специального пространства "среды" не существует - setenv просто динамически распределяет пространство для строк (например, с malloc), как обычно. Поскольку среда не содержит указаний на то, откуда взялась каждая строка, для setenv или unsetenv невозможно освободить пространство, которое могло быть динамически выделено предыдущими вызовами setenv.

«Поскольку он не копирует переданную строку, вы не можете вызвать ее с локальным именем, и нет гарантии, что выделенная куча строка не будет перезаписана или случайно удалена». Цель putenv - убедиться, что если у вас есть выделенная куча строка, ее можно удалить с целью . Вот что подразумевается под текстом «единственная функция, доступная для добавления в среду без утечек памяти». И да, вы можете вызывать его локально, просто удалите строку из окружения (putenv("FOO=") или unsetenv), прежде чем вернуться из функции.

Дело в том, что использование putenv делает процесс удаления строки из окружения полностью детерминированным. Принимая во внимание, что setenv в некоторых существующих реализациях изменяет существующую строку в среде, если новое значение короче (чтобы избежать всегда утечки памяти), и поскольку оно сделало копию, когда вы вызывали setenv, вы не контролируете первоначально динамически размещенной строки, поэтому вы не можете освободить ее, когда она будет удалена.

Между тем, само setenv (или unsetenv) не может освободить предыдущую строку, поскольку - даже игнорируя putenv - строка может исходить из исходного окружения, а не выделяться предыдущим вызовом setenv .

(Весь этот ответ предполагает правильно реализованный путь, т. Е. не тот, что вы упоминали в glibc 2.0-2.1.1.)

5 голосов
/ 03 мая 2011

Прочитайте раздел RATIONALE справочной страницы setenv из Открытой группы базовых спецификаций, выпуск 6.

putenv и setenv должны быть совместимы с POSIX.Если у вас есть код с putenv, и код работает хорошо, оставьте его в покое.Если вы разрабатываете новый код, вы можете рассмотреть setenv.

Посмотрите исходный код glibc , если вы хотите увидеть пример реализации setenv (stdlib/setenv.c) или putenv (stdlib/putenv.c).

4 голосов
/ 04 мая 2011

Более того (хотя я не проверял это), поскольку одно из применений переменных среды - передача значений в дочернюю среду, это кажется бесполезным, если дочерний процесс вызывает одну из функций exec (). Я не прав в этом?

Это не так, как среда передается ребенку. Все различные варианты exec() (которые вы найдете в разделе 3 руководства, поскольку они являются библиотечными функциями) в конечном итоге вызывают системный вызов execve() (который вы найдете в разделе 2 руководства). Аргументы:

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

Вектор переменных среды передается явно (и может быть частично построен из результатов ваших вызовов putenv() и setenv()). Ядро копирует их в адресное пространство нового процесса. Исторически существовал предел размера вашей среды, полученный из пространства, доступного для этой копии (аналогично пределу аргумента), но я не знаком с ограничениями для современного ядра Linux.

3 голосов
/ 03 мая 2011

Я бы настоятельно рекомендовал не использовать ни одну из этих функций. Либо можно использовать безопасно и без утечек, если вы будете осторожны, и только одна часть вашего кода отвечает за изменение среды, но трудно понять, что правильно и опасно, если какой-либо код может использовать потоки и может читать среду (например, для часового пояса, локали, конфигурации DNS и т. д.).

Единственные две цели, которые я могу придумать для изменения среды, - это изменение часового пояса во время выполнения или передача измененной среды дочерним процессам. В первом случае вам, вероятно, придется использовать одну из этих функций (setenv / putenv), или вы можете пройти environ вручную, чтобы изменить ее (это может быть безопаснее, если вы беспокоитесь другие потоки могут одновременно пытаться прочитать окружение). Для последнего использования (дочерние процессы) используйте одну из функций exec -семейства, которая позволяет вам указать свой собственный массив среды, или просто clobber environ (глобальный) или используйте setenv / putenv в дочернем процесс после fork, но до exec, и в этом случае вам не нужно заботиться об утечках памяти или безопасности потоков, потому что других потоков нет, и вы собираетесь уничтожить свое адресное пространство и заменить его на новый образ процесса.

...