简介:之前在学习小林大佬的八股文时,摘录了一些个人认为比较重要的内容,方便后续自己复习。【持续更新ing ~💯】
注:加五角星标注的,是当前掌握不牢固的,需要继续深入学习的内容 ★ \color{red}{★} ★
文章目录
- 一、前言(略)
- 二、硬件结构(略)
- 三、操作系统结构(略)
- 四、内存管理
- 五、进程管理
- 六、调度算法(略)
- 七、文件系统
- 八、设备管理 (略)
- 九、网络系统
- 十、Linux命令(略)
- 十一、学习心得(略)
- 十二、常见问题(个人补充)
一、前言(略)
二、硬件结构(略)
三、操作系统结构(略)
四、内存管理
4.1 虚拟内存
虚拟内存 是一种计算机系统内存管理的技术,它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间)。而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。
说下虚拟内存的作用?★★★★★ \color{red}{说下虚拟内存的作用? ★★★★★} 说下虚拟内存的作用?★★★★★
- 第一,虚拟内存可以使得进程的运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性。而对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
- 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,这些页表是私有的,这就解决了多进程之间地址冲突的问题。
- 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
缺页中断★★★★★ \color{red}{缺页中断 ★★★★★} 缺页中断★★★★★
当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常。进入系统内核空间分配物理内存、将磁盘数据以页的方式加载到内存中、更新进程页表,最后再返回用户空间,恢复进程的运行。
流程:缺页异常(虚拟地址不在页表中,访问数据不在内存中) → 分配物理空间 → 加载磁盘数据到内存 → 更新进程页表(虚拟内存和物理内存之间的映射关系) → 返回用户空间 & 恢复进程运行
4.2 malloc的内存分配
malloc() 分配内存过程?
实际上,malloc() 并不是系统调用,而是 C 库里的函数,用于动态分配内存。malloc 申请内存的时候,会有两种方式向操作系统申请堆内存。
- 用户分配的内存小于 128 KB阈值:通过
brk()
系统调用从【堆】分配内存 。其实就是通过 brk() 函数将「堆顶」指针向高地址移动,获得新的内存空间。 - 用户分配的内存大于 128 KB阈值:通过
mmap()
系统调用在【文件映射区域】分配内存。也就是从文件映射区“偷”了一块内存。
注意,不同的 glibc 版本定义的阈值也是不同的。
malloc() 分配的是物理内存吗?
malloc() 分配的是虚拟内存。
如果分配后的虚拟内存没有被访问的话,虚拟内存是不会映射到物理内存的,这样就不会占用物理内存了。
只有在访问已分配的虚拟地址空间时,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。
malloc 申请的内存,free 释放内存会归还给操作系统吗?
- malloc 通过
brk()
方式申请的内存,free 释放内存的时候,并不会把内存归还给操作系统,而是缓存在 malloc 的内存池中,待下次使用; - malloc 通过
mmap()
方式申请的内存,free 释放内存的时候,会把内存归还给操作系统,内存得到真正的释放。
为什么不全部使用 mmap 来分配内存? ★ \color{red}{★} ★
频繁通过 mmap()
分配内存的话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
为了改进这两个问题,malloc 通过 brk()
系统调用在堆空间申请内存时,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放时,就缓存在内存池中。
等下次申请内存时,就直接从内存池取出对应的内存块就行了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低 CPU 的消耗。
既然 brk 那么牛逼,为什么不全部使用 brk 来分配? ★ \color{red}{★} ★
随着系统频繁地 malloc 和 free ,尤其对于小块内存,堆内将产生越来越多不可用的碎片,导致“内存泄露”。而这种“泄露”现象使用 valgrind 是无法检测出来的。
所以,malloc 实现中,充分考虑了 brk 和 mmap 行为上的差异及优缺点,默认分配大块内存 (128KB) 才使用 mmap 分配内存空间。
free() 函数只传入一个内存地址,为什么能知道要释放多大的内存?
malloc 返回给用户态的内存起始地址比进程的堆空间起始地址多了 16 字节,这里就保存了该内存块的描述信息:比如有该内存块大小及起始地址等。这样当执行 free() 函数时,free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的分析出当前的内存块的大小,自然就知道要释放多大的内存了。
4.3 内存满了会发生什么
内存分配过程 ★ \color{red}{★} ★
应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存,这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler (缺页中断函数)处理。
缺页中断处理函数会看是否有空闲的物理内存,如果有就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作,回收的方式主要是两种:直接内存回收和后台内存回收。
-
后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程是异步的,不会阻塞进程的执行。
-
直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会放最后的大招了 ——触发 OOM (Out of Memory)机制。
OOM Killer 机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源。如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。
哪些内存可以被回收(可被回收的内存类型)? ★ \color{red}{★} ★
可被回收的内存类型有文件页和匿名页:
- 文件页:内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)
- 干净页:大部分文件页,都可以直接释放内存,需要时再从磁盘读取即可。
- 脏页:而那些被应用程序修改过,并且暂时还没写入磁盘的数据(脏页),就得先写入磁盘,然后才能进行内存释放。
- 匿名页:部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如 堆、栈 数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存。
回收内存带来的性能影响
【文件页】的回收:对于干净页是直接释放内存,这个操作不会影响性能;而对于脏页会先写回到磁盘再释放内存,这个操作会发生磁盘 I/O 的,会影响系统性能。
【匿名页】的回收:如果开启了 Linux Swap 机制,那么 Swap 机制会将不常访问的匿名页换出到磁盘中,并释放内存给其他更需要的进程使用。再次访问这些内存时,再从磁盘换入到内存中,这个操作会发生磁盘 I/O,是会影响系统性能的。
文件页和匿名页的回收都是基于 LRU 算法,也就是优先回收不常访问的内存。回收内存的操作基本都会发生磁盘 I/O 的,如果回收内存的操作很频繁,意味着磁盘 I/O 次数会很多,这个过程势必会影响系统的性能。
针对回收内存导致的性能影响,常见的解决方式(稍微了解即可)
- 设置
/proc/sys/vm/swappiness
,调整文件页和匿名页的回收倾向,尽量倾向于回收文件页; - 设置
/proc/sys/vm/min_free_kbytes
,调整 kswapd 内核线程异步回收内存的时机; - 设置
/proc/sys/vm/zone_reclaim_mode
,调整 NUMA 架构下内存回收策略,建议设置为 0,这样在回收本地内存之前,会在其他 Node 寻找空闲内存,从而避免在系统还有很多空闲内存的情况下,因本地 Node 的本地内存不足,发生频繁直接内存回收导致性能下降的问题;
如何保护一个进程不被 OOM 杀掉呢?(稍微了解即可)
我们可以通过调整进程的 /proc/[pid]/oom_score_adj
值,来降低被 OOM killer 杀掉的概率
4.4 在 4GB 物理内存的机器上,申请 8G 内存会怎么样? ★ \color{red}{★} ★
这个问题在没有前置条件下,就说出答案就是耍流氓。这个问题要考虑三个前置条件:
- 操作系统是 32 位的,还是 64 位的?
- 申请完 8G 内存后会不会被使用?
因为只有虚拟内存被真正访问后,触发了缺页中断,才会分配对应的物理内存 - 操作系统有没有使用 Swap 机制?
所以,我们要分场景讨论。简单总结下:
- 在 32 位操作系统,因为进程最大只能申请 3 GB 大小的用户空间虚拟内存,所以直接申请 8G 内存,会申请失败。
- 在 64位 位操作系统,因为进程最大只能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:
- 如果没有 Swap 分区,因为 4 GB的物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
- 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;
swap机制 ★ \color{red}{★} ★
Swap 就是把一块磁盘空间或者本地文件,充当内存来使用,防止物理内存不够用。
- 它包含换出和换入两个过程:
- 换出(Swap Out) ,是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存;
- 换入(Swap In),是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来;
- 优缺点:
应用程序实际可以使用的内存空间将远远超过系统的物理内存。由于硬盘空间的价格远比内存要低,因此这种方式无疑是经济实惠的。
当然,频繁地读写硬盘,会显著降低操作系统的运行速率,这也是 Swap 的弊端。 - 触发条件:
- 内存不足:采用直接内存回收(Direct Page Reclaim)。直接内存回收是同步的过程,会阻塞当前申请内存的进程。
- 内存闲置:应用程序在启动阶段使用的大量内存在启动后往往都不会使用(不活跃)。通过运行的后台守护进程 - kSwapd,我们可以将这部分只使用一次的内存交换到磁盘上为其他内存的申请预留空间。由于kSwapd 是后台进程,所以回收内存的过程是异步的,不会阻塞当前申请内存的进程。
如何避免预读失效和缓存污染的问题 ★ \color{red}{★} ★
在这之前先介绍几个概念,便于后续理解:
Linux 和 MySQL 的缓存
- Linux 操作系统的缓存:在应用程序读取文件数据时,Linux 操作系统是会对读取的文件数据进行缓存的,会缓存在文件系统中的 Page Cache 。
Page Cache 属于内存空间里的数据,由于内存访问比磁盘访问快很多,在下一次访问相同的数据就不需要通过磁盘 I/O 了,命中缓存就直接返回数据即可。
因此,Page Cache 起到了加速访问数据的作用。- MySQL 的缓存:
MySQL 的数据是存储在磁盘里的,为了提升数据库的读写性能,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),Buffer Pool 属于内存空间里的数据。
有了缓冲池后:
- 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
- 当修改数据时,首先是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘。
磁盘数据页预读的原因:
首先,操作系统出于空间局部性原理:靠近当前被访问数据的数据,在未来很大概率会被访问到。因此会存在磁盘数据的预读。
因此,预读机制带来的好处就是减少了 磁盘 I/O 次数,提高系统磁盘 I/O 吞吐量。
其次再来说下 预读失效 和 缓存污染 :
- 预读失效:如果这些被提前加载进来的页,并没有被访问,相当于这个预读工作是白做了,这个就是预读失效。
- 缓存污染:当我们在批量读取数据时,由于数据被访问了一次,这些大量数据都会被加入到「活跃 LRU 链表」里,然后之前缓存在活跃 LRU 链表(或者 young 区域)里的热点数据全部都被淘汰了,如果这些大量的数据在很长一段时间都不会被访问的话,那么整个活跃 LRU 链表(或者 young 区域)就被污染了。
传统的 LRU 算法无法避免下面这两个问题:
- 预读失效 导致缓存命中率下降;
- 缓存污染 导致缓存命中率下降;
为了解决「预读失效」的问题,Linux 和 MySQ