Linux 虚拟地址空间
为了防止不同进程在物理内存中运行二队物理内存的争夺,采用了虚拟内存。
虚拟内存使得不同进程在运行的过程中,它看到的是自己独自占有了当前系统的4G内存。所有进程共享同一物理内存,每个进程只是把自己目前需要的虚拟内存空间映射并存储到物理内存上。在每个进程创建加载时,内核只是为进程“创建“了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表。实际上并不立即就把虚拟内存对应位置的程序数据和代码拷贝到物理内存中,只是建立好了虚拟内存和磁盘文件之间的映射。等到运行到对应的程序段或访问相应的数据段,才会通过缺页异常来拷贝数据。在进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应的设置,当进程真正访问到此数据时,才会引发缺页异常。
请求分页系统、请求分段系统和请求段页式系统都是针对虚拟内存的,通过请求实现内存与外存的信息置换。
虚拟内存的好处:
- 扩大地址空间;
- 内存保护:每个进程运行在各自的虚拟内存地址空间,互相不干扰。虚拟内存还对特定的内存地址提供写保护,可以防止代码或数据被恶意篡改。
- 公平内存分配。采用了虚存之后,每个进程都相当于有相同大小的虚存空间。
- 当进程通信时,可采用虚存共享的方式实现。
- 当不同的进程使用相同的代码时,比如库文件中的代码,物理内存中可以只存储一份代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存
- 虚拟内存很适合在多道程序设计系统中使用,许多程序的片段同时保存在内存中。当一个程序等待它的一部分读入内存时,可以把CPU交给另一个进程使用。可以提供系统并发度
- 当程序需要分配连续内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片
虚拟内存的代价:
- 虚存的管理需要建立很多数据结构,这些数据结构要占用额外的内存
- 虚拟地址到物理地址的转换,增加了指令执行的时间
- 页面换入换出需要磁盘IO,比较耗时
- 如果一页中只有一部分数据,会浪费内存
Linux 页表寻址
- 页式内存管理,内存分成固定长度的一个个页片。操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表,页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。通过页表,由逻辑地址的高位部分先找到逻辑地址对应的页基地址,再由页基地址偏移一定长度就得到最后的物理地址,偏移的长度由逻辑地址的低位部分决定。一般情况下,这个过程都可以由硬件完成,所以效率还是比较高的。页式内存管理的优点就是比较灵活,内存管理以较小的页为单位,方便内存换入换出和扩充地址空间。
程序内存管理
一个程序本质上都是由text段,data段,BSS段三个段组成。一个可执行程序在存储时(没有调入内存)分为代码段,数据段和未初始化数据区。
调入内存后:
BSS段:未初始化数据区。通常用来存放程序中未初始化的全局变量和静态变量的一块内存区域。属于静态分配,程序结束后静态变量资源由系统自动释放。
数据段:存放程序中已经初始化的的全局变量。也属于静态分配
代码段:存放程序执行代码的一块内存。这部分区域的大小在程序运行前就已经确定,并且在内存区域属于只读。在代码段中,也可能包含一些只读的常数变量
代码段和数据段在编译时已经分配了空间,而BSS段不占用可执行文件的大小,它是由链接器来获取内存的。BSS段的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将他们设置为0。
可执行程序在运行时又多处两个区域:栈区和堆区。
例子:
1 | A *a = new A(); |
(1)A *a :a是一个局部指针,在栈区分配内存(4或8字节空间)设地址为0x000m。
(2)new A :通过new在堆区申请类A大小的空间(0x000n为首地址)
(3)a = new A :将指针a的内存区域填入堆区申请到的地址。即 *(0x000m) = 0x000n
(4)a->i :先找到指针a的地址,通过a的地址和i在类A中的偏移得到a->i地址,对该地址进行赋值操作。
例子:
给一个类,里边有static,virtual,说明一下这个类的内存分布
(1) static修饰成员变量。对于静态成员变量,对每个类对象都是一样的(其值可以更新)。static变量在data段分配内存,属于本类的所有对象共享,不属于特定的类对象,在没有产生类对象前就可以使用。
注意:static非const变量只能在类内定义,在类外初始化!如果在类内初始化,会导致每个类都包含该静态成员,这是矛盾的。
1 | class A |
能够在类内初始化的成员只有static const变量。
(2) const成员。只能在构造函数后的初始化列表中初始化!
1 | #include <iostream> |
(3) static修饰成员函数。静态成员函数不与任何对象相联系,因此不具有this指针。它无法访问属于类对象的非静态数据成员(因为没有this指针,只能访问所有类都一样的成员),也无法访问非静态成员函数,只能调用静态成员函数。static修饰的成员函数,在代码区分配内存。
(4) C++继承与虚函数。
C++多态分为静态多态和动态多态。静态多态是通过重载和函数模板来实现的,在编译的时候确定。动态多态通过虚函数和继承关系来实现,执行动态绑定,在运行时候确定,也叫动态联编。
动态多态实现的条件:
a.虚函数
b.一个基类的指针或引用指向派生类的对象
基类指针在调用虚成员函数的时候,会去查找该对象的虚函数表。在类继承的时候,虚函数表直接从基类继承过来,如果覆盖了其中某个虚函数,那么虚函数的指针就会被替换,因此可以根据指针准确找到调用的函数。
如果一个类是局部变量,则该类存储在栈区。
如果一个类是new、malloc动态申请的,存储在堆区。
如果一个类是virtual继承来的子类,则该类的虚函数表指针和该类其他成员一起存储。虚函数表指针指向只读数据段中的类虚函数表,虚函数表中存放着一个个函数指针,函数指针指向代码段中的具体函数。如果类中成员是vitual属性,则会隐藏父类对应的属性。
静态变量什么时候初始化
static变量存储在静态数据段和bss段。
C语言是在代码执行之前进行初始化,属于编译器初始化。保存在可执行文件的数据段中。
C++引入了对象,对象的生成必须调用构造函数,因此C++规定全局或局部静态对象当且仅当对象首次用到时进行构造。
union联合体的使用
参考https://blog.csdn.net/huqinweI987/article/details/23597091
union的基本特性:定义union下边的成员变量共享一块内存,每一个成员在任一时刻有且只有一个成员使用此块内存。C语言只管分配一段空间,至于里边放什么内容并不会管。
union的内存分配是按照union成员变量占地最大的那个变量分的。
判断linux大小端:大端指低字节存储在高地址,小端指低字节存储在低地址。联合体变量总是从低地址存储。
1 | #include<stdio.h> |