进程和子进程
父子进程有独立的数据段、堆、栈,共享代码段
Linux中每个进程都有4G的虚拟地址空间(独立的3G用户空间和共享的1G内核空间),fork()创建的子进程也不例外。子进程资源的由来:
- 1G内核空间既然是所有进程共享,因此fork()创建的子进程自然也将拥有;
- 3G的用户空间是从父进程进程而来。
进程fork和写时复制
fork()创建子进程时继承了父进程的数据段、代码段、栈段、堆,注意从父进程继承来的是虚拟地址空间,同时也复制了页表(没有复制物理块)。因此,此时父子进程拥有相同的虚拟地址,映射的物理内存也是一致的(独立的虚拟地址空间,共享父进程的物理内存)。
写时复制技术:由于父进程和子进程共享物理页面,然而,只要页面被共享,它们就不能被修改,内核将其标记为“只读”。无论父进程和子进程何时试图对一个共享的页面执行写操作,这时内核就把这个页复制到一个新的页面给这个进程,并标记为可写,同时修改页表,把原来的只读页面标记为“可写”,留给另外一个进程使用。
注意:内核在为子进程分配物理内存时,并没有将代码段对应的数据另外复制一份给子进程,最终父子进程代码段映射的是同一块物理内存(代码段在单个进程内部本来就是只读的)。
fork之后跟随exec
fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,linux中引入了“写时复制“技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。由于写时复制,在fork之后exec之前两个进程有独立的虚拟地址空间,共享物理内存。只有其中一方需要写操作时,再为子进程的数据段、栈、堆分配物理空间。但在子进程上调用exec时,会清空栈、堆,以及和父进程共享的空间,重新加载新的代码段,这样避免了“写时复制”拷贝共享页面的机会,父进程也同时独自拥有了原来共享的物理内存(可对其读写操作)。
fork出来子进程之后,父子进程哪个先调度直接决定了是否需要拷贝的问题?内核一般会先调度子进程,因为很多情况下子进程是要马上执行exec,而避免无用的复制。如果父进程先调度很可能写共享页面,会产生“写时复制”的无用功。所以,一般是子进程先调度滴。
在网上看到还有个细节问题就是,fork之后内核会通过将子进程放在队列的前面,以让子进程先执行,以免父进程执行导致写时复制,而后子进程执行exec系统调用,因无意义的复制而造成效率的下降。
如果不是因为exec,子进程执行时很可能修改内存,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
一次调用两次执行
int main(int argc, charchar ** argv ){
//由于会返回两次,下面的代码会被执行两遍
//如果成功创建子进程:
//1. 父进程返回子进程ID,因此(父进程)会走一遍“分支3”
//2. 子进程返回0,因此(子进程)会走一遍“分支2”
pid_t pid = fork();
if (pid < 0){ //分支1
fprintf(stderr, "error!");
}else if( 0 == pid ){//分支2
printf("This is the child process!");
_exit(0);
}else{//分支3
printf("This is the parent process! child process id = %d", pid);
}
//可能需要时候wait或waitpid函数等待子进程的结束并获取结束状态
exit(0);
}
僵尸进程和孤儿进程
正常情况下,子进程是通过父进程创建的,子进程再创建新的子进程。但是子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程到底什么时候结束。当一个进程完成它的工作终止之后,它的父进程需要调用wait()或者waitpid()系统调用取得子进程的终止状态
僵尸进程
当一个子进程结束运行(一般是调用exit、运行时发生致命错误或收到终止信号所导致)时,子进程的退出状态(返回值)会回报给操作系统,系统则以SIGCHLD信号将子进程被结束的事件告知父进程,此时子进程的进程控制块(PCB)仍驻留在内存中。一般来说,收到SIGCHLD后,父进程会使用wait系统调用以获取子进程的退出状态,然后内核就可以从内存中释放已结束的子进程的PCB;而如若父进程没有这么做的话,子进程的PCB就会一直驻留在内存中,也即成为僵尸进程
简单来说,当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程。wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.
孤儿进程
孤儿进程则是指当一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。