深度思考操作系统面经

1 堆和栈的区别:(如果记的不太清楚,可以类比jvm中的堆和栈的区别,大差不差)

  • 存储位置:堆是在计算机内存中动态分配的区域,而栈是在计算机内存中由操作系统自动分配和管理的区域。
  • 管理方式:堆中的内存由程序员管理和释放,而栈中的内存由操作系统自动管理。
  • 生命周期:堆中的对象生命周期由程序员控制,可以跨函数调用存在;而栈中的对象生命周期只在函数调用期间存在。
  • 访问速度:栈的访问速度比堆更快,因为栈是基于LIFO (后进先出) 方式访问的。
  • 大小限制:堆的大小受到计算机系统的物理内存限制,而栈的大小受到操作系统限制。
  • 存储内容:栈主要存储函数的局部变量和函数调用信息,而堆主要存储动态分配的对象和数据。

1.1 栈的访问速度比堆更快,因为栈是基于LIFO (后进先出) 方式访问的。为什么

这个说法是一个简化的描述,实际上栈访问速度比堆更快的原因并不仅仅是因为其LIFO特性。栈和堆之间速度差异的原因主要是:

  1. 内存分配方式:堆内存是动态分配的,这意味着每次分配都可能需要搜索足够大的空闲空间来满足请求,而且在释放时可能还需要处理内存碎片问题。相对地,栈内存的分配仅仅是移动栈指针,这是一个固定的开销。

  2. 空间局部性:栈用于存储局部变量和函数调用信息。局部变量通常是函数中频繁访问的数据。由于它们存储在栈上,并且栈是连续的内存区域,这使得CPU的缓存更容易高效地缓存这部分数据。而堆上的对象可能散布在不同的内存区域,导致缓存效率不如栈高。

  3. 管理开销:堆内存管理涉及到分配和释放的开销,包括可能的内存碎片处理、垃圾回收(在一些语言中)等。相比之下,栈内存的管理简单得多,仅涉及调整栈指针。

  4. 预测性:由于栈是LIFO结构,CPU可以更容易地预测数据的存储位置,这有助于优化。

  5. 同步和并发:在多线程环境中,堆是所有线程共享的资源,所以堆上的操作可能需要额外的同步机制以避免数据竞争。而每个线程都有自己的栈,所以栈上的操作不需要这样的开销。

总的来说,栈的速度之所以比堆快,是因为它有简单的管理方式、良好的空间局部性和较少的并发管理开销。但这并不意味着栈总是比堆更好——两者各有用途和优点。

2 OS的锁

2.1 在操作系统中,锁是用于控制多个进程或线程访问共享资源的基本工具。以下是几种典型的锁及其描述:

  1. 互斥锁 (Mutex Lock):

    • 互斥锁保证任何时候只有一个线程可以持有该锁,从而保证对共享资源的独占访问。
    • 用于保护临界区,防止同一时间有多个线程进入临界区。
  2. 读写锁 (Read-Write Lock):

    • 允许多个线程同时读共享资源,但在写入资源时只允许一个线程。
    • 当某个线程持有写锁时,其他线程不能获取读锁或写锁。
  3. 自旋锁 (Spinlock):

    • 当线程尝试获取自旋锁并失败时,它会不断循环尝试获取锁,而不是进入睡眠状态。这在锁被持有时间较短的情况下是有效的,因为线程可以快速获取锁而不用等待。
    • 适用于锁持有时间短和线程不想在获取锁时睡眠的场景。
  4. 信号量 (Semaphore):

    • 是一个更为通用的同步工具,可以看作是可以持有多个资源的锁。
    • 信号量可以限制对资源的并发访问数,而不仅仅是一个。
  5. 条件变量 (Condition Variable):

    • 常与互斥锁结合使用,允许线程等待某个条件为真以获取锁。
    • 当某个条件不满足时,线程可以被放到休眠状态,直到另一个线程更改了条件并唤醒等待的线程。
  6. 屏障 (Barrier):

    • 用于同步一组线程,使它们在某个点上达到共同的执行点。
    • 当所有线程都到达屏障时,它们都会被释放继续执行。
  7. 读者-写者锁 (Reader-Writer Lock):

    • 类似于读写锁,但更强调“读者优先”或“写者优先”的策略。
    • “读者优先”允许多个读者并发访问资源,直到一个写者来到。而“写者优先”会让写者有更高的优先级。
  8. 递归锁 (Recursive Lock):

    • 允许一个线程多次获取同一个锁,而不会造成死锁。
    • 通常用于需要多次进入临界区的复杂函数或方法。

这些锁的实现和语义可能因操作系统和具体应用而异。选择合适的锁类型取决于具体的同步需求和使用场景。

OS在各个锁上是如何实现的(大概的实现)

在操作系统中,锁的实现通常涉及硬件级的支持、系统调用和高效的数据结构设计。以下是几种常见的锁及其基本实现原理:

  1. 互斥锁 (Mutex Lock)

    • 基本实现: 使用原子操作,如Test-and-SetCompare-and-Swap。这些原子操作是由硬件直接支持的。
    • 当线程尝试获取锁时,它使用原子操作检查锁的状态。如果锁是空闲的,则线程获取锁;否则,线程可能被阻塞或自旋。
  2. 读写锁 (Read-Write Lock)

    • 基本实现: 包含两部分:一个读锁计数器和一个互斥锁。互斥锁用于写操作,读锁计数器用于跟踪当前的读线程数量。
    • 读取时,读锁计数器增加;写入时,首先要获得互斥锁。
  3. 自旋锁 (Spinlock)

    • 基本实现: 当锁被占用时,线程会在一个循环中不断尝试获取锁,而不进入休眠状态。
    • 也是基于原子操作实现的,如Test-and-Set
  4. 信号量 (Semaphore)

    • 基本实现: 通常包含一个整数值和一个等待队列。整数值代表可用资源的数量。
    • 当线程尝试获得资源而资源不足时,线程会被放入等待队列。当其他线程释放资源时,等待队列中的线程可能会被唤醒。
  5. 条件变量 (Condition Variable)

    • 基本实现: 与互斥锁结合使用。条件变量通常包含一个等待队列。
    • 当某个条件不满足时,线程会进入等待状态。当条件变为真时,线程会被唤醒。
  6. 屏障 (Barrier)

    • 基本实现: 屏障通常包含一个计数器和一个等待队列。计数器跟踪已到达屏障的线程数。
    • 当所有线程都到达屏障时,它们都会被释放继续执行。
  7. 读者-写者锁 (Reader-Writer Lock)

    • 基本实现: 与读写锁类似,但可能包括其他策略,如优先权策略,来决定何时允许读或写操作。
  8. 递归锁 (Recursive Lock)

    • 基本实现: 与互斥锁类似,但它还跟踪锁的拥有者和锁的持有计数。
    • 当当前持有锁的线程再次尝试获取锁时,持有计数增加而不是被阻塞。

这些锁的实现会涉及到操作系统的调度策略、上下文切换和内存管理。锁的选择和使用也会受到程序的同步模式、性能需求和应用场景的影响。

猜你喜欢

转载自blog.csdn.net/yxg520s/article/details/132159207