web-dev-qa-db-ja.com

exec()関数とそのファミリーについて説明してください

exec()関数とそのファミリーとは何ですか?この関数はなぜ使用され、どのように機能しますか?

誰でもこれらの機能を説明してください。

87
user507401

簡単に言えば、UNIXにはプロセスとプログラムの概念があります。プロセスは、プログラムが実行されるものです。

UNIXの「実行モデル」の背後にある簡単なアイデアは、実行できる操作が2つあるということです。

1つは fork() で、現在のプログラムの複製(状態を含む)を含む新しいプロセスを作成します。プロセスにはいくつかの違いがあり、どちらが親でどちらが子であるかを判別できます。

2つ目は exec() で、現在のプロセスのプログラムを新しいプログラムに置き換えます。

これらの2つの簡単な操作から、UNIX実行モデル全体を構築できます。


上記にさらに詳細を追加するには:

fork()exec()の使用は、新しいプロセスを開始する非常に簡単な方法を提供するという点でUNIXの精神を例示しています。

fork()呼び出しは、現在のプロセスのほぼ複製を作成し、ほぼすべての点で同一です(everythingはコピーされません。たとえば、一部の実装ではリソース制限がコピーされます) 、しかし、考えは可能な限り近いコピーを作成することです)。 1つのプロセスがfork()を呼び出し、2つのプロセスが戻ります-奇妙に聞こえますが、非常にエレガントです

新しいプロセス(子と呼ばれる)は別のプロセスID(PID)を取得し、古いプロセス(親)のPIDを親PID(PPID)として保持します。

2つのプロセスはまったく同じコードを実行しているため、どちらがどちらであるかを知る必要があります-fork()の戻りコードがこの情報を提供します-子は0を取得し、親は子のPIDを取得します(fork()が失敗した場合、子は作成されず、親はエラーコードを受け取ります)。そうすれば、親は子のPIDを知っており、それと通信したり、キルしたり、待機したりすることができます(子はgetppid()の呼び出しで常に親プロセスを見つけることができます)。

exec()呼び出しは、プロセスの現在の内容全体を新しいプログラムに置き換えます。プログラムを現在のプロセススペースにロードし、エントリポイントから実行します。

そのため、fork()exec()は、現在のプロセスの子として実行される新しいプログラムを取得するために、順番に使用されることがよくあります。シェルは通常、findのようなプログラムを実行しようとするたびにこれを行います。シェルはフォークし、その後、子はfindプログラムをメモリにロードし、すべてのコマンドライン引数、標準I/Oなどを設定します。

ただし、一緒に使用する必要はありません。たとえば、プログラムに親コードと子コードの両方が含まれている場合、プログラムがfork()を伴わずにexec()を呼び出すことは完全に受け入れられます(実行に注意する必要があります。各実装には制限がある場合があります)。これは、TCPポートで単純にリッスンし、親がリッスンに戻っている間に特定のリクエストを処理するために自分のコピーをフォークするデーモンに非常に多く使用されます(そして今でも使用されています)。この状況では、プログラムには親コード子コードの両方が含まれます。

同様に、終了したことを知っており、別のプログラムを実行したいだけのプログラムは、子供のためにfork()exec()、そしてwait()/waitpid()を必要としません。 exec()を使用して、現在のプロセス空間に直接子をロードできます。

一部のUNIX実装には、コピーオンライトと呼ばれるものを使用する最適化されたfork()があります。これは、プログラムがそのスペースで何かを変更しようとするまで、fork()のプロセススペースのコピーを遅らせるためのトリックです。これは、プロセス空間全体をコピーする必要がないという点で、fork()ではなくexec()のみを使用するプログラムに役立ちます。 Linuxでは、fork()はページテーブルのコピーと新しいタスク構造のみを作成し、exec()は2つのプロセスのメモリを「分離」するという面倒な作業を行います。

execisforkに続いて呼び出された場合(これがほとんどの場合です)、プロセス空間への書き込みが発生しますそして、子プロセス用にコピーされます。

Linuxには、さらに最適化されたvfork()もあり、2つのプロセス間でほぼすべてeverythingを共有します。そのため、子ができることには一定の制限があり、子がexec()または_exit()を呼び出すまで親は停止します。

2つのプロセスは同じスタックを共有しているため、親は停止する必要があります(そして、子は現在の関数から戻ることはできません)。これは、fork()の直後にexec()が続くという古典的なユースケースでは、わずかに効率的です。

exec呼び出しのファミリー全体(execlexecleexecveなど)がありますが、ここでのexecはそれらのいずれかを意味します。

次の図は、bashコマンドを使用してディレクトリをリストするためにlsシェルが使用される典型的なfork/exec操作を示しています。

+--------+
| pid=7  |
| ppid=4 |
| bash   |
+--------+
    |
    | calls fork
    V
+--------+             +--------+
| pid=7  |    forks    | pid=22 |
| ppid=4 | ----------> | ppid=7 |
| bash   |             | bash   |
+--------+             +--------+
    |                      |
    | waits for pid 22     | calls exec to run ls
    |                      V
    |                  +--------+
    |                  | pid=22 |
    |                  | ppid=7 |
    |                  | ls     |
    V                  +--------+
+--------+                 |
| pid=7  |                 | exits
| ppid=4 | <---------------+
| bash   |
+--------+
    |
    | continues
    V
227
paxdiablo

Exec()ファミリーの関数には、異なる動作があります。

  • l:引数は文字列のリストとしてmain()に渡されます
  • v:引数は、ストリングの配列としてmain()に渡されます
  • p:実行中の新しいプログラムを検索するパス/ s
  • e:呼び出し側が環境を指定できます

あなたはそれらを混ぜることができます、したがって、あなたは持っています:

  • int execl(const char * path、const char * arg、...);
  • int execlp(const char * file、const char * arg、...);
  • int execle(const char * path、const char * arg、...、char * const envp []);
  • int execv(const char * path、char * const argv []);
  • int execvp(const char * file、char * const argv []);
  • int execvpe(const char * file、char * const argv []、char * const envp []);

それらのすべてについて、初期引数は実行されるファイルの名前です。

詳細については、 exec(3)manページ を参照してください。

man 3 exec  # if you are running a UNIX system
30
T04435

execファミリの関数は、プロセスに別のプログラムを実行させ、実行していた古いプログラムを置き換えます。つまり、あなたが電話した場合

execl("/bin/ls", "ls", NULL);

lsプログラムは、プロセスID、現在の作業ディレクトリ、およびexeclを呼び出したプロセスのユーザー/グループ(アクセス権)を使用して実行されます。その後、元のプログラムは実行されなくなります。

新しいプロセスを開始するには、forkシステムコールが使用されます。オリジナルを置き換えずにプログラムを実行するには、fork、次にexecを実行する必要があります。

16
Fred Foo

execforkと組み合わせてよく使用されますが、これについても質問されているので、それを念頭に置いて説明します。

execは、現在のプロセスを別のプログラムに変換します。ドクター・フーを見たことがあれば、これは彼が再生するときのようなものです-彼の古い体は新しい体に置き換えられます。

これがプログラムとexecで発生する方法は、OSカーネルがexecにプログラム引数(最初の引数)として渡すファイルが実行可能かどうかを確認するためにチェックするリソースの多くです。現在のユーザー(exec呼び出しを行うプロセスのユーザーID)によって、もしそうであれば、現在のプロセスの仮想メモリマッピングを仮想メモリに置き換えて新しいプロセスを作成し、argvおよびenvpこの新しい仮想メモリマップの領域へのexec呼び出しで渡されたデータ。他にもいくつかのことが発生する可能性がありますが、execを呼び出したプログラム用に開いていたファイルは新しいプログラム用に開いたままで、同じプロセスIDを共有しますが、execは停止します(execが失敗しない限り)。

これがこのように行われる理由は、runninganewprogramをこのような2つのステップに分けることです。 2つのステップの間にいくつかのことができます。最も一般的なことは、新しいプログラムで特定のファイルが特定のファイル記述子として開かれていることを確認することです。 (ここで、ファイル記述子はFILE *と同じではなく、カーネルが知っているint値であることを思い出してください)。これを行うことができます:

int X = open("./output_file.txt", O_WRONLY);

pid_t fk = fork();
if (!fk) { /* in child */
    dup2(X, 1); /* fd 1 is standard output,
                   so this makes standard out refer to the same file as X  */
    close(X);

    /* I'm using execl here rather than exec because
       it's easier to type the arguments. */
    execl("/bin/echo", "/bin/echo", "hello world");
    _exit(127); /* should not get here */
} else if (fk == -1) {
    /* An error happened and you should do something about it. */
    perror("fork"); /* print an error message */
}
close(X); /* The parent doesn't need this anymore */

これにより、実行が完了します。

/bin/echo "hello world" > ./output_file.txt

コマンドシェルから。

7
nategoose

exec関数とそのファミリは何ですか。

exec関数ファミリは、execlexeclpexecleexecv、およびexecvpなど、ファイルの実行に使用されるすべての関数です。それらはすべてexecveのフロントエンドであり、さまざまな呼び出し方法を提供します。

この関数が使用される理由

EXEC関数は、ファイル(プログラム)を実行(起動)するときに使用されます。

そしてそれはどのように機能しますか。

現在のプロセスイメージを、起動したイメージで上書きすることで機能します。現在実行中のプロセス(execコマンドを呼び出したプロセス)を、起動した新しいプロセスで(終了することにより)置き換えます。

詳細: このリンクを参照

5
Reese Moore

exec(3,3p)関数replace現在のプロセスを別のプロセスに置き換えます。つまり、現在のプロセスstopsで、代わりに別のプログラムが実行され、元のプログラムが持っていたリソースの一部を引き継ぎます。

プロセスがfork()を使用すると、それ自体の複製コピーが作成され、この複製がプロセスの子になります。 fork()は、カーネルからcloneを2回返すLinuxのclone()システムコールを使用して実装されます。

  • ゼロ以外の値(子のプロセスID)が親に返されます。
  • ゼロの値が子に返されます。
  • メモリ不足などの問題のために子が正常に作成されない場合、-1がfork()に返されます。

例でこれを理解しましょう。

pid = fork(); 
// Both child and parent will now start execution from here.
if(pid < 0) {
    //child was not created successfully
    return 1;
}
else if(pid == 0) {
    // This is the child process
    // Child process code goes here
}
else {
    // Parent process code goes here
}
printf("This is code common to parent and child");

この例では、exec()は子プロセス内では使用されないと想定しています。

ただし、親と子はいくつかのPCB(プロセス制御ブロック)属性が異なります。これらは:

  1. PID-子と親の両方が異なるプロセスIDを持っています。
  2. 保留中の信号-子は、親の保留中の信号を継承しません。子プロセスの作成時には空になります。
  3. メモリーロック-子は親のメモリーロックを継承しません。メモリロックは、メモリ領域をロックするために使用できるロックであり、このメモリ領域をディスクにスワップすることはできません。
  4. レコードロック-子は親のレコードロックを継承しません。レコードロックは、ファイルブロックまたはファイル全体に関連付けられています。
  5. 子のプロセスリソース使用率とCPU時間はゼロに設定されます。
  6. また、子は親からタイマーを継承しません。

ただし、子メモリはどうですか?子用に新しいアドレススペースが作成されますか?

いいえの答え。 fork()の後、親と子の両方が親のメモリアドレス空間を共有します。 Linuxでは、これらのアドレス空間は複数のページに分割されます。子が親メモリページの1つに書き込む場合にのみ、そのページの複製が子に対して作成されます。これは書き込み時コピーとも呼ばれます(子が書き込みを行う場合にのみ親ページをコピーします)。

コピーオンライトを例で理解しましょう。

int x = 2;
pid = fork();
if(pid == 0) {
    x = 10;
    // child is changing the value of x or writing to a page
    // One of the parent stack page will contain this local               variable. That page will be duplicated for child and it will store the value 10 in x in duplicated page.  
}
else {
    x = 4;
}

しかし、なぜコピーオンライトが必要なのですか?

典型的なプロセスの作成は、fork()-exec()の組み合わせによって行われます。最初にexec()の機能を理解しましょう。

Exec()関数のグループは、子供のアドレス空間を新しいプログラムに置き換えます。子内でexec()が呼び出されると、親のアドレススペースとはまったく異なる別のアドレススペースが子用に作成されます。

Fork()に関連付けられた書き込み時コピーメカニズムがなかった場合、子の複製ページが作成され、すべてのデータが子のページにコピーされます。新しいメモリの割り当てとデータのコピーは非常に高価なプロセスです(プロセッサの時間と他のシステムリソースを消費します)。また、ほとんどの場合、子供はexec()を呼び出し、それが子供の記憶を新しいプログラムに置き換えます。したがって、コピーオンライトがなければ、最初に行ったコピーは無駄になっていたでしょう。

pid = fork();
if(pid == 0) {
    execlp("/bin/ls","ls",NULL);
    printf("will this line be printed"); // Think about it
    // A new memory space will be created for the child and that   memory will contain the "/bin/ls" program(text section), it's stack, data section and heap section
else {
    wait(NULL);
    // parent is waiting for the child. Once child terminates, parent will get its exit status and can then continue
}
return 1; // Both child and parent will exit with status code 1.

なぜ親は子プロセスを待つのですか?

  1. 親は、自分の子供にタスクを割り当て、タスクが完了するまで待つことができます。その後、他の作業を実行できます。
  2. 子が終了すると、プロセス制御ブロックを除き、子に関連付けられているすべてのリソースが解放されます。現在、子供はゾンビ状態にあります。 wait()を使用して、親は子のステータスについて問い合わせてから、PCBを解放するようカーネルに要求できます。親が待機を使用しない場合、子はゾンビ状態のままになります。

なぜexec()システムコールが必要なのですか?

Fork()でexec()を使用する必要はありません。子が実行するコードが親に関連付けられたプログラム内にある場合、exec()は必要ありません。

しかし、子供が複数のプログラムを実行しなければならない場合を考えてください。シェルプログラムの例を見てみましょう。 find、mv、cp、dateなどの複数のコマンドをサポートします。これらのコマンドに関連付けられたプログラムコードを1つのプログラムに含めるか、必要に応じて子にこれらのプログラムをメモリに読み込ませますか?

それはすべてユースケースに依存します。クライアントに2 ^ xを返す入力xを指定したWebサーバーがあります。 Webサーバーは、リクエストごとに新しい子を作成し、計算するように要求します。これを計算してexec()を使用する別のプログラムを作成しますか?または、親プログラム内に計算コードを書くだけですか?

通常、プロセスの作成にはfork()、exec()、wait()およびexit()呼び出しの組み合わせが含まれます。

1
Shivam Mitra