据我所知,当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() ]。