40

私は環境変数について少し考えていて、いくつかの質問/観察があります。

  • putenv(char *string);

    この呼び出しには致命的な欠陥があるようです。渡された文字列をコピーしないため、ローカルで呼び出すことはできず、ヒープに割り当てられた文字列が上書きされたり誤って削除されたりしないという保証はありません。さらに (私はテストしていませんが)、環境変数の 1 つの用途は子の環境に値を渡すことであるため、子がexec*()関数の 1 つを呼び出すと、これは役に立たないように見えます。私はそれで間違っていますか?

  • 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 年のSingle Unix Specificationputenv() (SUS) バージョン 2 にはリストがありますが、setenv()またははありませんunsetenv()。次の改訂 (2004 年) では、setenv()との両方が定義unsetenv()されました。

渡された文字列をコピーしないため、ローカルで呼び出すことはできず、ヒープに割り当てられた文字列が上書きされたり誤って削除されたりしないという保証はありません。

おっしゃる通り、ほとんどの場合、ローカル変数を渡すのは適切ではありputenv()ません。例外は、ほとんど存在しないという点であいまいです。文字列が (et al を使用して) ヒープに割り当てられている場合はmalloc()、コードがそれを変更しないようにする必要があります。もしそうなら、それは同時に環境を変更しています。

さらに (私はテストしていませんが)、環境変数の 1 つの用途は子の環境に値を渡すことであるため、子がexec*()関数の 1 つを呼び出すと、これは役に立たないように見えます。私はそれで間違っていますか?

関数は環境のexec*()コピーを作成し、それを実行されたプロセスに渡します。そこは問題ありません。

Linux のマニュアル ページは、glibc 2.0-2.1.1 が上記の動作を放棄し、文字列のコピーを開始したことを示していますが、これによりメモリ リークが発生し、glibc 2.1.2 で修正されました。このメモリリークが何であるか、またはどのように修正されたかは、私には明らかではありません。

メモリ リークが発生するのは、一度putenv()文字列を呼び出した後は、その文字列がまだ使用されているかどうかがわからないため、その文字列を再度使用することはできませんが、値を上書きして値を変更することはできます (環境内の別の位置にある環境変数の名前に変更します)。したがって、スペースを割り当てputenv()た場合、変数を再度変更すると、クラシックはそれをリークします。データのコピーを開始すると、引数への参照が保持されなくなっputenv()たため、割り当てられた変数が参照されなくなりましたが、ユーザーは環境がそれを参照していると予想したため、メモリ リークが発生しました。putenv()修正が何であったかはわかりません — 4 分の 3 は、以前の動作に戻ることを期待していました。

setenv()文字列をコピーしますが、それがどのように機能するか正確にはわかりません。プロセスのロード時に環境用のスペースが割り当てられますが、固定されています。

元の環境空間は固定されています。変更を開始すると、ルールが変更されます。を使用してもputenv()、新しい変数を追加した結果、または既存の変数をより長い値を持つように変更した結果、元の環境が変更され、拡張される可能性があります。

ここで何らかの (恣意的な?) 規則が働いていますか? たとえば、現在使用されているよりも多くのスロットを env 文字列ポインター配列に割り当て、必要に応じてヌル終了ポインターを下に移動しますか?

それがsetenv()メカニズムが行う可能性が高いことです。(グローバル) 変数environは、環境変数へのポインターの配列の先頭を指します。ある時点でメモリの 1 つのブロックを指し、別の時点で別のブロックを指している場合、環境はそのように切り替えられます。

新しい (コピーされた) 文字列のメモリは、環境自体のアドレス空間に割り当てられていますか? それが大きすぎて収まらない場合は、ENOMEM を取得するだけですか?

はい、ENOMEM を取得できますが、かなりの努力が必要です。また、環境が大きくなりすぎると、他のプログラムを適切に実行できなくなる可能性があります。環境が切り捨てられるか、実行操作が失敗します。

上記の問題を考慮して、setenv() よりも putenv() を好む理由はありますか?

  • setenv()新しいコードで使用します。
  • を使用するように古いコードを更新しますsetenv()が、それを最優先事項にしないでください。
  • putenv()新しいコードでは使用しないでください。
于 2011-05-03T23:16:46.237 に答える
5

The Open Group Base SpecificationIssue6のマニュアルページのRATIONALEセクションをお読みください。setenv

putenv両方ともsetenvPOSIX準拠であると想定されています。コードが含まれていて、コードが正常に機能する場合はputenv、そのままにしておきます。新しいコードを開発している場合は、検討することをお勧めしますsetenv

()または( )の実装例を見たい場合は、glibcのソースコードを見てください。setenvstdlib/setenv.cputenvstdlib/putenv.c

于 2011-05-03T17:56:32.740 に答える
5

特別な「環境」スペースはありません。setenvmallocは、通常どおりに文字列にスペースを動的に割り当てます(たとえば)。環境には、その中の各文字列がどこから来たのかを示すものが含まれていないため、setenvへの以前の呼び出しによって動的に割り当てられた可能性のあるスペースを解放することはできsetenvません。unsetenv

「渡された文字列はコピーされないため、ローカルで呼び出すことはできません。また、ヒープに割り当てられた文字列が上書きされたり、誤って削除されたりしないという保証はありません。」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

さらに(私はテストしていませんが)、環境変数の1つの使用法は子の環境に値を渡すことであるため、子がexec()関数の1つを呼び出す場合、これは役に立たないようです。私はそれで間違っていますか?

それは環境が子供に渡される方法ではありません。のさまざまなフレーバーexec()(ライブラリ関数であるため、マニュアルのセクション3にあります)はすべて、最終的にシステムコールexecve()(マニュアルのセクション2にあります)を呼び出します。引数は次のとおりです。

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

環境変数のベクトルは明示的に渡されます(putenv()およびsetenv()呼び出しの結果から部分的に構築される場合があります)。カーネルはこれらを新しいプロセスのアドレス空間にコピーします。歴史的に、このコピーに使用できるスペースから派生した環境のサイズには制限がありました(引数の制限と同様)が、私は最新のLinuxカーネルの制限に精通していません。

于 2011-05-03T21:26:50.150 に答える
3

これらの関数のいずれも使用しないことを強くお勧めします。注意してコードの一部のみが環境の変更を担当している限り、どちらも安全に漏れなく使用できますが、コードがスレッドを使用している可能性があり、環境を読み取る可能性がある場合、正しく取得するのは難しく、危険です(たとえば、タイムゾーン、ロケール、DNS 構成などの目的)。

環境を変更する目的として考えられるのは、実行時にタイムゾーンを変更することと、変更された環境を子プロセスに渡すことの 2 つだけです。前者の場合、おそらくこれらの関数 ( setenv/ putenv) のいずれかを使用する必要があるか、environ手動で変更することができます (他のスレッドが同時に環境を読み取ろうとする可能性がある場合は、これがより安全かもしれません)。後者の使用 (子プロセス) の場合execは、独自の環境配列を指定できる -family 関数の 1 つを使用するか、単純に clobber environ(グローバル) を使用するか、子プロセスの前後で / を使用setenvします。putenvforkexecこの場合、他のスレッドがなく、アドレス空間を破壊して新しいプロセス イメージに置き換えようとしているため、メモリ リークやスレッド セーフを気にする必要はありません。

于 2011-05-03T18:25:33.947 に答える