首先我们要明白一个概念:什么是地址?
地址是指向内存区域的一个编号,每一个进程都有4G的进程地址空间。
那么系统到底是如何给进程分配内存的呢?
结论:分页管理+虚拟地址空间
看图进一步理解
如上图是系统给进程分配内存的逻辑图, 操作系统用一个进程控制块的数据结构(进程属性的集合)来描述进程信息,也就是PCB
(process control block),Linux操作系统下的PCB是 task_struct
,程序运行时它会被装载到RAM里来储存进程信息。
如下图是task_struct
结构的具体信息:
如图可看出task_struct
结构中包含mm_struct
,其中task_struct
还包含进程的很多信息,如:进程唯一标识符pid
、进程状态、进程优先级等。
mm_struct
这个结构体描述出了虚拟地址空间,页表记录了虚拟地址空间与物理地址空间之间的转换关系。其中*mm
指向内存区描述符的指针,*mm
结构体中的pgd指向页全局目录,*mmap
指向线性区对象的链表头,通过页表进行物理映射。
举例说明进程地址空间管理方式:
我们知道fork()
函数可以创建子进程,而fork()
之后产生的子进程,与父进程共享一份代码,且数据各自私有一份,当我们任意一个写入数据时,就会发生写时拷贝。
写时拷贝:当fork之后创建子进程之后,父子进程共享代码块,其数据起初也是共享的,它们通过其进程控制块(PCB)内的虚拟地址空间通过页表映射到物理内存上的位置也是一样的,当父子之间任意一个写入或修改数据,便以写时拷贝的方式各自拥有数据的一份副本,也就是在这个时候,其对应物理内存上的位置发生了变化。
#include
#include
#include
int value = 0;
int main()
{
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
exit(1);
}
else if(pid ==0)
{ //child
printf(" I am child: %d, value = %d, %p\n", getpid(), value, &value ");
}
else
{
printf(" I am child: %d, value = %d, %p\n", getpid(), value, &value ");
}
sleep(1);
return 0;
}
结果发现父子进程打印的变量值和地址都相同,因为子进程并未对变量进行任何修改,我们在修改上述代码,在子进程中将value
值改为10,再次运行:
#include
#include
#include
int value = 0;
int main()
{
pid_t pid = fork();
if(pid < 0)
{
perror("fork");
exit(1);
}
else if(pid ==0)
{ //child
value = 10;
printf(" I am child: %d, value = %d, %p\n", getpid(), value, &value ");
}
else
{
printf(" I am child: %d, value = %d, %p\n", getpid(), value, &value ");
}
sleep(1);
return 0;
}
由上图我们发现,子进程跟父进程的value
是同一块地址,但是value
的值不一样了,按照我们的程序地址空间来说,同一个地址内的内容应该是一样的,这是为什么呢?
实际上程序的地址空间不是内存,而是由一个mm_struct
结构体描述的,因此程序地址空间应该叫进程的虚拟地址空间。
所以我们获取到的地址都是虚拟地址,只是一个编号而已,并不是真正的物理内存地址。
所以当我们调用fork()
函数的时候,父子进程在物理地址上共用一份代码,并且数据各自私有,其对应的虚拟地址空间的内容是一样的,虽然父进程与子进程的value地址看似一样,实则在物理内存上并不一致。
那么问题又来了,我们访问一个变量的时候是如何通过虚拟地址访问到内存的呢?
这个时候我们就要引入一个概念:页表
页表的定义:内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字(甚至字节),但是内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。正因为如此,MMU以页(page)大小为单位来管理内存中的页表。从虚拟内存的多角度来看,页就是最小单位。
页表是一个结构体,它记录了虚拟地址与物理地址之间的转换关系。
由此可知,页表的作用是将访问的虚拟地址通过映射转换为物理地址进而访问到内存的。
这么做的目的是为了更好的保护物理内存,防止物理内存被破坏以及侵占。
此外,因为页表中还记录了要访问的这块地址的属性,所以页表还有一个重要功能就是:内存访问控制----通过对虚拟地址的权限标志(只读等)来实现对内存的访问控制。
总结:当我们访问一个变量的时候,其实就是通过页表将虚拟地址转换为物理地址而进行访问的。
拓展:早期的内存分派机制、
进程地址空间管理超详细版(包含缺页异常、反向映射等)