函数原型
#include //头文件
pid_t fork(void);
//返回值:成功时父进程返回子进程的id,子进程返回0;失败时父进程返回-1;先返回谁是不确定的,不同平台不一样
我们可以用一段程序来测试fork函数
#include
#include
#include
#include
int main()
{
int count = 1;
pid_t pid ;
pid = fork();
if(pid < 0)
{
perror("fork error:");
return -1;
}
else if(pid == 0)
{
count++;
printf("this is child, pid=%d,count=%d(%p)\n",getpid(),count,&count);
}
else
{
wait(NULL);
printf("this is father, pid=%d,count=%d(%p)\n",getpid(),count,&count);
}
return 0;
}
由运行结果可以看到,父子进程的pid不一样;count值不一样,但是虚拟地址相同。
解释:
1.每个进程被创建都有一个独一无二的标识符,所以父子进程pid不一样。
2.然后在程序中当pid等于0,也就是当运行子进程的时候我们让count++,而父进程不做变化,之后打印出来的两个count值不一样,可以说明父子两个进程是各自拥有自己的数据空间(新版本的fork只复制父进程的页表,与父进程共享虚拟地址空间),也就是他们得数据是不共享的。
3.但是两个count的地址一样(这里打印出来的是虚拟地址),原因在于虚拟地址和计算机内存中物理地址之间是通过MMU和页表联系在一块的,(虚拟地址映射到物理地址的方法不一样)虽然虚拟地址一样,他们映射到内存中的物理地址不一样,打印出来的count值也就不一样。
根据上面的结果我们可以总结fork函数的以下特点:
内核只为新生成的子进程创建虚拟空间结构,它们复制于父进程的虚拟空间结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父子进程中有更改相应的段的行为发生时,再为子进程相应的段分配物理空间。
Linux的fork()使用写时拷贝(copy-on-write)页实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个地址空间,而是让父进程和子进程共享一个拷贝(物理页面)。只有在需要写入的时候,数据才会复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使地址空间的页的拷贝被推迟到实际发生写入的时候。
对于上面的代码来说,当没有执行count++操作时,此时父子进程是共享物理页面的,内核只为子进程分配了虚拟地址空间和唯一的进程标识符,而当执行count++操作后,内核才会为子进程分配物理页面,并且复制父进程的数据。所以当一个子进程没有进行写操作的时候,内核是不会分配物理页面给子进程的,从而节省了内存,并且提高了运行效率。
#include //头文件
#include
pid_t vfork(void);
#include
#include
#include
#include
int main()
{
int count = 1;
pid_t pid ;
pid = vfork();
if(pid < 0)
{
perror("fork error:");
return -1;
}
else if(pid == 0)
{
count++;
printf("this is child, pid=%d,count=%d(%p)\n",getpid(),count,&count);
exit(0);
}
else
{
wait(NULL);
printf("this is father, pid=%d,count=%d(%p)\n",getpid(),count,&count);
}
return 0;
}
结果与fork的结果类似,但是有一个唯一的区别是count值相等,原因是此时父子进程共享页表项。
参考资料:
Linux—创建进程(fork和vfork的区别)
《linux内核设计与分析》