本文要讲解以下内容:
1.程序地址空间
2.fork() / vfork()函数
一.程序地址空间。
(1.)首先我们看一下C语言的内存空间分布图:
(2.)接下来我们更加深度的理解以下进程的地址空间
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<errno.h>
5
6 int g_val = 0;
7
8 int main()
9 {
10 pid_t id = fork();
11 if(id<0)
12 {
13 perror("fork");
14 return -1;
15 }
16 else if(id == 0)
17 {
18 printf("child[%d]: %d : %p\n",getpid(),g_val,&g_val);
19 }
20 else
21 {
22 printf("parent[%d]: %d : %p\n",getpid(),g_val,&g_val);
23 }
24
25 sleep(1);
26 return 0;
27 }
更具上边的代码以及运行结果我们可以看出,父子进程的id不同,但是定义的全局变量g_val的结果和地址是相同的。这是由于子进程是按照父进为模板拷贝的,如果父进程不做任何修改那么子进程也不会有变化
接下来我们对上边代码做一定的修改:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<errno.h>
5
6 int g_val = 0;
7 int main()
8 {
9 pid_t id = fork();
10 if(id<0)
11 {
12 perror("fork");
13 return -1;
14 }
15 else if(id == 0)
16 {
17 g_val = 100;
18 printf("child[%d]: %d : %p\n",getpid(),g_val,&g_val);
19 }
20 else
21 {
22 printf("parent[%d]: %d : %p\n",getpid(),g_val,&g_val);
23 }
24
25 sleep(1);
26 return 0;
27 }
我们可以看到输出的全局变量的地址一样但是内容却不一样,这就很神奇,那么我们可以初步得出以下结论:
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 地址是一样的,说明,该地址绝对不是物理地址
- 在Linux地址下,这种地址叫做虚拟地址
- 我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户是看你不到的,由OS统一管理
- OS必须把虚拟地址变为物理地址
下边我们用图来解释进程的地址空间
注意:
上面的图就足以说明,同一个变量只是虚拟地址相同,但是内容通过页表被映射到物理地址空间的不同位置,这也就很好的解释了上边那段代码为何地址相同,值不同
二. fork() / vfork()函数
fork函数
1.首先利用man fork 来认识fork函数
总结:
- 如果调用失败直接返回-1
- 如果调用成功,则有两个返回值,子进程返回0,父进程返回子进程id
- 父子进程代码共享,数据各自开辟空间,私有一份。(采用写时拷贝)(上边那段代码为例)
什么是写时拷贝??
通常,父子代码共享,父子不在写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式进行,请见下图:
2.我们利用fork函数的返回值来进行分流,将父子进程分开
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<errno.h>
4 int main()
5 {
6 int ret = fork();
7 if(ret < 0)
8 {
9 perror("fork");
10 return -1;
11 }
12 else if(ret == 0)
13 {
14 printf("I am child: %d! , ret: %d\n",getpid(),ret);
15 }
16 else
17 {
18 printf("I am father: %d! , ret: %d\n",getpid(),ret);
19 }
20 sleep(1);
21 return 0;
22 }
我们可以看出我们不仅将父子进程进行了分流还验证了父子进程的返回值
3.进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
4.当一个进程调度fork之后,就有两个二进制代码相同的进程。而且他们运行到相同的地方,但是他们都是各自运行互不干扰,看下边代码:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<errno.h>
4 #include<stdlib.h>
5 int main()
6 {
7 pid_t pid;
8
9 printf("Before:pid is %d\n",getpid());
10 if((pid = fork()) < 0)
11 {
12 perror("fork");
13 exit(1);
14 }
15 printf("After:pid is %d ,fork return %d\n",getpid(),pid);
16 sleep(1);
17 return 0;
18 }
我们可以看到在打印了三行代码一行fork之前,两行fork之后,那么为什么在fork之后没有打印fork之前的pid尼???
接下来我们用图解告诉大家
所以我们可以看到fork之前父进程独立执行,fork之后,父子进程两个执行流分别执行。
注意:fork之后,谁先执行完全由调度器决定
5.fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。
- 一个进程要执行一个不同的程序
6.fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
vfork
vfork的返回值以及创建失败的返回值和fork一样,这里就不做详细说明了
注意:
- vfork用于创建一个子进程,而子进程和父进程共享地址空间,fork的子进程具有独立的地址空间
- vfork保证子进程先运行,在他调用exit或(exec)之后父进程才可能被调用运行
下边是vfork的使用程序:
2 #include<unistd.h>
3 #include<errno.h>
4 #include<stdlib.h>
5 int ret = 100;
6 int main()
7 {
8 pid_t pid;
9
10 if((pid = vfork()) < 0)
11 {
12 perror("fork");
13 exit(1);
14 }
15 else if(pid == 0)
16 {
17 sleep(5);
18 ret = 200;
19 printf("child ret: %d\n",ret);
20 exit(0);
21 }
22 else
23 {
24 printf("parent ret: %d\n",ret);
25 }
26
27 return 0;
28 }
根据上边的结果我们可以看子进程改变了父进程的变量值,因此子进程在父进程的地址空间中运行
2.fork和vfork的区别
- fork的子进程具有独立的地址空间,vfork父子进程共享地址空间
- vfork创建的子进程,在子进程退出后,父进程才可以运行
- fork 子进程拷贝父进程的数据段,代码段,vfork子进程与父进程共享数据段
- fork 父子进程的执行次序不确定,vfork 保证子进程先运行,在调用exec 或exit 之前与父进程数据是共享的,在它调用exec
或exit 之后父进程才可能被调度运行。 - vfork 保证子进程先运行,在她调用exec 或exit 之后父进程才可能被调度运行。如果在 调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。