五.内存
具体内容参考昨天笔记
1. 进程映像
代码:map.c
通过size命令可以观察特点可执行程序的(的进程实例)的代码区
(text)、数据区(data)和BSS(bss)的字节数,以及他们十进制(dec)和十六进制(hex)形式的总和。
Linux系统的二进制模块(ELF (Excutable and Linkable Format)可执行可连接文件格式):
1. 目标模块(.o)
2. 静态库(.a)
3. 动态库(.so)
4. 可执行程序
所谓编译和链接就是将用高级语言编写的文本格式的源代码文件翻译成机器指令,形成ELF格式的可执行文件
所谓可执行程序的加载就是将磁盘上的ELF格式的可执行文件读入内存形成进程映像的过程
2. 虚拟内存
32位机器,地址总线和数据总线的宽度是32位,地址范围就是
---------------0——2^32-1(4G-1)------------------ ---> 虚拟内存
/ \
+-------------------+ +-----------------+
| 半导体内存(内存条) |<-------->| 交换分区(换页文件)| ---> 物理内存
+-------------------+ +-----------------+
+-------+ +---------+
|地址总线| | 数据总线 |---+
+-------+ +---------+ \
^ +---+ |
物理-|CPU|< 数据-----+
地址 +---+
-
在(32位)系统中所允许的每个进程,都拥有各自独立的4G字节大小的地址空间,谓之虚拟内存。
-
用户程序中使用的都是地址空间中的虚拟内存,但真正能存储代码和数据的却是永远无法被直接访问的物理内存。
-
广义的物理内存不仅包括半导体材质的内存条,还包括磁盘上的交换分区(swap)或者换页文件(Pagefile)。
-
当半导体内存不够用时,可以把一些长期闲置的代码和数据从半导体内存缓存到交换分区或换页文件中,这叫页面换出,一旦需要使用那些曾被换出的代码数据,再把他们从交换分区或换页文件恢复到半导体内存中,这叫页面换入,因此,系统中的虚拟内存可以比半导体内存大很多。
-
虚拟内存到物理内存,或者说地址空间和到内存空间的映射关系,由操作系统内核通过内存映射表动态维护。
-
虚拟内存技术一方面保护了系统的安全,把用户进程和内核进程,用户进程和用户进程,在对内存的访问上完全隔离开,另一方面又借助交换分区或换页文件,允许应用程序去以一种完全透明的方式,使用比实际半导体内存大得多的存储空间
何为内存分配?
- 在地址空间中划出一定大小的范围,加以标记,表示再用。
- 在物理内存中去找到相应的空间,建立映射,记录在内存映射表中
何为内存释放?
- 解除物理内存和虚拟内存之间的映射关系,改映射表。
- 将虚拟内存中的地址范围标记为非占用状态,复用地址。
为了避免内存映射建立和解除的频率过高,降低系统运行性能,现代版本的操作系统一般都会通过集约优化的方式减少映射建立和解除内存映射的频率
3. 用户空间与内核空间
-
在32位系统上的每个进程都拥有“独立”的4G字节大小的虚拟内存。
-
其中高地址的1G虚拟内存成为内核空间,所映射的物理内存为系统中的全部进程所共享,存放系统内核的代码和数据。
-
其余低地址的3G虚拟内存称为用户空间,不同的进程会映射到不同的物理内存中,存放该进程自己的代码和数据。
进程1 物理内存 进程2 ----- ------- ----- 1G ——> 系统内核 <---- 1G ----- ------- ----- 3G ——> 用户空间 /--->3G ----- ------- / ----- | 用户空间<----| -------
用户空间的代码不能直接访问内核空间的代码和数据,可以通过内核系统调用进入内核状态,简介地与系统内核交互
/ Ring 0 - 内核态,高度警惕,安全等级高
/ Ring 1 \
Inter CPU处理机 |——>Linux 不用
\ Ring 2 /
\ Ring 3 - 用户态,放松警惕,安全等级低
/处于用户态,访问用户空间的代码和数据,队安全性要求低
进程 系统调用
\处于内核态,访问内核空间的代码和数据,安全性要求高
4. 内存壁垒与段错误
-
每个进程的用户空间所对应的物理内存是各自独立的,系统为每个进程的用户空间维护一张专属于该进程的内存映射表,记录虚拟内存到物理内存的映射关系,因此不同进程之间交换虚拟内存地址是毫无意义的
进程1 进程2 ---------------------------- ------------------------------ int a = 10; printf("%d\n", a); // 10 int *p = &a; ------------> int* q = p; *q = 20; // 非法,段错误 printf("%d\n", a); // 10
-
所有进程的内核空间都对应着相同的物理内存区域,系统为所有进程内核空间维护同一张内存映射表init_mm.png,记录内核空间虚拟内存到物理内存的映射关系,因此不同进程通过系统内存调用访问的内核代码和数据都是用一份。
-
用户空间的内存映射表会随着进程的切换而切换,内核空间的内存映射表则无需随着进程的切换而切换
-
一切对虚拟内存的越权访问都将导致段错误
-
试图访问没有映射到物理内存的虚拟内存
int *p; *p = 10; //段错误 int *q = (int *)malloc(sizeof(int)); *q = 20; free(q); // 释放了映射有可能就被解除·8- *q = 30; // 段错误
-
试图对只读内存做写操作
char *p= "Hello, World"; // p指向一块只读内存 p[7] = 'W'; // 段错误 char s[] = "hello, world"; // 拷贝到栈中 s[7] = 'W'; // OK
-
代码:vm.c
5. 虚拟内存的分配和释放
针对进程映像中堆内存区域通过编程的方法,在运行期间实现动态内存的分配与释放。
堆尾(堆顶)指针:当前堆内存中最后一个字节的下一个字节的地址。
空堆:
L------------------------------->H
-----------| BSS |_
^
分配4字节的堆内存
L------------------------------->H
-----------| BSS |xxxx_
^
分配8字节的堆内存
L------------------------------->H
-----------| BSS |xxxxYYYYYYYY_
^
释放4字节的堆内存:
L------------------------------->H
-----------| BSS |xxxxYYYY_
^
释放8字节的堆内存:
L------------------------------->H
-----------| BSS |_
^
1. sbrk
以相对方式分配和释放虚拟内存
#include <unistd.h>
void *sbrk(intptr_t increment);
成功返回调用该函数之前的堆尾指针,失败返回-1
increment - 堆内存增加字节数
>0 - 分配虚拟内存
<0 - 释放虚拟内存
=0 - 获取当前堆尾
void *p = sbrk(4);
V
---|BSS|0000XXXX_
^
++++++++++++************
\__________/\__________/
4096 4096(4k=页)
void *q = sbrk(0);
v
---|BSS|0000XXXX_
^
++++++++++++************
\__________/\__________/
4096 4096(4k=页)
void *r = sbrk(-8);
v
---|BSS|_
^
++++++++++++
\__________/
4096
代码:sbrk.c
2. brk
以绝对方式分配和释放虚拟内存
#include <unistd.h>
int brk(void *end_data_segment);
成功返回0,失败返回-1.
end_dara_segment:新堆尾指针
>当前堆尾指针 - 分配虚拟内存
<当前堆尾指针 - 释放虚拟内存
=当前堆尾指针 - 什么也没有做
原堆尾指针 + 堆内存增量 = 新堆尾指针
分两步做:
第一步: void *p = sbrk(0); // 返回当前堆尾
第二步: brk(p+4); // 新堆尾指针
v
---|BSS|0000XXXX_
^
++++++++++++************
\__________/\__________/
4096 (4k=页)
brk(p) // 对应的堆尾是XXXX
v
---|BSS|0000_
^
++++++++++++
\__________/
4096
代码:brk.c
总结两个函数:
- 事实上,sbrk和brk不过是移动堆尾指针的两种方法,移动过程中还有兼顾虚拟内存和物理内存之间映射关系的建立和解除(以页为单位)
- 用sbrk分配内存比较方便,用多少内存就传多少增量参数,同时返回指向新分配内存区域的指针,但用sbrk左一次性释放内存比较麻烦,因为必须将所有的既往增量进行累加。
- 用brk释放内存比较方便,只需将堆尾指针设回到一开始的位置即可,但用brk分配内存比较麻烦,因为必须根据所需要的内存大小计算出堆尾指针的绝对位置
- 最好的方法是将这两个函数结合起来,分配内存用sbrk,一次性释放内存用brk。
代码:sb.c
3. mmap
建立虚拟内存到物理内存或磁盘文件文件的映射。
#include <sys/mman.h>
void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
成功返回映射区(虚拟)内存的起始地址,失败返回MAP_FAILED(-1)。
start:映射区(虚拟)内存起始地址,NULL系统自定选定后返回
length:映射区字节数,自动按页(4096-)圆整
int((length + 4096)/4096) * 4096
port:访问权限,可取以下值:
PROT_READ - 映射区可读
PROT_WRITE - 映射区可写
PROT_EXEC - 映射区可执行
PROT_NONE - 映射区不可访问
flags:映射标志
MAP_ANONYMOUS | MAP_PRIVATE - 虚拟内存到物理内存的映射
MAP_SHARED - 虚拟内存到磁盘文件的映射
fd \
|---> 只用于虚拟内存到物理内存的映射,忽略之
offest /
虚拟内存 物理内存
| | | |
start-->+-------+ +-------+
/| | | |
length ||映射区 |<----->| |
\| | | |
+-------+ +-------+
| | | |
4. munmap
基础虚拟内存到物理内存或磁盘文件的映射
int munmao(void *start, size_t length);
成功返回0,失败返回-1
start:映射区(虚拟)内存起始地址,必须是内存页边界
length:映射区字节数,自动按页(4096-)圆整
int((length + 4096)/4096) * 4096