大厂系列:操作系统八股文,速速收藏(九)

#牛客AI配图神器#70.协程和线程有什么区别?

执行模型

  • 线程:线程是操作系统管理的基本执行单元。每个线程有自己独立的堆栈和程序计数器(PC),可以被操作系统调度和切换。线程的创建和调度通常由操作系统的内核管理,因此线程之间的切换涉及到上下文切换(保存和恢复线程的状态)。
  • 协程:协程是用户级别的轻量级执行单元,它是在单一线程内通过用户空间的调度来实现并发的。协程的切换不涉及操作系统的内核调度,而是由程序员或协程库控制。协程之间的切换仅涉及保存和恢复协程的执行状态(如程序计数器、局部变量等),通常不需要操作系统干预。

资源消耗

  • 线程:创建线程需要分配独立的内存空间(如栈空间),并且线程之间的上下文切换需要保存和恢复较多的上下文信息(如寄存器、堆栈等),因此线程的创建和切换通常比较消耗资源。
  • 协程:协程非常轻量级,因为它们只需要保存少量的状态信息(如局部变量、栈指针等)。协程的创建和切换通常不需要操作系统内核的参与,因此它们的开销较小。

调度方式

  • 线程:线程的调度由操作系统的内核控制,通常是抢占式调度或基于时间片的调度。操作系统负责选择哪个线程运行,并且在需要时进行线程的切换。
  • 协程:协程的调度是协作式的,通常由用户或者协程库管理。当协程主动让出控制权(通过yield、await、sleep等操作),调度器会选择下一个需要执行的协程。协程之间的切换是显式的,程序员控制何时切换,而不像线程那样由操作系统自动切换。

上下文切换

  • 线程:线程切换需要操作系统介入,保存当前线程的上下文(如寄存器、栈等),并加载下一个线程的上下文。这个过程比较重,且涉及系统调用和内核态的操作,因此线程切换的开销较大。
  • 协程:协程切换只涉及保存和恢复少量的状态信息,通常是在用户态内完成,不涉及系统调用或内核态的切换。因此,协程切换的开销非常小,速度快。

并发性

  • 线程:线程的并发性依赖于操作系统的调度以及系统硬件的支持(如多核CPU)。多个线程可以并行执行,尤其是在多核处理器上,多个线程可以在不同的核心上同时运行,利用多核的计算能力。
  • 协程:协程通常在单核处理器上实现并发执行,所有的协程共享同一个线程。协程的并发是通过时间片轮转的方式来模拟的,尽管它们是并发执行的,但本质上是在单线程中交替执行。

适用场景

  • 线程:适用于需要真正并行执行的场景,特别是当程序可以并行处理多个独立任务时。例如,IO密集型操作、需要高并发处理的网络应用等。
  • 协程:适用于高并发、IO密集型场景,尤其是当任务之间需要频繁切换、并且每个任务的执行时间较短时。协程能够有效减少线程切换的开销,适合处理大量短时间的任务(如爬虫、Web请求处理等)。

同步机制

  • 线程:线程之间需要使用同步机制(如互斥锁、信号量、条件变量等)来保证线程间的数据一致性和避免竞争条件。线程同步可能会带来性能瓶颈和死锁问题。
  • 协程:协程通常通过事件循环、消息队列或异步编程来实现同步机制。由于协程在同一个线程内执行,它们共享相同的内存空间,通常不需要像线程那样的复杂同步机制。

71.悲观锁和乐观锁有什么区别?

悲观锁(Pessimistic Locking):假设并发场景中会频繁发生冲突,因此每次操作都认为会发生冲突。为了保证数据的一致性,悲观锁会在访问资源之前加锁,确保操作期间数据不会被其他线程或进程修改。悲观锁的主要思想是“我不信任其他线程,会采取措施避免冲突”。

乐观锁(Optimistic Locking):假设并发场景中冲突发生的概率较低,因此在操作时并不立即加锁,而是允许其他线程访问数据。在执行操作时,乐观锁会先读取数据,进行操作,最后检查数据是否在期间发生了变化,如果数据没有变化,则提交更新;如果发生了变化,操作会失败,需要重新尝试。乐观锁的主要思想是“我相信其他线程不会修改数据,所以我不需要加锁”。

72.乐观锁怎么实现?

基于版本号的乐观锁实现

每个记录都有一个与之关联的版本号。当数据被读取时,系统会读取该记录的版本号,并在更新时,检查该版本号是否与之前读取时相同。如果相同,则更新该记录,并增加版本号。如果不相同,说明在此期间其他事务已经修改了该记录,更新操作会失败,并且需要重新尝试。

实现步骤

  1. 读取数据时,读取记录的版本号。
  2. 在进行修改时,检查该记录的版本号是否与读取时的版本号相同。
  3. 如果版本号相同,则更新记录并增加版本号。
  4. 如果版本号不同,则表示数据

基于时间戳的乐观锁实现

每个记录都附带一个时间戳(或修改时间)。在读取数据时,记录该时间戳。当进行更新时,检查数据的时间戳是否与读取时一致。如果一致,则进行更新,并修改时间戳;如果不一致,说明数据已被其他操作修改,更新失败,通常返回错误或者需要重试。

实现步骤

  1. 读取数据时,读取记录的时间戳。
  2. 在进行修改时,检查该记录的时间戳是否与读取时的一致。
  3. 如果时间戳一致,则更新记录并修改时间戳。
  4. 如果时间戳不同,则操作失败。

73.操作系统死锁怎么产生的?

死锁是指在多进程或多线程的执行过程中,由于资源的竞争和进程的互相等待,导致某些进程永远无法继续执行的情况。死锁通常由四个必要的条件同时满足而产生,它们被称为死锁的四个必要条件。这些条件是:

互斥条件:某些资源在某个时刻只能被一个进程占用。即资源是不可共享的,若一个进程占用某个资源,其他进程无法使用该资源,直到资源被释放。

请求与保持条件:一个进程在请求新的资源时,已经持有至少一个资源,但仍然没有释放它。也就是说,进程持有某些资源并继续请求更多资源。

不剥夺条件:已经分配给进程的资源,在进程使用完之前不能被强制剥夺。即资源只能在进程自行释放时才会释放,而不是操作系统强制回收。

循环等待条件:存在一个进程等待链,其中每个进程都在等待下一个进程所持有的资源,从而形成一个闭环。例如,进程A等待进程B持有的资源,进程B等待进程C持有的资源,而进程C又等待进程A持有的资源,形成一个死锁循环。

74.如何避避免死锁?

死锁预防

死锁预防通过避免死锁的四个必要条件中的一个或多个条件来减少死锁发生的可能性。

  • 破坏互斥条件:互斥条件要求某个资源在任何时刻只能由一个进程占用。通过将某些资源设置为可共享(例如,允许多个进程同时读同一个文件),可以避免死锁。然而,这并不适用于所有资源,因为某些资源本身不支持共享(如打印机或数据库锁)。
  • 破坏请求与保持条件:请求与保持条件指的是进程在持有一些资源的同时,继续请求其他资源。为了避免这个条件,可以要求进程在请求资源时,一次性请求所有需要的资源。如果不能全部获得,进程必须释放已获得的资源,并在稍后重新尝试。虽然这种方法能避免请求与保持条件,但可能会降低系统的效率。
  • 破坏不剥夺条件:不剥夺条件意味着一旦进程获得了某个资源,就不能强制夺回。为避免死锁,可以让操作系统在进程无法继续执行时强制回收资源,将资源从一个进程剥夺并重新分配给其他进程。这种方法可能导致进程间的频繁资源抢夺,从而降低系统效率。
  • 破坏循环等待条件:循环等待条件是死锁产生的核心,它是指一组进程互相等待对方持有的资源。为了避免这个条件,可以为所有资源赋予一个优先级,并要求进程按照优先级顺序请求资源。这样,进程就不会形成一个等待环路,因为它们只能按顺序请求资源,从而避免死锁。

死锁避免

死锁避免是通过动态地检查系统的资源分配状态来避免死锁的发生。系统根据进程的资源请求情况做出判断,确保不会进入死锁状态。

  • 银行家算法:银行家算法是一种典型的死锁避免算法,它通过模拟每个进程的最大资源需求以及当前系统可用资源,来判断当前的资源分配是否安全。如果资源分配后,系统有可能进入死锁状态,银行家算法会拒绝该请求,从而避免死锁的发生。银行家算法通过维护一个资源分配图来判断系统的安全性,确保进程请求资源时,系统不会进入不安全的状态。
  • 资源分配图:资源分配图是死锁避免的另一种方法,通过图形化的方式表示进程与资源之间的关系。每个资源和进程都用节点表示,边表示资源的分配与请求关系。系统会不断检查资源分配图,确保不存在环路,如果存在环路(即死锁),则拒绝资源请求。资源分配图的检查机制可以有效避免死锁,但计算复杂度较高。

死锁检测与恢复

死锁检测与恢复并不是避免死锁,而是在死锁发生时采取措施恢复系统正常运行。死锁检测机制可以监控系统的状态,定期检查是否存在死锁,如果检测到死锁,则通过一些策略来恢复系统。

  • 检测死锁:系统可以通过资源分配图或者其他监控工具定期检测死锁。如果检测到存在环形依赖,即进程间相互等待,系统认为发生了死锁。
  • 恢复死锁:终止进程:一旦发现死锁,可以选择终止某些进程来打破死锁。可以选择终止某个进程,或者选择一组进程,通过释放资源来打破死锁状态。回滚进程:除了终止进程外,系统还可以通过回滚已执行的进程,恢复到一个安全状态。这需要系统支持进程状态的保存和恢复机制。资源剥夺:另一种恢复死锁的方法是强制剥夺某些进程持有的资源,将这些资源分配给其他进程,从而打破死锁。该方法可能导致进程不确定性行为。

资源的有序分配

通过为每个资源类型分配一个唯一的编号,进程在请求资源时,必须按照资源编号的升序或降序来申请。这种方法可以有效避免循环等待条件,从而避免死锁。通过严格的资源请求顺序,进程间不可能形成循环等待。

使用更细粒度的锁

使用更细粒度的锁可以减少资源的竞争,避免进程因等待较大范围的资源而进入死锁状态。例如,数据库中的表锁可能导致死锁,使用行级锁可以减少资源的争用,降低死锁发生的概率。

75.发生死锁时,怎么排查?

查看死锁日志

大多数操作系统和应用程序都会记录系统日志,尤其是在发生异常时。死锁通常会导致某些进程或线程无法继续执行,因此查看相关日志(如操作系统的内核日志或应用程序日志)可以帮助快速定位死锁发生的位置。

  • 操作系统日志:在 Linux 上,可以通过 dmesg 或 /var/log/syslog 来查看内核相关的日志。在 Windows 上,可以查看事件查看器中的应用程序和系统日志。
  • 数据库日志:数据库系统如 MySQL、Oracle 等通常会记录死锁信息。如果数据库发生死锁,它们会在日志中提供详细的死锁信息,包括参与死锁的进程、锁的情况等。

使用死锁检测工具

许多操作系统和开发环境提供了死锁检测工具,可以帮助开发者识别并定位死锁。

  • Linux:Linux 提供了 ps、top 等工具可以查看进程状态。如果一个进程处于 "D" 状态(不可中断的等待),通常意味着它正在等待某些资源,可能涉及死锁。此外,strace 或 gdb 可以用来跟踪和调试进程,帮助找出死锁的根源。
  • Java:Java 提供了 jstack 命令,可以输出 JVM 的堆栈信息,包括线程的状态。当出现死锁时,jstack 会显示哪些线程相互等待,从而帮助开发者定位死锁问题。
  • 数据库:如 MySQL 等数据库系统内置了死锁检测功能。如果数据库出现死锁,它会在日志中记录下死锁的详细信息,包括被锁住的表和进程信息,帮助快速定位死锁源。

资源分配图分析

资源分配图是一种用于描述系统中进程与资源之间关系的图形结构。进程和资源在图中分别用节点表示,进程请求和持有资源的边表示为有向边。当多个进程和资源发生死锁时,图中会形成一个环。通过定期分析资源分配图,可以快速发现死锁。

  • 构建资源分配图:记录每个进程持有和请求的资源,构建一个有向图。如果图中存在环,则说明死锁发生。
  • 手动分析图:通过查看资源分配图中的环,可以手动分析出哪些进程和资源造成了死锁。

使用调试工具(如 gdb)

在进程发生死锁时,可以使用调试工具如 gdb(GNU 调试器)来附加到死锁的进程,查看堆栈信息以及线程状态。通过查看进程的栈跟踪,可以看到哪些线程在等待资源,哪些资源被占用。

  • 查看线程堆栈:通过 gdb 可以检查每个线程的堆栈信息,找出哪些线程互相等待。
  • 查看锁信息:在死锁的情况下,线程通常会持有一些锁,检查这些锁的状态可以帮助判断死锁的原因。

检查代码中的资源争用情况

代码中的资源竞争是导致死锁的常见原因,尤其是在多线程或多进程的环境下。可以通过以下几个步骤进行排查:

  • 资源锁定顺序:检查代码中所有进程或线程如何请求资源,是否按照固定的顺序获取锁。死锁通常发生在多个线程或进程同时请求多个资源时。如果这些资源的请求顺序不一致,就可能形成循环等待,从而导致死锁。
  • 锁粒度:检查程序是否使用了过大的锁粒度。过大的锁(例如,锁住整个数据结构)可能导致线程等待时间过长,从而增加死锁的风险。
  • 死锁检测代码:可以在代码中添加日志,记录每个线程或进程持有和请求的锁,帮助开发者发现潜在的死锁点。

查看进程间的锁竞争

在多进程程序中,死锁通常是由于进程间对共享资源的锁竞争引起的。通过查看进程间的锁信息,可以帮助发现死锁。

  • 数据库系统:如果是数据库系统中的死锁,可以查看数据库的锁信息,查看哪些事务正在持有锁,哪些事务正在等待锁。如果存在环形依赖,就说明死锁已经发生。
  • 操作系统:通过操作系统提供的 ps、top、lsof 等工具查看进程的锁状态。查看哪些进程持有锁,哪些进程在等待锁,如果发现等待的进程之间有循环依赖,说明死锁发生。

使用超时机制进行检测

许多操作系统和应用程序都提供了超时机制来检测死锁。当一个进程或线程在等待资源时,若超出了预定的时间限制,系统会自动终止该进程或线程,从而避免死锁。开发人员可以通过设置适当的超时阈值来防止系统进入死锁状态。

  • 设置锁超时:在多线程编程中,可以设置锁的超时机制。如果一个线程长时间无法获取锁,就可以抛出异常或做一些恢复操作,避免死锁。
  • 数据库事务超时:数据库系统可以设置事务的超时,如果一个事务执行时间过长,系统会自动回滚该事务,以避免死锁的发生。

76.为什么操作系统会有虚拟内存?

操作系统引入虚拟内存的原因主要是为了提供更高效的内存管理,提升系统的性能和稳定性,尤其是在多任务环境下。虚拟内存使得操作系统能够更好地利用硬件资源并为程序提供更大的内存空间。具体原因包括以下几点:

扩展内存空间:物理内存(RAM)有限,尤其在旧式计算机或嵌入式系统中,RAM 的容量通常较小。虚拟内存通过使用硬盘空间作为扩展,将物理内存的使用限制推向了一个更高的层次。例如,当程序请求的内存超出物理内存大小时,操作系统会将部分数据暂时存储到硬盘上的交换空间(Swap),从而使得程序可以使用更多的内存,虽然这会带来性能上的开销,但相较于物理内存的局限性,虚拟内存提供了更大的空间。

程序隔离和安全性:虚拟内存提供了内存隔离的机制,每个程序(进程)运行时都有自己的虚拟地址空间。即使多个程序运行在同一台计算机上,它们的内存地址空间也是独立的,不会相互干扰。这不仅有效防止了进程间的内存冲突,也增强了系统的稳定性和安全性。如果一个进程试图访问未分配或其他进程的内存地址,操作系统会捕获这个错误并进行处理,避免了程序间的恶意访问或错误访问。

内存共享和映射:虚拟内存允许多个进程共享某些内存区域。例如,通过内存映射(memory-mapping)技术,操作系统可以让多个进程共享同一块物理内存区域,而每个进程仍然可以拥有自己的虚拟地址空间。常见的应用场景如共享库(Shared Libraries),多个程序可以通过虚拟内存访问同一份代码或数据,而不需要将其复制到每个进程的地址空间中。

简化内存管理:通过虚拟内存,操作系统可以实现更为灵活的内存管理策略,如页面置换(paging)和段式管理(segmentation)。在虚拟内存中,程序的内存被划分为固定大小的“页面”或更大的“段”,操作系统可以根据需要将程序的一部分页面加载到物理内存中,同时将不活跃的页面换出到硬盘。这种机制能够充分利用物理内存,并且避免了内存碎片的问题。

支持多任务处理:虚拟内存使得操作系统能够更好地管理多任务,确保每个进程可以独立运行,而不需要担心其他进程的内存影响。操作系统可以为每个进程提供一个完整的、独立的虚拟地址空间,多个进程共享同一物理内存时,操作系统能够确保它们互不干扰。

程序调度与优化:虚拟内存允许操作系统根据程序的行为动态调整内存的分配。例如,操作系统可以通过页面置换算法(如最近最少使用算法LRU)来选择哪些页面应该保留在物理内存中,哪些页面应该被换出。这使得操作系统能够更有效地管理内存,提升多任务环境中的程序响应能力。

77.虚拟内存有什么作用?

扩展内存空间:虚拟内存可以将物理内存的限制克服,通过硬盘或其他存储设备作为扩展,使得程序可以使用比实际物理内存更大的地址空间。例如,当物理内存不足时,操作系统会将一些数据暂时存储到硬盘上的交换空间(Swap),从而为其他程序或进程提供更多的内存。

内存隔离与保护:每个进程都有独立的虚拟地址空间,这样就避免了不同进程之间的内存冲突。虚拟内存机制确保了一个进程无法直接访问另一个进程的内存区域,增强了操作系统的安全性。如果一个进程越界访问内存,操作系统会抛出错误,防止系统崩溃或进程间的恶意访问。

内存共享与映射:虚拟内存允许多个进程共享某些内存区域而不互相干扰。例如,多个进程可以共享同一个库文件或同一个数据区域,通过内存映射(memory-mapping)技术实现。每个进程仍然拥有独立的虚拟地址空间,但它们可以通过映射共享的区域访问相同的物理内存。这样可以有效减少内存消耗,提高效率。

简化内存管理:虚拟内存为操作系统提供了灵活的内存管理方式。通过分页(paging)或分段(segmentation)机制,操作系统可以将内存划分为固定大小的块(页面或段),而不必依赖连续的内存空间。虚拟内存允许操作系统动态加载和换出数据,使得物理内存的使用更加高效,避免了内存碎片化的问题。

支持多任务并发:虚拟内存使得操作系统能够更高效地运行多个进程,每个进程都可以认为它拥有一个连续的内存空间,尽管实际上物理内存可能是分散的或有限的。这样,操作系统可以在不同进程之间快速切换,并为每个进程提供相对独立的运行环境,提升了多任务处理的能力。

程序无感知的内存扩展:虚拟内存使得程序员不需要关注物理内存的具体限制。程序可以假定有一个大而连续的内存空间,操作系统会自动处理物理内存和虚拟内存之间的映射。程序员可以专注于逻辑内存的需求,而不必担心底层的内存管理细节。

内存保护与容错:虚拟内存还可以提供内存保护的机制。当程序访问它不应该访问的内存区域时,操作系统能够捕获该行为并防止程序崩溃或导致系统错误。此外,虚拟内存的机制还可以帮助操作系统进行容错,例如通过将页面从磁盘加载到内存中,程序即使在物理内存不足的情况下也可以继续运行。

提高系统响应性:虚拟内存使得操作系统能够按需加载和换出程序的数据,而不是一次性加载所有数据。这样,操作系统可以在物理内存有限的情况下,保证重要的程序部分始终处于内存中,提升系统响应速度。

78.什么是内存分段?

内存分段(Segmentation)是一种将内存划分为多个逻辑区域的技术,每个区域可以有不同的大小和用途。它通过段表来管理段的基地址和大小,并为每个段提供独立的访问权限。分段为程序提供了灵活的内存管理和更好的保护,但也带来了外部碎片的潜在问题。分段与分页通常可以结合使用,在现代操作系统中,常常将分页和分段结合起来使用,以兼顾灵活性和效率。

79.什么是内存分页?

内存分页(Paging)是一种内存管理技术,通过将内存和虚拟地址空间分成固定大小的块,简化了内存管理,避免了碎片化问题,并为操作系统提供了虚拟内存的支持。分页系统通过页表来管理虚拟地址到物理地址的映射关系,提供了内存保护、简化的内存管理,并支持大于物理内存的虚拟内存空间。尽管分页技术有其缺点,如内存开销和内部碎片,但它仍然是现代操作系统中广泛采用的内存管理方案。

80.段式管理和页式管理会出现内存碎片吗?

段式管理将程序的内存空间分为不同的段(如代码段、数据段、栈段等),每个段可以根据程序的需求动态分配大小。在段式管理中,内存碎片主要表现为外部碎片

  • 外部碎片是指内存中存在一些大小不一的空闲区域,它们不足以容纳一个完整的段。随着程序的运行,内存中的这些空闲空间可能无法满足新的内存请求,从而导致内存的利用率下降。
  • 由于段是动态分配的且段的大小可能不一致,因此段式管理会导致外部碎片的产生。如果空闲内存块大小不适合分配给下一个请求的段,尽管系统中有足够的总内存,但实际上没有足够的连续空间来满足分配需求。

页式管理将内存划分为固定大小的页,每个页通常是4KB。内存被划分为固定大小的页框,不同的程序会占用若干个页框。页式管理中的碎片主要表现为内部碎片

  • 内部碎片是指在每个分配的页框内,如果程序的数据并没有完全填满该页框,那么剩余的未使用空间就会成为碎片。由于页的大小是固定的,通常一个页框大小为4KB,因此,如果程序分配了一个页,但实际上只用了其中的一部分空间,那么剩余的空间就变成了内部碎片。
  • 举例来说,如果程序需要存储一个2KB的数据,并且操作系统分配了一个4KB的页给该程序,那么这4KB中的2KB会被实际使用,剩下的2KB就是未被使用的空闲空间,这部分空间就是内部碎片。
  • 段式管理的碎片问题:主要是外部碎片,因为每个段的大小可能不同,如果在内存中没有足够大的连续空间来容纳一个新的段,就可能会导致内存浪费,无法分配给新的段。
  • 页式管理的碎片问题:主要是内部碎片,因为每个页的大小是固定的,如果程序的数据没有填满整个页,那么就会浪费该页内的空间。
  • 段式管理的外部碎片可以通过紧凑操作或内存压缩来解决。操作系统可以在不使用内存时,进行内存整理(压缩)来消除外部碎片,或者通过分页技术来将内存分割成固定大小的块,避免外部碎片。
  • 页式管理的内部碎片通常较难避免,但可以通过使用更小的页面来减少碎片。更小的页面能够更精细地划分内存空间,减少浪费。例如,一些现代操作系统和硬件支持更大的页面(如2MB或更大),以减少内存碎片的影响。
全部评论

相关推荐

评论
2
4
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务