diff --git "a/source/_posts/Backend/operating-system/\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/source/_posts/Backend/operating-system/\345\206\205\345\255\230\347\256\241\347\220\206.md" new file mode 100644 index 00000000..e9b781b7 --- /dev/null +++ "b/source/_posts/Backend/operating-system/\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -0,0 +1,137 @@ +--- +title: 内存管理 +date: 2022-11-19 22:32:12 +categories: +- Backend +tags: +- Operating System +- 操作系统 +--- + +# 内存管理 + +## 虚拟内存 + +### 引入的目的 + +把进程所使用的地址「隔离」开来,即让操作系统为每个进程分配独立的一套「**虚拟地址**」,互不干涉。但是有个前提每个进程都不能访问物理地址,至于虚拟地址最终怎么落到物理内存里,对进程来说是透明的,操作系统已经把这些都安排的明明白白了。 + +### 实现 + +> **操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。** +> +> 如果程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址,这样就不会冲突了。 + +![](https://img-blog.csdnimg.cn/72ab76ba697e470b8ceb14d5fc5688d9.png) + +### 操作系统管理虚拟地址与物理地址之间的关系:**分页和分段** + +#### 内存分段 Segmentation + +分段的好处是能产生连续的内存空间,但是会出现外部内存碎片和内存交换过大的问题。 + +段选择因子和段内偏移量: + +- **段选择子**就保存在段寄存器里面。段选择子里面最重要的是**段号**,用作段表的索引。**段表**里面保存的是这个**段的基地址、段的界限和特权等级**等。 +- 虚拟地址中的**段内偏移量**应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。 + +![](https://img-blog.csdnimg.cn/c5e2ab63e6ee4c8db575f3c7c9c85962.png) + +但它也有一些不足之处: + +- 第一个就是**内存碎片**的问题。(外部内存碎片)原因:些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求。解决方法是内存交换。 +- 第二个就是**内存交换的效率低**的问题。原因:经常会产生外部碎片,需要进行和硬盘之间的内存交换,重新Swap区域,会产生性能瓶颈。 + +#### 内存分页 Paging + +**分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小**。这样一个连续并且尺寸固定的内存空间,我们叫**页**(*Page*)。在 Linux 下,每一页的大小为 `4KB`。 + +![](https://img-blog.csdnimg.cn/388a29f45fe947e5a49240e4eff13538.png) + +##### 缺页异常 + +而当进程访问的虚拟地址在页表中查不到时,系统会产生一个**缺页异常**,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。 + +##### 内部碎片 + +**采用了分页,页与页之间是紧密排列的,所以不会有外部碎片。**但是,因为内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费,所以针对**内存分页机制会有内部内存碎片**的现象。 + +##### 换入换出 + +如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为**换出**(*Swap Out*)。一旦需要的时候,再加载进来,称为**换入**(*Swap In*)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,**内存交换的效率就相对比较高。** + +![](https://img-blog.csdnimg.cn/8f187878c809414ca2486b0b71e8880e.png) + +##### 缺陷 + +有**空间**上的缺陷。 + +在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万 (2^20) 个页,每个「页表项」需要 4 个字节大小来存储,那么整个 4GB 空间的映射就需要有 `4MB` 的内存来存储页表。 + +这 4MB 大小的页表,看起来也不是很大。但是要知道每个进程都是有自己的虚拟地址空间的,也就说都有自己的页表。 + +那么,`100` 个进程的话,就需要 `400MB` 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。 + +##### 多级页表 + +将页表(一级页表)分为 `1024` 个页表(二级页表),每个表(二级页表)中包含 `1024` 个「页表项」,形成**二级分页**。 + +![](https://img-blog.csdnimg.cn/19296e249b2240c29f9c52be70f611d5.png) + +> 如果使用了二级分页,一级页表就可以覆盖整个 4GB 虚拟地址空间,但**如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表**。 + +> 那么为什么不分级的页表就做不到这样节约内存呢? +> +> 我们从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以**页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项** + +> 对于 64 位的系统,两级分页肯定不够了,就变成了四级目录 + +##### TLB + +多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。 + +我们就可以利用这一特性,把最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(*Translation Lookaside Buffer*) ,通常称为页表缓存、转址旁路缓存、快表等。 + +![](https://img-blog.csdnimg.cn/a3cdf27646b24614a64cfc5d7ccffa35.png) + +#### 段页式内存管理 + +##### 管理方式 + +- 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制; +- 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页; + +这样,地址结构就由**段号、段内页号和页内位移**三部分组成。 + +![](https://img-blog.csdnimg.cn/8904fb89ae0c49c4b0f2f7b5a0a7b099.png) + +### Linux的虚拟地址空间分布 + +在 Linux 操作系统中,虚拟地址空间的内部又被分为**内核空间和用户空间**两部分,不同位数的系统,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,如下所示: + +![](https://img-blog.csdnimg.cn/3a6cb4e3f27241d3b09b4766bb0b1124.png) + +- `32` 位系统的内核空间占用 `1G`,位于最高处,剩下的 `3G` 是用户空间; +- `64` 位系统的内核空间和用户空间都是 `128T`,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。 + +> 内核空间与用户空间的区别: +> +> - 进程在用户态时,只能访问用户空间内存; +> - 只有进入内核态后,才可以访问内核空间的内存; + +虽然每个进程都各自有独立的虚拟内存,但是**每个虚拟内存中的内核地址,其实关联的都是相同的物理内存**。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。 + +![](https://img-blog.csdnimg.cn/48403193b7354e618bf336892886bcff.png) + +### 用户空间分布 + +![](https://img-blog.csdnimg.cn/img_convert/b4f882b9447760ce5321de109276ec23.png) + +通过这张图你可以看到,用户空间内存,从**低到高**分别是 6 种不同的内存段: + +- 程序文件段(.text),包括二进制可执行代码; +- 已初始化数据段(.data),包括静态常量; +- 未初始化数据段(.bss),包括未初始化的静态变量; +- 堆段,包括动态分配的内存,从低地址开始向上增长; +- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长; +- 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 `8 MB`。当然系统也提供了参数,以便我们自定义大小;