当vfork被调用时,父进程真的被挂起了吗?

当vfork被调用时,父进程真的被挂起了吗?

据我所知,当vfork调用时,子进程使用与父进程相同的地址空间,并且子进程在父进程的 ss 变量中所做的任何更改都会反映到父进程上。我的问题是:

  • 当子进程产生时,父进程是否被挂起?
  • 如果是,为什么?
  • 它们可以并行运行(像线程一样)?毕竟,线程和进程都调用同一个clone()函数。

经过一番研究和谷歌搜索,我发现父进程并没有真正挂起,而是调用线程被挂起。即使是这样,当子进程执行exit()or时exec(),父进程如何知道子进程已经退出呢?如果我们从子进程返回会发生什么?

答案1

您的问题部分是基于错误的命名约定。内核中的“控制线程”是用户中的进程。因此,当您读到 vfork“调用线程已挂起”时,请考虑“进程”(或“重量级线程”,如果您愿意)而不是“多线程进程”中的“线程”。

  • 所以是的,父进程被挂起。

vfork语义是针对非常常见的情况定义的,其中一个进程(最常见的是 shell)会分叉,弄乱一些文件描述符,然后exec另一个进程就位。内核人员意识到,如果他们跳过复制,他们可以节省大量的页面复制开销,因为他们exec只会丢弃那些复制的页面。 vforked 子进程在内核中确实有自己的文件描述符表,因此对其进行操作不会影响父进程,保持语义fork不变。

  • 为什么?因为 fork/exec 很常见、昂贵且浪费

考虑到“内核控制线程”的更准确定义,它们能否并行运行的答案显然是

  • 不,父进程将被内核阻塞,直到子进程退出或执行

家长怎么知道孩子已经退出了?

  • 事实并非如此,内核知道并阻止父进程获得任何 CPU,直到子进程离开。

至于最后一个问题,我怀疑内核会检测到返回中涉及的子堆栈操作,并用不可捕获的信号向子进程发出信号,或者直接杀死它,但我不知道细节。

答案2

我不是linux程序员,但今天面临同样的问题,我做了以下测试:

#include<unistd.h>
#include<signal.h>
#include<errno.h>
#include<fcntl.h>
#include<cassert>
#include<cstring>
#include<string>
#include<algorithm>
#include<vector>
#include<map>
#include<set>
#include<iostream>
#include<fstream>
#include<sstream>
#include<list>
using namespace std;
int single_talk(int thread_id){
  fprintf(stderr,"thread %d before fork @%d\n",thread_id,time(0));
  int pid=vfork();
  if(-1==pid){
    cerr << "failed to fork: " << strerror(errno) << endl;
    _exit(-3);//serious problem, can not proceed
  }
  sleep(1);
  fprintf(stderr,"thread %d fork returned %d @%d\n",thread_id,pid,time(0));
  if(pid){//"CPP"
    fprintf(stderr,"thread %d in parent\n",thread_id);
  }else{//"PHP"
    sleep(1);
    fprintf(stderr,"thread %d in child @%d\n",thread_id,time(0));
    if(-1 == execlp("/bin/ls","ls",(char*)NULL)){
      cerr << "failed to execl php : " << strerror(errno) << endl;
      _exit(-4);//serious problem, can not proceed
    }
  }
}
void * talker(void * id){
  single_talk(*(int*)id);
  return NULL;
}
int main(){
  signal(SIGPIPE,SIG_IGN);
  signal(SIGCHLD,SIG_IGN);
  const int thread_count = 44;
  pthread_t thread[thread_count];
  int thread_id[thread_count];
  int err;
  for(size_t i=0;i<thread_count;++i){
    thread_id[i]=i;
    if((err = pthread_create(thread+i,NULL,talker,thread_id+i))){
      cerr << "failed to create pthread: " << strerror(err) << endl;
      exit(-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;
      exit(-17);
    }
  }
}

我已经用 编译它g++ -pthread -o repro repro.cpp并用 运行./repro。我在输出中看到的是,所有事情都是轮流同时发生的:首先所有 pthread 运行 vfork,然后全部等待一秒钟,然后在子进程现实中“唤醒”,然后所有子进程运行 exec(),最后所有父进程都醒来向上。

对我来说,这证明如果一个 pthread 调用 vfork,它不会挂起其他 pthread - 如果挂起,那么它们将无法调用 vfork(),直到调用 exec() 为止。

答案3

当子进程产生时,父进程是否被挂起?

是的。

如果是的话为什么?

因为父进程和子进程共享地址空间。特别是堆栈的地址空间。如果父进程尝试继续,它可能会调用另一个函数并丢弃子进程的调用堆栈。所以它不会运行直到exec()_exit()

它们可以并行运行(如线程)?毕竟线程和进程都调用相同的clone()函数。

的确。仅挂起调用线程。往上看。

父进程如何知道子进程已经退出?

它调用wait4().相反,你会问父母如何知道要继续。事实并非如此。当两个定义的事件之一发生时,内核使其再次运行。

如果我们从子进程返回会发生什么?

vfork()返回到已释放的堆栈帧,接下来return;是通往未定义行为的快速路径。

答案4

如果我们从子进程返回会发生什么?

“但是,在子上下文中运行时从调用 vfork() 的过程返回是行不通的,因为从 vfork() 的最终返回将返回到不再存在的堆栈帧。”

我并不从字面上理解这一点。在每个 POP(或 RETURN)指令之后,堆栈区域中的字节并不会真正消失。但是,如果您返回、继续运行并执行 PUSH(或 CALL)指令,它将替换堆栈上的先前值。

vfork()如果您调用然后执行任何操作来修改进程的任何内存,这只是通常会发生的奇怪事情的一个突出示例。

[从技术上讲,我假设与 Linux 中的行为相同。 vfork() 的其他行为在技术上是 POSIX 允许的。我不知道是否有人发现了 POSIX 技术灵活性的用途,除了提供与 fork() 相同的 vfork() ]。

相关内容