OS面试篇
用户态和内核态
用户态和内核态的区别?
内核态和用户态是操作系统中的两种运行模式。它们的主要区别在于权限和可执行的操作:
- 内核态(Kernel Mode):在内核态下,CPU可以执行所有的指令和访问所有的硬件资源。这种模式下的操作具有更高的权限,主要用于操作系统内核的运行。
- 用户态(User Mode):在用户态下,CPU只能执行部分指令集,无法直接访问硬件资源。这种模式下的操作权限较低,主要用于运行用户程序。
内核态的底层操作主要包括:内存管理、进程管理、设备驱动程序控制、系统调用等。这些操作涉及到操作系统的核心功能,需要较高的权限来执行。
分为内核态和用户态的原因主要有以下几点:
- 安全性:通过对权限的划分,用户程序无法直接访问硬件资源,从而避免了恶意程序对系统资源的破坏。
- 稳定性:用户态程序出现问题时,不会影响到整个系统,避免了程序故障导致系统崩溃的风险。
- 隔离性:内核态和用户态的划分使得操作系统内核与用户程序之间有了明确的边界,有利于系统的模块化和维护。
内核态和用户态的划分有助于保证操作系统的安全性、稳定性和易维护性。
进程管理
线程和进程的区别是什么?
- 本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
- 在开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
- 稳定性方面:进程中某个线程如果崩溃了,可能会导致整个进程都崩溃。而进程中的子进程崩溃,并不会影响其他进程。
- 内存分配方面:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。
- 包含关系:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线
进程,线程,协程的区别是什么?
- 首先,我们来谈谈进程。进程是操作系统中进行资源分配和调度的基本单位,它拥有自己的独立内存空间和系统资源。每个进程都有独立的堆和栈,不与其他进程共享。进程间通信需要通过特定的机制,如管道、消息队列、信号量等。由于进程拥有独立的内存空间,因此其稳定性和安全性相对较高,但同时上下文切换的开销也较大,因为需要保存和恢复整个进程的状态。
- 接下来是线程。线程是进程内的一个执行单元,也是CPU调度和分派的基本单位。与进程不同,线程共享进程的内存空间,包括堆和全局变量。线程之间通信更加高效,因为它们可以直接读写共享内存。线程的上下文切换开销较小,因为只需要保存和恢复线程的上下文,而不是整个进程的状态。然而,由于多个线程共享内存空间,因此存在数据竞争和线程安全的问题,需要通过同步和互斥机制来解决。
- 最后是协程。协程是一种用户态的轻量级线程,其调度完全由用户程序控制,而不需要内核的参与。协程拥有自己的寄存器上下文和栈,但与其他协程共享堆内存。协程的切换开销非常小,因为只需要保存和恢复协程的上下文,而无需进行内核级的上下文切换。这使得协程在处理大量并发任务时具有非常高的效率。然而,协程需要程序员显式地进行调度和管理,相对于线程和进程来说其编程模型更为复杂。
为什么进程崩溃不会对其他进程产生很大影响?
主要是因为:
- 进程隔离性:每个进程都有自己独立的内存空间,当一个进程崩溃时,其内存空间会被操作系统回收,不会影响其他进程的内存空间。这种进程间的隔离性保证了一个进程崩溃不会直接影响其他进程的执行。
- 进程独立性:每个进程都是独立运行的,它们之间不会共享资源,如文件、网络连接等。因此,一个进程的崩溃通常不会对其他进程的资源产生影响。
你说到进程是分配资源的基本单位,那么这个资源指的是什么?
虚拟内存、文件句柄、信号量等资源。
讲下为什么进程之下还要设计线程?
我们举个例子,假设你要编写一个视频播放器软件,那么该软件功能的核心模块有三个:
- 从视频文件当中读取数据;
- 对读取的数据进行解压缩;
- 把解压缩后的视频数据播放出来;
对于单进程的实现方式,我想大家都会是以下这个方式:
对于单进程的这种方式,存在以下问题:
- 播放出来的画面和声音会不连贯,因为当 CPU 能力不够强的时候,Read 的时候可能进程就等在这了,这样就会导致等半天才进行数据解压和播放;
- 各个函数之间不是并发执行,影响资源的使用效率;
那改进成多进程的方式:
对于多进程的这种方式,依然会存在问题:
- 进程之间如何通信,共享数据?
- 维护进程的系统开销较大,如创建进程时,分配资源、建立 PCB;终止进程时,回收资源、撤销PCB;进程切换时,保存当前进程的状态信息;
那到底如何解决呢?需要有一种新的实体,满足以下特性:
- 实体之间可以并发运行;
- 实体之间共享相同的地址空间;
这个新的实体,就是线程( Thread ),线程之间可以并发运行且共享相同的地址空间。
多线程比单线程的优势,劣势?
- 多线程比单线程的优势:提高程序的运行效率,可以充分利用多核处理器的资源,同时处理多个任务,加快程序的执行速度。
- 多线程比单线程的劣势:存在多线程数据竞争访问的问题,需要通过锁机制来保证线程安全,增加了加锁的开销,并且还会有死锁的风险。多线程会消耗更多系统资源,如CPU和内存,因为每个线程都需要占用一定的内存和处理时间。
多线程是不是越多越好,太多会有什么问题?
多线程不一定越多越好,过多的线程可能会导致一些问题。
- 切换开销:线程的创建和切换会消耗系统资源,包括内存和CPU。如果创建太多线程,会占用大量的系统资源,导致系统负载过高,某个线程崩溃后,可能会导致进程崩溃。
- 死锁的问题:过多的线程可能会导致竞争条件和死锁。竞争条件指的是多个线程同时访问和修改共享资源,如果没有合适的同步机制,可能会导致数据不一致或错误的结果。而死锁则是指多个线程相互等待对方释放资源,导致程序无法继续执行。
进程切换和线程切换的区别?
1.进程切换:进程切换涉及到更多的内容,包括整个进程的地址空间、全局变量、文件描述符等因此,进程切换的开销通常比线程切换大。
2.线程切换:线程切换只涉及到线程的堆栈、寄存器和程序计数器等不涉及进程级别的资源,因此线程切换的开销较小。
线程切换为什么比进程切换快,节省了什么资源?
线程切换比进程切换快是因为线程共享同一进程的地址空间和资源,线程切换时只需切换堆栈和程序计数器等少量信息,而不需要切换地址空间,避免了进程切换时需要切换内存映射表等大量资源的开销,从而节省了时间和系统资源。
线程切换详细过程是怎么样的?上下文保存在哪里?
线程切换的详细过程可以分为以下几个步骤:
- 上下文保存:当操作系统决定切换到另一个线程时,它首先会保存当前线程的上下文信息。上下文信息包括寄存器状态、程序计数器、堆栈指针等,用于保存线程的执行状态。
- 切换到调度器:操作系统将执行权切换到调度器(Scheduler)。调度器负责选择下一个要执行的线程,并根据调度算法做出决策。
- 上下文恢复:调度器选择了下一个要执行的线程后,它会从该线程保存的上下文信息中恢复线程的执行状态。
- 切换到新线程:调度器将执行权切换到新线程,使其开始执行。
上下文信息的保存通常由操作系统负责管理,具体保存在哪里取决于操作系统的实现方式。一般情况下,上下文信息会保存在线程的控制块(Thread Control Block,TCB)中。
进程的状态(五种状态),如何切换?
一个完整的进程状态的变迁如下图:
进程五种状态的变迁
再来详细说明一下进程的状态变迁:
- NULL -> 创建状态:一个新进程被创建时的第一个状态;
- 创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时变为就绪状态,这个过程是很快的;
- 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给 CPU 正式运行该进程;
- 运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
- 运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
- 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求 I/O 事件;
- 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
进程上下文有哪些?
各个进程之间是共享 CPU 资源的,在不同的时候进程之间需要切换,让不同的进程可以在 CPU 执行,那么这个一个进程切换到另一个进程运行,称为进程的上下文切换。
在详细说进程上下文切换前,我们先来看看 CPU 上下文切换
大多数操作系统都是多任务,通常支持大于 CPU 数量的任务同时运行。实际上,这些任务并不是同时运行的,只是因为系统在很短的时间内,让各个任务分别在 CPU 运行,于是就造成同时运行的错觉。
任务是交给 CPU 运行的,那么在每个任务运行前,CPU 需要知道任务从哪里加载,又从哪里开始运行。
所以,操作系统需要事先帮 CPU 设置好 CPU 寄存器和程序计数器。
CPU 寄存器是 CPU 内部一个容量小,但是速度极快的内存(缓存)。我举个例子,寄存器像是你的口袋,内存像你的书包,硬盘则是你家里的柜子,如果你的东西存放到口袋,那肯定是比你从书包或家里柜子取出来要快的多。
再来,程序计数器则是用来存储 CPU 正在执行的指令位置、或者即将执行的下一条指令位置。
所以说,CPU 寄存器和程序计数是 CPU 在运行任何任务前,所必须依赖的环境这些环境就叫做 CPU 上下文。
既然知道了什么是 CPU 上下文,那理解 CPU 上下文切换就不难了。
CPU 上下文切换就是先把前一个任务的 CPU 上下文(CPU 寄存器和程序计数器)保存起来,然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务。
系统内核会存储保持下来的上下文信息,当此任务再次被分配给 CPU 运行时,CPU 会重新加载这些上下文,这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行.
上面说到所谓的「任务」,主要包含进程、线程和中断。所以,可以根据任务的不同,把 CPU 上下文切换分成:进程上下文切换、线程上下文切换和中断上下文切换。
进程的上下文切换到底是切换什么呢?
进程是由内核管理和调度的,所以进程的切换只能发生在内核态。所以,进程的上下文切换不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。
通常,会把交换的信息保存在进程的 PCB,当要运行另外一个进程的时候,我们需要从这个进程的 PCB取出上下文,然后恢复到 CPU 中,这使得这个进程可以继续执行,如下图所示:
大家需要注意,进程的上下文开销是很关键的,我们希望它的开销越小越好,这样可以使得进程可以把更多时间花费在执行程序上,而不是耗费在上下文切换。
进程间通讯有哪些方式?
Linux 内核提供了不少进程间通信的方式:
- 管道
- 消息队列
- 共享内存
- 信号
- 信号量
- socket
Linux 内核提供了不少进程间通信的方式,其中最简单的方式就是管道,管道分为「匿名管道」和「命名管道」。
匿名管道顾名思义,它没有名字标识,匿名管道是特殊文件只存在于内存,没有存在于文件系统中,shell 命令中的「|」竖线就是匿名管道,通信的数据是无格式的流并且大小受限,通信的方式是单向的,数据只能在一个方向上流动,如果要双向通信,需要创建两个管道,再来匿名管道是只能用于存在父子关系的进程间通信,匿名管道的生命周期随着进程创建而建立,随着进程终止而消失。
命名管道突破了匿名管道只能在亲缘关系进程间的通信限制,因为使用命名管道的前提,需要在文件系统创建一个类型为 p 的设备文件,那么毫无关系的进程就可以通过这个设备文件进行通信。另外,不管是匿名管道还是命名管道,进程写入的数据都是缓存在内核中。另一个进程读取数据时候自然也是从内核中获取,同时通信数据都遵循先进先出原则,不支持 lseek 之类的文件定位操作。
消息队列克服了管道通信的数据是无格式的字节流的问题,消息队列实际上是保存在内核的「消息链表」,消息队列的消息体是可以用户自定义的数据类型,发送数据时,会被分成一个一个独立的消息体,当然接收数据时,也要与发送方发送的消息体的数据类型保持一致,这样才能保证读取的数据是正确的。消息队列通信的速度不是最及时的,毕竟每次数据的写入和读取都需要经过用户态与内核态之间的拷贝过程。
共享内存可以解决消息队列通信中用户态与内核态之间数据拷贝过程带来的开销,它直接分配一个共享空间,每个进程都可以直接访问,就像访问进程自己的空间一样快捷方便,不需要陷入内核态或者系统调用,大大提高了通信的速度,享有最快的进程间通信方式之名。但是便捷高效的共享内存通信,带来新的问题,多进程竞争同个共享资源会造成数据的错乱。
那么,就需要信号量来保护共享资源,以确保任何时刻只能有一个进程访问共享资源,这种方式就是互斥访问。信号量不仅可以实现访问的互斥性,还可以实现进程间的同步,信号量其实是一个计数器,表示的是资源个数,其值可以通过两个原子操作来控制,分别是 P 操作和 V 操作。
与信号量名字很相似的叫信号,它俩名字虽然相似,但功能一点儿都不一样。信号是异步通信机制,信号可以在应用进程和内核之间直接交互,内核也可以利用信号来通知用户空间的进程发生了哪些系统事件,信号事件的来源主要有硬件来源(如键盘 Cltr+C )和软件来源(如 kill 命令)一旦有信号发生,进程有三种方式响应信号 1. 执行默认操作、2. 捕捉信号、3. 忽略信号。有两个信号是应用进程无法捕捉和忽略的,即 SIGKILL 和 SIGSTOP,这是为了方便我们能在任何时候结束或停止某个进程。
前面说到的通信机制,都是工作于同一台主机,如果要与不同主机的进程间通信,那么就需要 Socket 通信了。Socket 实际上不仅用于不同的主机进程间通信,还可以用于本地主机进程间通信,可根据创建 Socket 的类型不同,分为三种常见的通信方式,一个是基于 TCP 协议的通信方式,一个是基于 UDP 协议的通信方式,一个是本地进程间通信方式。
管道有几种方式?
管道在Linux中有两种方式:匿名管道和命名管道。
- 匿名管道:是一种在父子进程或者兄弟进程之间进行通信的机制,只能用于具有亲缘关系的进程间通信,通常通过pipe系统调用创建。
- 命名管道:是一种允许无关的进程间进行通信的机制,基于文件系统,可以在不相关的进程之间进行通信
信号和信号量有什么区别?
- 信号:一种处理异步事件的方式。信号是比较复杂的通信方式,用于通知接收进程有某种事件发生,除了用于进程外,还可以发送信号给进程本身。
- 信号量:进程间通信处理同步互斥的机制。是在多线程环境下使用的一种设施,它负责协调各个线程,以保证它们能够正确,合理的使用公共资源。
共享内存怎么实现的?
共享内存的机制,就是拿出一块虚拟地址空间来,映射到相同的物理内存中。这样这个进程写入的东西,另外一个进程马上就能看到了,都不需要拷贝来拷贝去,传来传去,大大提高了进程间通信的速度。
线程间通讯有什么方式?
Linux系统提供了五种用于线程通信的方式:互斥锁、读写锁、条件变量、自旋锁和信号量。
- 互斥锁(Mutex):互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥锁加锁的线程将会阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥锁加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。
- 条件变量(Condition Variables):条件变量(cond)是在多线程程序中用来实现"等待--》唤醒"逻辑常用的方法。条件变量利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使“条件成立”。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。线程在改变条件状态前必须首先锁住互斥量函数pthread_cond_wait把自己放到等待条件的线程列表上,然后对互斥锁解锁(这两个操作是原子操作)。在函数返回时,互斥量再次被锁住。
- 自旋锁(Spinlock):自旋锁通过 CPU 提供的 CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。一般加锁的过程,包含两个步骤:第一步,查看锁的状态,如果锁是空闲的,则执行第二步;第二步,将锁设置为当前线程持有;使用自旋锁的时候,当发生多线程竞争锁的情况加锁失败的线程会「忙等待」,直到它拿到锁。CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。这里的「忙等待」要么两个步骤都不执行。这里的「忙等待」可以用 while 循环等待实现,不过最好是使用 CPU 提供的 PAUSE 指令来实现,因为可以减少循环等待时的耗电量。
- 信号量(Semaphores):信号量可以是命名的(有名信号量)或无名的(仅限于当前进程内的线程),用于控制对资源的访问次数。通常信号量表示资源的数量,对应的变量是一个整型(sem)变量,另外,还有两个原子操作的系统调用函数来控制信号量的,分别是:P 操作:将 sem 减 1,相减后,如果 sem < 0,则进程/线程进入阻塞等待,否则继续,表明 P 操作可能会阻塞;V 操作:将 sem相加后,如果 sem <= 0,唤醒一个等待中的进程/线程,表明 V 操作不会阻塞;
- 读写锁(Read-Write Locks):读写锁从字面意思我们也可以知道,它由「读锁」和「写锁」两部分构成,如果只读取共享资源用「读锁」加锁,如果要修改共享资源则用「写锁」加锁。所以,读写锁适用于能明确区分读操作和写操作的场景。读写锁的工作原理是:当「写锁」没有被线程持有时,多个线程能够并发地持有读锁,这大大提高了共享资源的访问效率,因为「读锁」是用于读取共享资源的场景,所以多个线程同时持有读锁也不会破坏共享资源的数据。但是,一旦「写锁」被线程持有后,读线程的获取读锁的操作会被阻塞,而且其他写线程的获取写锁的操作也会被阻塞。所以说,写锁是独占锁,因为任何时刻只能有一个线程持有写锁,类似互斥锁和自旋锁,而读锁是共享锁,因为读锁可以被多个线程同时持有。知道了读写锁的工作原理后,我们可以发现,读写锁在读多写少的场景,能发挥出优势。
除了互斥锁你还知道什么锁?分别应用于什么场景?
还有读写锁、自旋锁、条件变量、信号量。
- 读写锁:读写锁允许多个线程同时读取共享资源,但只允许一个线程进行写操作。适用于读操作频繁、写操作较少的场景,可以提高并发性能。
- 自旋锁:自旋锁是一种忙等待锁,线程在获取锁时不会进入阻塞状态,而是循环忙等待直到获取到锁。适用于临界区很小且锁的持有时间很短的场景,避免线程频繁切换带来的开销。
- 条件变量:条件变量用于线程间的同步和通信。它通常与互斥锁一起使用,线程可以通过条件变量等待某个条件满足,当条件满足时,其他线程可以通过条件变量发送信号通知等待线程。
- 信号量:信号量是一种计数器,用于控制对共享资源的访问。它可以用来限制同时访问资源的线程数量,或者用于线程间的同步。
进程调度算法有哪些?
先来先服务调度算法
最简单的一个调度算法,就是非抢占式的先来先服务(First Come First Severd, FCFS)算法了。
顾名思义,先来后到,每次从就绪队列选择最先进入队列的进程,然后一直运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。这似乎很公平,但是当一个长作业先运行了,那么后面的短作业等待的时间就会很长,不利于短作业。FCFS 对长作业有利,适用于 CPU 繁忙型作业的系统,而不适用于 I/O 繁忙型作业的系统。
最短作业优先调度算法
最短作业优先(Shortest Job First, SJF)调度算法同样也是顾名思义,它会优先选择运行时间最短的进程来运行,这有助于提高系统的吞吐量。
这显然对长作业不利,很容易造成一种极端现象。比如,一个长作业在就绪队列等待运行,而这个就绪队列有非常多的短作业,那么就会使得长作业不断的往后推,周转时间变长,致使长作业长期不会被运行。
高响应比优先调度算法
前面的「先来先服务调度算法」和「最短作业优先调度算法」都没有很好的权衡短作业和长作业。那么,高响应比优先 (Highest Response Ratio Next, HRRN)调度算法主要是权衡了短作业和长作业。每次进行进程调度时,先计算「响应比优先级」,然后把「响应比优先级」最高的进程投入运行,响应比优先级」的计算公式:
从上面的公式,可以发现:
- 如果两个进程的「等待时间」相同时,「要求的服务时间」越短,「响应比」就越高,这样短作业的进程容易被选中运行;
- 如果两个进程「要求的服务时间」相同时,「等待时间」越长,「响应比」就越高,这就兼顾到了长作业进程,因为进程的响应比可以随时间等待的增加而提高,当其等待时间足够长时,其响应比便可以升到很高,从而获得运行的机会;
时间片轮转调度算法
最古老、最简单、最公平且使用最广的算法就是时间片轮转(Round Robin, RR)调度算法。
每个进程被分配一个时间段,称为时间片(Quantum),即允许该进程在该时间段中运行。
- 如果时间片用完,进程还在运行,那么将会把此进程从 CPU 释放出来,并把 CPU 分配另外一个进程;
- 如果该进程在时间片结束前阻塞或结束,则 CPU 立即进行切换;
另外,时间片的长度就是一个很关键的点:
- 如果时间片设得太短会导致过多的进程上下文切换,降低了 CPU 效率;
- 如果设得太长又可能引起对短作业进程的响应时间变长。将通常时间片设为
20ms~50ms
通常是一个比较合理的折中值。
最高优先级调度算法
前面的「时间片轮转算法」做了个假设,即让所有的进程同等重要,也不偏袒谁,大家的运行时间都一样。但是,对于多用户计算机系统就有不同的看法了,它们希望调度是有优先级的,即希望调度程序能从就绪队列中选择最高优先级的进程进行运行,这称为最高优先级(Highest Priority First,HPF)调度算法。进程的优先级可以分为,静态优先级或动态优先级:
- 静态优先级:创建进程时候,就已经确定了优先级了,然后整个运行时间优先级都不会变化;
- 动态优先级:根据进程的动态变化调整优先级,比如如果进程运行时间增加,则降低其优先级,如果进程等待时间(就绪队列的等待时间)增加,则升高其优先级,也就是随着时间的推移增加等待进程的优先级。
该算法也有两种处理优先级高的方法,非抢占式和抢占式:
- 非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。
- 抢占式:当就绪队列中出现优先级高的进程,当前进程挂起,调度优先级高的进程运行。
但是依然有缺点,可能会导致低优先级的进程永远不会运行。
多级反馈队列调度算法
多级反馈队列(Multilevel Feedback Queue)调度算法是是「时间片轮转算法」和「最高优先级算法」的综合和发展。
顾名思义:
- 「多级」表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
- 「反馈」表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列;
来看看,它是如何工作的:
- 设置了多个队列,赋予每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短;
- 新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,以此类推,直至完成;
- 当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让较高优先级的进程运行;
可以发现,对于短作业可能可以在第一级队列很快被处理完。对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待的时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。
锁
为什么并发执行线程要加锁?
并发执行线程需要加锁主要是为了保护共享数据,防止出现"竞态条件"。"竞态条件"是指当多个线程同时访问和操作同一块数据时,最终结果依赖于线程的执行顺序,这可能导致数据的不一致性。
通过加锁,我们可以确保在任何时刻只有一个线程能够访问共享数据,从而避免"竞态条件",确保数据的一致性和完整性。
自旋锁是什么?应用在哪些场景?
自旋锁加锁失败后,线程会忙等待,直到它拿到锁。
自旋锁是通过 CPU 提供的 CAS
函数(Compare And Swap),在「用户态」完成加锁和解锁操作,不会主动产生线程上下文切换,所以相比互斥锁来说,会快一些,开销也小一些。
一般加锁的过程,包含两个步骤:
- 第一步,查看锁的状态,如果锁是空闲的,则执行第二步;
- 第二步,将锁设置为当前线程持有;
CAS 函数就把这两个步骤合并成一条硬件级指令,形成原子指令,这样就保证了这两个步骤是不可分割的,要么一次性执行完两个步骤,要么两个步骤都不执行。
比如,设锁为变量 lock,整数 0 表示锁是空闲状态,整数 pid 表示线程 ID,那么 CAS(lock, 0, pid) 就表示自旋锁的加锁操作,CAS(lock, pid, 0) 则表示解锁操作。
使用自旋锁的时候,当发生多线程竞争锁的情况,加锁失败的线程会「忙等待」,直到它拿到锁。这里的「忙等待」可以用 while
循环等待实现,不过最好是使用 CPU 提供的 PAUSE
指令来实现「忙等待」,因为可以减少循环等待时的耗电量。
自旋锁是最比较简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。需要注意,在单核 CPU 上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。
如果你能确定被锁住的代码执行时间很短,就不应该用互斥锁,而应该选用自旋锁,否则使用互斥锁。
乐观锁和悲观锁有什么区别?
乐观锁:
- 基本思想:乐观锁假设多个事务之间很少发生冲突,因此在读取数据时不会加锁,而是在更新数据时检查数据的版本(如使用版本号或时间戳),如果版本匹配则执行更新操作,否则认为发生了冲突。
- 使用场景:乐观锁适用于读多写少的场景,可以减少锁的竞争,提高并发性能。例如,数据库中的乐观锁机制可以用于处理并发更新同一行数据的情况。
悲观锁:
- 基本思想:悲观锁假设多个事务之间会频繁发生冲突,因此在读取数据时会加锁,防止其他事务对数据进行修改,直到当前事务完成操作后才释放锁。
- 使用场景:悲观锁适用于写多的场景,通过加锁保证数据的一致性。例如,数据库中的行级锁机制可以用于处理并发更新同一行数据的情况。
内存管理
介绍一下操作系统内存管理
操作系统设计了虚拟内存,每个进程都有自己的独立的虚拟内存,我们所写的程序不会直接与物理内存打交道。
有了虚拟内存之后,它带来了这些好处:
- 第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
- 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
- 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
什么是虚拟内存和物理内存?
- 虚拟内存:是操作系统提供给每个运行中程序的一种地址空间,每个程序在运行时认为自己拥有的内存空间就是虚拟内存,其大小可以远远大于物理内存的大小。虚拟内存通过将程序的地址空间划分成若干个固定大小的页或段,并将这些页或者段映射到物理内存中的不同位置,从而使得程序在运行时可以更高效地利用物理内存。
- 物理内存:物理内存是计算机实际存在的内存,是计算机中的实际硬件部件。
未完待续~~~~