web-dev-qa-db-ja.com

pthreadsとvfork

そのうちの1つがvforkを実行しているときに、pthreadに実際に何が起こるかを確認しようとしています。仕様では、子プロセスがexec *または_exitを呼び出すまで、親の「制御スレッド」は「一時停止」されるとされています。私が理解しているように、コンセンサスは、親プロセス全体(つまり、すべてのpthreadを含む)が中断されていることを意味します。実験で確認したいのですが。これまでにいくつかの実験を実行しましたが、そのすべてが他のpthreadが実行されていることを示唆しています。私はLinuxの経験がないので、これらの実験の解釈が間違っていると思います。これらの結果の実際の解釈を学ぶことで、私の人生におけるさらなる誤解を避けることができます。だからここに私がしたexeprimentsがあります:

実験I

#include<unistd.h>
#include<signal.h>
#include<errno.h>
#include<cstring>
#include<string>
#include<iostream>
using namespace std;
void * job(void *x){
  int pid=vfork();
  if(-1==pid){
    cerr << "failed to fork: " << strerror(errno) << endl;
    _exit(-3);
  }
  if(!pid){
    cerr << "A" << endl;
    cerr << "B" << endl;
    if(-1 == execlp("/bin/ls","ls","repro.cpp",(char*)NULL)){
      cerr << "failed to exec : " << strerror(errno) << endl;
      _exit(-4);//serious problem, can not proceed
    }
  }
  return NULL;
}
int main(){
  signal(SIGPIPE,SIG_IGN);
  signal(SIGCHLD,SIG_IGN);
  const int thread_count = 4;
  pthread_t thread[thread_count];
  int err;
  for(size_t i=0;i<thread_count;++i){
    if((err = pthread_create(thread+i,NULL,job,NULL))){
      cerr << "failed to create pthread: " << strerror(err) << endl;
      return -7;
    }
  }
  for(size_t i=0;i<thread_count;++i){
    if((err = pthread_join(thread[i],NULL))){
      cerr << "failed to join pthread: " << strerror(err) << endl;
      return -17;
    }
  }
}

44個のpthreadがあり、それらはすべてvforkを実行し、子でexecを実行します。各子プロセスは、vforkとexec「A」および「B」の間で2つの出力操作を実行します。理論は、出力がネストせずにABABABABABAを読み取る必要があることを示唆しています。ただし、出力は完全に混乱しています。例:

AAAA



BB
B

B

実験II

Vforkの後にI/O libを使用するのは悪い考えかもしれないと思って、job()関数を次のように置き換えました。

const int S = 10000000;
int t[S];
void * job(void *x){
  int pid=vfork();
  if(-1==pid){
    cerr << "failed to fork: " << strerror(errno) << endl;
    _exit(-3);
  }
  if(!pid){
    for(int i=0;i<S;++i){
      t[i]=i;
    }
    for(int i=0;i<S;++i){
      t[i]-=i;
    }
    for(int i=0;i<S;++i){
      if(t[i]){
        cout << "INCONSISTENT STATE OF t[" << i << "] = " << t[i] << " DETECTED" << endl;
      }
    }
    if(-1 == execlp("/bin/ls","ls","repro.cpp",(char*)NULL)){
      cerr << "failed to execlp : " << strerror(errno) << endl;
      _exit(-4);
    }
  }
  return NULL;
}

今回は、2番目のループが最初のループの結果を元に戻すように、2つのループを実行します。したがって、最後にグローバルテーブルt[]は初期状態に戻る必要があります(定義上、すべてゼロです)。子プロセスに入ると、他のpthreadがフリーズし、現在の子がループを終了するまでvforkを呼び出せなくなる場合、配列は最後にすべてゼロになるはずです。また、vfork()の代わりにfork()を使用すると、上記のコードで出力が生成されないことを確認しました。ただし、fork()をvfork()に変更すると、stdoutに大量の不整合が報告されます。

実験III

ここでもう1つの実験について説明します https://unix.stackexchange.com/a/163761/88901 -スリープの呼び出しが含まれていましたが、実際には、長いものに置き換えても結果は同じでしたforループ。

3
qbolec

vork のLinuxのマニュアルページは非常に具体的です。

vfork()fork(2)とは異なり、呼び出し元のthreadは子が終了するまで中断されます。

これはプロセス全体ではありませんが、実際にはthreadを呼び出しています。この動作はPOSIXまたは他の標準によって保証されていません。他の実装は異なることを行う可能性があります(単純なvforkforkを実装することを含む)。

(Rich Felkerは、この動作を vforkは危険と見なされます で指摘しています。)

マルチスレッドプログラムでforkを使用することは、すでに推論するのに十分難しいので、vforkを呼び出すことは少なくとも同じくらい悪いことです。テストは未定義の動作でいっぱいです。vfork- type関数と_exitexecでさえも)を除いて、exit 'd子内で関数を呼び出すこと(I/Oを行うことは言うまでもありません)も許可されていません。そして戻ることは騒乱を引き起こします)。

これは、intsでのアトミックな読み取りと書き込みの関数呼び出しを出力しないコンパイラ/実装を想定した場合、ほぼ未定義の動作がないと私が信じているあなたの例です。 (1つの問題は、startの後にvforkに書き込むことです。これは許可されていません。)エラー処理を省略して、短くしました。

#include<unistd.h>
#include<signal.h>
#include<errno.h>
#include<atomic>
#include<cstring>
#include<string>
#include<iostream>

std::atomic<int> start;
std::atomic<int> counter;
const int thread_count = 4;

void *vforker(void *){
  std::cout << "vforker starting\n";
  int pid=vfork();
  if(pid == 0){
    start = 1;
    while (counter < (thread_count-1))
      ;
    execlp("/bin/date","date",nullptr);
  }
  std::cout << "vforker done\n";
  return nullptr;
}

void *job(void *){
  while (start == 0)
    ;
  counter++;
  return NULL;
}

int main(){
  signal(SIGPIPE,SIG_IGN);
  signal(SIGCHLD,SIG_IGN);
  pthread_t thread[thread_count];
  counter = 0;
  start   = 0;

  pthread_create(&(thread[0]), nullptr, vforker, nullptr);
  for(int i=1;i<thread_count;++i)
    pthread_create(&(thread[i]), nullptr, job, nullptr);

  for(int i=0;i<thread_count;++i)
    pthread_join(thread[i], nullptr);
}

考え方は次のとおりです。通常のスレッドは、アトミックグローバル変数start1になるのを待って(ビジーループ)、グローバルアトミックカウンターをインクリメントします。 vforkを実行するスレッドは、vforkの子でstart1に設定し、他のスレッドがカウンターをインクリメントするのを待ちます(再びビジーループ)。

他のスレッドがvforkの間に中断された場合、進行はありません。中断されたスレッドはcounterをインクリメントしません(start1に設定される前に中断されていたため)、vforkerスレッドはスタックします。無限のビジーウェイト。

3
Mat