操作系统面试理解(八股文)

操作系统:

并发:指宏观上看起来两个程序在同时运行,比如说在单核CPU上的多任务。但是从微观上看两个程序的指令是交织着运行的,指令之间交错执行,在单个周期只运行一次。这方法不能提高计算机的性能,只能提高效率。

并行:指严格物理意义上的同时运行,比如多核CPU,两个程序分别运行在两个核上,两者之间互不影响,单个周期内每个程序都运行了自己的指令,也就是运行了两个指令。这样提高了计算机的性能。

什么是进程?

进程是操作系统中比较重要的抽象概念之一,是资源分配的基本单位,是独立运行的基本单位。

进程的定义是一个执行中程序的实例。三部分组成:进程控制块PCB,程序段,数据段。

操作进程:1:创建fork()

2:回收子进程()

3、加载并运行程序

4、进程终止

进程之间的通讯:管道、系统IPC(包括消息队列、信号量、信号、共享内容等)、以及套接字socket.

孤儿进程:父进程退出了,子进程还在运行。

僵尸进程:进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait获waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保持在系统中的这些进程是僵尸进程。

什么是线程?

1、是进程划分的任务,是一个进程内可调度的实体,是CPU调度的基本单位,用于保证程序的实时性,实现进程内部的并发。

2、线程是操作系统可识别的最小执行和调度单位。每个线程都单独占用一个虚拟处理器:独自的寄存器组,指令计数器和处理器状态。

3、每个线程完成不同的任务,但是属于同一个进程的不同线程之间共享同一地址空间,打开的文件队列和其他内核资源。

为什么需要线程?

线程产生的原因:进程可以使多个程序能并发执行,以提高资源的利用率和系统的吞吐量;但是其具有一些缺点:

1、进程在同一时刻只能做一个任务,很多时候不能充分利用CPU资源。

2、进程在执行的过程中如果发生阻塞,整个进程就会被挂起,即使进程中其他任务不依赖于等待的资源,进程仍会被阻塞。

引入线程就是为了解决以上进程的不足,线程具有以下的优点:

1、从资源上讲,开辟一个线程所需要的资源要远小于一个进程。

2、从切换效率上来讲,线程切换速度更快,比起进程。

3、从通信机制上来讲,线程间方便的通信机制。对不同进程来说,它们具有独立的地址空间,要进行数据的传递只能通过进程间通信的方式进行。线程则不然,属于同一个进程的不同线程之间共享同一地址空间,所以一个线程的数据可以被其他线程感知,线程之间可以直接读写数据段(如全局变量)来进行通信(需要一些同步措施)。

1、线程与进程的区别:一个线程只能属于一个进程,而同一个进程可以有多个线程,但至少有一个线程。

2、进程在执行过程中拥有独立的地址空间,而多个线程共享进程的地址空间。

3、进程是资源分配的最小单位,线程是CPU调度的最小单位。

4、通信:由于同一进程中的多个线程具有相同的地址空间,使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程段(如全局变量)来进行通信(需要一些同步方法,以保证数据的一致性)。

5、进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对负杂。

6、进程间不会相互;一个进程内某个线程挂掉将导致整个进程挂掉。

7、进程适合于多核、多机分布;线程适用于多核。

多线程模型:

1、多对一模型:将多个用户级线程映射到一个内核级线程。该模型下,线程在用户空间进行管理,效率较高。

2、一对一模型:将内核线程与用户线程一一对应。优点是一个小城阻塞时,不会影响到其他线程的执行。该模型具有更好的并发性。缺点是内核线程数量有上线,会限制用户线程的数量。更多的内核线程数也给线程切换带来了额外的负担。linux和windows操作系统家族都是使用一对一模型。

3、多对多模型。将多个用户级线程映射到多个内核级线程上。结合了多对一模型和一对一模型的特点。

进程同步的方法:就是等哪个资源有空,然后去进行进程同步

1、互斥锁

2、读写锁

3、条件变量

4、记录锁

5、信号量

6、屏障

线程同步的方法:

操作系统中,属于同一进程的线程具有相同的地址空间,线程之间共享数据变得简单高效。遇到竞争的线程同时修改同一数据或是写作的线程设置同步点的问题,需要使用一些线程同步的方法来解决这些问题。

与进程同步方法类似。

进程同步与线程同步的difference?

进程之间的地址空间不同,不能感知对方的存在,同步时需要将锁放在多进程共享的空间。而线程之间共享同一地址空间即可。

死锁是怎么产生的?

死锁是指两个或两个以上进程在执行过程中,因争夺资源而造成的下相互等待的现象。产生死锁需要满足下面四个条件。

1、互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问该资源,只能等待,直至占有该资源的进程使用完成后释放资源。

2、占有并等待条件:进程获得一定的资源后,又对其他资源发出请求,但是该资源可能被其他进程占有,此事请求阻塞,但该进程不会释放自己已经占有的资源。

3、非抢占条件:进程已获得资源,在未完成使用之前,不可被剥夺,只能在使用后自己释放。

4、循环等待条件:进程发送死锁后,必然存在一个进程-资源之间的环形链。

如何解决死锁问题?

解决死锁方法就是破坏产生的条件。主要方法:

1、资源一次性分配,这样就不会有请求了(破坏请求条件)

2、只要有一个资源得不到分配,也不给这个进程分配其他资源(破坏占有并等待条件)。

3、可抢占式资源:即当进程新的资源未得到满足时,释放已占有的资源,从而破坏不可抢占的条件

4、资源有序分配法:系统给每类资源赋予一个序号,每个进程按编号递增的请求资源,释放则相反,从而破坏环路等待的条件。

Linux内核 与windows内核

Windows和linux可以是我们比较常见的两款操作系统。

Windows基本占领了电脑时代的市场,商业上取得了很大的成就,但是它并不开源。

对于服务器操作的操作系统基本都是linux,而且内核源码也是开源的,任何人都可以下载,并增加自己的改动或功能,linux最大的魅力在于,全世界有非常多的技术大佬为它贡献代码

What is 内核?计算机由各种外部硬件设备组成,比如内存、cpu、硬盘等,如果每个应用都要和这些硬件设备对接通讯协议,那样太累了,所以这个中间人就由内核来负责,让内核作为应用连接硬件设备的桥梁,应用程序只需关系与内核交互,不用关心硬件的细节。

内核有哪些能力?

现代操作系统,内核一般会提供4个基本能力:

管理进程、线程,决定哪个进程、线程使用CPU,也就是进程调度的能力;

管理内存,决定内存的分配和回收,也就是内存管理的能力;

管理硬件设备,为进程与硬件设备之间提供通信能力,也就是硬件通信能力;

提供系统调用,如果应用程序要运行更高权限运行的服务,那么就需要有系统调用,它是用户程序与操作系统之间的接口。

内核是怎么工作的?

内核具有很高的权限,可以控制cpu、内存、硬盘等硬件,而应用程序具有权限很小,因此大多数操作系统,把内存分为两个区域:

内核空间,这个内存空间只有内核程序才能访问;

用户空间,这个内存空间专门给应用程序使用;

用户空间的代码只能访问一个局部的内存空间,而内核空间的代码可以访问所有内存空间。因此,当程序使用用户空间时,我们常说程序在用户态执行,而当程序使内核空间时候,程序则在内核态执行。

应用程序如果需要进入内核空间,就需要通过系统调用,下面来看看系统调用的过程:

内核程序执行在内核态,用户程序执行在用户态。当应用程序使用系统调用时,会产生一个中断。发生中断后,CPU会中断当前在执行的用户程序,转而跳转到中断处理程序,也就是开始执行内核程序。内核处理完后,主动出发中断,把CPU执行权限交回给用户程序,回到用户态继续工作。

Monolithic Kernel

Monolithic Kernel的意思是宏内核,Linux内核架构就是宏内核,意味着Linux的内核是一个完整的可执行程序,且拥有最高的权限。

宏内核的特征是系统内核的所有模块,比如进程调度、内存管理、文件系统、设备驱动等,都运行在内核态。

不过,Linux也实现了动态加载内核模块的功能,例如大部分设备驱动是以可加载模块的形式存在的,与内核其他模块解耦,让驱动开发和驱动加载更为方便、灵活。

与宏内核相反的是微内核,微内核架构的内核只保留最基本的能力,比如进程调度、虚拟机内存,中断等,把一些应用放到用户空间,比如驱动程序、文件系统等。这样服务与服务之间是隔离的,单个服务出现故障或者完全攻击,也不会导致整个操作系统挂掉,提高了操作系统的稳定性和可靠性。

微内核功能少,可移植性高,相比宏内核有一点不好的地方在于,由于驱动程序不在内核中,而且驱动程序一般会频繁调用底层能力的,于是驱动和硬件设备交互就需要频繁切换到内核态,这样会带来性能损耗。华为的鸿蒙操作系统的内核架构就是微内核。

还有一种内核叫做混合类型内核,它的架构有点像微内核,内核里面会有一个最小版本的内核,然后其他模块会在这基础上搭建,然后实现的时候会跟宏内核类似,也就是把整个内核做成一个完整的程序,大部分服务都在内核中,这就像是宏内核的方式包裹着一个微内核。

windows设计

当今windows7、window10使用的内核叫windowNT,NT全称叫New Technology。

Windows与Linux一样,同样支持MultuTask和SMP,但是不同的是,Window的内核设计是混合型内核,在上图你可以看到内核中一个MicroKernel模块,这个就是最小版本的内核,而整个内核实现是一个完整的程序,含有非常多的模块。

windows的可执行文件叫PE,称为可移植执行文件,拓展名通常是.exe,.dll,.sys等。

对于内核的架构一般有三种类型:

宏内核:包括多个模块,整个内核像一个完整的程序;

微内核:有一个最小版本的内核,一些模块和服务则由用户态管理;

混合内核:是宏内核和微内核的结合体,内核中抽象出了微内核的概念,也就是内核中会有一个小型的内核,其他模块就在这个基础上搭建,整个内核就是完整的程序;

Linux的内核设计是采用了宏内核,Window的内核设计则是采用了混合内核。

进程

虚拟内存与物理内存

操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。

如果程序要访问虚拟地址的时候,由操作系统转换成不同的物理地址,这样不同的进程运行的时候,写入的是不同的物理地址。

所以我引入概念:

程序所使用的的内存就是虚拟内存地址。

实际在硬件中的空间地址就是物理内存地址。

操作系统引入虚拟内存,进程持有的虚拟地址会通过CPU芯片中的内存管理单位(MMU)的映射关系,来转换编程物理地址,然后通过物理地址访问内存

.

内存分段:

程序是由若干个逻辑分段组成,如可由代码分段、数据分段、栈段、堆栈组成。不同的段是有不同的属性,所以就用分段的形式把这些段分离出来。

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

段选择因子和段内偏移量:

段选择子就保持在段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。段表里面保持的是这个段的基地址、段的界限和特权等级等。

虚拟地址中的段内偏移量应该位于0和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

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

如果要访问段3中偏移量500的虚拟地址,我们可以计算出物理地址为,段3基地址7000+偏移量500 = 7500。

分段的办法很好,解决了程序本身不需要关系的具体物理内存地址问题,但是它也有一些不足之处:

第一个就是内存碎片的问题。

第二个就是内存交换的效率低的问题。

Why 存在内存碎片问题?

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

内存碎片主要分为,内部内存碎片和外部内存碎片。

内部分段管理可以做到段根据实际需求分配内存,所以有多少需求就分配多大的段,所以不会出现内部内存碎片。

但是由于每个段的长度不固定,所以多个段未必能恰好使用到所有的内存空间,会产生了多个不连续的小物理内存,导致新的程序无法被加载,所以会出现外部内存碎片的问题。

解决外部内存碎片的问题就是内存交换。

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

内存碎片主要分为,内部内存碎片与外部内存碎片。内存分段管理可以做到段根据实际需求分配内存,所以有多少需求就分配多大的段,所以不会出现内部内存碎片。

但是由于每个段的长度不固定,所以多个段未必能恰好使用所有的内存空间,会产生多个不连续的小物理内存,导致新的程序无法被装载,所以会出现外部内存碎片的问题。

解决外部内存碎片的问题就是内存交换。

可以把音乐程序占用的那256MB空间写到硬盘上,然后再从硬盘上读回来到内存里。不过再读回的时候,我们不能装载回原来的位置,而是紧紧耕者那已经被占用了的512MB内存后面。这样就能空缺处连续的256MB空间,于是新的200MB程序就可以装载进来。

这个内存交换空间,在Linux系统里,也就是我们看到的Swap空间,这块空间就是从硬盘划分出来的,用于内存与硬盘空间的交换。

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

对于多进程的系统来说,用分段的方式,外部内存碎片很容易产生,产生了外部内存碎片,那不得不重新Swap内存区域,这个过程会产生性能瓶颈。

因为硬盘的访问速度要比内存慢太多了,每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。

所以,如果内存交换的时候,交换的是一个占内存空号。

对于一个内存地址转换,其实就是这样三个步骤:

1、把虚拟内存地址,切分成页号和偏移量;

2、根据页号,从页表里面,查询对应的物理内存地址。

3、直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。

简单的分页有什么缺陷?

空间上的缺陷。

多级页表?

要解决上面的问题,需要采用一种叫做多级页表的解决方案。

TLB

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

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

我们就可以利用这一特,把最常访问的几个页表项存储到访问素的更快的硬件。

段页式内存管理:

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

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

先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;

接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,在划分固定大小的页;

接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;

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

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

段页式地址变换中要得到物理地址需经过三次内存访问:

第一次访问段表,得到页表起始地址;

第二次访问页表,得到物理页号;

第三次将物理页号与页内位移组合,得到物理地址。

可用软、硬件结合的方法实现段页式地址变换,这样虽然增加了硬件成本和系统开销,但提高了内存的利用率

关于linux内存的理解?

Linux也是用页式内存管理,但同时也涉及了段机制

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

在Linux操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围页不同。

通过这里可以看出:

32位系统的内核空间占用1G,位于最高处,剩下3G是用户空间;

64位系统的内核空间和用户空间都是128T,分别占据了整个内存空间的最高和最低处,剩下的中间部分是未定义的。

再来说说,内核空间与用户空间的区别:

进程在用户态时候,只能访问用户空间的内存;

只有进入内核态后,才能访问内核空间的内存;

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

接下来,进一步了解虚拟空间的分布情况,以32位系统为例,画了一张图来表示它们的关系;

通过这张图,你可以看到用户空间内存,从低到高分别是6种不同的内存段:

程序文件段(.text),包括二进制可执行代码;

已初始化数据段(.data),包括静态常量;

未初始化数据段(.bss),包括未初始化的静态变量;

堆段:包括动态分配的内存,从低地址开始向上增长;

文件映射段,包括动态库、共享内存等,从低地址开始向上增长。

栈段,包括局部变量和函数低啊用的上下文等。栈的大小是固定的,一般是8MB。当然系统也提供了参数,以便我们自定义大小;

在这7个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用C标准库的malloc()或者mmap(),就可以分别在堆和文件映射段动态分配内存。

总结下来:为了在多进程环境下,使得进程之间的内存不受影响,相互隔离,于是操作系统就为每个进程独立分配了一套虚拟地址空间,每个程序只关心自己的虚拟地址就可以,实际上大家的虚拟地址都一样,但分布到物理地址内存是不一样的。作为程序,也不用关心物理地址的事情。

每个进程都有自己的虚拟空间,而物理空间只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术(swap),把不常用的内存暂时存到硬盘(swap out 换出),在必要的时候再装载回物理内存(换入)。

那既然有了虚拟地址空间,那必然要把虚拟地址映射到物理地址,这事情通常由操作系统来维护。

那么对于虚拟地址与物理地址的映射关系,可以有分段和分页的方式,同时两者结合也是可的。

内存分段是根据程序的逻辑角度,分成栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。但是每个段的大小都不是统一的,这就会导致外部内存碎片和内存交换效率低的问题。

于是,就出现了内存分页,把虚拟空间的物理空间分成大小固定的页,如在Linux系统中,每一页的大小为4KB。由于分了页之后,就不会产生细小的内存碎片,解决了内存分段的外部内存碎片的问题。同时在内存交换的时候,写入硬盘也就是一个页或几个页,这就大大提高了内存交换的效率。

同时,为了解决简单分页产生的页表过大的问题,就有了多级页表,它解决了空间上的问题,但这就会导致CPU在寻址的过程中,需要有很多层表参与,加大了时间上的开销。于是根据程序的局部性原理,在CPU芯片中加入TLB,负责缓存最近常被访问的页表项,大大提高了地址的转换速度。

Linux系统主要采用分页管理,但是由于intel处理器的发展史,但是因为Intel发展史,Linux系统无法避免分段管理。于是,Linux就把所有段的基地址设为0,也就是意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了CPU逻辑地址的概念,所以段只被用于访问控制和内存保护。

另外,Linux系统中虚拟空间分布可分为用户态和内核态两部分,其中用户态的分布:代码段、全局变量、BSS(指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。)、函数栈、堆内存、映射区。

最后说下,虚拟内存的作用?

第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU访问内存会有明显的重复性访问的倾向性,对于哪些没有被经常使用到的内存,我们可以把它换出物理内存之外,比如硬盘上的swap区域。

第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。

第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。

malloc会分配多大的虚拟内存?

malloc()在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是而是会预分配更大的空间作为内存池。

具体会预分配多大的空间,跟malloc使用的内存管理器有关系。

Malloc是如何分配内存的?

实际上,malloc()并不是系统调用,而是C库里的函数,用于动态分配内存。

malloc申请内存的时候,会有两种方式向操作系统申请堆内存。

1、通过brk()系统调用从堆内存分配的内存

2、通过mmap()系统调用在文件映射区域分配内存;

方式一:实现的方式很简单,就是通过brk()函数将堆分配内存

方式二:通过mmap()系统调用在文件映射区域分配内存。

方式一实现的方式很简单,就是通过brk()函数将堆定指针向高地址移动,获得新的内存空间。如下图:

方式二通过mmap()系统调用中私有匿名映射的方式,在文件映射区分配一块内存,也就是从文件映射区偷了一块内存。

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

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

  1. 如果用户分配的内存小于128k,则通过brk()申请内存;

  1. 如果用户分配的内存大于128k,则通过mmap()申请内存;

注意,不同的glibc版本定义的阈值也是不同的。

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

不是的,malloc()分配的是虚拟内存。

如果分配后的虚拟内存没有被访问的话,是不会将虚拟内存映射到物理内存,这样就不会占用物理内存了。

只有在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现虚拟内存对应页没有在物理内存中,就会出发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系。

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

malloc()在分配内存的时候,并不是老老实实按用户预期申请的字节数来分配内存空间大小,而是会预分配更大的空间作为内存池。

具体会预分配多大的空间,跟 malloc 使用的内存管理器有关系

Free释放内存,会归还给操作系统吗?

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

malloc通过mmap()方式申请的内存,free释放内存的时候,会把内存归还给操作系统,内存得到真正的释放。

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

因为向操作系统申请内存,是要通过系统调用的,执行系统调用是要进入内核态的,然后再回到用户态,运行态的切换会耗费不少时间。

所以,申请内存的操作应该避免频繁的系统调用,如果都用mmap来分配内存,等于每次都要执行系统调用。

另外,因为mmap分配的内存每次释放的时候,都会归还操作系统,于是每次mmap分配的虚拟地址都是缺页状态的,然后第一次访问该虚拟地址的时候,就会触发缺页中断。

也就是说,频繁通过mmap分配的内存的话,不仅每次都会发送运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致CPU消耗较大。

为了改进这两个问题,malloc通过brk()系统调用在堆空间申请内存的时候,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。

等下次在申请内存的时候,就直接从内存池取出对应的内存块就可以了,而且可能这个内存块的虚拟地址与物理地址的映射关系还存在,这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低了CPU的消耗。

既然brk那么牛逼,为什么不全部使用brk来分配?

前面我们提到了brk从堆空间分配的内存,并不会归还给操作系统,那么我们考虑这样一个场景。

如果我们连续申请了10k,20k,30k这三片内存,如果10k与20k这两片释放了,变为了空闲内存空间,如果下次申请的内存小于30k,就可以重用这空间

但是如果下次申请的内存大于30k,没有可用的空闲内存空间,必须向os申请,实际使用内存继续增大。

因此,随着系统频繁地malloc和free,尤其对于小块内存,堆内存将产生越来越多不可用的碎片,导致内存泄漏,而这种泄漏现象使用valgrind是无法检测出来的。

所以,malloc实现中,充分考虑了sbrk和mmap行为上的差异及优缺点,默认分配大块内存(128k)才使用mmap分配内存空间。

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

前面提到,malloc返回给用户态的内存起始地址比进程的堆空间起始地址多了16字节,这个多出来的16字节就是保存内存块的描述信息,比如有该内存块的大小。

这样当执行free()函数时,free会传入进来的内存地址向左偏移16字节,然后从这16字节的分析出当前内存块的大小,自然知道要释放多大的内存。

内存分配的过程?

应用程序通过malloc函数申请内存时,实际上申请的是虚拟地址,此时并不会分配物理内存,如果有,就直接分配物理内存,并建立虚拟内存和物理内存之间的映射关系。

如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作,回收的方式主要有两种:直接内存回收和后台内存回收。

后台内存回收:在物理内存紧张的时候,会唤醒kswapd内核线程来回收内存,这个回收内存的过程是异步的,不会阻塞进程的执行。

直接内存回收:如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。

如果直接内存回收后,空闲的物理内存任然无法满足此次物理内存的申请,那么内核就会释放大招 —出发OOM(out of memory)机制。

OOM killer机制会根据算法选择一个占用物理内存较高德进程,然后将其杀死,以便释放内存资源,如果物理内存依旧不足,OOM killer会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。

哪些内存可以被回收?

系统内存紧张的时候,就会进行回收内测的工作,那具体哪些内存是可以被回收的呢?

系统内存紧张的时候,就会进行回收内测的工作,那具体哪些内存是可以被回收的呢?

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

文件页:内核缓存的磁盘数据和内核缓存的文件数据都叫文件页。大部分文件页,都可以直接释放内存,以后有需要的时候,再从磁盘重新读取就可以了。而哪些被应用程序修改过,并且暂时没有写入磁盘的数据,就得先写入磁盘,然后才能进行内存释放,所以,回收干净页的方式是直接释放内存。

哪些内存可以被回收?

系统内存紧张的时候,就会进行回收内测的工作,那具体哪些内存可以被回收呢?

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

文件页:内核缓存的磁盘数据和内核魂村的文件数据都叫文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而哪些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘再释放内存。

匿名页:这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆,栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收方式是通过linux的swap机制,swap会把不常访问的内存先写入磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

文件页和匿名页的回收都是基于LRU算法,也就是优先回收不常访问的内存。LRU回收算法,实际上维护着active和inactive两个双向链表,其中:

active_list 活跃内存页连表,这里存放的是最近被访问过的内存页;

inactive_list 不活跃内存也链表。这里存放的是很少被访问(非活跃)的内存页;

越接近链表尾部,就表示内存页越不常访问。这样,在回收内存时,系统就可以根据活跃程序,优先回收不活跃的内存。

活跃和非活跃的内存页,按照类型的不同,又分为文件页与匿名页。可以从/proc/meminfo中,查询它们的大小.

回收内存带来的性能影响

在前面我们指定了回收内存的两种方式。

一种是后台内存回收,也就是kswapd内核线程,这种事异步回收,不会阻塞进程。

一种是直接内存回收,是同步回收,会阻塞进程,这样就会造成很长时间的延迟,以及系统的CPU利用率会升高,最终引起系统负荷彪高。

可被回收的内存类型有文件页和匿名页:

文件页的回收:对于干净页是直接释放内存,这个操作不会影响性能,而对于脏页会先写回到磁盘再释放内存,这个操作会发生磁盘的IO的,这个操作会影响系统的性能。

匿名页的回收:开起了swap机制,那么swap机制会将不常访问的匿名页换出磁盘中,下次访问时,再从磁盘换入内存中,这个操作会影响系统的性能,整个操作会影响系统性能。

调整文件页与匿名页的回收倾向

从文件页和匿名页的回收操作来看,文件页的回收操作系统堆系统的影响相比匿名页的回收操作会少一点。因为文件页对于干净页回收是不会发送I/O的,而匿名页的Swap换入换出的这两个操作都会发生磁盘I/O。

Linux提供了一个/proc/sys/vm/swappiness选项,用来调整文件页和匿名页的回收倾向。

swappiness的范围是0-100,数值越大,越积极使用swap,也就是更倾向于回收匿名页;数值越小,越消极使用swap,也就是更倾向于回收文件页。

一般建议swappiness是设置未0(默认是60),这样在回收内存的时候,会更倾向于文件页的回收,但是并不代表不会回收匿名页。

针对这个问题,解决办法就是,可以通过尽早出发后台内存回收来避免应用程序进行直接内存回收。

什么条件才能出发kswapd内核线程回收内存呢?

内核定义了三个内存阈值,用来衡量当前剩余内存是否充裕或者紧张,分别是:

页最小阈值;

页最低阈值;

页高阈值。

这三个内存阈值会划分为四种内存的使用情况,如下图:

kswapd会定期扫描内存的使用情况,根据神域内存的情况来进行内存回收的工作。

图中绿色部分:如果剩余内存大于页高阈值,说明剩余内存是充足的;

图中蓝色部分:如果剩余内存在页高阈值和页低阈值之间,说明内存有一定压力,但还可以满足应用程序申请内存的请求;

图中橙色部分:如果剩余内存在页低阈值和页最小阈值之间,说明内存压力比较大,剩余内存不多了,这时kswapd0会执行内存回收,直到剩余内存大于阈值为止。虽然会触发内存回收,但不会阻塞应用程序,因为两者关系是异步的。

图中红色部分:如果剩余内存小于页最小阈值,说明用户可用内存都耗尽了,此时就会触发直接内存回收,这时应用程序就会被阻塞,因为两者关系都是同步的。

可以看到,当剩余内存页(pages_free)小于页最小阈值(pages_low),就会触发kswapd进行后台回收,然后kswapd会一直回收剩余内存页(pages_free)大于页高阈值(pages_high)。

也就是说,kswapd的活动空间只有page_low与page_min之间这段区域,如果剩余内测低于了pages_min会触发直接内存回收,高于了pages_high 右不会唤醒kswapd。

页低阈值可以通过内核选项/proc/sys/vm/min_free_kbytes(该参数代表系统所保留空闲内存的最低限)来间接设置。

min_free_kbytes虽然设置的是页最小阈值,但是页高阈值和页低阈值都是根据页最小阈值计算生成的。它们之间的计算关系如下:

pages_min = min_free_kbytes

pages_low = pages_min*5/4

pages_high = pages_min*3/2

如果系统时不时发生抖动,并且通过sar-B观察到pgscand数值很大,那大概率是因为直接内存回收导致的,这时可以增大min_free_kbytes这个配置选项来及早的触发后台回收,然后继续观察pgsacnd是否会降低为0.

增大了min_free_kbytes配置后,这会使得系统预留过多的空闲设置,从而在一定程度上降低了应用程序可使用的内存量,在在一定程序上浪费了内存。极端情况下设置min_free_kbytes接近实际物理内存大小时,留给应用程序的内存就会太少而可能会频繁地导致OOM的发生。

所以在调整min_free_kbytes之前,需要先思考一下,应用程序更加关注什么,如果关注延迟那就适当地增大min_free_kbytes。如果关注内存的使用量那就适当地调小min_free_kbytes。

numa架构下的内存回收策略;

什么是numa架构?

再说NUMA架构前,先给大家说说SMP架构,这架构都是针对CPU的。

SMP指的是一种多个CPU处理器共享资源的电脑硬件架构,也就是说每个CPU地位平等,它们共享相同的物理资源,包括总线、内存、IO、操作系统等。每个CPU访问的时间都是相同的,因此,这种系统页被称为一致性存储访问结构。

随着CPU处理器核数的增多,多个CPU通过一个总线访问内存,这样总线的带宽压力会比较大,同时每个CPU可用带宽会减少,这也就是SMP架构的问题。

为了解决SMP架构的问题,就研制出了NUMA结构,即非一致存储访问结构。NUMA架构将每个CPU进行了分组,每一组CPU都用node来表示,一个node可能包含多个CPU。

NUMA架构将每个CPU进行分组,每一组CPU用Node来表示,一个Node可能包含多个CPU。

每个node有自己独立的资源,包括内存、IO等,每个Node之间可以通过互联模块总线(QPI)进行通信,所以,也就意味着每个Node上的CPU都可以访问到整个系统中的所有内存。但是,访问远端Node的内存要比访问本地的耗时更多。

NUMA与回收内存的关系:

具体选那种模式,可以通过/proc/sys/vm/zone_reclaim_mode来控制。它支持以下几个选项:

0(默认值):在回收本地内存之前,在其他node寻找空闲内存;

1:只回收本地内存;

2:只回收本地内存,在本地回收内存时,可以将文件页中的脏页写回硬盘,以回收内存。

3、只回收本地内存,在本地回收内存,可以用swap方式回收内存。

在使用NUMA架构的服务器,如果系统出现还有一半内存的时候,却发现系统频繁触发直接内存回收,导致了影响了系统性能,那么大概率是因为zone_reclaim_mode没有设置未0,导致当本地内存不足的时候,只选择回收本地内存的方式,而不去使用其他Node的空闲内存。

虽然说访问远端Node的内存比访问本地内存要耗时很多,但是相比内存回收的危害而言,访问远端Node的内存带来的影响还是比较小的。因此,zone_reclaim_mode一般建议设置为0。

如果保护一个进程不被OOM杀掉呢?

在系统空闲内存不足的情况,进程申请了一个很大的内存,如果直接内存回收都无法回收出足够大的内存,那么就会出发OOM机制,内核就会根据算法选择一个进程杀掉。

Linux到底是根据什么标准来选择被杀的进程呢?这就要提到一个在Linux内核里有一个oom_badness()函数,它会把系统中可以被杀掉的进程扫描一遍,并对每个进程打分,得分最高的进程就会被首先杀掉。

进程得分的结果受下面两个方面影响:

第一,进程已经使用的物理内存页面数。

第二,每个进程的OOM校准值oom_score_adj。他是可以通过/proc/[pid]/oom_score_adj来配置的。我们可以在设置-1000到1000之间的任意数值,调整进程被OOM kill的几率。

函数oom_badness()里最终计算方法是这样的:points = process_pages + oom_score_adj*totalpages/1000 #用「系统总的可用页面数」乘以 「OOM 校准值 oom_score_adj」再除以 1000,最后再加上进程已经使用的物理页面数,计算出来的值越大,那么这个进程被 OOM Kill 的几率也就越大

每个进程的oom_score_adj默认值都为0,所以最终得分跟进程自身消耗的内存有关,消耗的内存越大越容易被杀掉。我们可以通过挑战oom_score_adj的数值,来改进进程的得分结果:

如果你不想某个进程被杀掉,那么你可以调整该进程的oom_score_adj,从而改变这个进程的得分结果,降低该进程被OOM杀死的概率。

如果你想某个进程无论如何都不能被杀掉,那就将oom_score_adj配置为-1000。

我们最好将一些很重的系统服务的oom_score_adj配置为-1000,比如sshd,因为这些系统服务一旦被杀掉,我们就很难登录系统。

总结:

内核在给应用程序分配物理空间的时候,如果空闲物理内存不够,那么会进行内存回收的工作,主要有两种方式:

后台内存回收:在物理内存紧张的时候,会唤醒kswapd内核线程进行回收内存,这个回收内存的过程异步,不会阻塞进程的执行。

直接内存回收:如果后台异步回收跟不上进程内存申请的速度,就会开始回收,这个回收的过程是同步的,会阻塞进程额执行。

可被回收的内存类型有文件页和匿名页:

文件页的回收:对于干净页直接释放内存,这个操作不会影响性能,而对于脏页(被应用程序修改过的, 并且暂时还没写入磁盘的数据)会先写回磁盘再释放内存,这个操作会发生磁盘I/O的,这个操作是会影响系统性能的。

匿名页的回收:如果开启了swap机制,那么Swap机制会将不常访问的匿名页换出到磁盘中,下次访问时,再从磁盘换到内存中,这个操作会影响系统性能的。

文件页(内存回收, 也就是系统释放掉可以回收的内存, 比如缓存和缓冲区, 就属于可回收内存)与匿名页(应用程序动态分配的堆内存)的回收都是基于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机制,OOMkiller会根据每个进程的内存占用情况和oom_score_adj的值进行打分,得分最高的就会被首先杀掉。

我们可以通过调整进程的 /proc/[pid]/oom_score_adj 值,来降低被 OOM killer 杀掉的概率。

操作系统虚拟内存的大小?

应用程序通过malloc函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。

当应用程序读写了这块虚拟内存,CPU就会去访问这个虚拟内存,这时会发现这个虚拟内存没有映射到物理内存,CPU就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的Page Fault Handler处理。

缺页中断处理函数会看是否有空闲的物理内存:

如果有,就直接分配物理内存,并建立虚拟内存和物理内存之间的映射关系。

如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作,如果回收内存工作结束后,空闲的物理内存仍无法满足此次物理内存的申请,那么内核就会放最后的大招

触发OOM机制。

32位操作系统和64位操作系统的虚拟地址空间大小是不同的,在Linux操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,如下所示:

通过这里可以看出:

32位系统的内核空间占用1g,位于最高处,剩下的3G是用户空间;

54位的内核空间和用户空间都是128T,分别占据整个内存空间的最高最低处,剩下的中间部分是未定义的。

所以,可以回到这个问题了:在32位操作系统、4GB物理内存的机器上,申请8GB内存,会怎么样?

因为32位操作系统,进程最多只能申请3GB大小的虚拟内存空间,所以申请8GB内存的话,在申请虚拟内存阶段就会失败(我手上没有32位操作系统测试,失败原因应该是OOM)

在64位操作系统、4GB物理内存的机器上,申请8GB内存,会怎么样?

64位操作系统,进程可以使用128TB大小的虚拟内存空间,所以进程申请8GB内存是没有问题的,因为进程申请内存是申请虚拟内存,只要不读写这个虚拟内存,操作系统就不会分配物理内存。

Swap机制的作用:

前面讨论在32位/64位操作系统环境下,申请的虚拟内存超过物理内存后会怎么样?

在32位操作系统,因为进程最大只能申请3GB大小的虚拟内存,所以直接申请8GB内存,会申请失败。

在64位操作系统,因为进程 最大只能申请128TB大小的虚拟内存,即使物理内存只有4GB,申请8GB内存也是没有问题,因为申请的内存是虚拟内存。

程序申请的虚拟内存,如果没有被使用,它是不会占用物理空间的。当访问这块虚拟内存后,操作系统才会进行物理内存分配。

Swap机制的作用:

前面讨论在32位/64位操作系统环境下,申请的虚拟内存超过物理内存后怎么样?

在32位操作系统,因为进程最大只能申请3GB大小虚拟内存,即使物理内存只有4GB,所以直接申请8GB内存,会申请失败。

在64位操作系统,因为进程最大只能申请128TB大小的虚拟内存,即使物理内存只有4GB,申请8G内存也是没有问题,因为申请的内存是虚拟内存。

程序申请的虚拟内存,如果没有被使用,它是不会占用物理空间的。当访问这块虚拟内存后,操作系统才会进行物理内存分配。

如果申请物理内存大小超过了空闲物理内存大小,就要看操作系统有没有开启Swap机制:

如果没有开启Swap机制,程序就会直接OOM;

如果有开启Swap机制,程序可以直接运行。

什么是Swap机制?

当系统的物理内存不够用的时候,就需要将物理内存中的一部分空间释放出来,以供当前运行的程序使用。哪些被释放的空间可能来自一些很长时间没有什么操作的程序,这些被释放的空间会临时保存到磁盘,等到那些程序要运行时,再从磁盘中恢复保存的数据到内存中。

另外,当内存使用存在压力的时候,会开始触发内存回收行为,会把这些不常访问的内存先写入磁盘,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

这种,将内存数据换出磁盘,又从磁盘中恢复数据到内存的过程,就是Swap机制负责的。

Swap就是把一块磁盘空间或者本地文件,当成内存来使用,它包含换入与换出两个过程:

换出,是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存;

换入,是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来;

使用Swap机制的优点是,应用程序实际可以使用的内存空间将远远超过系统的物理内存。由于硬盘空间的价格远比内存要低,因此这种方式无疑是经济实惠的。当然,频繁地读写硬盘,会显著降低操作系统的运行速率,这也是Swap的弊端。

Linux中的Swap机制会在内存不足和内存闲置的场景下触发:

内存不足;当系统需要的内存超过了可用的物理内存时,内核会将内存中不常用的内存页交换到磁盘上为当前进程让出内存,保证正在执行的进程的可用性,这个内存回收的过程是强制的直接内存回收。 直接内存回收是同步的过程,会阻塞当前申请内存的进程。

内存闲置:应用程序在启动阶段使用 的大量内存在启动后往往都不会使用,通过后台运行的守护进程,我们可以将这部分只使用一次的内存交换到磁盘上为其他内存的申请预留空间。kSwapd是linux负责页面知乎的守护进程,它也是负责交换闲置内存的主要进程,它会在空闲内存低于一定水位时,回收内存页中的空闲内存保证系统中的其他进程可以尽快获得申请的内存。kSwapd是后台进程,所以回收内存的过程是异步的。不会阻塞当前申请内存的进程。

Linux提供了两种不同的方法启用Swap,分别是Swap分区和Swap文件。

Swap分区是硬盘上的独立区域,该区域只会用于交换分区,其他的文件不能存储在该区域上,我们可以使用swapon -s命令查看当前系统上的交换分区;

Swap文件是文件系统中的特殊文件,它与文件系统中的其他文件页没有太多区别;

Swap换入换出的是什么类型的内存?

内核缓存的文件数据,因为都有对应的磁盘文件,所以在回收文件数据的时候,直接写回到对应的文件就可以了。

但是像进程的堆、栈数据等,它们是没有实际载体,这部分内存被称为匿名页。而且这部分内存很有可能要再次被访问,所以不能直接释放内存,于是就需要有一个能保存匿名页的磁盘载体,这个载体就是Swap分区。

匿名页回收的方式就是通过Linux的Swap机制,Swap会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。

什么是OOM?

内存溢出是指应用系统是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出。

有开启Swap机制:

至此,验证完成了,简单总结下:

在32位操作系统,因为进程最大只能申请3GB大小的内存,所以,直接申请8GB内存,会申请失败。

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

如果没有Swap分区,因为物理空间不够,进程会被操作系统杀掉,原因是OOM(内存溢出)

如果有Swap分区,即使物理内存只有4gb,程序也能正常使用8GB的内存,进程可以正常运行。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值