Skip to content

Commit

Permalink
update toc
Browse files Browse the repository at this point in the history
  • Loading branch information
rbmonster committed Jun 29, 2022
1 parent 4575254 commit 101430a
Showing 1 changed file with 130 additions and 0 deletions.
130 changes: 130 additions & 0 deletions src/main/java/com/other/OPERATING_SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,130 @@ Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用
### 内存分页
分段的好处就是能产生连续的内存空间,但是会出现内存碎片和内存交换的空间太大的问题。

内存分页(Paging): 分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,称为页(Page)。在 Linux 下,每一页的大小为4KB。
> 当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点



虚拟地址与物理地址之间通过页表来映射\
**页表**是存储在内存里的,内存管理单元(MMU)就做将虚拟内存地址转换成物理地址的工作。
> 而当进程访问的虚拟地址在页表中查不到时,系统会产生一个**缺页异常**,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/memory-page.png)

分页是怎么解决分段的内存碎片、内存交换效率低的问题?
> 由于内存空间都是预先划分好的,也就不会像分段会产生间隙非常小的内存,这正是分段会产生内存碎片的原因。而**采用了分页,那么释放的内存都是以页为单位释放的,也就不会产生无法给进程使用的小内存。**
- 换出(Swap Out): 如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上。
- 换入(Swap In): 将从内存页面释放而暂存在硬盘上的数据,重新加载进来。

内存分页相比内存分段好处:
1. 对于需要内存交换的情况,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
2. 分页的方式使得我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是**只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去**

![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/memory-page-swap.png)


在分页机制下,虚拟地址分为两部分,**页号和页内偏移**。页号作为页表的索引,**页表包含物理页每页所在物理内存的基地址**,这个基地址与页内偏移的组合就形成了物理内存地址

![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/memory-page-structure.png)



#### 多级分表
对于一个内存地址转换,简单的三个步骤总结:
- 把虚拟内存地址,切分成页号和偏移量;
- 根据页号,从**页表**里面,查询对应的物理页号;
- 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。

> 简单分页管理的缺陷?
> `32` 位的环境下,虚拟地址空间共有 `4GB`,假设一个页的大小是 `4KB(2^12)`,那么就需要大约 `100 万 (2^20)` 个页,每个「页表项」需要 `4` 个字节大小来存储,那么整个 `4GB` 空间的映射就需要有 `4MB` 的内存来存储页表。\
> `4MB` 大小的页表,看起来也不是很大。但是要知道每个进程都是有自己的虚拟地址空间的,也就说都有自己的页表。\
> 那么,`100` 个进程的话,就需要 `400MB` 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。
多级页表(Multi-Level Page Table)\
在 32 位和页大小 4KB 的环境下,一个进程的页表需要装下 100 多万个「页表项」,并且每个页表项是占用 4 字节大小的,于是相当于每个页表需占用 4MB 大小的空间。\
我们把这个 100 多万个「页表项」的单级页表再分页,将页表(一级页表)分为 1024 个页表(二级页表),每个表(二级页表)中包含 1024 个「页表项」,形成二级分页。

进一步节省内存方案:**如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。**
> 做个简单的计算,假设只有 20% 的一级页表项被用到了,那么页表占用的内存空间就只有 4KB(一级页表) + 20% * 4MB(二级页表)= 0.804MB,这对比单级页表的 4MB 是不是一个巨大的节约?
对于 64 位的系统,两级分页肯定不够了,就变成了四级目录,分别是:
- 全局页目录项 PGD(Page Global Directory)
- 上层页目录项 PUD(Page Upper Directory)
- 中间页目录项 PMD(Page Middle Directory)
- 页表项 PTE(Page Table Entry)

![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/multiply-memory-page.png)


#### TLB
多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。

程序是有局部性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。

`TLB(Translation LookAside Buffer)`: 在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,通常称为页表缓存、转址旁路缓存、快表等。

在 CPU 芯片里面,封装了内存管理单元(Memory Management Unit)芯片,它用来完成地址转换和 TLB 的访问与交互。 有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。

![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/TLB.png)


### 段页式内存管理
内存分段和内存分页并不是对立的,它们是可以组合起来在同一个系统中使用的,那么组合起来后,通常称为段页式内存管理。

段页式内存管理实现的方式:
1. 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
2. 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;
这样,地址结构就由**段号、段内页号和页内位移**三部分组成。

用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号

段页式地址变换中要得到物理地址须经过三次内存访问:
- 第一次访问段表,得到页表起始地址;
- 第二次访问页表,得到物理页号;
- 第三次将物理页号与页内位移组合,得到物理地址。


### Linux 内存管理

Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制。 这主要是Intel 处理器发展历史导致的

Linux 系统中的每个段都是从 0 地址开始的整个 4GB 虚拟空间(32 位环境下),也就是所有的段的起始地址都是一样的。这意味着,Linux 系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了处理器中的逻辑地址概念,段只被用于访问控制和内存保护。

![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/linux-memory-1.png)


每个进程都各自有独立的虚拟内存,**每个虚拟内存中的内核地址,其实关联的都是相同的物理内存**。进程切换到内核态后,就可以很方便地访问内核空间内存。

![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/linux-memory-2.png)


看看用户空间分布的情况,以 32 位系统为例。其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。

![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/other/operatingsystem/linux-memory-3.png)


### 两种分配内存的调用
C 库里的函数`malloc()`,用于动态分配内存。`malloc` 申请内存的时候,会有两种方式向操作系统申请堆内存。
1. 通过 `brk()` 系统调用从堆分配内存。类似与碰撞指针,直接用指针移位表示内存占用。
2. 通过 `mmap()` 系统调用在**文件映射区域**分配内存;

malloc 申请的内存,free 释放内存会归还给操作系统吗?
- malloc 通过 `brk()` 方式申请的内存,free 释放内存的时候,**并不会把内存归还给操作系统**,而是缓存在 malloc 的内存池中,待下次使用;
- malloc 通过` mmap()` 方式申请的内存,free 释放内存的时候,**会把内存归还给操作系统,内存得到真正的释放**


**mmap与brk分配对比**

mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。
也就是说,频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。

brk的分配方式,调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。\
等下次在申请内存的时候,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗。\
brk的分配方式问题在于,如果申请的空间没办法复用,那么将会导致堆内将产生越来越多不可用的碎片,导致“内存泄露”。


## 进程管理

Expand Down Expand Up @@ -347,6 +471,12 @@ Linux 的内核设计是采用了宏内核,Window 的内核设计则是采用



# 计算机组成原理

## 局部性原理
// TODO
[计算机组成原理:局部性原理](https://blog.csdn.net/zhizhengguan/article/details/121172704)


# 中间件设计资料
![image](https://raw.githubusercontent.com/rbmonster/file-storage/main/learning-note/design/systemdesign/disk-memory.png)
Expand Down

0 comments on commit 101430a

Please sign in to comment.