子进程是父进程的一段拷贝,在fork的时候除了pid,父进程的资源都拷贝给了子进程,假如在fork之前代码中有一个全局变量,那么在fork之后变量在内存中的地址是不会发生变化的,也就是说父进程和子进程中这个变量的地址是一样的,假如我们定义了一个全局变量,那么我们知道,在程序的运行的时候,他存放在 全局数据区了 也就是BSS区.
接着看一段代码
#include<stdio.h>
#include<unistd.h>
#include<error.h>
#include<stdlib.h>
int g_val = 100;
int main () {
int pid = fork();
if(pid < 0 ){
perror("fork error");
return 0;
}
if(pid == 0){
g_val = 0;
printf("child: %d g_val = %d,&g_val = %p\n",getpid(),g_val,&g_val);
}
if(pid > 0){
sleep(3);
printf("parent : %d g_val = %d,&g_val = %p\n",getpid(),g_val,&g_val);
}
return 0;
}
运行结果
上面的程序很简单就是fork了一份子进程,如果按照我们开头所说的那么输出的结果中可以看出地址是一样的,验证了我们之前所说的话,子进程是父进程的拷贝,但是父进程中的g_val也应该是100,但是他的值却是0,这是怎么回事呢,他们的输出地址是一样的,但是他们的内容不一样,那么我们就有几个猜想了
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
但是地址是一样的,那么就说明这个地址是有玄机的!
那么事实上,在Linux地址下,这种地址叫做虚拟地址!
我们在c/c++语言中所看到的地址,全部都是虚拟地址,我们实际所用的物理地址,用户一概看不到,由系统来做统一的管理,也就是由系统完成虚拟地址和物理地址的对应
也就是说,我们通常所说的,程序的地址空间以及变量空间,就是不准确的,准确的说应该是进程的地址空间,那么进程就应该存在着一个操作虚拟空间地址的关系,以及虚拟空间和物理地址有一个映射的关系
那么就有下图
上一篇我总结了task_struct进程描述符,的相关问题,刚才也已经说了,如果Linux中所说的是虚拟地址的话,那么他们进程以及虚拟地址还有物理地址就存在着相对应的关系,我们在上图中只是给出了,并没有解释,现在来做个解释
task_struct是进程描述符,他是来描述一个进程的,那么既然是描述进程的**,那么就也有关于这个进程的虚拟地址的描述了**
大概在sched.h中的1400行左右就有这个描述
struct mm_struct *mm, *active_mm;
mm_struct所在的文件是mm_types.h,可以在390多行找到关于其的定义
往下查找我们就可以找到相关的虚拟地址中关于栈,堆的定义
每个区域是依靠着两个指针进行维护的,比如[start_data,end_data)是用来维护data段,[start_code,end_data)用来维护code段,[start_brk,brk),用来维护heap和heap的指针。[start_stack,end_stack)是用来维护stack段空间范围。mmap_base是维护共享映射区的起始地址。bss段表示的是所有的未初始化的全局变量,为了效率,对处在bss段的变量,将它们匿名映射到“零页”,这样提高了程序的加载效率。
那么第一步我们就理解了,PCB通过mm_struct这个结构体里面的数据描述,来组织管理进程的地址空间
值得注意的是,每一个进程都有每一个独有的PCB,也就是说每一个进程有每一个独享的mm_struct,这样进程就有一个独立的地址空间了,这样和其他进程就不会冲突了,当进程之间的地址空间被共享的时候,我们可以理解为这个时候是多个进程使用一份地址空间,这就是线程。
每一个进程的用户空间在32位的平台上就是上面这个图的情况,对于物理内存当中的内核kernel,是只存在一份,所有的进程是用来共享的,
内核当中会利用PCB(进程控制块)来管理不同的进程(PCB在内核中以链表的形式串起来(也叫任务队列)),这样。对于linux的体系结构来说,linux当中为了保护虚拟内核空间不被修改,所以linux体系结构是这样的
这种三层的体系结构,保证进程只能对最外面的应用程序进行修改,保证了内存的安全性。
接下来就是第二步了,虚拟地址空间就和地址空间有一个映射的关系,我们通过上面两个图中可以看出,有一个page table (页表)这个东西,从图中就可以看出是完成虚拟地址到物理地址的映射关系的那一个纽扣了
关于页表
Linux kernel 使用内存管理的时候,采取的是页式的管理方式,应用程序给出的内存地址是虚拟地址,是经过若干层的页表的转换才能得到真正的物理地址,所以相对来说,进程的地址空间是一份虚拟的地址空间,每一个地址通过页表的转换映射到所谓的物理地址空间上。在这里所共享的1G的kernel在内存地址是只存一份的,但是对于每一个进程其他的3G的空间,是存储其他不同的东西,另外,页表具有权限限定,这样也就提供给了每块内存区域,比如我定义了:
char * p=“hello world”;
这里的“hello world ”是一个常量字符串,它被存放在只读常量存储区(虚拟地址中),所以这个区域的页表的属性就是只读,这样就可以高效的维护整个进程的地址空间。
每一个进程都会有一个进程描述符,task_struct,task_strust当中的mm指针指向每个进程的内存描述符,而对于每个mm_struct,有都会有单独的页表,
pgt区间是用来维护页表的目录,每一个进程的都有自己的页表目录,需要注意进程的页目录和内核的页目录是不一样的,当程序调度器调度程序运行的时候,这个时候这个地址就会转换成为物理地址,linux一般采用三级页表进行转换