Linux进程的地址空间
1. 前言
在编写程序语言的代码时,打印输出一个变量的地址时,这个地址在内存中是以什么形式存在的?一个地址可以存储两个不同的值吗?
运行以下代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("father is running, pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if(id == 0)
{
//child
int cnt = 0;
while(1)
{
printf("I am child process, pid: %d, ppid: %d. g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
cnt++;
if(cnt == 5)
{
g_val = 300;
printf("I am child process, change %d -> %d\n", 100, 300);
}
}
}
else
{
//father
while(1)
{
printf("I am father process, pid: %d, ppid: %d. g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
}
这个代码打印了父进程和子进程的数据。运行结果发现,在某个时刻
g_val
的值由100改成300,但它的地址没有改变,也就是说此时同一个“地址”里存储了两个不同的值。
发生这种现象的原因,是**不同的进程由各自的地址空间。**地址空间本质就是内核中的一个结构体对象。
2. 地址空间
每个进程都有自己独立的地址空间和独立的页表,页表储存着虚拟地址和物理地址的映射。子进程会拷贝父进程的很多内核数据结构(包括地址空间和页表)。当子进程的数据发生改变时,OS会将被改变的数据进行写时拷贝,存入新的物理地址,同时子进程的页表中对应的物理地址映射发生改变。
在程序中打印的是虚拟地址,但真实的物理地址不一定相同。这是父进程和子进程的变量有相同的“地址”,但变量数据可以不同的原因。
为什么要有地址空间?
1.将内存空间的无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域。
物理空间中的堆区、栈区、代码区、数据区、共享区、命令行参数和环境变量可能都是无序的,但是通过页表的映射关系可以将无序变为有序管理起来。
2.允许进程管理模块和内存模块进行解耦。
在实际的进程运行中,代码申请的空间短时间内不一定使用。OS可以把代码申请的空间的虚拟地址映射的物理地址先让给别的进程使用,等该进程需要使用的时候再把这块物理地址留给该进程。这种类似于写时拷贝的策略就是解耦,能够提升物理地址的使用效率。
3.拦截非法请求。
进程在运行时,如果遇到非法请求,如代码非法访问了已经释放的空间,在物理地址的层面没有办法阻止这种访问。但在地址空间中,OS会查询页表找对应的地址映射,如果没找到映射,则说明这个访问是非法访问,OS会把这个请求拦截下来(即报错)。
2.1 内核空间
在地址空间的大小一共有4GB,其中[0,3]GB 是用户空间,[3,4]GB是内核空间。我们所写的变量、函数只能够占用用户空间,而系统调用等由操作系统执行的操作都在内核空间中。内核空间的数据有内核级页表来映射到物理地址,与用户级页表不同的是,每一个进程的地址空间有一个用户级页表,但内核级页表是所有进程的内核空间共用一张内核级页表。
3. 页表
3.1 页表介绍
页表中不仅存储着虚拟地址和物理地址的映射,它内部还有各个地址的rwx权限的信息。CPU的cr寄存器中的cr3( 页目录基地址寄存器)存储着页目录的起始地址,所以CPU通过MMU、cr等寄存器,就可以直接访问物理地址。
并且OS在查页表时识别到错误也会有不同判断和处理方式:
1.是不是数据不在物理空间上
引发缺页中断
2.是不是数据需要写时拷贝
进行写时拷贝
3.都不是以上情况,进行异常处理
3.2 内存结构
OS进行内存管理,不是以字节为单位的,而是以内存块为单位的,默认大小4KB。一个内存块叫做页框(页帧)。
CPU访问内存时,不是按照实际需要的大小访问,无论是存取指令还是存取数据,所访问的存储单元都趋于聚集在一个较小的连续区域中(4KB),这样的访问方式称为局部性原理。局部性原理能减少写时拷贝的次数,以空间换时间,提高CPU的访问效率。
所以,OS在管理内存时,也是通过一个结构体类型来管理的:
struct page
{
int flag;//是否被占用,是否是脏页,是否被锁定
int mode;
}
struct page memory[1048576];//用数组管理。对内存的管理工作转换成了对这个数组的增删查改
3.2 页表的实际结构
页表实际并不是一张表,因为假设在32位系统下,一个虚拟地址占4个字节,地址空间有4GB,如果页表是一张表,页表包含映射关系和状态标记符,总大小至少超过300GB,显然与事实不符。
在32位中,一个虚拟地址有32个比特位,其中高10个比特拿来作为页目录的索引。页目录每个下标的内容存着页表的地址,指向一个页表,页目录一共有1024个索引;中间的10个比特位拿来作为页表的索引,页表里的每个内容存着指向页框的起始地址;低12位比特位从[0,4095]共4096个,拿来作为偏移量在页框内偏移,以此便能找到页框内的每一个内容。
经过这样的变换,页表的最终大小为
1024
∗
4
b
i
t
e
+
1024
∗
2
K
B
=
2
M
B
+
4
K
B
1024*4bite+1024*2KB=2MB+4KB
1024∗4bite+1024∗2KB=2MB+4KB
在64位的系统中,页表经过更多次的变换,但原理相同。
虚拟地址&(0xFFF)=页框号