内存管理

佐手、 提交于 2019-11-29 05:40:01

如何进行内存管理

为了让每个进程认为独占地使用内存,并且让每个进程看到的内存是一致的,操作系统对物理内存、磁盘进行了抽象,抽象出虚拟内存。并且把虚拟内存、物理内存以相同固定大小的进行切分管理(分页),虚拟内存中叫页,物理内存中的叫页帧。

每个进程虚拟地址空间是独立的。用户访问的是虚拟内存的地址,即虚拟地址。需要通过 CPU 芯片上的内存管理单元 MMU 硬件根据页表翻译成物理地址,才能真正访问内存。

页表:每个进程都有它的独立的页表(放在内存里),用来存对虚拟页、物理页的映射。页表可以有多级页表,以时间换取空间(实际上,多级页表的地址翻译,并不比单级页表慢很多)。

为什么用分页机制

如果直接按一个个程序加载到内存,会出现内存碎片

后来出现分段机制,按程序的各段来存储,从而减少碎片,但是还是有很多。

所以引出分页,把程序分成更小的页(一般大小为 4KB)来管理内存。分得更小,会增加负荷,但实际上利大于弊。

硬件关系

通过虚拟地址访问数据:

MMU 先通过它里面的 TLB 缓存查询,如果没有,则去内存中的页表进行查询。成功翻译成物理地址后,访问一级缓存获取数据。如果没有则访问二级缓存(可能还有三级缓存)。还是没有就访问内存

物理内存不够时:

将不用的页面换出到磁盘中的 swap 分区里。

内存空间布局

进程的虚拟地址空间

包含:

  • 程序代码和静态变量(根据可执行文件进行初始化,并固定了大小)
  • 堆(可在运行时由 malloc 动态分配)
  • 共享库、共享内存(动态链接库的 .so 文件映射到这里,一般由 mmap 分配)
  • 栈(运行时创建的用户栈)
  • 内核内存(为内核保留的。应用程序不可读写或者直接使用。)—— 每个进程看到的内核空间其实是一样的,是同一个空间(虚拟内存、物理内存都只有一份)。虽然每个进程内核栈是各用各的,但是如果想知道的话,还是能够知道其他每个进程的内核栈。

进程如何描述自己的虚拟地址空间

Linux 中进程的虚拟地址空间,有内核内存。内核内存有个区域是每个进程都不相同的,如页表、内核栈等。

内核为每个进程维护一个单独的 task_struct 任务结构(进程控制块 PCB),包含着内核态运行所需要的信息,如 PID、指向用户栈的指针、可执行目标文件的名字等。

task_struct 中有一个条目指向着 mm_struct,它描述虚拟内存的状态。mm_struct 里有个 mmap 变量,是一个 vm_area_struct(区域结构)链表,这个链表的每个 vm_area_struct 都描述着虚拟内存中的一个区域,如区域的起始处、结束处、区域内所有页的读写权限、页面是否与其他进程共享等。(mm_struct 里还有 pgd,指向进程的页表)

如何分配内存

分配虚拟内存

c 语言的 malloc 等方法。

用户可通过 malloc 在堆上分配内存,malloc 内部是用 brk 系统调用实现的。brk 会给进程创建 vm_area_struct 来分配小块虚拟内存。

malloc 如果是申请大块内存,就会用 mmap 。

什么时候分配物理内存

内存里没有时,即出现缺页时会触发缺页异常/中断,让缺页异常处理程序去处理并分配内存。

虚拟内存分配,不代表真正分配了物理内存,只有需要写的时候发生缺页中断/异常,才会去分配物理内存,即按需分配。并把虚拟地址和物理地址映射起来。

处理程序返回时,CPU 会重新返回到引起缺页的那条指令,该指令再次被送到 MMU 进行翻译。因为此时 MMU 能正常翻译地址,所以就不会产生缺页中断,程序会继续执行下去。

按需进行页面调度:目前现代系统都是在缺页异常发生时才会去分配 / 缓存到内存。

rocketMQ:

https://mp.weixin.qq.com/s?__biz=MjM5OTMyNzQzMg==&mid=2257483760&idx=1&sn=6f93ef7c6e4aba6af791737918752fa9&chksm=a447f45793307d4111a1cbc2a4d0f79a8c6915a67f41b9a9356cb5fc4699fd7ed82534c3e2c5&mpshare=1&scene=1&srcid=%23rd

一些对实时性要求很高的中间件,例如 rocketmq,把消息存储在一个大小为 1G 的文件中,为了加快读写速度,会将这个文件映射到内存后,对每个页写一比特数据,这样就可以把整个 1G 文件都加载进内存,在实际读写的时候就不会发生缺页了,这个在 rocketmq 内部叫做文件预热。

分配算法

伙伴系统

对于分配比较大的内存,如分配(32 位里是 4 KB)级别或以上的,可以使用伙伴系统(页面分配器)。

slab 机制

比一个页小的内存,可以用 slab 机制。

内存不够时怎么处理——swap

将不用的页面换出到磁盘中的 swap 分区里。

从物理内存中找出牺牲页进行分配。如果这个牺牲页面有被修改过,则需要先换出到磁盘,然后再把新的页面换入,并更新页表。

page cache

https://www.cnblogs.com/beifei/archive/2011/06/12/2078840.html

在内核中为每个文件单独维护一个 page cache。page cache 保存用户进程访问过的该文件的内容。

进程对文件的读写时会直接操作 page cache,内核会在适当的时候将 page cache 中的内容写到磁盘上(也可以手动控制回写),这样可以大大减少磁盘的访问次数,从而提高性能。

page cache 中的内容以为单位保存在内存中。

当用户要访问文件中的某个偏移量上的数据时,内核会以偏移量为索引,找到相应的内存页,如果该页没有读入内存,则需要访问磁盘读取数据。

为了提高页的查询速度并且节省 page cache 占用的内存,linux 内核使用来保存 page cache 中的页。

普通的 write 调用只是将数据写到 page cache 中,并将其标记为 dirty 就返回了,磁盘 I/O 通常不会立即执行。这样做的好处是减少磁盘的回写次数,提供吞吐率,不足就是机器一旦意外挂掉,page cache中的数据就会丢失。一般安全性比较高的程序会在每次 write 之后,调用 fsync 立即将 page cache 中的内容回写到磁盘中。

内存映射——mmap

内存映射包含虚拟内存与物理内存、磁盘之间的映射。

而 mmap 系统调用是实现内存映射文件的一种方法。

与 read 系统调用的区别

read 系统调用的过程:

  1. 发生用户态到内核态的转换,内核找出文件描述符以及 inode,从而找出磁盘地址。
  2. 读取硬盘文件中的对应数据,并拷贝到 page cache 中。
  3. 将 read 中需要的数据,从 page cache 拷贝到用户空间中。

即 read 为了提高效率使用了页缓存机制,但这需要经历两次拷贝,以及用户态、内核态的转换,所以性能并不高。

mmap 系统调用与 read 的区别:

mmap 系统调用是将硬盘文件映射到用户空间中,从而让进程可以直接访问自身地址空间的虚拟地址来访问文件内容。

所以,

  1. mmap 只需要一次系统调用,后续操作不需要系统调用(即第一次以后不用从用户态切换到内核态)。
  2. 不需要在 page cache 和用户空间之间的拷贝

所以频繁对一个文件进行读取操作时,mmap 会比 read 更高效一些。

mmap 过程

首先,从进程虚拟空间中找出一块空闲区域,并为此区域分配一个 vm_area_struct。(创建虚拟映射区域)

把待映射的文件描述符放到文件表,且找出磁盘地址。并建立页表,把磁盘地址、虚拟地址进行映射。(地址映射)

当进程要访问该空间时,会引起缺页异常,此时把文件内容拷贝到物理内存。(分配内存)

所以 mmap 只发生一次数据拷贝。

mmap 缺点

  1. 一次内存映射的开销(映射、分页故障、解除映射、更新硬件内存管理单元的超前转换缓冲器)实际上比调用一次 read / write 大。
  2. 映射区域大小是页大小的整数倍,所以对于小文件,会造成浪费。
  3. 使用 mmap 必须指定大小,所以对变长文件不适合。
  4. 如果更新文件的操作很多,mmap 避免两态拷贝的优势就被摊还,最终还是落在了大量的脏页回写及由此引发的随机 IO 上。

所以 mmap 在效率上不一定会比带缓冲区的快。

https://stackoverflow.com/questions/45972/mmap-vs-reading-blocks

http://www.leviathan.vip/2019/01/13/mmap%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/

mmap 使用场景

读写大文件。链接动态库文件。

在 malloc 内部,如果是要求分配大内存,会去使用 mmap 进行内存映射。

进程之间共享内存。

rocketMQ 等。

brk 与 mmap

都是系统调用。

brk 用来分配小块内存,mmap 用来分配大块内存。

C 语言的 malloc 函数内部,如果是大块内存,会去调用 mmap,反之调用 brk。

brk 是通过移动堆顶的位置来分配内存。这些内存释放后不会立刻归还,而是被缓存起来,这样就可以重复使用,进而减少缺页异常而去分配内存的次数。

mmap 是在文件映射段里找一块空闲内存进行分配。mmap 分配的物理内存,会在释放时直接归还系统。

两个方法都是按需分配物理内存,不会在调用时直接分配。

操作系统启动过程、内存初始化

从系统开机到用户能使用内存,经过哪些过程

与其他模块

系统调用模块

设备管理模块

文件系统模块

进程管理模块

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!