一、fork作用
我们都知道fork可以用于进程的创建,那首先我们来了解一下fork的两种用法才真正的了解我们何时才会用到fork来进行进程的创建
用法一:一个父进程希望复制自己,使父、子进程同时执行不同的代码段。
这在网络服务进程中是最常见的——父进程等待客户端的服务请求,当达到请求到达时,父进程调用fork,时子进程处理此请求。父进程则等待下一个服务请求到达。
用法二:一个进程要执行一个不同的程序
这个用法对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。此概念我会在后续的博文中持续更新。
注意:在某些操作系统中会把fork和exec这两个操作组合成一个,并称其为spawn。但是在UNIX系统中是将两个操作分开的,因为在很多场合需要单独使用fork,后面不跟随exec操作,使得子进程在这两个操作之间可以更改自己的属性,例如I/O重定向,用户ID、信号安排等等。
了解了fork的用法过后,就像用刀要用到关键地方,此时我们用fork创建进程也就知道了拿来做什么了,接下来就得好好了解一些这把刀了~
二、fork()特性
1、父子进程之间的关系
这时候,我们就要明确两个概念了——父进程和子进程。由fork创建的进程叫做子进程。他的函数原型如下:
#include<unsitd.h>
pid_t fork(void);
他们的关系如下图所示:
如下代码可以进行父子进程之间的关系验证:
int main()
{
pid_t n = fork();
assert(-1 != n);
if(0 == n)
{
printf("Hello: mypid = %d, myppid = %d\n", getpid(), getppid());
}
else
{
sleep(1); // 保证新进程先执行完
printf("World: n = %d, mypid = %d\n", n, getpid());
}
exit(0);
}
运行结果如下:
2、父子进程返回情况
fork函数被调用一次,但是返回两次。两次返回的唯一区别就是子进程返回的是0,而父进程返回值则是新子进程的进程ID。
父进程返回值是新子进程的进程ID的原因是因为一个进程的子进程可以有多个,并且没有一个函数是一个进程可以获得其所有子进程的进程ID。
但是子进程返回值确是0是因为一个进程只会有一个父进程,所以子进程总是可以调用getppid来获得父进程的ID
如下面这个程序,请问输出结果是什么?
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
int main()
{
pid_t n = fork();
assert(-1 != n);
if(0 == n)
{
printf("Hello\n");
}
else
{
printf("World\n");
}
exit(0);
}
运行结果如下:
原因分析:这个结果就和fork的返回值有关,调用一次有两个返回结果,首先打印world,因为子进程返回的值是0,所以打印hello.
3、父子进程执行情况
一般来说,在fork之后是父进程先执行还是子进程先执行是不确定的。这取决于内核所使用的调度算法。如果要求父子进程之间相互同步则要求某种形式的进程间通讯。fork方法调用之后,父子进程都从fork调用之后的执行开始执行,该指令大致分为call fork 和mov pid eax两条。父子进程也就是从mov指令结束过后开始执行的。如下图所示的程序:
根据上述的执行情况,该程序的运行结果是不定的,父子进程谁先执行谁后执行都是随机的,如下图所示为执行两次该程序的不同结果:
三、写时拷贝技术
为了更加形象的弄明白这个技术,我们首先通过部分代码实例来挨个分析。
下面这两段代码是测试全局、局部、堆区数据,父子进程是否共享,子进程修改对父进程没有影响变化
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <string.h>
int gdata1 = 10;
static gdata2 = 10;
int main()
{
int ldata1 = 10;
static ldata2 = 10;
int *ptr = (int *)malloc(4);
*ptr = 10;
pid_t n = fork();
assert(-1 != n);
if(0 == n)
{
printf("child: %d, %d, %d, %d, %d\n", gdata1, gdata2, ldata1, ldata2, *ptr);
gdata1 = 20;
gdata2 = 20;
ldata1 = 20;
ldata2 = 20;
*ptr = 20;
printf("child: %d, %d, %d, %d, %d\n", gdata1, gdata2, ldata1, ldata2, *ptr);
}
else
{
sleep(2); // 保证子进程将数据已经修改并输出
printf("father: %d, %d, %d, %d, %d\n", gdata1, gdata2, ldata1, ldata2, *ptr);
}
exit(0);
}
运行结果为:
int gdata1 = 10;
static gdata2 = 10;
int main()
{
int ldata1 = 10;
static ldata2 = 10;
int *ptr = (int *)malloc(4);
*ptr = 10;
pid_t n = fork();
assert(-1 != n);
if(0 == n)
{
printf("child: 0x%x, 0x%x, 0x%x, 0x%x, 0x%x\n", &gdata1, &gdata2, &ldata1, &ldata2, ptr);
gdata1 = 20;
gdata2 = 20;
ldata1 = 20;
ldata2 = 20;
*ptr = 20;
printf("child: 0x%x, 0x%x, 0x%x, 0x%x, 0x%x\n", &gdata1, &gdata2, &ldata1, &ldata2, ptr);
}
else
{
sleep(2); // 保证子进程将数据已经修改并输出
printf("father: 0x%x, 0x%x, 0x%x, 0x%x, 0x%x\n", &gdata1, &gdata2, &ldata1, &ldata2, ptr);
}
exit(0);
}
运行结果:
仔细观察上述代码我们会发现子进程对全局、局部、堆区数据的修改并不会影响父进程的数据和地址,并且,父子进程的地址一样。父子进程的地址都一样是因为在这儿所打印的地址都是逻辑地址,就是我们所说的程序上的偏移地址,要通过页表映射才能转成物理地址。操作系统为每一个进程维护一个页表,虽然说他们的逻辑地址都是一样的但是物理地址却是不一样的。
1、概念
有了上述程序的铺垫,就可以引出我们写时拷贝技术的概念:不执行一个父进程数据段、栈和堆的完全复制,这些区域由父、子进程共享,而内核将他们的访问权限改为只读的。如果父、子进程任何一个试图修改这些区域,则内核只为修改区域的那快内存制作一个副本,并且是以虚拟存储器系统中的“一页”为单位赋值。
2、特点
写时拷贝技术有三个特性,下面我们就通过数据是如何拷贝的来了解一下这三个特性。代码实现如下:
int main()
{
int size = 1024 * 1024 * 1024; // 设置申请的基数位1G
char *ptr = (char *)malloc(size * 2); // 一共申请2G空间
// 循环使用申请的空间
int i = 0;
for(; i < 32; ++i)
{
sleep(1);
memset(ptr + i * 1024 * 1024 * 34, 'a', 1024 * 1024 * 34);//初始化32兆
}
pid_t n = fork();
assert(-1 != n);
if(0 == n)
{
// 循环使用申请的空间
printf("child start\n");
int i = 0;
for(; i < 32; ++i)
{
sleep(1);
memset(ptr + i * 1024 * 1024 * 32, 'b', 1024 * 1024 * 32);//相当于对数据的一个修改
}
}
else
{
sleep(35);
}
free(ptr);
exit(0);
}
在上述这个代码中我们首先用malloc申请了1G的空间并循环使用申请的空间。我们来思考一下fork()实现会不会直接将父进程的所有数据空间拷贝给子进程呢?
首先,程序还没执行之前,我们系统的cpu和交换分区运行情况如下:
当fork执行后,系统的cpu和交换分区运行情况如下:
我们发现,在执行fork之后,cpu并没有一下子就占用很多内存,交换分区也由执行之前的没有到慢慢增加。由此,就可以得到我们写时拷贝技术的的第一个特性:.malloc申请空间并不是malloc成功后就直接将物理内存空间分配给用户,而是在用户使用的时候才会给用户分配物理内存空间。malloc调用成功只是将虚拟地址,空间上的堆区空间分配给用户。
第二个特性:fork方法并不会直接将父进程的数据空间复制给子进程,而是子进程在修改数据空间上的数据时,才会给子进程分配空间
再等待一会儿,程序结束,回收用户空间系统的cpu和交换分区运行情况如下:
我们可以看到,用户空间的回收则是呈阶梯式下降,就可以得到我们写时拷贝技术的的第三个特性:释放空间时,会直接将物理内存空间释放