操作系统面试真题

月伴飞鱼 2024-08-24 09:51:06
面试题相关
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!

什么是用户态和内核态

用户态和内核态是操作系统的两种运行状态。

  • 内核态
    • 处于内核态的 CPU 可以访问任意的数据,包括外围设备
      • 比如网卡、硬盘等,处于内核态的 CPU 可以从一个程序切换到另外一个程序
        • 并且占用 CPU 不会发生抢占情况,一般处于特权级 0 的状态我们称之为内核态。
  • 用户态
    • 处于用户态的 CPU 只能受限的访问内存,并且不允许访问外围设备
      • 用户态下的 CPU 不允许独占,也就是说 CPU 能够被其他程序获取。

那么为什么要有用户态和内核态呢?

  • 这个主要是访问能力的限制的考量,计算机中有一些比较危险的操作
    • 比如设置时钟、内存清理,这些都需要在内核态下完成
    • 如果随意进行这些操作,那你的系统得崩溃多少次。

为什么要区分用户态和内核态呢?

在操作系统中,用户态(User Mode)和内核态(Kernel Mode)是两种不同的执行状态。

这种区分主要是为了保障操作系统的稳定性,安全性以及提高效率。

以下是这种区分存在的几个主要理由:

保护操作系统及关键内核数据:

  • 在内核态,操作系统代码有权访问系统的所有资源
    • 而在用户态,应用程序的活动受到限制,不能直接访问硬件或内核数据结构
  • 所有涉及硬件操作的请求都必须通过内核代码(例如设备驱动程序)来完成。
    • 这种限制可以避免用户态的应用程序意外或恶意地破坏系统资源。

安全性:

  • 用户态和内核态的划分是操作系统进行权限管理和访问控制的主要方式
    • 它可以防止用户级应用程序越权访问和操作系统资源,提高系统的安全性。

系统稳定性:

  • 如果所有程序都在同一权限级别运行,一个程序的错误可能导致整个系统崩溃。
  • 通过将权限划分为用户态和内核态,可以有效地防止用户程序的错误影响到整个系统,提高系统的稳定性。

效率:

  • 内核态与用户态的切换会带来一定的开销,但这背后的好处是系统运行的效率和稳定性。
  • 例如,IO操作通常需要在内核态进行,以防止用户程序直接控制硬件设备
    • 这样可以防止多个程序同时对一个设备进行操作,引起混乱。

因此,划分用户态和内核态,为操作系统的运行提供了一个有序、可控和安全的环境。

谈一谈你对中断的理解?

中断是计算机系统中的一种基本操作,用于处理外部事件或内部异常。

  • 在计算机运行过程中,可能会发生各种外部或内部的事件,比如硬件故障、IO请求、定时器事件等
    • 这些事件需要在合适的时机中断正常的程序执行。

中断的作用是打断当前的程序执行流程,转而执行与中断事件相关的处理程序。

  • 处理程序执行完毕后,再返回到原来的程序继续执行。
    • 中断可以实现多任务的并发执行,提高系统的响应能力和效率。

中断可以分为硬件中断和软件中断。硬件中断是由硬件设备触发的,比如外部设备的请求、时钟中断等。

  • 软件中断是由软件程序主动触发的,比如系统调用、异常处理等。

中断和异常有什么区别?

中断(Interrupt)和异常(Exception)都是在计算机运行过程中对特殊的条件或者事件的响应

  • 但是它们之间还是有一些明确的区别的。

触发条件:

中断通常是由外部事务触发的,如用户输入、外设请求等。

  • 这些事件不一定与当前执行的程序有关。
  • 而异常通常是由程序本身在运行过程中产生的,比如除零错误、非法指令等。

处理方式:

  • 中断的处理通常是将当前的程序执行指针保存起来,然后切换到中断处理程序去执行,处理完后返回原来指针处继续执行。
    • 而异常处理则需要首先确定是不是可以恢复的错误,如果可以恢复,那么在处理完成后可以从出错的地方继续执行
      • 如果不可恢复,那么可能需要终止程序。

终止与持续:

  • 一般来说,处理完中断后,CPU会恢复执行被中断的程序,而异常可能会导致程序的终止。

预期性:

  • 中断是可以被预期且常规的行为。
  • 例如,系统可以预期硬件设备的中断,并根据这些中断进行响应。
    • 然而异常则是非预期的,它们是因为程序错误、硬件问题或其他不可预料的条件产生的。

指向性:

  • 中断指向的是特定的中断服务程序,而异常指向的是错误处理程序或者是系统。

什么是栈空间?

栈是一种后进先出(LIFO)的数据结构,也就是说最后进入的元素会被首先取出。

  • 每当一个函数被调用时,系统会为其在栈空间上分配一块内存,这个内存区域被叫做一个栈帧
  • 这个栈帧将存储这个函数需要用到的局部变量,参数,以及函数返回的地址。

当函数执行结束后,对应的栈帧就会被释放,相关的存储空间将供后续的函数调用使用。

  • 由于栈使用类似于压栈和出栈的操作,所以速度很快,但是容量有限
    • 过深的函数调用或者递归可能导致栈空间耗尽,这就是常说的栈溢出。

例如,你可能会写一段递归代码来解决问题。

每次调用这段代码的一个新实例时,都会有一个新的栈帧被创建以存储这次调用的信息。

  • 无限或过深的递归可能会导致栈空间耗尽,导致所谓的 栈溢出 错误。

什么是堆空间?

堆空间它用来存储程序运行过程中动态分配的数据。

  • 与栈空间不同,堆空间的大小不是在编译时确定的,而是在运行时通过程序的需求动态分配的。
    • 这就意味着你可以在运行时根据需要创建和销毁数据。

当程序需要一块动态内存时,程序会向操作系统发出请求。

操作系统如果同意了这个请求,会在堆上找到一块合适的空闲空间,然后将其分配给程序。

  • 程序用完这块内存后,需要显式地将其释放,否则会造成内存泄露。

举个例子,如果你需要根据用户输入创建一个数组,你可能无法预先知道数组的大小

  • 这时候你就需要在堆上动态地创建这个数组。

  • 因为在栈上创建数组需要预先知道其大小,且在函数体结束后会自动释放。

但是,堆空间的管理相对复杂。动态分配和释放内存需要更多的计算资源

  • 因为操作系统需要查找可用的空闲空间,且可能出现内存碎片问题。

另外,由于程序员通常需要在程序中显式管理堆内存

  • 所以还可能出现内存泄漏(忘记释放不再需要的内存)或者是野指针(指向已经释放的内存)等问题。

栈空间和空间堆有什么区别?各自优缺点呢?

栈空间和堆空间在编程中都被用于数据存储

  • 但它们的用途,分配方式,生命周期,甚至大小,都有显著的不同。

存储内容:

栈主要存储局部变量和函数调用的信息,比如函数的返回地址和参数。

  • 堆被用来存储动态分配的数据,例如动态数组,对象
    • 或者其他需要在程序运行中根据需要动态创建和销毁的数据。

生命周期:

  • 栈空间中的数据在定义它们的函数返回之后就会自动销毁,生命周期较短。
    • 而堆上的数据需要程序显式地创建和销毁,因此它们的生命周期可以被精确地控制
      • 但同时也使内存管理变得更加复杂。

分配方式:

  • 栈空间是由编译器自动分配和释放的,非常快速。
  • 但是,堆空间是由程序在运行时动态分配和释放的,这通常需要更多的计算资源。

大小限制:

  • 栈空间的大小通常在程序启动时就被固定,所以它的空间通常比较小。
  • 而堆的大小通由系统的可用内存来决定,所以它的空间通常比较大。

优点和缺点:

  • 栈空间的优点是管理简单,速度快。
  • 但它的缺点是空间有限,不能动态分配,只能用于存储生命周期短且大小已知的数据。

堆空间的优点是能够动态地分配大量的内存。

  • 但是它的缺点是需要手动管理,可能会引发内存泄漏或碎片,而且开销较大。

什么是物理地址?

物理地址,又称实际地址或绝对地址,是数据在计算机系统中物理内存(RAM)的实际位置或者地址。

  • 这个地址是总线或者内存控制器用来读取或者写入特定的物理内存的。

逻辑地址在经过CPU的内存管理单元(MMU)的地址转换后,变成物理地址。

  • 这个物理地址才是数据真正存储的地方。
  • 操作系统、内存管理单元以及硬件一起工作,把高级的程序抽象(逻辑地址)与底层硬件实现(物理内存)连接起来。

什么是逻辑地址?

在操作系统中,逻辑地址(有时也被称为虚拟地址),是在运行过程中的程序或进程所看到的地址。

  • 这个地址是由 CPU 生成的,并且不同于物理地址,后者是数据在主存储器RAM中的实际地址。

对于程序来说,它只需要对内存进行抽象的、逻辑的操作,不用关心具体数据在物理内存中的位置。

  • 这就是为什么操作系统需要引入逻辑地址的原因。

什么是虚拟内存?

虚拟内存是操作系统提供的一种内存管理技术,它通过将实际内存和磁盘空间组合使用

  • 给每个进程提供一个抽象的、看似连续的地址空间。

在虚拟内存中,每个进程能够访问的内存空间大于实际物理内存的容量。

  • 操作系统会根据进程的需要,将部分内存数据存储在物理内存中
    • 并将未使用的数据存储在磁盘上,以便于管理和利用。

通过使用虚拟内存,操作系统能够为每个进程提供独立的地址空间,使得进程之间相互隔离,更安全稳定。

  • 同时,虚拟内存也提供了一种内存扩展机制,使得系统能够运行更多的程序
    • 而不会因为物理内存不足而导致程序崩溃。

为什么需要虚拟内存?

虚拟内存有以下几个重要的作用和好处:

扩展可用内存:

  • 虚拟内存使得每个进程可以访问比物理内存更大的内存空间。
    • 当物理内存不足时,虚拟内存可以将一部分不常用的数据暂时存储在磁盘上,以释放出物理内存供其他进程使用。
      • 这样,系统能够同时运行更多的程序,提升了系统的整体运行能力。

进程隔离和保护:

  • 虚拟内存为每个进程提供了独立的地址空间,使得进程之间相互隔离。
    • 这样,一个进程不能直接访问其他进程的内存空间,保护了进程的数据和代码的安全性。
      • 如果一个进程出现了异常或崩溃,只会影响到该进程本身,而不会对其他进程造成影响。

简化内存管理:

  • 虚拟内存使得内存管理更加简化。
    • 操作系统可以在物理内存和磁盘之间进行数据交换,将内存调度和分配操作集中在物理内存上
      • 而不需要关心具体的物理地址。
      • 这样简化了内存分配和释放的操作,提高了内存管理的效率。

内存共享和进程通信:

  • 虚拟内存允许多个进程共享同一部分内存,这样不同的进程可以轻松地共享数据和信息
    • 实现进程间的通信和协作。
    • 例如,多个进程可以共享同一块内存区域,实现高效的数据交换和共享资源。

分页与分段有什么区别?

分页和分段都是内存管理的策略,但它们的目的和方式是不同的:

分页:

  • 分页是一种内存管理技术,它将虚拟内存空间和物理内存空间分割成固定大小的单元,我们称这个单元为
  • 分页是为了解决内存碎片的问题,因为分页可以让每一块内存空间都被有效利用。
    • 分页是透明的,也就是说这个过程对用户程序是不可见的。
    • 用户程序看到的仍然是一个连续的内存空间。

分段:

  • 对于分段,其主要目标是将程序自身的逻辑结构反映到物理存储器中去。
  • 在逻辑上,程序员根据代码的逻辑关系将程序分成大小不等的段,比如说代码段、数据段等。
    • 然后根据程序的需要,将这些段加载到内存中。
    • 分段是可见的,也就是说程序员在编写程序的时候可以看到分段的效果。

什么是进程?

在操作系统中,进程是指正在执行的程序实例。

它是计算机系统中的基本执行单位,拥有独立的内存空间和系统资源。

  • 每个进程都有自己的指令序列、数据和执行环境。

进程的创建是通过操作系统调度和管理的,当一个程序被执行时,操作系统会为其创建一个独立的进程。

  • 每个进程都有一个唯一的进程标识符(PID),用于在系统中标识和管理进程。

进程的主要特征包括:

  • 独立性:
    • 每个进程都有独立的内存空间和系统资源,不会受其他进程的影响。
  • 执行状态:
    • 进程可以处于运行、就绪、阻塞等不同的执行状态,根据进程调度算法决定执行顺序。
  • 上下文切换:
    • 由于操作系统需要在不同进程之间进行切换,进程可以通过上下文切换保存和恢复自己的执行环境。
  • 通信与同步:
    • 进程可以通过进程间通信机制实现信息的交换和共享资源,也可以通过同步机制实现协调和合作。

进程有哪些常见状态?

进程可以处于以下五种状态:

运行状态(Running):

  • 正在CPU上执行指令的进程处于运行状态。

等待状态(Waiting):

  • 也称作阻塞状态。
    • 当进程需要某些资源以继续运行(例如,等待用户输入或等待文件读取)时,它将转入等待状态。
      • 直到所需资源可用时,才会被重新放入可运行队列。

就绪状态(Ready):

  • 当进程已经准备好在CPU上运行,但由于其他进程正在CPU上运行,而暂时无法运行,此时就处于就绪状态。
    • 就绪状态的进程会被放在一个队列中,等待CPU资源。

创建状态(New):

  • 当进程刚被创建但还未被调度到运行时,它处于创建状态。

终止状态(Terminated):

  • 一个进程完成了它的全部工作,或者被其他进程杀死,或者出现异常终止时,它就处于终止状态。
    • 此时,操作系统将回收进程占用的资源并销毁该进程。

这些状态之间的转变会根据CPU调度,资源申请等情况来进行。

什么是僵尸进程?

僵尸进程(Zombie Process)指的是一个已经结束执行的子进程

  • 但其父进程尚未调用wait()waitpid()函数来获取子进程的终止状态信息。
    • 在这种情况下,子进程的进程控制块(PCB)仍然存在系统中,但没有正常退出,因此处于僵尸状态。

僵尸进程的主要原因是进程在结束执行后,父进程并没有及时处理子进程的终止状态信息。

通常,父进程会通过调用wait()waitpid()函数来等待子进程的退出,并获取其终止状态。

  • 若父进程没有这样做,子进程就会成为僵尸进程。

僵尸进程不会占用系统资源(如CPU、内存等)

  • 它们只占用一个进程ID(PID)和一些系统资源(如进程表项、部分内存等)。

  • 但如果系统中存在大量僵尸进程,可能会耗尽可用的资源。

虽然僵尸进程本身无害,但过多的僵尸进程可能是不可取的。

  • 为了避免僵尸进程的积累,父进程应适时调用wait()waitpid()来获取终止状态信息
    • 并通过kill()或终止自身来回收僵尸进程。
    • 在某些情况下,可以使用信号处理程序,如SIGCHLD信号来自动处理子进程的退出状态。

总之,僵尸进程是指在父进程没有及时回收子进程终止状态时,子进程成为已经结束但仍占用系统资源的状态。

  • 及时处理子进程的终止状态,是保持系统健康运行的重要措施。

如何僵尸进程太多,会出现什么问题?

当系统中存在太多的僵尸进程时,可能会导致以下问题:

资源浪费:

  • 僵尸进程会占用系统的一些资源(如PID、内存等)
  • 当僵尸进程过多时,会导致系统的资源浪费,降低系统的运行效率。

进程管理困难:

  • 僵尸进程会使得进程管理变得复杂。
    • 在进程终止后,其父进程需要调用wait()函数或者相关的系统调用来回收其资源
    • 如果父进程没有正确处理僵尸进程,会导致僵尸进程无法完全终止并释放资源。

进程满载:

  • 在某些情况下,如果系统产生大量的僵尸进程,可能会导致系统的进程表被填满,无法再创建新的进程来运行其他任务。

内存泄漏:

  • 僵尸进程的资源无法被回收释放,可能会导致内存泄漏问题,最终导致系统的性能降低或崩溃。

因此,及时处理僵尸进程是非常重要的,可以采取合适的方法来清理僵尸进程

  • 如使用适当的系统调用来回收资源,或者通过编写合适的代码来处理僵尸进程。

如何处理过多的僵尸进程?

处理过多僵尸进程的几种方法:

修复父进程:

  • 确保父进程正确处理子进程的终止状态,使用wait()waitpid()等系统调用来回收僵尸进程的资源。
    • 父进程在fork子进程后,应该在适当的时候使用这些系统调用来检查子进程的终止状态,并进行处理。

手动杀死僵尸进程:

  • 可以使用kill命令结合僵尸进程的PID来手动杀死僵尸进程。
  • 使用命令ps aux | grep Z来查找僵尸进程的PID,然后使用kill PID命令来杀死该进程。
  • 但这只是临时性的处理方式,仍然需要修复父进程来避免僵尸进程的产生。

编写垃圾回收程序:

  • 可以编写一个定期运行的垃圾回收程序来扫描系统中的僵尸进程,并回收它们的资源。
    • 这个程序可以使用系统调用waitpid()来检查并回收僵尸进程的资源。

修改子进程创建方式:

  • 通过修改子进程的创建方式,可以避免产生僵尸进程。
    • 例如,使用fork()后紧接着调用exec()_exit()来替换原始的进程映像
      • 并在父进程中忽略SIGCHLD信号,这样子进程终止时不会成为僵尸进程。

调整系统资源限制:

  • 适当调整系统的资源限制,如进程表大小、最大进程数等限制,以防止系统中过多的僵尸进程积累。

不同的方法可以根据实际情况进行选择和组合,以有效地处理过多的僵尸进程。

进程的地址空间里面有什么?

进程的地址空间是指操作系统为每个进程分配的虚拟内存空间,用于存储进程的代码、数据和堆栈等信息。

进程的地址空间通常分为以下几个部分:

代码段(Text Segment):

  • 也称为程序段,用于存放进程的可执行代码。
    • 该部分通常是只读的,包含了程序的指令集,如函数、循环、条件语句等。

数据段(Data Segment):

  • 用于存储全局变量、静态变量和常量等数据。
    • 数据段可以分为:
      • 初始化的数据段(Initialized Data Segment
      • 初始化的数据段(Uninitialized Data Segment)。
  • 初始化的数据段包含了已经初始化的全局变量和静态变量等数据,存储在静态存储区,通常是可读写的。
  • 未初始化的数据段,也称为BSS段(Block Started by Symbol
    • 包含了未初始化的全局变量和静态变量等数据,存储在静态存储区,通常是可读写但初始值为0。

堆(Heap):

  • 堆是动态分配的内存空间,用于存储动态分配的数据。
    • 堆空间通常由程序员通过动态内存分配函数(如malloc()、new)进行管理
      • 用于存储动态数组、链表等动态数据结构。
      • 堆的大小和生命周期由程序员显式控制,需要在不再使用时手动释放。

栈(Stack):

  • 栈用于存储函数调用时相关的信息,如局部变量、函数参数、返回地址等。
    • 栈的生命周期与函数的调用和返回有关,每当调用一个函数时
      • 在栈上会创建一个新的栈帧用于保存函数的信息,当函数返回时,栈帧会被销毁。

进程间的通信方式有哪些?各自有哪些优缺点?

进程间通信(Inter Process Communication,IPC)是一个进程与另一个进程传输和分享数据的机制。

主要有以下几种方式:

管道(Pipe):

  • 管道是最早的进程间通信机制,数据可以在父子或兄弟进程间单向流动。
    • 管道的优点是简单易用,但缺点是数据只能在有亲缘关系的进程之间传输
      • 并且是无格式的字节流,需要进程自行解析。

消息队列(Message Queue):

  • 消息队列是一种先进先出的队列结构,允许进程将消息发送到队列,并允许其他进程根据消息的优先级从队列中读取。
    • 优点是可以在无关进程间传输数据,支持数据的优先级设定。
    • 缺点是数据读写需要系统调用,消耗相对较高,复杂消息可能需要额外处理逻辑。

共享内存(Shared Memory):

  • 共享内存允许多个进程访问同一块内存空间,是最快的IPC方式。
    • 优点是无需系统调用,直接读写内存,效率较高。
    • 缺点是需要手动解决进程间的同步问题,开发难度相对较高。

信号(Signal):

  • 信号是一种简单的进程间通信方式,用来通知接收进程有某事件发生。
    • 它的优点是简单,可以异步地通知事件。
    • 缺点是信息量有限,只能传递一个数量,不能携带更复杂的信息。

套接字(Socket):

  • 套接字可以在不同机器上的进程间通信。
  • 它的优点是可以进行跨机器的通信,通用性强。
  • 缺点是开发相对复杂,数据读写需要系统调用,效率较低。

信号量(Semaphore):

  • 信号量常用于多个进程间的同步和互斥问题。

一个进程可以创建多少线程?

在理论上,一个进程可以创建的线程数目由其系统资源限制,比如内存、CPU等。

实际上,当一个进程创建大量线程时,由于每个线程都都需要一定的系统资源(例如,存储线程的上下文消息、栈空间等)

  • 所以当系统资源耗尽时,无法再创建新的线程。

从操作系统的角度讲,没有硬性的规定一个进程最多可以创建多少个线程

  • 具体的数量取决于操作系统的实现和配置。

具体来说,可用线程数量的限制因素包括:

系统资源:

  • 每个线程都需要一定的系统资源,如内存空间、栈空间、寄存器等。
    • 可用的线程数量受到可用的物理内存和虚拟内存的限制。

32位和64位系统的限制:

  • 在32位系统中,每个进程的地址空间通常被限制在2GB或3GB,其中一部分用于存储进程本身和操作系统的代码和数据。
    • 因此,可用的线程数量也会受到地址空间的限制。
    • 而在64位系统中,可用的地址空间更大,可以支持更多的线程。

线程栈大小:

  • 每个线程在运行时都需要一定大小的栈空间,用于存储局部变量、函数调用和其他运行时数据。
    • 线程栈的大小是有限的,栈空间越大,可用的线程数量就越少。

系统设定的限制:

  • 操作系统可能针对特定的应用程序或系统需求设置了线程数量的上限。
    • 这些限制可能是硬性的,也可能是可以配置或调整的。

因此,实际上一个进程可以创建的线程数量是受限制的,需要考虑到系统的资源和限制因素。

在开发应用程序时,应根据具体需求和系统资源状况合理配置和管理线程的数量。

进程的调度算法有哪些?

调度算法是指:根据系统的资源分配策略所规定的资源分配算法。

常用的调度算法有如下几个

先来先服务(FCFS,First-Come, First-Served):

  • 按照进程到达的先后顺序进行调度,先到达的进程先执行,适用于短作业时间的场景。
    • 但长作业时间的进程可能会导致等待时间较长,又称为非抢占调度算法。

最短作业优先(SJF,Shortest Job First):

  • 根据进程的执行时间,选择剩余时间最短的进程优先执行。
    • 这种算法可以减少平均等待时间,但需要准确估计每个进程的执行时间,且不适用于长作业时间的进程。

优先级调度(Priority Scheduling):

  • 为每个进程分配优先级,优先级高的进程先执行。
    • 可以根据进程的优先级动态调整调度顺序,但可能会导致低优先级的进程饥饿。

轮转调度(Round Robin):

  • 将CPU时间切片分配给每个进程,按照轮转的方式进行调度。
    • 每个进程在一个时间片内执行,如果时间片用完,则将进程放到队列尾部继续等待,适用于多任务并发执行。

多级反馈队列调度(Multilevel Feedback Queue):

  • 将进程根据优先级划分为多个队列,每个队列具有不同的时间片大小。
    • 进程根据到达时间和优先级进入对应的队列,并按照轮转调度算法执行。
    • 可根据进程的行为和执行情况调整优先级和时间片大小。

最短剩余时间优先(SRTF,Shortest Remaining Time First):

  • 类似于SJF算法,但考虑到新进程的到达时间,如果有更短剩余执行时间的进程到达,则抢占当前进程。

这些调度算法都有各自的特点和适用场景,根据不同的需求和系统环境

  • 选择适合的调度算法可以提高系统的性能和响应性。

进程终止的方式

进程终止的方式有以下几种:

正常终止:

  • 进程完成任务后,调用exit()系统调用或者main函数执行完毕,进程会自动终止。

异常终止:

  • 进程遇到致命错误,例如除零错误、内存访问错误等,操作系统会强制终止进程。

人工终止:

  • 用户通过操作系统提供的终止命令(如kill命令)来终止进程。

父进程终止:

  • 父进程终止时,所有子进程会收到一个SIGCHLD信号,子进程会被操作系统终止。

系统关机:

  • 当系统关闭时,操作系统会终止所有运行的进程。

需要注意的是,进程的终止并不是立即发生的,而是通过信号通知进程终止

  • 进程在收到终止信号后会进行相应的处理,最终终止自己。

什么是线程?

在操作系统中,线程是进程的一部分,是进程内的一个执行单元。

  • 与进程相比,线程更轻量级,多个线程可以在同一个进程中并发执行。

线程共享进程的内存空间和系统资源,每个线程有独立的程序计数器(PC)和栈空间

  • 但它们可以访问共享的数据和全局变量。

线程的主要特征包括:

  • 并发执行:
    • 多个线程可以在不同的处理器或核心上同时执行,从而实现并发性。
  • 共享内存:
    • 线程之间共享同一个进程的地址空间,可以互相访问和修改共享数据。
  • 轻量级:
    • 相对于进程来说,线程的创建、销毁和切换开销较小,执行效率更高。
  • 协作与通信:
    • 线程之间可以通过共享内存进行通信和协作,也可以使用同步机制控制线程的执行顺序。

线程切换要保存哪些上下文?

当发生线程切换时,操作系统需要保存当前线程的上下文,以便在下次线程被再次调度执行时得以恢复。

上下文主要包括以下内容:

寄存器值:

  • 这包括了通用寄存器,程序计数器(存放当前线程正在执行的指令地址)
    • 程序状态字(存放执行指令的结果的状态,如零,负,溢出等)等。

堆栈指针:

  • 每个线程有自己的函数调用栈,堆栈指针标识了当前线程在自己的栈空间中的位置。
    • 回到这个线程时,它可以恢复到正确的函数调用位置。

程序计数器:

  • 这个值标识了线程执行到哪里。
    • 当线程切换回来时,它将从这个位置继续执行。

内核栈指针:

  • 每个线程有一个内核栈,存放在内核中的数据,这个指针标识当前线程在内核内存中的位置。

线程状态:

  • 这包括了线程的优先级,信号掩码,错误码等。

虚拟内存信息:

  • 这通常包括有关线程内存管理的信息,比如页表等。

当线程切换发生时,操作系统会保存当前线程的上述上下文,加载目标线程的上下文

  • 然后将控制权转交给目标线程,这样目标线程就能接着上次的运行状态继续执行了。

值得注意的是,线程切换是有性能开销的,因为涉及到保存和加载上下文的操作

  • 所以过于频繁的线程切换可能会影响性能。

线程间的通信方式有哪些?各自有哪些优缺点?

线程间的通信方式通常利用同一个进程下线程所共享的资源来实现。

主要有以下几种方式:

锁机制(Locks):

  • 当多个线程需要访问共享资源时,可以使用锁机制来避免并发问题。
    • 一个线程在访问资源时可以锁定该资源,阻止其他线程的访问,直到该线程释放锁。
      • 锁机制简单而直接,但必须小心处理,否则可能导致死锁。

信号量(Semaphores):

  • 信号量是一个更为高级的同步机制,可以控制多个线程对共享资源的访问。
    • 信号量有一个计数器和一个等待队列组成,计数器表示可用的资源数目。
      • 优点是可以控制资源的同时访问数,缺点是使用不当也可能导致死锁。

条件变量(Condition Variables):

  • 条件变量是另一种同步机制,允许一个线程等待某个条件满足。
    • 当条件满足时,可以通知一个或多个正在等待的线程。条件变量通常与互斥锁一起使用。
      • 优点是能够实现更复杂的同步,如按顺序访问等。
      • 缺点是使用不当可能导致死锁或饥饿现象。

事件驱动(Event-driven):

  • 在事件驱动的模型中,线程之间通过等待和触发事件来进行通信。
  • 这种方式不仅适用于线程间的通信,也可以用于进程或异步输入/输出等的通信。
    • 优点是适应性强,可以应对多种不同的通信需求。
    • 缺点是需要编程模型支持,且在设计和实现上可能较为复杂。

线程本地存储(Thread-Local Storage,TLS):

  • 有些变量是线程不安全的,例如静态变量,全局变量等
    • 这些变量如果在多线程环境下共享,可能会造成不可预料的结果。
    • 为了解决这个问题,我们可以为每个线程提供一份该变量的副本,这就是线程本地存储。
      • 此方案的优点是能避免资源竞争,缺点是会增加内存的使用。

进程与线程有什么区别?

它们有以下几个主要区别:

资源占用:

  • 进程:每个进程拥有独立的内存空间和系统资源,如文件描述符、打开的文件等。
    • 进程间的通信需要使用进程间通信(IPC)机制。
  • 线程:多个线程共享同一个进程的内存空间和系统资源,线程之间可以通过共享内存进行通信。

调度和切换:

  • 进程:进程是独立的执行实体,操作系统以进程为单位进行调度,进程的切换开销相对较大。
  • 线程:线程是进程的一部分,线程的调度和切换开销较小,因为它们共享进程的上下文。

并发性和并行性:

  • 进程:多个进程可以并发执行,每个进程都有自己的地址空间,可以在多个处理器或核心上并行执行。
  • 线程:多个线程可以在同一个进程内并发执行,共享进程的地址空间,可以在同一个处理器或核心上并行执行。

用户态与内核态:

  • 进程:进程切换涉及到用户态到内核态的切换,需要较高的权限和开销。
  • 线程:线程切换只涉及用户态的切换,开销较小。

创建和销毁:

  • 进程:创建和销毁进程的开销较大,包括分配独立的内存空间、初始化数据结构等。
  • 线程:创建和销毁线程的开销相对较小,线程依赖于进程的内存和资源完成创建过程。

进程是独立的执行实体,拥有独立的内存空间和系统资源

而线程是进程内的执行单元,共享进程的内存空间和系统资源。

  • 线程的切换和通信开销较小,并发性更高。
  • 选择使用进程还是线程,取决于具体的应用需求。

什么是协程吗?和线程有什么区别?

协程(Coroutine)是一种用户级别的轻量级线程。

  • 它们的调度完全由用户控制,而不是由操作系统内核控制。
    • 与线程不同,协程的上下文切换极其快速且成本低,主要因为它所需保存和恢复的状态较少。

对于协程和线程的比较,以下四个方面:

切换开销:

  • 线程由系统内核控制,切换开销大

    协程由程序员在用户空间控制,切换开销小。

调度:

  • 线程是抢占式调度,需要操作系统来进行线程的调度切换
  • 协程是非抢占式的,由协程自身决定何时进行切换,这也是其使用复杂性的来源之一。

数据共享和同步:

  • 线程并发编程需要考虑锁等同步机制的问题
    • 而协程在同一时间只有一个运行,它对共享资源的访问不需要加锁
      • 只需要确保在协程切换的时候保存好共享资源的状态即可。

应用场景:

  • 线程适合CPU密集型任务
  • 协程适合IO密集型任务。

阻塞和非阻塞有什么区别?

阻塞是指任务在等待某个操作完成时,暂停自己的执行,并等待操作完成后再继续执行。

  • 在阻塞状态下,任务会一直等待,直到所需的资源或结果就绪。
  • 在此期间,任务不能执行其他操作。
    • 例如,当一个线程调用阻塞式IO操作时,它会被挂起,直到IO操作完成后才能继续执行。

非阻塞是指任务在等待某个操作完成时,不会暂停自己的执行,而是立即返回,继续执行其他任务。

  • 非阻塞的任务会周期性地查询所需资源或结果的状态,判断是否就绪,从而决定是否继续执行。
    • 例如,在进行非阻塞式IO操作时,任务会立即返回,并周期性地检查IO操作的状态,直到IO完成后再处理结果。

简单来说,阻塞是等待结果时暂停自己的执行

  • 非阻塞是等待结果时继续执行其他任务。

在实际应用中,阻塞和非阻塞可以用在不同的场景中。

阻塞适用于需要确保结果完整性和依赖顺序的情况,而非阻塞适用于需要提高并发性和响应性的情况。

  • 选择适合的阻塞和非阻塞方式可以提高程序的效率和性能。

同步和异步有什么区别?

同步(Synchronous)操作是在一个操作完成之前,不进行下一个操作。

这是一种阻塞调用,也就是说,进行某项操作的过程中,不得不停下来等待,直到这个操作完成。

  • 例如,当你在核对大批量的数据时,你需要等待所有数据都加载完毕才能继续进行下一项操作,这就是同步

异步(Asynchronous)操作是不需要立刻得到结果,即使未完成也可进行其它操作。

这是一种非阻塞调用,也就是说,还没得到结果,就继续做别的事情,不会因为单一操作的等待而阻塞。

  • 例如,你去网上订一张火车票,由于网站服务器繁忙,订票需要一些时间,但是你不会就一直盯着屏幕等
    • 而是可以一边浏览新闻或者查看其他信息一边等待订票结果,这就是异步操作。

这两种方式各有利弊,选择使用同步还是异步,主要取决于具体的需求和场景。

说下你对并发和并行的理解?

并发(Concurrency)是指系统能够处理多个任务的能力,这并不意味着这些任务一定会同时进行。

  • 并发的任务可能会交错进行,因此并发可以在单核CPU上实现。

这是因为CPU可以通过时间片轮转或其他任务切换策略,在各个任务之间快速切换,给人以它们在同时进行的错觉。

并行(Parallelism)则是指系统同时执行多个任务的能力。

  • 并行显然需要硬件的支持,如多核心或多处理器。

在这种情况下,多个任务确实可以在同一时间内进行。

  • 例如,现代的多核CPU可以让我们在看电影的同时进行视频编码,每一个任务在不同的处理器核心上执行,这就是并行。

总的来说,如果你有两个线程在单核心的CPU上,那么可能会通过交错执行达到并发。

如果你的电脑有多个核心或处理器,你就可以在多个核心或处理器上同时执行多个线程,这是并行

谈一谈你对操作系统的理解?

操作系统的主要作用包括:

资源管理:

  • 操作系统负责管理计算机的硬件资源,如处理器、内存、硬盘和外部设备等,以便合理地分配和利用这些资源。
  • 它通过调度算法和资源分配机制,确保每个任务或进程都能得到适当的资源。

进程管理:

  • 操作系统能同时运行多个程序,通过进程管理,它可以控制程序的执行、调度和协作,以便提高计算机的整体效率。
  • 它负责创建、终止、挂起和恢复进程,以及管理进程之间的通信与同步。

文件管理:

  • 操作系统负责管理计算机的文件系统,方便用户存储和获取数据,确保数据的安全性和完整性。
  • 它提供了文件的创建、读写、删除和重命名等操作,以及文件的权限管理和保护。

用户界面:

  • 操作系统提供了与计算机交互的用户界面,可以是命令行界面或图形用户界面(GUI),使得用户可以方便地使用计算机。
  • 用户可以通过输入指令或点击图标进行操作和访问系统功能。

错误检测和恢复:

  • 操作系统能够监测和处理软件和硬件错误,提供错误检测和恢复的机制,以保证计算机的稳定性和可靠性。
  • 它可以监测和捕获程序的异常、处理硬件故障、提供备份和恢复机制等。

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

乐观锁和悲观锁是并发控制中两种不同的策略,用于处理多个线程对共享资源的并发访问问题。

它们的区别如下:

悲观锁(Pessimistic Locking):

  • 悲观锁的策略是在访问共享资源之前,假设会发生冲突并进行保护。
    • 在悲观锁机制下,如果一个线程要访问共享资源,它会假设其他线程可能会对该资源进行修改
      • 因此会将资源加锁,直到完成操作后才会释放锁。

乐观锁(Optimistic Locking):

  • 乐观锁的策略是在访问共享资源时不加锁,而是在更新操作时进行冲突检测。
    • 线程在读取共享资源时,不会对其加锁,而是记录下读取时的版本号或其他标识信息。
    • 在提交更新操作时,会再次检查共享资源是否被其他线程修改过。
      • 如果没有冲突,就执行更新操作
      • 如果有冲突,就放弃当前更新并重新尝试。

性能比较:

  • 悲观锁会在访问共享资源之前就加锁,即使没有实际的冲突,也会造成性能的损失。
    • 而乐观锁避免了大部分的锁竞争,提高了并发性能。
    • 但是,如果冲突频繁发生,乐观锁需要不断地进行重试,可能会导致性能下降。

悲观锁假设会有冲突发生,因此在访问共享资源前进行加锁

而乐观锁假设不会有冲突发生,在更新操作时进行冲突检测。

  • 选择哪种锁策略应根据具体场景和需求来决定。

什么是死锁?

死锁(Deadlock)是指在多任务环境下:

  • 当两个或更多的任务各自拥有一个资源并且等待获取另一个任务持有的资源时,就会发生的一种状态。

涉及的任务无法继续执行,因为每个任务都在等待其他任务释放资源,但是没有任务会释放它的资源,因为它们都在等待。

  • 这就形成了一个循环的等待状态,从而导致了死锁。

死锁的四个必要条件:

互斥条件:

  • 一个资源只能由一个任务拥有,在资源释放之前任何其他任务都无法请求到。

占有并等待:

  • 一个任务持有至少一个资源,但又申请新的资源,而新资源正被别的任务持有
    • 所以申请任务阻塞,但又对自己已获得的资源保持不放。

不可抢占:

  • 别的任务不能把已获得的资源从任务中强行回收,资源只能由获得它的任务自行释放。

循环等待:

  • 存在一种可能,即任务之间形成一种任务-资源的环形链,链中每个任务都占有下一个任务所需的资源。

只要这四个条件中的任意一个得不到满足,就不会发生死锁。

操作系统的设计者通过算法来破坏这些条件从而避免死锁。

  • 例如,可以采用资源按顺序分配策略来避免循环等待,通过设置资源申请超时来避免无限期的资源等待等方法。

解决死锁的基本方法?

解决死锁的主要方法可以归结为四类:

  • 预防、避免、检测和恢复。

死锁预防:

  • 预防策略的主要思想是破坏造成死锁的四个必要条件中的至少一个。
    • 例如,可以通过资源互斥访问的限制或者一次性请求所有所需资源的方式来阻止占有并等待的条件
      • 或者在任务请求资源时先检查这是否会引起循环等待,等等。
    • 这种策略的问题是可能会导致资源的低效使用。

死锁避免:

  • 死锁避免采取了一种更加精细的策略。
    • 它需要保持关于系统当前的哪些资源被哪些任务占用、哪些资源是空闲的、哪些任务正在等待资源等的信息。
    • 然后,操作系统在每次有资源请求时,都会先检查是否授予该资源可能导致系统进入不安全状态(即可能死锁)
      • 如果是,就不给任务分配资源。

死锁检测和恢复:

  • 有时,我们可能认为死锁可能发生得比较少,或者避免死锁的成本比较高
    • 所以我们愿意冒险,但是当检测到死锁时需要有一种恢复办法。
    • 这就需要一种检测死锁的算法。
    • 当检测到死锁后,通常的做法是中止一些任务或抢占一些资源,以解除死锁状态。

忽略死锁:

  • 这是一种鸵鸟算法–即忽略问题的存在。
    • 这种做法假定死锁很少发生,即使发生了,系统崩溃或重启可能对用户影响更小。
    • 尽管这种做法并不总是有用,但是在某些具体情况下
      • 特别是在一些非关键的用户交互式系统中,这可能是一种实用的方法。

总的来说,避免和预防死锁是一种权衡,需要在资源的有效利用和系统稳定性之间做出平衡。

在实际系统中,可能会使用多种策略与技术相结合的方式来处理死锁。

怎么解除死锁?

一旦死锁已经发生,解除死锁可能会涉及到比较复杂的操作。

下面介绍几种解除已发生死锁的方法:

资源抢占:

  • 选择一些进程,并强制终止它们或抢占它们占有的资源,然后将这些资源分配给其他进程。
  • 这种方法需要谨慎选择终止或抢占的进程,以及决策哪些资源应该被抢占、如何选择抢占的顺序等。

回滚(Rollback):

  • 回滚是将一部分进程的状态和操作撤销到先前的状态,通过释放资源来解除死锁。
    • 回滚涉及到保存和回复进程状态的机制,需要合理地决定回滚的程度和方式,以及如何避免进一步的死锁发生。

进程终止:

  • 选择一些死锁的进程,将它们终止并释放它们占用的资源,以解除死锁。
    • 终止进程会导致数据丢失或系统服务中断,因此需要权衡决策。

页面置换算法有哪些?

页面置换算法是操作系统中用来管理内存的方法,它决定了当内存已经满了

  • 我们应该置换出哪个页面来为新的页面提供空间。

以下是一些常见的页面置换算法:

最佳页面置换 (OPT, Optimal Page Replacement)

  • 这个算法会选择是否使用最少的页面。
  • 实际上,它通常是一种理论算法,因为要了解一个页面在未来将被多少次引用,这是非常困难的。

最近最少使用 (LRU, Least Recently Used):

  • LRU 置换掉最近最少被使用的页面。
  • 这种算法假设如果一个页面在最近一段时间没有被使用,那么在未来一段时间中也不太可能被使用,这个也是最常用的算法。

先进先出(FIFO):

  • FIFO算法按照页进入内存的时间先后,选择最早进入的页进行淘汰。
  • 这个算法易于理解和实现,但可能会出现 Belady现象,也称为FIFO异常
    • 也就是当内存页帧数增加时反而导致页面错误率增加。

CLOCK(时钟):

  • 这是一种改善版的FIFO算法,它设置了一个循环链表装载页面,有一个指针指向最旧的页面
    • 每次置换从这个指针开始搜索,若此位置页面未被访问,则置换,否则取消访问位,并前移此指针。
支付宝打赏 微信打赏

如果文章对你有帮助,欢迎点击上方按钮打赏作者!