web-dev-qa-db-ja.com

fork()は0を返すタイミングをどのように知るのですか?

次の例をご覧ください。

int main(void)
{
     pid_t  pid;

     pid = fork();
     if (pid == 0) 
          ChildProcess();
     else 
          ParentProcess();
}

したがって、間違っている場合は、fork()が実行されると子プロセスが作成されます。さて、これで answer fork()は2回返されます。これは、親プロセスに対して1回、子プロセスに対して1回です。

つまり、フォークコール中に2つの別個のプロセスが存在し、終了後ではありません。

今では、子プロセスに0を返し、親プロセスに正しいPIDを返す方法を理解する方法がわかりません。

これは本当に混乱するところです。この answer は、プロセスのコンテキスト情報をコピーし、戻り値を手動で0に設定することでfork()が機能することを示しています。

最初に、関数への戻りは単一のレジスタに配置されると言っているのは正しいですか? シングルプロセッサ環境では、プロセスは1つの値のみを返すサブルーチンを1つだけ呼び出すことができるため(ここで間違っている場合は修正してください)。

ルーチン内で関数foo()を呼び出し、その関数が値を返すとしましょう。その値はBARというレジスタに保存されます。関数が値を返すたびに、特定のプロセッサレジスタを使用します。したがって、プロセスブロックの戻り値を手動で変更できる場合は、返される値を変更できます。機能は正しいですか?

だから、fork()がどのように機能するかを考えるのは正しいですか?

49
Machina333

How動作の大部分は無関係です-開発者が特定のレベル(UNIX APIへのコーディング)で作業している場合、実際に知っておく必要があるのはthat動作です。

とはいえ、好奇心やある程度の深さで理解する必要があることは一般的には良い特性であると認識しているため、このcouldを実行する方法はいくつもあります。

最初に、関数は1つの値のみを返すことができるという主張は正しい限りですが、プロセスの分割後、実際にはtwo実行中の関数のインスタンスが1つあることを覚えておく必要があります各プロセスで。それらはほとんど互いに独立しており、異なるコードパスをたどることができます。次の図は、これを理解するのに役立ちます。

_Process 314159 | Process 271828
-------------- | --------------
runs for a bit |
calls fork     |
               | comes into existence
returns 271828 | returns 0
_

singleforkのインスタンスは(他のC関数ごとに)1つの値のみを返すことができますが、実際には複数のインスタンスが実行されていることがわかります。ドキュメントで複数の値を返します。


これがcouldの動作の可能性の1つです。

fork()関数が実行を開始すると、現在のプロセスID(PID)が保存されます。

次に、戻るときが来て、PIDが保存されているものと同じ場合、それが親になります。それ以外の場合は、子です。擬似コードは次のとおりです。

_def fork():
    saved_pid = getpid()

    # Magic here, returns PID of other process or -1 on failure.

    other_pid = split_proc_into_two();

    if other_pid == -1:        # fork failed -> return -1
        return -1

    if saved_pid == getpid():  # pid same, parent -> return child PID
        return other_pid

    return 0                   # pid changed, child, return zero
_

split_proc_into_two()呼び出しには多くの魔法があり、ほぼ確実にその方法で動作しないことに注意してください(a)。それは単にその周辺の概念を説明するためのものです。

  • 分割前に元のPIDを取得します。これは、分割後の両方のプロセスで同一のままです。
  • 分割を行います。
  • 分割後の現在のPIDを取得します。これは、2つのプロセスでdifferentになります。

この答え をご覧になることもできます。これは_fork/exec_の哲学を説明しています。


(a) それは私が説明したよりもほぼ確実に複雑です。たとえば、MINIXでは、forkの呼び出しはカーネルで実行され、プロセスツリー全体にアクセスできます。

次の行に沿って、単純に親プロセス構造を子の空きスロットにコピーします。

_sptr = (char *) proc_addr (k1); // parent pointer
chld = (char *) proc_addr (k2); // child pointer
dptr = chld;
bytes = sizeof (struct proc);   // bytes to copy
while (bytes--)                 // copy the structure
    *dptr++ = *sptr++;
_

次に、子構造にわずかな変更を加えて、次の行を含めて適切になるようにします。

_chld->p_reg[RET_REG] = 0;       // make sure child receives zero
_

だから、私が主張したスキームと基本的に同じですが、呼び出し元に何を返すかを決定するためにコードパス選択ではなくデータ変更を使用します-言い換えれば、次のようなものが表示されます:

_return rpc->p_reg[RET_REG];
_

fork()の最後にあるため、親プロセスか子プロセスかに応じて正しい値が返されます。

54
paxdiablo

Linuxでは、fork()はカーネルで発生します。実際の場所は __do_fork_ here です。簡略化すると、fork()システムコールは次のようになります。

_pid_t sys_fork() {
    pid_t child = create_child_copy();
    wait_for_child_to_start();
    return child;
}
_

そのため、カーネルでは、fork()は実際にonceを親プロセスに返します。ただし、カーネルは親プロセスのコピーとして子プロセスも作成します。しかし、通常の関数から戻る代わりに、子プロセスの新しく作成されたスレッド用に新しいkernel stackを合成的に作成します。そして、そのスレッド(およびプロセス)へのコンテキストスイッチ。新しく作成されたプロセスがコンテキスト切り替え関数から戻ると、子プロセスのスレッドは、fork()からの戻り値として0を使用してユーザーモードに戻ります。


基本的に、ユーザーランドのfork()は、カーネルがスタック/リターンレジスターに入れた値を返す単なるラッパーです。カーネルは、新しい子プロセスをセットアップして、このメカニズムを介して唯一のスレッドから0を返すようにします。子pidは、read(2)などのシステムコールからの他の戻り値と同様に、親システムコールで返されます。

29
Antti Haapala

まず、マルチタスクがどのように機能するかを知る必要があります。すべての詳細を理解することは有用ではありませんが、すべてのプロセスはカーネルによって制御されるある種の仮想マシンで実行されます。プロセスには独自のメモリ、プロセッサ、レジスタなどがあります。 (魔法はカーネルにあります)、時間の経過とともに仮想コンテキスト(プロセス)を物理マシンにスワップする機械がいくつかあります。

次に、カーネルがプロセスを分岐すると(fork()はカーネルへのエントリです)、parentプロセス内のほぼすべてのコピーをchildプロセス、必要なすべてを変更できます。これらの1つは、対応する構造を変更して、現在のfork呼び出しから子と親の子のpidに0を返すことです。

注:「フォークは2回返される」と言うと、関数呼び出しは1回だけ返されます。

クローンマシンについて考えてみましょう。1人で入場しますが、2人が退場します。1人はあなたで、もう1人はクローンです(わずかに異なります)。マシンのクローン作成中は、クローンとは異なる名前を設定できます。

10

Forkシステムコールは、新しいプロセスを作成し、親プロセスから多くの状態をコピーします。ファイル記述子テーブルのコピー、メモリマッピングとその内容などが取得されます。その状態はカーネル内にあります。

カーネルがすべてのプロセスを追跡するものの1つは、システムコール、トラップ、割り込み、またはコンテキストスイッチからの復帰時にこのプロセスが復元する必要があるレジスタの値です(ほとんどのコンテキストスイッチはシステムコールまたは割り込みで発生します)。これらのレジスタはsyscall/trap/interruptに保存され、ユーザーランドに戻るときに復元されます。システムコールは、その状態に書き込むことにより戻り値を返します。これがforkの動作です。親フォークは1つの値を取得し、子プロセスは別の値を処理します。

分岐したプロセスは親プロセスとは異なるため、カーネルはそれに対して何でもできます。レジスタに値を指定し、メモリマッピングを指定します。実際に、戻り値以外のほとんどすべてが親プロセスと同じであることを確認するには、さらに手間がかかります。

8
Art

実行中のプロセスごとに、カーネルにはレジスタのテーブルがあり、コンテキスト切り替えが行われたときにロードバックします。 fork()はシステムコールです。特別な呼び出しを行うと、プロセスはコンテキストスイッチを取得し、呼び出しを実行するカーネルコードは別の(カーネル)スレッドで実行されます。

システムコールによって返される値は、アプリケーションがコール後に読み取る特別なレジスタ(x86ではEAX)に配置されます。 fork()呼び出しが行われると、カーネルはプロセスのコピーを作成し、各プロセス記述子のレジスタの各テーブルに適切な値0とpidを書き込みます。

2
vz0