小林coding

内存管理

虚拟内存是什么?

  • 为了在多进程环境下,使得进程之间的内存地址不受影响,相互隔离,于是操作系统就为每个进程独立分配一套虚拟地址空间.
  • 那既然有了虚拟地址空间,那必然要把虚拟地址「映射」到物理地址,这部分是由操作系统内存管理单元做的。
  • 虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。

虚拟内存有什么用处?

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

映射方式有哪几种?

  • 内存分段是根据程序的逻辑角度,分成了栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。但是每个段的大小都不是统一的,这就会导致外部内存碎片和内存交换效率低的问题。
  • 内存分页,把虚拟空间和物理空间分成大小固定的页,如在 Linux 系统中,每一页的大小为 4KB。由于分了页后,就不会产生细小的内存碎片,解决了内存分段的外部内存碎片问题。同时在内存交换的时候,写入硬盘也就一个页或几个页,这就大大提高了内存交换的效率。
  • 将两者结合起来构成段页映射方式。

为了优化映射的速度有哪几种优化措施?

  • 为了解决简单分页产生的页表过大的问题,就有了多级页表,它解决了空间上的问题,但这就会导致 CPU 在寻址的过程中,需要有很多层表参与,加大了时间上的开销。只有第一层页表需要全部加载,后面的用到的时候再加载。因为虚拟地址必须有映射关系。
  • 于是根据程序的局部性原理,在 CPU 芯片中加入了 TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度。

申请一块内存,整个分配过程是什么样的?

  • 应用程序通过 malloc 函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
  • 当应用程序读写了这块虚拟内存,CPU 就会去访问这个虚拟内存, 这时会发现这个虚拟内存没有映射到物理内存, CPU 就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的 Page Fault Handler (缺页中断函数)处理。
  • 缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
  • 如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作。

内存回收工作是怎么进行的?

  • 如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作,回收的方式主要是两种:直接内存回收和后台内存回收。
    • 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒 kswapd 内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
    • 直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。

如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,怎么办?

  • 内核就会触发 OOM (Out of Memory)机制。
  • OOM Killer 机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer 会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。
  • 算法对的打分为:
    进程得分的结果受下面这两个方面影响:
    • 第一,进程已经使用的物理内存页面数。
    • 第二,每个进程的 OOM 校准值 oom_score_adj。它是可以通过 /proc/[pid]/oom_score_adj 来配置的。

4G内存申请8G空间,会有什么问题?

在 32 位操作系统,因为进程理论上最大能申请 3 GB 大小的虚拟内存,所以直接申请 8G 内存,会申请失败。
在 64位 位操作系统,因为进程理论上最大能申请 128 TB 大小的虚拟内存,即使物理内存只有 4GB,申请 8G 内存也是没问题,因为申请的内存是虚拟内存。如果这块虚拟内存被访问了,要看系统有没有 Swap 分区:

  • 如果没有 Swap 分区,因为物理空间不够,进程会被操作系统杀掉,原因是 OOM(内存溢出);
  • 如果有 Swap 分区,即使物理内存只有 4GB,程序也能正常使用 8GB 的内存,进程可以正常运行;

什么是swap分区?

  • 当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。那些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间会被临时保存到磁盘,等到那些程序要运行时,再从磁盘中恢复保存的数据到内存中。
  • 另外,当内存使用存在压力的时候,会开始触发内存回收行为,会把这些不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
  • 这种,将内存数据换出磁盘,又从磁盘中恢复数据到内存的过程,就是 Swap 机制负责的。

在读取数据的时候,我们往往会多读一些数据进来。可是采用lru算法替换数据,会把热点数据替换掉,你有什么好方法解决这个问题吗?

  • 将lru中的数据分为两个部分,冷数据和热数据。
  • 将预读的数据放到冷数据中,当使用的时候再放到热数据。
  • 热数据淘汰之后,先放到冷数据,完全淘汰的时候再淘汰。

但是如果只访问一次lru就变换位置,还是会有缓存污染的情况存在,你怎么解决?

  • 为了避免「缓存污染」造成的影响,我们可以提高了升级为热点数据的门槛。

调度算法

说说进程常见的调度算法。

先来先服务调度算法:每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。
最短作业优先调度算法:优先选择运行时间最短的进程来运行。
高响应比优先调度算法:每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行。
优先级计算方法:

时间片轮转调度算法:
每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行。
如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外一个进程;
如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换;
最高优先级调度算法:多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(Highest Priority First,HPF)调度算法。
多级反馈队列调度算法:

  • 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短;
  • 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
  • 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;
    https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/操作系统/进程和线程/28-多级队列.jpg
    可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。

说说内存页面置换算法

  • 最佳页面置换算法(OPT):置换在「未来」最长时间不访问的页面。是理想算法,难以实现。
  • 先进先出置换算法(FIFO):选择在内存驻留时间很长的页面进行中置换。
  • 最近最久未使用的置换算法(LRU):选择最长时间没有被访问的页面进行置换。开销比较大,实际应用中比较少使用。
  • 时钟页面置换算法(Lock):时钟页面置换算法就可以两者兼得,它跟 LRU 近似,又是对 FIFO 的一种改进。
  • 该算法的思路是,把所有的页面都保存在一个类似钟面的「环形链表」中,一个表针指向最老的页面。
  • 当发生缺页中断时,算法首先检查表针指向的页面:
  • 如果它的访问位位是 0 就淘汰该页面,并把新的页面插入这个位置,然后把表针前移一个位置;
  • 如果访问位是 1 就清除访问位,并把表针前移一个位置,重复这个过程直到找到了一个访问位为 0 的页面为止;
    https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/操作系统/调度算法/时钟置换算法.png
    加个图片
  • 最不常用置换算法(LFU):当发生缺页中断时,选择「访问次数」最少的那个页面,并将其淘汰。
  • 它的实现方式是,对每个页面设置一个「访问计数器」,每当一个页面被访问时,该页面的访问计数器就累加 1。在发生缺页中断时,淘汰计数器值最小的那个页面。

说说磁盘调度算法

磁盘是圆形结构,分为不同的磁道。寻道时间是最长的。
常见的磁盘调度算法有:

  • 先来先服务算法:先到来的请求,先被服务。
  • 最短寻道时间优先算法:优先选择从当前磁头位置所需寻道时间最短的请求。会产生饥饿现象。产生饥饿的原因是磁头在一小块区域来回移动。
  • 扫描算法:磁头在一个方向上移动,访问所有未完成的请求,直到磁头到达该方向上的最后的磁道,才调换方向,这就是扫描(Scan)算法。类似于电梯。但是存在这样的问题,中间部分的磁道会比较占便宜,中间部分相比其他部分响应的频率会比较多,也就是说每个磁道的响应频率存在差异。
  • 循环扫描算法:只有磁头朝某个特定方向移动时,才处理磁道访问请求,而返回时直接快速移动至最靠边缘的磁道,也就是复位磁头,这个过程是很快的,并且返回中途不处理任何请求,该算法的特点,就是磁道只响应一个方向上的请求。循环扫描算法相比于扫描算法,对于各个位置磁道响应频率相对比较平均。
  • LOOK与C-LOOK算法:
    我们前面说到的扫描算法和循环扫描算法,都是磁头移动到磁盘「最始端或最末端」才开始调换方向。
    那这其实是可以优化的,优化的思路就是磁头在移动到「最远的请求」位置,然后立即反向移动。
    那针对 SCAN 算法的优化则叫 LOOK 算法,它的工作方式,磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,反向移动的途中会响应请求。
    而针 C-SCAN 算法的优化则叫 C-LOOK,它的工作方式,磁头在每个方向上仅仅移动到最远的请求位置,然后立即反向移动,而不需要移动到磁盘的最始端或最末端,反向移动的途中不会响应请求。

文件系统

传统的文件传输调用read和write方法,会发生什么事情?

发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:
第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。
https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/操作系统/零拷贝/传统文件传输.png
同时,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。

怎么优化文件传输的性能?

采用零拷贝技术,减少上下文切换的次数。

有哪几种零拷贝技术?

零拷贝技术实现的方式通常有 2 种:

  • mmap + write
  • sendfile

mmap + write是怎么做的?

应用进程调用了 mmap() 后,DMA 会把磁盘的数据拷贝到内核的缓冲区里。接着,应用进程跟操作系统内核「共享」这个缓冲区;
应用进程再调用 write(),操作系统直接将内核缓冲区的数据拷贝到 socket 缓冲区中,这一切都发生在内核态,由 CPU 来搬运数据;
最后,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程是由 DMA 搬运的。
此时仍然需要通过 CPU 把内核缓冲区的数据拷贝到 socket 缓冲区里,而且仍然需要 4 次上下文切换,因为系统调用还是 2 次。

sendfile怎么做的?

在 Linux 内核版本 2.1 中,提供了一个专门发送文件的系统调用函数 sendfile(),函数形式如下:

include <sys/socket.h>

ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
它的前两个参数分别是目的端和源端的文件描述符,后面两个参数是源端的偏移量和复制数据的长度,返回值是实际复制数据的长度。
首先,它可以替代前面的 read() 和 write() 这两个系统调用,这样就可以减少一次系统调用,也就减少了 2 次上下文切换的开销。
其次,该系统调用,可以直接把内核缓冲区里的数据拷贝到 socket 缓冲区里,不再拷贝到用户态,这样就只有 2 次上下文切换,和 3 次数据拷贝。如下图:
https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/操作系统/零拷贝/senfile-3次拷贝.png

如果网卡支持 SG-DMA(The Scatter-Gather Direct Memory Access)技术(和普通的 DMA 有所不同),sendfile怎么优化的?

sendfile() 系统调用的过程发生了点变化,具体过程如下:
第一步,通过 DMA 将磁盘上的数据拷贝到内核缓冲区里;
第二步,缓冲区描述符和数据长度传到 socket 缓冲区,这样网卡的 SG-DMA 控制器就可以直接将内核缓存中的数据拷贝到网卡的缓冲区里,此过程不需要将数据从操作系统内核缓冲区拷贝到 socket 缓冲区中,这样就减少了一次数据拷贝;
https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost2/操作系统/零拷贝/senfile-零拷贝.png
这就是所谓的零拷贝(Zero-copy)技术,因为我们没有在内存层面去拷贝数据,也就是说全程没有通过 CPU 来搬运数据,所有的数据都是通过 DMA 来进行传输的。。
零拷贝技术的文件传输方式相比传统文件传输的方式,减少了 2 次上下文切换和数据拷贝次数,只需要 2 次上下文切换和数据拷贝次数,就可以完成文件的传输,而且 2 次的数据拷贝过程,都不需要通过 CPU,2 次都是由 DMA 来搬运。
所以,总体来看,零拷贝技术可以把文件传输的性能提高至少一倍以上。

了解page cache吗?讲讲他的作用。

内核缓冲区就是磁盘高速缓存。
PageCache 的优点主要是两个:

  • 缓存最近被访问的数据:程序运行的时候,具有「局部性」,所以通常,刚被访问的数据在短时间内再次被访问的概率很高,于是我们可以用 PageCache 来缓存最近被访问的数据,当空间不足时淘汰最久未被访问的缓存。
  • 预读功能:假设 read 方法每次只会读 32 KB 的字节,虽然 read 刚开始只会读 0 ~ 32 KB 的字节,但内核会把其后面的 32~64 KB 也读取到 PageCache,这样后面读取 32~64 KB 的成本就很低,如果在 32~64 KB 淘汰出 PageCache 前,进程读取到它了,收益就非常大。

linux命令

衡量网络性能的有哪些?

  • 带宽,表示链路的最大传输速率,单位是 b/s (比特 / 秒),带宽越大,其传输能力就越强。
  • 延时,表示请求数据包发送后,收到对端响应,所需要的时间延迟。不同的场景有着不同的含义,比如可以表示建立 TCP 连接所需的时间延迟,或一个数据包往返所需的时间延迟。
  • 吞吐率,表示单位时间内成功传输的数据量,单位是 b/s(比特 / 秒)或者 B/s(字节 / 秒),吞吐受带宽限制,带宽越大,吞吐率的上限才可能越高。
  • PPS,全称是 Packet Per Second(包 / 秒),表示以网络包为单位的传输速率,一般用来评估系统对于网络的转发能力。

网络吞吐率和PPS如何查看?

  • 可以使用 sar 命令当前网络的吞吐率和 PPS,用法是给 sar 增加 -n 参数就可以查看网络的统计信息,比如
    sar -n DEV,显示网口的统计数据;
    sar -n EDEV,显示关于网络错误的统计数据;
    sar -n TCP,显示 TCP 的统计数据
    rxpck/s 和 txpck/s 分别是接收和发送的 PPS,单位为包 / 秒。
    rxkB/s 和 txkB/s 分别是接收和发送的吞吐率,单位是 KB/ 秒。
    rxcmp/s 和 txcmp/s 分别是接收和发送的压缩数据包数,单位是包 / 秒。

进程线程

一个进程能创造多少个线程?

  • 32 位系统,用户态的虚拟空间只有 3G,如果创建线程时分配的栈空间是 10M,那么一个进程最多只能创建 300 个左右的线程。
  • 64 位系统,用户态的虚拟空间大到有 128T,理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。

线程崩溃,进程一定会崩溃吗?

一般来说如果线程是因为非法访问内存引起的崩溃,那么进程肯定会崩溃

进程是如何崩溃的?

CPU 执行正常的进程指令
调用 kill 系统调用向进程发送信号
进程收到操作系统发的信号,CPU 暂停当前程序运行,并将控制权转交给操作系统
调用 kill 系统调用向进程发送信号(假设为 11,即 SIGSEGV,一般非法访问内存报的都是这个错误)
操作系统根据情况执行相应的信号处理程序(函数),一般执行完信号处理程序逻辑后会让进程退出
注意上面的第五步,如果进程没有注册自己的信号处理函数,那么操作系统会执行默认的信号处理程序(一般最后会让进程退出),但如果注册了,则会执行自己的信号处理函数,这样的话就给了进程一个垂死挣扎的机会,它收到 kill 信号后,可以调用 exit() 来退出,但也可以使用 sigsetjmp,siglongjmp 这两个函数来恢复进程的执行

为什么线程崩溃不会导致 JVM 进程崩溃?

虚拟机内部定义了信号处理函数,而在信号处理函数中对这两者做了额外的处理以让 JVM 不崩溃,另一方面也可以看出如果 JVM 不对信号做额外的处理,最后会自己退出并产生 crash 文件 hs_err_pid_xxx.log(可以通过 -XX:ErrorFile=/var/log/hs_err.log 这样的方式指定),这个文件记录了虚拟机崩溃的重要原因。

posted @   qirmcmww  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!
点击右上角即可分享
微信分享提示