Linux系统为每一个进程维护一个单独的地址空间,包含了进程的内存分区即:代码段、数据、运行时堆栈,共享库等部分。
【实验与演示】
堆与全局变量的比较:
struct A
{
char ch;
int n;
};
A a;
A b;
int main()
{
A* ptr=new A;
cout<<"a_address:"<<&a<<endl;
cout<<"b_address:"<<&b<<endl;
cout<<"a.ch:"<<a.ch<<endl;
cout<<"a.n:"<<a.n<<endl;
cout<<"ptr-ch:"<<ptr->ch<<endl;
cout<<"ptr-n:"<<ptr->n<<endl;
cout<<"ptr:"<<ptr<<endl;
}
运行结果:
- a b两个为未初始化的结构体,它们位于进程地址空间的.bss段,是请求二进制为0的
- ptr创建的结构体位于运行时堆区域,同样是请求二进制0的
- 运行时堆区域的地址空间 在 .bss段上方,因此地址值更大。
int main()
{
A a;
A b;
A* ptr=new A;
cout<<"a_address:"<<&a<<endl;
cout<<"b_address:"<<&b<<endl;
cout<<"a.ch:"<<a.ch<<endl;
cout<<"a.n:"<<a.n<<endl;
cout<<"ptr-ch:"<<ptr->ch<<endl;
cout<<"ptr-n:"<<ptr->n<<endl;
cout<<"ptr:"<<ptr<<endl;
}
运行结果:
- a b是在栈中创建的局部变量,C++对它们的操作只是简单的改变RSP指针,而不是初始化为0(性能考虑)
- ptr为堆中创建,C++会进行初始化为0(对象类型还将调用默认构造函数)
- 同时注意到:&a的地址是比ptr地址更大,因为栈位于进程地址空间的最上方,其地址值最大。
进一步查看小端序还是大端序:
struct A
{
int ch;
int n;
};
int main()
{
A a;
A b;
A* ptr=new A;
cout<<&a.ch<<endl;
cout<<&a.n<<endl;
cout<<&(ptr->ch)<<endl;
cout<<&(ptr->n)<<endl;
}
运行结果:
操作系统决定大小端序,可以看到Linux下为小端序(网络字节顺序通常为大端序)
除了进程私有的虚拟地址空间外,内核虚拟内存地址包含内核的代码和数据,所有进程的这部分区域被映射到同一个物理页面。
另外,与每一个进程相关的进程信息,例如进程的页表,mm内存映射结构等也存放在内核地址空间中,由内核进行维护。
Linux将这些不同区域称为段(segement)一个段包含若干的个连续的虚拟页。不同的区域之间在地址空间中不一定连续。
例如:

程序的堆空间有一定的大小上限。对于还没有malloc的地址空间,其虚拟页是未分配的,也就是不占用实际的磁盘、内存。
每个进程在内核虚拟区域记录了一些信息,内核为每个进程维护者一个单独的任务结构(task_struct)。任务结构中包含了指向内核运行该进程所需的信息:
- PID
- 指向用户栈的指针
- 可执行目标文件的名字
- 程序计数器
典型地,这些信息在程序开始加载运行,和进程间切换时(保护、还原现场)将被使用到。
【内存映射】
Memory Mapping :将一个虚拟内存区域与一个磁盘上的对象关联起来,以初始化这个虚拟内存区域的内容。
虚拟内存区域可以映射到两种对象中:
①Linux文件系统中的普通文件:一个区域被映射到一个普通磁盘文件的连续部分。例如一个可执行的目标文件,这样文件被划分为页大小相等的片,每一片包含了一个虚拟页面的初始化内容。当CPU第一次引用到虚拟页面时,进行页面调度,将磁盘这些部分的内容加载到主内存中。
②匿名文件:一个区域也可以映射到匿名文件。匿名文件由内核创建,包含初始化的二进制0.其并不是存在于磁盘中,而是创建在内存中,并驻留在内存,当引用到匿名文件时,即为请求二进制为0的页。典型的有:匿名管道文件
【共享对象】
虚拟内存系统集成到文件系统中,从而提供一种简单高效的将程序和数据加载进内存的方式。
进程将文件系统的文件抽象为相应的地址空间,同时注意到,各个进程之间有着相同的只读代码区域。例如每个运行中的Linux shell程序的Bash进程有着相同的代码区域。通过内存映射,它们访问的一份代码的同一份副本,而无需每个进程都拷贝一份副本,大大节约了空间,Linux进程地址空间中的共享库就是采取这种方式。
内存映射提供这样一种机制,用来控制多个进程共享对象。同时需要注意到,任何进程对共享对象的修改,将直接影响到其它进程。因此对于进程私有的对象,若其被共享到其它进程,一旦对象发生修改,将触发写时复制。
物理内存中保存一份私有对象的副本,多个进程共享这一副本,一旦一个进程试图写私有对象某个页面,将会在物理内存创建一个新副本,同时其中进程更新页表指向新的物理页面。
写时复制体现:
【fork函数】
fork函数用于在父进程中衍生出一个一模一样的子进程,其本质基于内存映射机制。fork系统调用时,内核为新进程创建各种数据结构,并分配唯一的PID。为新进程分配虚拟地址空间,它创建的是原先进程的相关副本。但是通过内存映射指向相同的物理页。
但一旦其中一个进程进行了写操作,就会触发写时复制,内核创建新的页面,以确保每个进程地址空间的私有性。
从内存映射角度看程序加载与执行:
程序的加载过程,Linux创建相应的地址空间,并不是直接将文件复制到物理内存,而是基于内存映射方式,将对应区域的内存页映射到可执行目标文件的相应段。同时,对于运行时堆栈,它们都是请求二进制0的。另外,对于动态链接库,其虚拟地址区域的页被映射到同一份文件副本,在第一次引用该部分内存时,被传输进入物理主存。所有使用该共享库的进程对应的地址空间都被映射到同一份物理内存区域。