作成プロセスを紹介する前に、Linuxでのプロセスメモリレイアウトを簡単に紹介しましょう。
スタック-すべての関数のローカル変数、引数、および戻りアドレスを格納するためのメモリ領域
ヒープ動的に割り当てられたメモリ領域
bss-初期化されていないすべてのグローバル変数と静的変数のメモリ領域
データ-初期化されたすべてのグローバル変数と静的変数のストレージメモリ領域
[^1] Typical memory layout of a process on Linux/x86-32
Linuxシステムでは、** fork()**を呼び出すことで新しいプロセスを作成できます。呼び出しプロセスは親プロセスであり、生まれる新しいプロセスは子プロセスです。
**fork()は2回返されるため特別です。つまり、2つの戻り値があります。これらの2つの戻り値によって、親プロセスと子プロセスを区別できます。親プロセスでは、 fock()**は子プロセスのプロセスIDを返します。子プロセスでは、成功した場合は0を返し、失敗した場合は-1を返します(失敗の理由についてはマニュアルを参照してください)。子プロセスは getpid()
を呼び出してプロセスIDを取得できます。
子プロセスが生成されると、bss、データセグメント、スタックセグメントなど、親プロセスからデータのコピーが取得されます。 CentOS 8は、** fork()**を呼び出した後、親プロセスと子プロセスの実行順序を保証できないことに注意してください。
出力から、2つのプロセスのデータが独立していることがわかります。
# include<unistd.h>#include<iostream>auto global_value =11;// stored in data segmentintmain(){auto local_value =66;// stored in stack segmentdouble* dPtr =newdouble(3.14);switch(fork()){case-1:
std::cout <<"failed to fork.\n";_exit(-1);case0:*dPtr =5.12;
std::cout <<"child process ID: "<<getpid()<<"\n";
std::cout <<"global value: "<<(++global_value)<<"\n";
std::cout <<"local value: "<<(++local_value)<<"\n";
std::cout <<"double pointer value: "<<(*dPtr)<<"\n";
std::cout <<"----------------------------------\n";_exit(0);}
std::cout <<"parent process ID: "<<getpid()<<"\n";
std::cout <<"global value: "<< global_value <<"\n";
std::cout <<"local value: "<< local_value <<"\n";
std::cout <<"double pointer value: "<<(*dPtr)<<"\n";
std::cout <<"----------------------------------\n";exit(0);}
出力結果:
[ me@localhost Documents]$ ./exe
parent process ID:3961
global value:11
local value:66
double pointer value:3.14----------------------------------
child process ID:3962
global value:12
local value:67
double pointer value:5.12----------------------------------[me@localhost Documents]$ ./exe
parent process ID:3988
global value:11
local value:66
double pointer value:3.14----------------------------------
child process ID:3989
global value:12
local value:67
double pointer value:5.12----------------------------------
ファイル記述子を継承します##
** fork()**を実行すると、子プロセスは親プロセスからファイル記述子のコピーを取得します。データのコピーと同じように、ファイル記述子もコピーですが、本質的に、これらのファイル記述子は、カーネルによって維持されている開いているファイルテーブルを指します。したがって、対応するファイル記述のオフセットとファイルステータスフラグは同じです。
[^2] Duplication of file descriptors during fork(), and closing of unused descriptors
# include<unistd.h>#include<fcntl.h>#include<iostream>intmain(){auto fd =open("/home/usr1/Documents/reading.txt", O_NONBLOCK);switch(fork()){case-1:
std::cout <<"failed to fork.\n";_exit(-1);case0:auto flags =fcntl(fd, F_GETFL);if(O_NONBLOCK & flags)
std::cout <<"O_NONBLOCK flag is on\n";_exit(0);}auto flags =fcntl(fd, F_GETFL);if(O_NONBLOCK & flags)
std::cout <<"O_NONBLOCK flag is on\n";close(fd);return0;}
出力結果:
O_NONBLOCK flag is on
O_NONBLOCK flag is on
子プロセスは、誕生直後に exec()
ファミリー関数を実行する可能性があるためです。これは、子プロセスによって親プロセスからコピーされたすべてのデータが洗い流され、コピー作業が無駄になることを意味します。効率上の理由から、COWが使用されました。原理は非常に単純です。** fork()**を呼び出した後、親プロセスと子プロセスは読み取り専用のメモリイメージを共有します。このメモリイメージを変更するプロセスがない場合、それらはすべて同じメモリへの影響を及ぼします。プロセスがデータを変更したい場合、カーネルはプロセスが独立して使用できるように、プロセスの新しいメモリイメージをコピーします。
参照:
[^1] 6.4 Virtual Memory Management, The Linux Programming Interface.
[^2] 24.2.1 File Sharing Between Parent and Child, The Linux Programming Interface.