【Linux】——进程创建fork

一、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和交换分区运行情况如下:
在这里插入图片描述
我们可以看到,用户空间的回收则是呈阶梯式下降,就可以得到我们写时拷贝技术的的第三个特性:释放空间时,会直接将物理内存空间释放

发布了98 篇原创文章 · 获赞 9 · 访问量 3646

猜你喜欢

转载自blog.csdn.net/qq_43412060/article/details/105442802