一次调用两次返回:一次在父进程中返回,返回值是子进程的pid;另一次是在子进程中返回,返回值为0。在Linux下如果内存没有被写的话,那么父子进程是共用地址空间的,所以内存中的同一个fork函数会在两个进程中调用到。在父进程中返回的就是子进程id,子进程中返回的是0。
子进程是父进程的副本:子进程获得父进程的数据段【意味着全部变量不互相影响】、堆栈段、栈的副本。(所谓副本是重新复制一份(或者写时复制),并且不共享)。子进程和父进程共享代码段,同一个物理内存段。
由于在fork之后经常跟随着exec,所以现在的很多实现并不执行一个父进程数据段、栈和堆的完全复制。作为替代,使用了写时复制(Copy-On-Write,COW)技术。这些区域由父、子进程共享,而且内核将它们的访问权限改变为只读的。如果父、子进程中的任一个试图修改这些区域,则内核只为修改区域的那块内存制作一个副本,通常是虚拟存储器系统中的一“页”。
子进程和父进程执行顺序:在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。如果要求父、子进程之间相互同步,则要求某种形式的进程间通信。
文件共享:在重定向父进程的标准输出时,子进程的标准输出也被重定向。实际上,fork的一个特性是父进程的所有打开文件描述符都被复制到子进程中。父、子进程的每个相同的打开描述符共享一个文件表项。
(1)父进程等待子进程完成。在这种情况下,父进程无需对其描述符做任何处理。当子进程终止后,它曾进行过读、写操作的任一共享描述符的文件偏移量已执行了相应的更新。
(2)父、子进程各自执行不同的程序段。在这种情况下,在fork之后,父、子进程各自关闭它们不需要使用的文件描述符,这样就不会干扰对方使用的文件描述符。这种方法是网络服务进程中经常使用的。
父进程和子进程共享的属性:
- 实际用户ID、实际组ID、有效用户ID、有效组ID。
- 附加组ID。
- 进程组ID。
- 会话ID。
- 控制终端。
- 设置用户ID标志和设置组ID标志。
- 当前工作目录。
- 根目录。
- 文件模式创建屏蔽字。
- 信号屏蔽和安排。
- 针对任一打开文件描述符的在执行时关闭(close-on-exec)标志。
- 环境。
- 连接的共享存储段。
- 存储映射。
- 资源限制。
父、子进程之间的区别是:
- fork的返回值。
- 进程ID不同。
- 两个进程具有不同的父进程ID:子进程的父进程ID是创建它的进程的ID,而父进程的父进程ID则不变。
- 子进程的tms_utime、tms_stime、tms_cutime已经tms_ustime均被设置为0.
- 父进程设置的文件锁不会被子进程继承。
- 子进程的未处理的闹钟(alarm)被清除。
- 子进程的未处理信号集设置为空集。
fork有下面两种用法:
(1)一个父进程希望复制自己,使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的——父进程等待客户端的服务请求。当这种请求到达时,父进程调用fork,使子进程处理此请求。父进程则继续等待下一个服务请求到达。
(2)一个进程要执行一个不同的程序。这对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
以下代码输出-的个数:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
int i;
for(i=0; i<2; i++){
fork();
printf("-");
}
wait(NULL);
wait(NULL);
return 0;
}
带有阴影的两个子进程,复制了其父进程的文件描述符,复制了父进程的标准输出缓冲区(标准IO缓冲)的内容。
参考:https://www.cnblogs.com/nufangrensheng/p/3509492.html
为什么子进程先运行?
内核一般会先调度子进程,因为很多情况下子进程是要马上执行exec,会清空栈、堆。。这些和父进程共享的空间,加载新的代码段。。。,这就避免了“写时复制”拷贝共享页面的机会。如果父进程先调度很可能写共享页面,会产生“写时复制”的无用功。所以,一般是子进程先调度滴。
int glob = 6; /* external variable in initialized data */
char buf[] = "a write to stdout\n";
int main(void)
{
int var; /* automatic variable on the stack */
pid_t pid;
var = 88;
if(write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) -1)
err_sys("write error");
printf("before fork\n"); /* we don't flush stdout */
if((pid = fork()) < 0){
err_sys("fork error");
}
else if(pid == 0){ /* child */
glob++; /* modify variables */
var++;
}
else{
sleep(2); /* parent */
}
printf("pid = %d, glob = %d, var = %d\n", getpid(), glob, var);
exit(0);
}
虚拟地址肯定相同,不能看出是否采用了写实复制。
区别于vfork:
vfork()函数:不会将父进程的地址空间完全复制到子进程中,因为vfork通常立即调用exec函数,分配地址空间,映射自己的物理内存,不需要映射到父进程的物理内存。并且,vfork的子进程先运行结束,父进程才运行。vfork的子进程在调用exec函数之前,运行在父进程的空间中。
fork需要复制页表项,是页表项指向相同的物理内存。vfork不需要复制页表项。