虚拟内存:
了解虚拟内存吗?
操作系统为每个进程都分配了各自的“虚拟地址”,而在CPU芯片中的内存管理单元(MMU)当中存在这虚拟地址和实际物理地址之中的映射关系。进程在使用虚拟地址的时候可以通过映射关系获得不同的物理地址,而这避免了内存中的使用冲突,因为每个进程都在使用不同的物理地址。
操作系统是怎么管理虚拟地址和物理地址之间的关系的?分段机制下虚拟地址和物理地址之间是如何映射的?
管理二者之间的关系有两种方式,一种是内存分段,另一种是内存分页。
内存分段:首先再内存分段机制下,每个程序都会被分成好多段,比如代码分段,数据分段,堆分段,栈分段等。而虚拟地址在分段机制下是由“段选择子”和“段内偏移量“构成的。段选择子中最重要的部分是段号,段号其实就是段表中的索引,段表中的内容会决定虚拟地址映射到哪个物理地址上面。段表有三部分组成,分别是段基地址,段界限,特权等级。一般来说段内偏移量应该是有明确范围的,从0到段界限之间会被认为是合理的范围。在合理范围内,段内偏移量根据它的段号找到它所对应的段基地址就可以计算出实际物理地址了,只需要将段基地址+段内偏移量就能得到。
如下图(下图来自小林coding的书):
但是同时内存分段也存在着缺点:
1.内存碎片:
内存碎片其实是两个问题,一个是外部内存碎片(映射到物理地址中内存的某些部分是不常用的,但却一直占用着内存空间,这造成了一定程度上的内存浪费。)。一个是外部内存碎片(内存被分成了很多不连续的部分,这也就导致稍微大一点的程序无法放入内存中,因为内存是不连续的并且被分成了很多的小块)正是为了解决较大的程序无法放入独立的小块内存的问题,需要实施内存交换。内存交换就是让小块的不连续的被程序占用的内存,写入硬盘中,然后再将其读到内存中,但是并不是读到和之前一样的位置,而是读到一个和其他程序连着的内存位置,这样就腾出了较多的连续的内存空间。这个交换的过程叫做内存交换,在Linux系统中,在硬盘中划出了一部分专门用来和内存进行内存交换,这部分叫做Swap空间。但是这个内存交换的问题同样引出了下一个问题——内存交换的效率低。
2.内存交换的效率低:
在交换的过程中,我们需要知道的一点是,硬盘的访问速度是比较慢的,所以如果是交换的比较大的内存数据的话,那么是非常浪费时间的。因此要解决这个问题,就要使用内存分页的机制。
操作系统是怎么管理虚拟地址和物理地址之间的关系的?分页机制下虚拟地址和物理地址之间是如何映射的?
内存分页:
内存分页就是将虚拟地址和物理地址切成一段段固定尺寸的大小,而这每一份连续且大小固定的内存空间就是一个页。在linux系统中,一个页的大小是4KB。
1.页面是在内存中的,内存管理单元(MMU)负责来完成虚拟地址到物理地址的转换。如果进程要访问的虚拟地址在页表中是找不到的,那么就会产生一个缺页异常。会重新更新页表,最后恢复进程的运行。
2.而由于虚拟地址中的每一部分都是划分好了的,所以就不会出现像内存分段中那样小的不连续的内存片段。而且,由于采取了分页处理,释放的内存都是以页为单位释放的,并不会出现那种无法被程序使用的小片段内存。
3.如果内存空间不够的话,系统就会自动把”最近没有被使用的程序“,写入硬盘当中(swap out)先腾出一部分内存空间来,供其他程序使用。当需要的时候,再从硬盘当中把它读回来(swap in)。而这个功能其实也解决了内存交换的效率低的问题。因为需要暂存在硬盘中的程序也只占用小部分内存,所以不会设计到去写入很大的程序到硬盘当中,这样效率更高了。
4.而且最重要的是在完成了虚拟地址和物理地址之间的映射之后,我们并不需要将程序全部加载到物理内存当中,在程序运行过程中,我们只需要把虚拟地址当中必要的当前需要的一些指令和数据加载到物理内存当中即可。(随用随加载)
以下这种方式是简单分页方式(存在缺陷的分页方式):
首先分页机制下,虚拟地址包含页号和页内偏移量两部分,其中页号可以被认为是页表的索引,页表包括虚拟页号,物理页号(其中含有对应的物理基地址)两部分。想通过虚拟地址找到对应物理地址的过程是:根据虚拟地址的页号从页表中找到对应的物理页号,然后用物理页号+页内偏移量就可以找到物理内存地址了(这是由于物理页号中含有对应的物理基地址)。
缺陷:由于操作系统允许多个进程同时运行,所以也导致页表占据的内存过大。
比如说一个进程就有一个虚拟地址,那么一个虚拟地址可以是4GB(32位CPU)和8GB(64位CPU),以4GB举例,那么每一个页表是4KB,一个虚拟地址大概需要1024*1024个页表来表示,每一个页表项占据4个字节的话(存储在内存当中),那就是4*1024*1024个字节的内存地址来存储页表,也就是4MB的大小,而这只是一个进程需要4MB大小的页表,而操作系统允许多个进程同时运行,这可能会导致页表占据太大的内存。
为了解决这个问题,决定使用多级页表的方式:
在分页的过程中,我们再将简单页表再次分页,分为一级页表和二级页表,也就是说当一个进程占用了满满4GB的内存时,我们需要一个一级页表(1024个页表项)和1024个二级页表(1024个页表项)来完成映射,此时也就是需要4KB+4MB的大小。但是其实我们一般不会为一个进程安排4GB的内存大小,如果为进程安排20%的4GB内存大小,那么在多级页表的机制下,就只占4KB+0.2*4MB=0.804MB,节约了很大空间。而且最重要的是,一级页表完成了对于所有虚拟地址的覆盖,而至于一级页表中没有用到的页表项,也就不需要产生对应的二级页表,如果有需要的时候再创建对应的二级页表完成映射即可。(一级页表覆盖虚拟内存,二级页表在需要时创建)对于那些已经分配的页表项,如果长时间没有使用的话,那么也会被swap out到硬盘当中,当需要使用时再swap in到内存当中。
段页式内存管理:
最好的方法并不是内存分段or内存分页,这两个并不是对立的,可以结合起来一起使用,效率会达到最高。(先分段再分页)地址结构分为三部分:段号,段内页号,页内位移。如果想要计算位置的话就用:先确定段号,然后找到对应的页表起始地址,然后再找到物理页号,通过页号+页内位移而找到位置。
TLB:
根据程序的局部性原理,在CPU芯片中加入了TLB,也可称为cache,负责缓存经常被访问的页表项,大大提高了地址的转换速度。
进程的几种状态和状态转换:
null->创建状态:进程被创建出来。
创建状态->就绪状态:被创建完之后并且完成了初始化之后,进程已经准备好运行了。
就绪状态->运行状态:进程被操作系统中的进程调度器选中,分配给CPU,使其进入运行状态。
运行状态->结束状态:程序被运行直到结束,进入结束状态,进程完成了使命,被销毁了,没有用了。
运行状态->阻塞状态:当进程运行的过程中,请求了某个事件且必须等待的时候比如I/O请求,就变成阻塞状态(阻塞状态和就绪状态不同的是,就绪状态下进程被分配给CPU后就可以立马运行,而阻塞状态下即使被分配给CPU也无法运行。)
运行状态->就绪状态:进程运行了一段时间了,并且时间片已经被用完了,所以就进入就绪状态,等待着下次被分配。
阻塞状态->就绪状态:进程所等待的事件已经完成,所以有阻塞状态变成就绪状态,随时等待着被进程调度器选中并且再次进入运行状态。
值得一提的是:如果大量的进程处于阻塞状态,那么就会占据很大一部分物理内存,这部分物理内存就等于被浪费了,所以操作系统通常会让阻塞状态的进程swap out换出到硬盘当中,当等待的事件完成的时候有条件可以再次运行的时候,再swap in换入到内存当中。
就产生了一个新的状态——挂起状态。
阻塞挂起状态:处于阻塞状态中的进程被swap out到了硬盘当中,等到某个事件的出现。
就绪挂起状态:处于就绪状态中的进程被swap out到了硬盘当中,随时可以进入到内存当中运行。
(我的理解是就绪挂起状态和就绪状态差不多,就绪挂起状态可以被随时拿到内存中进入就绪状态,等待着被进程调度器调用)
这样节约了大量的物理内存空间。
线程和进程的区别与联系:
相同点:
1.线程和进程都具有阻塞,运行,就绪三种状态,并且线程也具有几种关系之间的转化关系。
不同点:
1.进程是资源分配的单位,而线程是CPU调度的单位。
2.进程是一个完整的资源平台,而线程只独享必不可少的资源,比如寄存器和栈。
3.线程在并发运行方面有着更少的时间和空间开销。
线程效率高的原因:
1.进程的创建时间比线程要长很多,因为进程在创建的过程当中需要分配资源,并且需要资源管理信息,但是线程并不涉及这些,线程只需要共享它们。
2.进程的终止时间要比线程长很多,因为进程在终止的时候需要释放很多的资源,但是线程并不需要。
3.同一个进程下的不同线程之间的切换效率要比进程之间的切换效率高,由于线程具有相同的地址空间,也就是说共享虚拟内存,所以同一进程下的不同线程使用着同一个页表,线程之间的切换不需要切换页表,但是进程之间的切换需要切换掉页表,而切换掉页表本身就是一个比较大的开销。
4.同一进程的线程之间共享文件资源和内存,那么线程之间进行数据传递的时候就不需要经过内核了,相比较来说线程要比进程有着更高的数据交互效率。
所以综上所述,线程在时间效率和空间效率上面都要比进程高。
并发和并行的区别:
并发:单个CPU处理多个任务,根据分配好的时间片,可以在一段时间内交替执行这些任务。但是在某一个时间点一定是只有一个任务被执行。造成了多个任务同时被处理的假象。
并行:多个处理器或者多核处理器同时处理多个任务,在某一个时间点上,有多个任务被执行。真正意义上的同时处理多个任务。
什么是内核态和用户态?如何实现两者之间的相互转换?
内核态也叫内核空间,然后用户态也叫用户空间。
用户态指的是非特权的执行状态,不能执行越权的操作,比如我们编写的程序,APP,客户端软件等都运行在用户态。
内核态指特权的执行状态,可以操作硬件,一般是指操作系统。
这两种设定是为了系统的安全和稳定。
当我们需要操作系统帮助完成一些用户态自己没有特权的操作时就会切换到内核态,比如read(),write()等读写磁盘上的文件,这种切换最常见的形式就是系统调用。但是这个系统调用的过程是比较耗时间的。
线程实现的三种方式?
1.用户线程(User Thread):在用户空间中实现的线程,由用户态的线程库管理的线程。
2.内核线程(Kernel Thread):在内核中实现的线程,由内核管理的线程。
3.轻量级进程(LightWeight Process):在内核中来支持用户线程。
用户线程如何理解,存在什么优势和缺陷?
用户线程是基于用户态中的线程管理库来实现的,所以线程控制块(TCB)也是在库里面来实现的,对于操作系统来说,操作系统是看不见的,它只能看见进程控制块(PCB)。所以说,对于用户线程的管理和调度,操作系统是不直接参与的,而是由用户级线程库函数来完成线程的管理的,包括创建,终止,调度等。
用户线程的模型是多对一的关系。
用户线程的优点:
1.可用于不支持线程技术的操作系统。
每个进程都有一个TCB列表,来记录各个线程的状态信息,比如栈指针,寄存器等信息,TCB是由用户级线程库函数来管理维护的,不需要操作系统。所以可用于不支持线程技术的操作系统。
2.用户线程的切换速度特别快。
用户线程的切换也是由线程库函数来完成的,不需要用户态和内核态的切换。
用户线程的缺点:
1.由于操作系统不参与线程的调度,如果一个线程发起了系统调用而阻塞,那进程所包含的用户线程就都不能执行了。
2.当一个线程开始运行后,如果它不主动交出CPU的使用权,其他线程是无法运行的。因为打断当前运行中的线程的权力只有操作系统有。但是用户线程不由操作系统管理。
内核线程如何理解?存在什么优势和缺陷?
内核线程是由操作系统管理的,TCB是放在操作系统中的,线程的创建,终止和管理等都是由操作系统来负责。
内核线程的模型是一对一的关系。
内核线程的优点:
1.在一个进程当中,如果某个内核线程发起系统调用而被阻塞,不会影响其他的内核线程运行。
内核线程的缺点:
1.内核线程的创建,终止,切换等都是通过系统调用的方式来进行,开销太大。
进程调度算法:
硬件时钟提供某个频率的周期性中断,可以根据如何处理时钟中断,来把调度算法分为两类:
1.非抢占式调度算法:挑选一个进程,让进程一直运行下去,直到这个进程被阻塞或者结束,才会调用其他的进程,也就是说根本不理会时钟中断这个事情。
2.抢占式调度算法:让一个进程在规定的时间内运行,时间到了的时候就把进程挂起,再从就绪队列里面挑选一个别的进程来运行。也可以称为是时间片机制。
调度原则:
CPU利用率,系统吞吐量,周转时间,等待时间,响应时间
原则一:如果运行的程序,发出了I/O事件的请求,进入了阻塞状态,那么从CPU利用率的角度来考虑,调度程序就会从就绪队列中选择一个进程来运行。
总结:调度程序应该确保CPU是始终匆忙的状态,可以提高CPU利用率。
原则二:有的进程执行某个任务花费的时间会比较长,如果这个进程一直占有着CPU的话,从提高系统吞吐率的角度来说(CPU在单位时间内完成的进程数量),调度程序就会权衡长任务进程和短任务进程的运行完成数量。
总结:长任务的进程会导致系统吞吐率下降,短任务的进程会导致系统吞吐率的上升。
原则三:调度程序应尽量让进程的周转时间(进程开始到结束的过程中,包含两部分时间,分别是进程运行时间与进程等待时间,进程等待时间+进程运行时间=周转时间)缩短,如果进程的等待时间很长但是运行时间很短,那周转时间也会很长,调度程序应该避免这种情况的发生。
总结:周转时间也是越小越好。周转时间是进程运行时间与进程阻塞时间的总和。
原则四:调度程序应该尽量减少就绪队列中的进程的等待时间。
总结:等待时间是进程在就绪状态下的等待时间,等待时间也是越小越好的。
原则五:响应时间对于调度程序来说也是比较重要的,比如鼠标,键盘等这种交互式比较强的应用。
总结:响应时间也是越小越好的,是衡量调度算法好坏的主要标准。
单核CPU系统中常见的调度算法:
1.先来先服务调度算法(First Come First Seved,FCFS):
非抢占式的先来先服务算法。每次都从就绪队列中第一个进程开始运行,让进程运行直到结束或者被阻塞,才会继续从就绪队列中选择第一个进程接着运行。不利于IO繁忙型。
2.最短作业优先调度算法(Short Job First,SJF):
这种调度算法会优先选择运行时间最短的进程来运行,有助于提高系统的吞吐量。但是如果长作业一直在就绪队列中等待着,也会使得长作业的等待时间不断变长,很难被执行。
3.高响应比优先调度算法(Highest Response Ratio Next,HRRN):
每次进行进程调度的时候,先计算一下“响应比优先级”,然后把”响应比优先级“最高的进程投入运行,”响应比优先级“的计算公式:
响应比优先级 = (等待时间+要求服务时间)/(要求服务时间)
这样的话短任务,短作业进程容易被选中,并且等待时间越长的进程也容易被选中。
4.时间片轮转调度算法(Round Robin,RR):
最公平的,使用最广的调度算法,每个进程都被分配一个时间片,每个进程都会在各自的时间片内的时间执行任务。时间片的长度应该设置在20ms——50ms之内。
5.最高优先级调度算法(Highest Priority First,HPF):
进程的优先级可以分为,静态优先级和动态优先级:
静态优先级:创建进程的时候,就已经确定好了优先级了,然后整个运行时间优先级都不会变化。
动态优先级:根据进程的动态变化调整优先级,比如进程运行时间变长了,就会降低优先级,如果进程等待时间增加了,则升高优先级。
最高优先级调度算法有两种处理优先级的方法,非抢占式和抢占式:
非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。
抢占式:当就绪队列中出现优先级高的进程,直接挂起当前进程,调度优先级高的进程运行。
缺点是:可能导致低优先级的进程永远不会运行。
6.多级反馈队列调度算法(Multilevel Feedback Queue,mfq):
是”时间片轮转算法“和”最高优先级算法“的综合。(有多个就绪队列,就绪队列是优先级越高的,CPU时间片越短。)
具体例子如下图所示:
进程间通信都有哪些方式?各有什么利弊和特点?
因为进程彼此之间享有独立的用户空间,所以彼此之间不能直接访问,因此需要在内核空间中进行通信。
1.管道:
管道是最简单的进程间通信方式,分为匿名管道和命名管道。
匿名管道是特殊的文件只存在于内存之中,不存在于文件系统中。匿名管道之间的通信是单向的,并且通信数据也是无格式的流且大小受限。如果要实现双向通信那么需要两个管道,并且匿名管道的通信只限于具有父子关系进程之间的通信。匿名管道的创建随着进程的创建而创建,销毁随着进程的结束而销毁。
命名管道打破了匿名管道只能实现父子关系间进程的通信的限制,在命名管道的前提下,会在文件系统中创建出一个p类型的设备文件,那么毫无关系的进程之间就可以实现通信了。但是无论是命名管道还是匿名管道,发送方的通信数据都是缓存在内核中的,并且接收方也是从内核中获取数据的。同时通信数据都遵守先进先出原则。
缺点:通信数据是无格式的字节流。
2.消息队列
消息队列没有管道通信所具有的缺点,它的通信数据是有格式的。消息队列其实就是存储在内核中的消息链表。消息队列中的消息体是可以用户进行自定义的。发送方在发送数据的时候,是将数据分成一个个独立的消息体发送出去的,所以接收方在接收的时候也要确保和发送方具有相同的消息体数据类型,否则就会出错。但是消息队列的缺点就是,通信不及时。因为通信数据的传递需要完成用户态和内核态之间的拷贝。
缺点:通信不及时,因为通信数据每次都需要完成用户态和内核态之间的拷贝过程。
3.共享内存
共享内存弥补了消息队列通信过程需要完成用户态和内核态数据拷贝的缺陷。提供了共享空间,每个进程都可以直接访问,就像访问自己内存空间一样方便。避免了陷入内核态或者系统调用,提高了通信的效率,是最快的通信方式。但是同时也带来了新的问题,就是多个进程竞争同一个共享空间的时候就会发生数据错乱。
因此需要信号量来保证共享资源的安全,信号量可以保证在同一时间只有一个进程访问共享空间,这就是互斥性,同样也可以实现进程同步。信号量是一个计数器,信号量代表着资源个数,可以通过两种原子操作来改变,P操作和V操作。
缺点:多个进程竞争同一个共享空间的时候就会发生数据错乱。
4.信号
信号是进程间通信机制中唯一的异步通信机制。信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间发生了哪些系统事件。信号事件主要有两大来源:硬件来源和软件来源,硬件来源指的是"Ctrl+c",软件来源指的是"kill"命令。一旦有信号来了,进程对于信号只有三种响应方式:1.执行默认操作2.捕捉信号3.忽略信号。但是有两个信号是进程无法捕捉和忽略的,是必须执行的,SIGKILL SEGSTOP 方便我们结束和中断线程。
之前的这些都是同一主机间的通信,要想实现不同主机间进程的通信还需要使用Socket通信。Socket通信不仅可以实现不同主机间的通信,还可以实现同一主机间进程的通信。根据创建Socket的类型不同,可以分为1.基于TCP协议的通信方式 2.基于UDP协议的通信方式 3.本地进程间通信方式。
在线程间不太关注通信的方式,更多关注的是共享资源的问题。信号量可以在线程中实现互斥与同步:
互斥:在任意时刻只有一个线程访问共享资源。
同步:保证线程A在线程B之前执行。
死锁是什么?代码实例?怎么利用工具排查死锁问题?
死锁的概念:
有两个线程,共享两个资源。线程A拿到了资源A的锁,线程B拿到了资源B的锁,但是这时候线程A在等待着线程B释放资源B的锁,并且尝试拿到资源B的锁,而同时线程B也在等待着线程A释放资源A的锁并且得到资源A的锁,本质上其实是两个互斥锁。由此而陷入了僵持,这种状态就叫做死锁。
代码实例:
public class ThreadTest10 { public static void main(String[] args) { Object obj1 = new Object(); Object obj2 = new Object(); Thread t1 = new Thread(new MyThread1(obj1,obj2)); Thread t2 = new Thread(new MyThread2(obj1,obj2)); t1.start(); t2.start(); } } class MyThread1 implements Runnable{ Object obj1 = new Object(); Object obj2 = new Object(); public MyThread1(Object obj1, Object obj2) { this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized(obj1){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(obj2){ } } } } class MyThread2 implements Runnable{ Object obj1 = new Object(); Object obj2 = new Object(); public MyThread2(Object obj1, Object obj2) { this.obj1 = obj1; this.obj2 = obj2; } @Override public void run() { synchronized(obj2){ try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized(obj1){ } } } }
怎么利用工具排查死锁问题:
如果想排查Java程序是否是死锁,则可以使用jstack工具,是jdk自带的线程堆栈分析工具。
如何避免死锁问题的发生:
产生死锁的四个条件:互斥条件,持有并等待条件,不可剥夺条件,环路等待条件(两个线程获取资源的顺序构成了环形链,比如线程A持有了资源2,而想请求资源1,线程B持有了资源1,而想请求资源2,这就是形成了资源请求等待的环形图)。
可以使用资源有序分配法来解决。
线程A和线程B获取资源的顺序要一样,线程A先请求资源1,然后再请求资源2,同样的,线程B也要先请求资源1,然后再请求资源2。线程A和线程B应该以相同的顺序申请自己想要的资源。
互斥锁的使用:
互斥锁和自旋锁对于加锁失败后的处理方式是不一样的。
互斥锁:在加锁失败后,会把失败的线程阻塞,让其进入休眠状态,线程释放CPU,给其他的线程。
但是在互斥锁加锁失败的过程中,也涉及到了两个上下文切换的开销。在得到锁被占有,线程无法得到锁的时候,需要将线程从运行状态调整为阻塞状态。在锁被释放的之后,将阻塞状态下的线程唤醒,变为就绪状态。然后在合适的时间点,将CPU交给该线程运行。
线程的上下文切换开销是不少的,因为当加锁失败的时候,从用户态陷入了内核态,我们需要内核帮我们切换线程。同一进程下的线程,彼此切换之间不需要切换共有的资源,只需要切换线程中的独享的资源,比如栈和寄存器等内的信息数据。但是如果我们运行的线程的时间很短的话,那么有可能上下文切换的时间已经超过了线程运行的时间,这种情况下,就不该使用互斥锁。
自旋锁的使用:
自旋锁是通过CPU提供的CAS函数,在用户态完成加锁和解锁操作。开销相比较互斥锁来说比较小。当发生多线程竞争锁的情况,加锁失败的线程会忙等待,自旋锁在单核CPU上,必须使用抢占式的调度机制才可以,否则就会一直不放弃CPU。
读写锁的使用:
读锁:当写锁没有被占有的时候,多个线程可以同时占用读锁,这样可以大大提高读取数据的效率。读锁是共享锁。
写锁:当写锁被占有的时候,其他的线程占有读锁的操作会被阻塞,并且别的线程获取写锁的操作也会被阻塞。写锁是独占锁。
读优先锁:在线程A获取读锁的时候,线程B获取写锁的操作会被阻塞,但是如果线程C来获取读锁是允许的,当线程A和线程C都已经完成了读操作的时候,线程B才可以被允许获得写锁。
写优先锁:在线程A获取读锁的时候,线程B获取写锁的操作会被阻塞,但是如果线程C来获取读锁也是不被允许的,会进入阻塞状态,当线程A完成读的操作之后,线程B就被唤醒并且可以获取写锁。
读优先锁容易造成,写饥饿。写优先锁容易造成,读饥饿。所以我们应该使用公平的读写锁。
方式:用队列把获取锁的线程排队,不管是读还是写线程都按照先进先出原则排队。
乐观锁和悲观锁:
乐观锁:我知道多线程操作共享资源的时候效率高,但是可能会引起数据错乱。我不管,我先让多个线程共同修改资源,然后完了事儿我自己再看看,如果经过我的验证,确实出现了数据错乱,别的线程已经修改过这个数据了,那就重新操作。有些时候如果出错的概率很低,并且容错成本也比较低的时候,可以使用乐观锁。比如:WPS在线文档 每次修改之后都通过验证版本号跟之前是否一致来判断是否出错。从某种角度可以理解为无锁编程。
悲观锁:我知道多线程操作共享资源的时候效率高,但是确实可能会引起数据错乱。所以,我干脆就给资源上锁,只允许在一个时刻有一个线程修改共享资源。很大程度上避免了意外的发生。互斥锁,自旋锁,读写锁,都是悲观锁。
内存页面置换算法:
缺页中断的处理流程:
当CPU在收到一个Load M指令后,会去找对应的页表中的位置,如果发现页表中的位置是无效的时候,就会发送给操作系统发出一个缺页中断请求,让操作系统执行缺页中断异常,向磁盘中查找有没有对应的页面位置,如果有,就将页面换入到空闲的物理内存当中,前提是也要查找一个物理内存中是否有空闲页,有的情况下找到空闲页,然后在换入到物理内存中。然后将之前那个页表中的无效的位置改为有效的,并再次执行Load M指令。
页面置换算法的功能:当出现缺页异常的时候,在操作系统找到了磁盘中缺少的那页,并且打算置换到空闲的物理内存中却发现物理内存已满的时候,选择被置换的物理页面的。选择一个物理页面换出到硬盘,把需要的页面换入到物理内存中。
常见的页面置换算法有如下几种:
最佳页面置换算法(OPT)
置换在未来,最长时间不被访问的页面。在真正的系统中无法实现,是一种理想的算法。
先进先出置换算法(FIFO)
选择在内存驻留时间很长的页面进行置换。
最近最久未使用的置换算法(LRU)
选择在内存中最长时间没有被访问的页面进行置换。
时钟页面置换算法(Lock)
把所有的页都保存在一个环形链表中,一个表针指向最老的页面。
当发生缺页中断的时候,首先检查表针指向的页面:
如果访问位是0就淘汰该页面,把新的插入这个位置,然后把表针前移一个位置。
如果访问为是1就清楚访问位,并且把表针前移一个位置,重复这个过程直到找到访问位为0的页面为止。
最不常用置换算法(LFU)
当发生缺页中断的时候,选择访问次数最少的页面,进行置换。
所以需要给每个页面设置一个访问计数器,每当一个页面被访问,该页面的访问计数器就累加一。
但是这样成本也是比较大的,并且是考虑不周全的。(没有考虑频率)