操作系统(三):内存管理

24. 虚拟内存有什么作用?

  • 第一,虚拟内存可以使得进程的运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
  • 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间都是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
  • 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。

25. 操作系统是如何管理虚拟地址和物理地址之间的关系?

主要有两种方式,分别是内存分段和内存分页

  • 内存分段:程序是由若干个逻辑分段组成的,即可有代码分段、数据分段、栈段、堆段组成。不同的段有不同的属性,所以就用分段的形式把这些段分离出来。虚拟地址是通过段表与物理地址进行映射的,每个段在段表中有一个项,在这个项找到段的基地址,再加上偏移量,就能找到物理内存中的地址。
  • 内存分页:分页是把整个虚拟内存空间和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为4KB。虚拟地址是通过页表与物理地址进行映射的,页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,通过这个基地址与页内偏移的组合就能找到物理内存中的地址。

26. 分段机制下,虚拟地址和物理地址是如何映射的?

分段机制下的虚拟地址有两部分组成:段选择因子和段内偏移量。

  • 段选择因子就保存在段寄存器里面。段选择因子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。
  • 虚拟地址中的段内偏移量应该位于0和段界限之间,如果段内偏移量是合法的,就将段地址加上段内偏移量得到物理内存地址。

虚拟地址是通过段表和物理地址进行映射的,分段机制会把程序的虚拟地址分成4个段,每个段在段表有一个项,在这一项找到段的基地址,再加上偏移量,于是就找到物理内存中的地址。


27. 内存分段会出现内存碎片吗?

内存碎片主要分为内部内存碎片和外部内存碎片。
内存分段管理可以做到段根据实际需求分配内存,所以有多少需求就分配多大的段,所以不会出现内部内存碎片。
但是由于每个段的长度不固定,所以多个段未必能恰好使用所有的内存空间,会产生多个不连续的小物理内存,导致新的程序无法被装载,会出现外部内存碎片的问题。


28. 如何解决外部内存碎片的问题?

解决外部内存碎片的方法就是内存交换。将部分的内存数据写回到硬盘的swap空间,在需要该数据时,再从硬盘加载到内存里。


29. 分段为什么会导致内存交换效率低的问题?

对于多进程的系统来说,使用内存分段管理的方式,外部内存碎片是很容易产生的,产生了外部内存碎片,就需要进行内存交换,这个过程会产生性能瓶颈。因为硬盘的访问速度要比内存慢太多了,每一次内存交换,我们都需要把一大段连续的内存空间数据写到硬盘上。所以,如果内存交换的时候,交换的是一个占内存空间很大的程序,会导致整个机器都会显得卡顿。


30. 分页是如何解决分段的外部内存碎片和内存交换效率低的问题?

  • 采用内存分页,页与页之间是紧密排列的,所以不会有外部碎片。但是,由于内存分页机制分配内存的最小单位是页,所以当程序不足一页的时候,也是分配一页的内存空间,页内就会出现内存浪费,也就是说内存分页机制会有内部内存碎片的现象。
  • 如果内存空间不够,操作系统会把其他正在运行的进程中的最近最少被使用的内存页面写回到硬盘上,称为换出。一旦需要的时候,再加载到内存中来,称为换入。所以,一次性写入磁盘的数据只有少数的一个页或者几个页,不会消耗太多的时间,内存交换的效率就相对比较高。
  • 更进一步地,采用内存分页管理,不需要一次性将程序加载到物理内存中,只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。

31. 简单的分页有什么缺陷吗?

有空间上的缺陷。因为操作系统可以同时运行多个进程,由于每个进程都有自己的虚拟地址空间,也就是说都有自己的页表。在32位的环境下,整个4GB空间的映射就需要4MB的内存来存储页表,如果运行着100个进程的话,就需要400MB的内存来存储页表,这时非常大的内存开销,更别说64位的环境了。


32. 如何解决简单分页产生的页表占据内存过大的问题?

为了减小页表所占内存,就需要采用多级页表。由于程序局部性原理,对于大部分程序而言,其使用到的空间远未达到4GB,如果某一个一级页表的页表项没有使用到,也就不需要创建这个页表项对应的二级页表,即在需要时才创建二级页表,依此类推,对于多级页表采用同样的原理创建对应的页表,这样,页表所占用的内存空间大大降低。


33. 64位的操作系统中,多级页表对应的目录是什么?

对于64位的操作系统,多级页表中有四级目录:

  • 全局页目录项 PGD(Page Global Directory);
  • 上层页目录项 PUD(Page Upper Directory);
  • 中间页目录项 PMD(Page Middle Directory);
  • 页表项 PTE(Page Table Entry);

34. TLB是什么?为什么需要有TLB?

  • 在CPU中有一个专门用于存放程序最常访问的页表项的Cache,这个Cache就是TLB(Translation Lookaside Buffer),通常称为页表缓存、转址旁路缓存、快表等。
  • 多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换需要通过查询多级页表,就会降低地址转换的速度,带来时间上的开销。由于程序具有局部性原理,相应的,执行程序所访问的存储空间也局限于某个内存区域。利用这一特性,把最常访问的几个页表项存储到TLB中,减少对常规页表的访问。

35. 段页式内存管理

段页式内存管理实现的方式:

  • 先将程序划分成多个逻辑意义的段,也就是分段机制;
  • 再把每个段划分为多个页,也就是对分段划分出来的连续空间在划分固定大小的页。

这样,地址结构由段号、段内页号和页内偏移三部分组成。

段页式内存管理从虚拟地址转换为物理地址的转换过程:

  • 第一次访问段表,得到页表起始地址;
  • 第二次访问页表,得到物理页号;
  • 第三次将物理页号与页内偏移量相结合,得到物理地址。

36. Linux系统采用了什么方式管理内存?以及Linux的虚拟地址空间是如何分布的?

  • Linux 系统主要采用了分页内存管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理。于是 Linux 就把所有段的基地址设为 0,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。
  • Linux 系统中虚拟空间分布可分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS、函数栈、堆内存、映射区。在这 7 个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。

37. malloc() 是如何分配内存的?

malloc() 不是系统调用,而是C库里的函数,用于动态分配内存。
malloc() 通过两种方式向操作系统申请内存:

  • 方式一:通过 brk()系统调用从堆分配内存;
  • 方式二:通过mmap()系统调用在文件映射区域分配内存;

方式一实现的方式很简单,通过brk()函数将堆顶指针向高地址移动,获得新的内存空间。
方式二通过mmap()系统调用中的私有匿名映射的方式,在文件映射区分配一块内存。


38. 什么场景下malloc()会通过brk()分配内存?什么场景下通过mmap()分配内存?

malloc()源码里默认定义了一个阈值:

  • 如果用户分配的内存小于128KB,则通过brk()申请内存;
  • 如果用户分配的内存大于128KB,则通过mmap()申请内存;

39. malloc()分配的是物理内存吗?

不是的,malloc()分配的是虚拟内存。
如果分配后的虚拟内存没有被访问的话,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存了。只有在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。


40. malloc(1)会分配多大的虚拟内存?

malloc()在分配内存的时候,会预分配更大的空间作为内存池。malloc(1)实际上预分配132KB的内存。


41. free 释放内存,会归还给操作系统吗?

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

42. 为什么不全部使用 mmap 来分配内存?

如果全部使用mmap()来分配内存,相当于每次都要执行系统调用,不仅每次都会发生运行态的切换,而且每次mmap()分配的虚拟地址都是缺页的,然后在第一次访问该虚拟地址时,会触发缺页中断。
而如果使用brk()来分配内存,会预分配更大的内存作为内存池,当内存释放的时候,就缓存在内存池中。等下次再申请内存的时候,就直接从内存池中取出对应的内存块,而且可能该内存块的虚拟地址与物理地址的映射关系还存在,这不仅减少了系统调用的次数,也减少了缺页中断的次数,将大大降低CPU的消耗,所以不能全部使用mmap()来分配内存。


43. 为什么不全部使用 brk 来分配内存?

由于通过brk()从堆空间分配的内存,并不会归还给操作系统,因此,随着系统频繁地malloc和free,尤其对于小块内存,堆中将产生越来越多不可用的碎片,导致“内存泄露”。
所以,malloc实现中,充分考虑了brk和mmap行为上的差异及优缺点,默认分配大块内存(128KB)才使用mmap分配内存空间。


44. free()函数只传入了一个内存地址,为什么能知道要释放多大的内存空间?

malloc返回给用户态的内存起始地址比进程的堆空间起始地址多16字节,这多出来的16字节保存了该内存块的描述信息,比如有该内存块的大小。当执行free()函数时,free会对传入进来的内存地址向左偏移16字节,然后从这16字节中分析出当前内存块的大小,自然也就知道要释放多大的内存空间。


45. 如果物理内存不足,那么内核就会进行内存回收工作,内存回收的方式有哪些?

回收内存的方式主要有以下两种:

  • 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒kswapd内核线程来回收内存,这个回收内存的过程是异步的,不会阻塞进程的执行。
  • 直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。

如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会触发OOM(Out of Memory)机制。

46. 在进行内存回收时,哪些内存可以被回收?

主要有两类内存可以被回收,而且它们的回收方式也不同。

  • 文件页:内核缓存的磁盘数据和内核缓存的文件数据都叫作文件页。回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。
  • 匿名页:这部分内存没有实际载体,所以不能直接释放内存,它们的回收方式是通过Linux的Swap机制,Swap会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

文件页和匿名页的回收都是基于LRU算法,也就是优先回收最近最少访问的内存。

47. 针对频繁的回收内存,磁盘的I/O次数会很多,如何降低内存回收对性能的影响?

  • 调整文件页和匿名页的回收倾向,一般建议将 /proc/sys/vm/swappiness 设置为 0(默认值是 60),这样在回收内存的时候,会更倾向于文件页的回收;
  • 尽早触发后台内存回收,来避免应用程序进行直接内存回收。
    内核定义了三个内存阀值:页高阀值、页低阀值、页最小阀值;
    当剩余内存页小于页低阀值时,就会触发后台内存回收,然后kswapd会一直回收到剩余内存页大于页高阀值。可通过设置页最小阀值间接设置页低阀值,可以增大页最小阀值来尽早地触发后台回收。
  • 调整NUMA(非一致存储访问)架构下的内存回收策略,在回收本地内存之前,会在其他 Node 寻找空闲内存,从而避免在系统还有很多空闲内存的情况下,因本地 Node 的内存不足,发生频繁的直接内存回收,导致性能下降的问题。

48. 如何保护一个进程不被OOM杀掉呢?

OOM killer 会根据每个进程的内存占用情况和oom_score_adj 的值进行打分,得分最高的进程就会首先被杀掉。我们可以通过调整进程的oom_sore_adj的数值,来改变进程的得分结果。如果不想某个进程被杀掉,可以将 oom_score_adj 设置为 -1000。


49. 在4GB物理内存的机器上,能够申请8GB内存吗?

  • 在32位操作系统中,因为进程理论上最大能申请3GB的虚拟内存,所以申请8GB内存会失败。
  • 在64位操作系统中,因为进程理论上最大能申请128TB的虚拟内存,即使物理内存只有4GB,申请8GB内存也没有问题,因为申请的内存是虚拟内存。如果这块内存被访问了,要看系统有没有Swap分区:
    • 如果系统没有Swap分区,因为物理内存不足,进程会被操作系统杀掉,原因是OOM(内存溢出);
    • 如果系统有Swap分区,即使物理内存只有4GB,程序也能正常使用8GB的内存,进程可以正常运行。

50. 操作系统会在读磁盘的时候会额外多读一些到内存中,但是最后这些数据也没用到,有什么改善的方法吗?

这是传统的LRU算法存在的预读失效现象,通过改进传统LRU链表来避免预读失效带来的影响,具体的改进如下:

  • 进行Linux操作系统实现了两个LRU链表:活跃LRU链表(active_list)和非活跃LRU链表(inactive_list)。
  • active_list 活跃内存页链表,用于存放最近被访问过(活跃)的内存页;inactive_list 不活跃内存页链表,用于存放很少被访问(不活跃)的内存页。
  • 有了这两个LRU链表后,预读页就只需要加入到 inactive_list 链表的头部,当页被真正访问的时候,才将页插入到 active_list 的头部。如果预读的页一直没有被访问,就会从 inactive_list 中移除,这样就不会影响 active_list 中的热点数据。

51. 批量读数据的时候,可能会把热点数据挤出去,有什么改善的办法吗?

这其实是传统LRU算法存在的缓存污染现象,通过改进传统LRU算法来避免缓存污染的问题,只要我们提高进入到活跃LRU链表(或者 young 区域)的门槛,就能有效地保证活跃LRU链表(或者 young 区域)里的热点数据不会轻易被替换掉。
Linux 操作系统和 MySQL Innodb 存储引擎分别提高了升级为热点数据的门槛:

  • Linux操作系统:在内存页被访问第二次的时候,才将页从 inactive_list 升级到 active_list 里;
  • MySQL Innodb:在内存页被访问第二次的时候,并不会马上将该页从 old 区域升级到 young 区域,还要进一步停留在 old 区域进行时间判断:
    • 如果第二次访问的时间与第一次访问的时间间隔在1秒内(默认值),那么该页就不会从 old 区域升级到 young 区域;
    • 如果第二次访问的时间与第一次访问的时间间隔超过1秒,那么该页就会从 old 区域升级到 young 区域。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小浩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值