操作系统八股总结
- 操作系统的四大功能:进程控制,内存管理,设备管理,文件管理
- 进程的定义:并发程序的执行,进程的同步与互斥
- ==进程的状态:创建,终止,就绪,运行,阻塞,挂起,激活以及相互转换关系==
- 进程同步问题:生产者消费者模型,信号量机制
- 进程的调度与死锁,死锁条件
- ==如何避免死锁==
- 存储器管理:只读存储器,随机存储器,相对地址和绝对地址
- 速度问题:CPU>寄存器>缓存>主存>磁盘缓存>硬盘
- 程序的装入和链接(动态装入,静态装入,重定位)
- 内存空间划分算法:首次适应;最佳适应;最坏适应
- ==段式存储和页式存储,以及段页式存储的优缺点,原理==
- ==页面置换算法(LRU,LFU,FIFO这些)==
- ==进程与线程的区别和联系==
- 多线程编程安全性
- ==线程池,线程的创建,同步,安全问题==
- 信号量机制
操作系统的四大功能:进程控制,内存管理,设备管理,文件管理
进程控制涉及到对正在执行的程序实例的管理。操作系统负责创建、运行和终止进程,同时通过调度算法决定哪一个进程可以在何时获得处理器资源。这种管理不仅要保证多个进程能够并发执行,还需要确保它们之间能够有效地进行同步与互斥,以避免因资源冲突而造成的数据不一致性。
内存管理则关注于如何合理分配和回收计算机内存。操作系统需要为每个进程提供独立的地址空间,同时确保不同进程之间的内存隔离,以防止相互干扰。此外,通过引入虚拟内存的概念,操作系统能够扩展可用内存,提高系统的整体性能。
设备管理是操作系统另一个关键功能,它涉及对系统中各种输入输出设备的控制和协调。操作系统通过设备驱动程序为应用程序提供统一的接口,使得不同类型的硬件能够被高效地访问。同时,设备管理还包括对设备的调度与错误处理,以保证设备的高效利用和系统的稳定性。
文件管理是操作系统对数据文件的组织与维护。操作系统管理文件的创建、删除、读写等操作,并通过文件系统提供目录结构,使用户和程序能够方便地查找和访问文件。此外,文件管理还需要考虑数据的安全性和完整性,确保文件在存储和传输过程中的保护。
进程的定义:并发程序的执行,进程的同步与互斥
进程的定义
进程可以被视为正在运行的程序的实例,它是操作系统进行资源管理和调度的基本单位。每个进程都有其独立的地址空间、代码、数据以及执行状态。
并发程序的执行
现代计算机系统通常需要同时处理多个进程,这种特性称为并发执行。并发使得多个进程能够在同一时间段内交替执行,尽管它们可能并不在物理上同时运行。这种机制充分利用了 CPU 的空闲时间,从而提高了系统的整体效率和响应能力,让用户能够流畅地使用多个应用程序。
进程的同步与互斥
在多个进程共同访问共享资源时。为了确保这些进程能够安全有效地协作,进程之间必须进行同步。这意味着,某些进程在访问关键资源之前需要等待其他进程完成其操作,以避免数据的不一致性。例如,当两个进程同时尝试写入同一个文件时,若缺乏适当的控制,最终结果可能会出现混乱。因此,操作系统提供了多种同步机制,如信号量和互斥锁,以协调对共享资源的访问。
互斥确保同一时间只有一个进程能够访问特定的共享资源。通过互斥锁等机制,操作系统能够有效地防止多个进程同时修改资源,避免了竞态条件的发生,从而保护了数据的一致性和完整性。
进程的状态:创建,终止,就绪,运行,阻塞,挂起,激活以及相互转换关系
进程的生命周期从它被创建时开始。当系统接收到一个新的程序执行请求时,会为该程序创建一个新的进程,并为其分配必要的资源(如内存空间、文件描述符等)。此时,进程处于就绪态,意味着它已准备好执行,但尚未获得CPU时间片。
随着系统调度器的运行,它会从就绪队列中选择一个进程来执行。被选中的进程会从其就绪态转换为运行态,并开始占用CPU执行其程序代码。这是进程生命周期中最活跃的阶段,也是进程执行实际工作的阶段。
然而,在运行过程中,进程可能会遇到需要等待某个事件发生的情况(如等待I/O操作完成、等待信号量释放等)。此时,进程会主动调用阻塞原语,将自己从运行态转换为阻塞态,并释放CPU资源。在阻塞态下,进程暂停执行并等待所等待的事件发生。
一旦进程所等待的事件发生,操作系统会将其从阻塞态唤醒,并将其状态转换回就绪态。此时,进程再次具备了执行的条件,但需要等待CPU的调度。
随着进程的执行完毕或因为某种原因(如错误、资源不足等)而无法继续执行时,它会进入终止态。在终止态下,进程会释放其占用的所有资源,并从系统中消失。
虽然挂起和激活不是进程生命周期中的基本状态,但它们是进程管理中的重要概念。挂起是指将进程从内存中暂时移出并保存到外存中的过程,这通常是因为内存资源不足、系统需要优化性能或用户/操作系统的特定需求。挂起可以发生在进程的任何状态(除了终止态)下。激活则是挂起状态的逆过程,即将进程从外存中重新调入内存,并恢复其执行。
进程同步问题:生产者消费者模型,信号量机制
在生产者消费者模型中,有两个角色:生产者和消费者。生产者负责生成数据并将其放入缓冲区,而消费者则从缓冲区中取出数据进行处理。由于缓冲区的大小是有限的,因此必须确保生产者在缓冲区已满时等待,而消费者在缓冲区为空时也需要等待。这就引发了对进程同步的需求,以防止出现竞争条件,比如生产者尝试在满的缓冲区插入数据,或消费者尝试从空的缓冲区提取数据。
为了解决这个问题,信号量机制作为一种常用的同步工具被广泛应用。信号量是一种整型变量,用于控制对共享资源的访问。它可以分为两类:计数信号量和二进制信号量。计数信号量用于表示可用资源的数量,而二进制信号量则类似于互斥锁,只允许一个进程访问资源。
在生产者消费者模型中,可以使用两个信号量:一个用于表示缓冲区中可用的产品数量,另一个用于表示缓冲区中的空闲空间数量。当生产者生产一个产品时,它会减少空闲空间的信号量并增加可用产品的信号量;相反,消费者在消费一个产品时会减少可用产品的信号量并增加空闲空间的信号量。这样,通过适当的信号量操作,生产者和消费者能够有效地协调彼此的执行,从而保证缓冲区的正确使用和数据的一致性。
进程的调度与死锁,死锁条件
进程调度是操作系统管理和分配 CPU 资源的重要机制,其目的是决定哪一个进程在何时获得执行权,以实现高效的多任务处理。根据不同的调度策略,操作系统可以采用先来先服务、最短作业优先、轮转时间片等不同方法来优化系统性能和响应时间。有效的进程调度能确保系统资源得到合理利用,同时提升用户体验。
在多进程环境中,调度可能会导致进程之间的竞争,从而引发死锁问题。死锁是指两个或多个进程在执行过程中,因为相互等待对方释放资源而无法继续执行的状态。这通常发生在进程对共享资源的请求与占用之间存在循环依赖的情况下。
死锁发生的条件有四个,称为死锁的必要条件:
-
互斥条件:至少有一个资源必须处于非共享模式,即某个资源只能被一个进程占用。如果另一个进程请求该资源,它必须等待。
-
占有且等待条件:一个进程已经持有了至少一个资源,并且正在等待获取其他资源。
-
不剥夺条件:已经分配给一个进程的资源在此进程完成之前不能被强行剥夺,只有在该进程释放这些资源后,其他进程才能使用。
-
循环等待条件:存在一种进程资源的循环链,其中每个进程都在等待下一个进程所持有的资源。
当上述四个条件同时满足时,就可能发生死锁。因此,通过设计合理的调度策略和资源分配机制,操作系统可以尽量避免死锁的产生,比如通过资源分配图、银行家算法等方法进行死锁检测与预防。
如何避免死锁
为了避免死锁,可以采取以下几种策略:
-
破坏死锁条件:
- 互斥条件:尽量减少使用互斥资源,或使用资源共享。
- 占有且等待:要求进程在请求新资源时,必须首先释放当前持有的资源,或者在开始执行前一次性请求所有需要的资源。
- 不剥夺条件:允许操作系统强制剥夺资源,即在必要时可以从正在运行的进程中收回资源。
- 循环等待:对资源进行排序,确保进程按照一定的顺序请求资源,从而避免形成循环。
-
资源分配算法:采用如银行家算法等动态分配资源的方法,通过安全性检查,在分配资源之前确保不会导致死锁。
-
死锁检测与恢复:周期性地检测系统中是否存在死锁。如果检测到死锁,可以通过杀死某些进程或强制释放资源来恢复系统。
-
预防策略:在设计阶段,将死锁考虑在内,设计算法和软件系统以最大限度降低死锁发生的可能性。
通过上述措施,可以有效降低和预防进程死锁的发生,提高系统的稳定性和效率。
存储器管理:只读存储器,随机存储器,相对地址和绝对地址
在计算机系统中,存储器管理是确保各个程序高效运行的重要机制。存储器主要分为两种类型:只读存储器(ROM)和随机存储器(RAM)。
只读存储器是一种特殊的存储设备,通常用于存放固件,比如计算机开机时所需的启动程序。这种存储器在制造时就被写入了数据,正常情况下无法再进行修改或只能有限度地修改。因此,它的数据在断电后依然能保持完好,这使得它非常适合用于保存关键的系统信息和指令。
与此相对的是随机存储器,它是一种能够在使用过程中不断读写的内存。随机存储器分为动态随机存取存储器(DRAM)和静态随机存取存储器(SRAM),它们都是易失性的存储器,也就是说,一旦电源断开,里面的数据将会消失。随机存储器的速度较快,适合用来存储正在处理的数据和程序代码,因而在计算机运行时起着至关重要的作用。
在计算机进行数据访问时,涉及到两种地址概念:绝对地址和相对地址。绝对地址是指计算机内存中特定位置的唯一标识符,能直接用于访问某一块内存。它就像是每个房间都有自己的门牌号,明确指向一个确切的地方。而相对地址则是以某个基准点为参考,表示与该基准点的距离。这种方式更灵活,可以适应程序在不同内存位置加载时的需求,使得程序的设计更加高效、便捷。
速度问题:CPU>寄存器>缓存>主存>磁盘缓存>硬盘
不同存储层次之间存在显著的速度差异,这主要与它们的物理特性、技术实现和设计目的有关
-
CPU:作为计算机的核心处理单元,CPU 是执行所有指令的地方。其速度非常快,通常在纳秒级(10^-9秒),这是因为 CPU 内部的电路和逻辑门能迅速进行运算和控制操作。CPU 的设计是为了优化执行效率,能够快速响应各种计算任务。
-
寄存器:寄存器是 CPU 内部的高速存储单元,其访问速度极快,因为它们位于处理器内部,与处理器的其他组件对接紧密。这种直接连接消除了数据传输过程中可能出现的延迟,因此寄存器用于存放当前正在处理的数据和指令。
-
缓存:缓存(Cache)是一种较小但速度非常快的存储空间,位于 CPU 和主存之间。缓存的访问速度快于主存(RAM),因为它使用更高级的电路设计和更少的访问延迟。缓存存储常用的数据和指令,以减少 CPU 访问主存的次数,从而提高整体性能。由于缓存容量有限,其设计侧重于存储最近使用或将要使用的数据,这种局部性原理(时间局部性和空间局部性)大幅提升了数据读取速度。
-
主存(RAM):主存属于易失性存储器,虽然其速度比硬盘快,但毕竟需要通过一定的总线和接口与 CPU 通信。主存的访问时间通常在微秒级(10^-6秒),慢于寄存器和缓存,主要是因为其存储介质(如 DRAM)需要时间来充电和放电,同时涉及更多的电气路径和控制逻辑。
-
磁盘缓存:磁盘缓存是在硬盘和主存之间设置的一块高速缓存区域,旨在加速对硬盘数据的访问。其访问速度比硬盘快得多,通常在微秒级。这种速度差异源于磁盘缓存使用的是 RAM 技术,而不是硬盘的机械读取机制。磁盘缓存会存储从硬盘中读取的数据,以便 CPU 可以更快速地访问。
-
硬盘:硬盘是计算机的持久存储设备,用于长期保存数据。其访问速度最慢,通常在毫秒级(10^-3秒)。这主要是由于硬盘是机械装置,读写头必须移动到正确的位置以访问数据,且存取速度受到旋转速度、寻道时间等因素的影响。此外,硬盘采用的磁性存储介质也使得读取和写入操作比较耗时。
各种存储层次因其物理特性、技术实现和设计目的的不同,导致了显著的速度差异。操作系统利用这些差异,通过有效管理和调度来优化数据访问和提高整体系统性能。
程序的装入和链接(动态装入,静态装入,重定位)
静态装入:在程序编译时将全部代码和数据装入内存,执行效率高,但缺乏灵活性。
动态装入:在程序运行时根据需要装入部分代码和数据,节省内存空间,灵活性强,但可能增加性能开销。
重定位:将程序地址转换为实际内存地址,支持程序在不同内存位置的加载,简化了开发和管理过程。
1. 静态装入
静态装入是在程序编译时将所有必要的代码和数据直接加载到内存中的一种方式。在这一过程中,可执行文件被完整地加载到特定的内存地址,操作系统在程序开始执行之前已经完成了所有的链接和装入工作。这种方法的优点是执行效率高,因为程序在运行时不需要进行额外的链接或加载。然而,它的缺点是缺乏灵活性,如果程序需要修改或更新,就需要重新编译和装入整个程序。
2. 动态装入
动态装入是在程序运行时根据需要将代码和数据加载到内存中的一种方式。在这种情况下,只有程序执行所需的部分代码会被加载,而不是整个程序。这使得程序可以节省内存空间,并允许多个程序共享相同的库。此外,动态装入支持模块化编程,可以根据需求动态链接库或模块,从而提高了灵活性和可扩展性。然而,动态装入可能导致一定的性能开销,因为在运行过程中会有额外的链接和加载操作。
3. 重定位
重定位是一个与装入相关的过程,涉及将程序中指定的地址(如变量和函数地址)转换为实际内存地址。静态重定位是在编译时确定内存地址,而动态重定位则在程序运行时由操作系统负责调整地址。重定位允许程序在不同的内存位置加载,这样可以有效利用内存,提高程序的灵活性。当使用重定位时,程序员不必关心其代码在内存中的确切位置,这大大简化了软件开发和管理。
这三者共同作用于操作系统的内存管理,使得程序能够高效、灵活地运行。
内存空间划分算法:首次适应;最佳适应;最坏适应
内存空间划分算法是一种用于动态分配内存的方法。
首次适应算法从内存的开始位置出发,寻找第一个足够大的空闲区块来满足进程的内存需求。这种方法简单且高效,因为它能迅速找到合适的内存区域。然而,频繁的内存分配和释放可能导致碎片化。
最佳适应算法会遍历所有空闲区块,选取最小的那个能够满足请求的区域。这种方式旨在最大限度地减少剩余空间,从而降低内存碎片的产生。虽然这种算法在理论上有助于提高内存利用率,但由于查找过程需要遍历整个列表,因此效率较低。
最坏适应算法选择最大的空闲区块进行分配,期望剩余的内存块依然足够大,以便满足后续的需求。这种方法的目的是避免大块内存的迅速消耗,尽管从实际效果来看,它并不能有效减少内存碎片,并且同样面临查找时间长的问题。
段式存储和页式存储,以及段页式存储的优缺点,原理
段式存储
段式存储是一种将程序和数据按照逻辑结构划分为多个段的内存管理方式。在这种方式中,每个段代表程序中的一个功能模块,例如一个函数或数组。每个段都有一个基地址和长度,CPU通过段号和段内偏移量来访问具体的内存位置。这种方法的优点在于它与程序的逻辑结构相符,非常自然和易于理解。在段式存储中,可以实现不同进程之间的代码共享,同时保护各自的数据和代码。然而,由于段的大小不固定,可能导致外部碎片的产生。此外,维护段表也需要占用一定的存储空间,这增加了系统的开销。
页式存储
页式存储是另一种内存管理方式,它将物理内存和逻辑地址空间都划分为固定大小的单元,称为页和页框。逻辑地址由页号和页内偏移组成,操作系统使用页表来实现逻辑页到物理页框的映射。这种方法的一个显著优点是消除了外部碎片的问题,因为所有的页都是固定大小的,只会存在少量的内部碎片。此外,页式存储允许灵活的内存分配和释放,使得系统能够高效地利用可用内存。然而,页式存储的缺点在于,为每个进程维护页表会占用额外的内存,并且随着程序规模的增大,管理页表的复杂性也随之增加。
段页式存储
段页式存储结合了段式存储和页式存储的优点,采用分层结构对内存进行管理。在这种模式下,逻辑地址空间被划分为多个段,每个段又可以进一步划分成多个固定大小的页。这样做不仅保留了段式存储的逻辑组织优势,还利用页式存储消除了外部碎片的问题,同时减少了内部碎片的影响。段页式存储支持模块化编程,便于代码共享和资源保护,但其实现的复杂性明显增加。系统需要同时维护段表和页表,这会带来更大的内存开销。因此,虽然段页式存储在很多方面表现出色,但在性能和管理上也提出了更高的要求。
页面置换算法(LRU,LFU,FIFO这些)
LRU(Least Recently Used,最近最少使用)
LRU算法基于一个假设,即如果某个页面最近被访问过,那么它在不久的将来可能会再次被访问。因此,LRU选择那些最长时间未被访问的页面进行替换。这通常通过维护一个页面访问的时间戳或引用计数来实现。LRU能够较好地适应实际工作负载,但其实现较为复杂,因为需要频繁更新页面的使用状态。
LFU(Least Frequently Used,最不常用)
LFU算法则是基于页面被访问的频率进行置换。它认为,如果某个页面在过去一段时间内被访问的次数最少,那么它在未来被访问的概率也低。因此,LFU会选择那些访问次数最少的页面进行替换。LFU可以有效地处理一些特定模式的访问,但在频繁变化的访问模式下可能表现不佳,并且同样存在实现复杂度高的问题。
FIFO(First-In-First-Out,先进先出)
FIFO算法是一种简单的页面置换策略,它按照页面进入内存的顺序进行替换。当需要替换页面时,FIFO选择最早进入内存的页面进行替换。这种方法实现简单,但并不总能保证性能,因为它无法根据页面的使用情况做出智能决策。某些情况下,FIFO可能会导致"Belady’s anomaly",即增加内存页数反而导致缺页率上升。
进程与线程的区别和联系
进程和线程是操作系统中用于实现并发和多任务处理的基本概念,二者有着重要的区别和联系。
区别
-
定义:进程是系统分配资源的基本单位,是程序在执行过程中所创建的一个实例,具有独立的地址空间;而线程是进程内的一个执行单元,同一进程内的多个线程共享进程的资源,如内存和文件句柄。
-
资源占用:进程拥有自己的内存空间、数据段和系统资源。线程则不拥有独立的内存空间,多个线程可以共享同一进程的内存和资源,这使得线程之间的通信更为高效。
-
调度与开销:进程切换需要保存和加载完整的进程上下文,开销相对较大;而线程切换只需保存和加载线程相关的状态,开销较小,因此线程的创建和销毁比进程更快。
-
并发性:进程间通信(IPC)相对复杂,因为它们拥有独立的地址空间;而同一进程中的线程可以通过共享内存等方式进行快速通信,但也需要考虑同步问题以避免竞争条件。
联系
-
组成关系:线程是进程的一个组成部分,一个进程至少有一个线程(主线程),而可以包含多个线程。进程为线程提供了运行的环境。
-
协同工作:进程和线程都是为了实现多任务和并发执行,进程通过内部的多个线程来提高应用程序的响应能力和处理性能。
进程是资源分配的基本单位,而线程是进程内部的执行单位。两者在实现并行计算时各有优势,能够根据具体需求选择使用。
多线程编程安全性
多线程编程安全性是指在多个线程并发执行时,确保数据完整性和系统稳定性的能力。随着线程的并发运行,可能会出现以下问题:
- 数据竞争:多个线程同时访问和修改共享数据,导致数据不一致。
- 死锁:两个或多个线程互相等待对方释放资源,造成所有相关线程无法继续执行。
- 活锁:线程持续响应彼此的状态变化,但无法推进进度,导致效率低下。
为确保多线程程序的安全性,可以采取一些策略,包括使用互斥锁、信号量和条件变量来同步线程访问共享资源。此外,设计时应考虑资源申请顺序及避免过度锁定,以减少死锁风险。合理管理线程之间的交互可以提升程序性能,确保数据的一致性和安全性。
线程池,线程的创建,同步,安全问题
线程池
线程池是一种管理和复用多个线程的机制。它预先创建一定数量的线程,并将这些线程放入一个池中,以便在需要时快速分配和重用。这样可以减少频繁创建和销毁线程的开销,提高系统性能。线程池能够有效控制并发的线程数,避免线程过多导致的资源竞争和上下文切换。
线程的创建
在线程编程中,可以通过不同的方式创建线程,常见的方法包括:
- 继承线程类:通过创建一个继承自线程类的子类,并重写其
run()
方法。 - 实现 Runnable 接口:定义一个实现了
Runnable
接口的类,并将其实例传递给线程对象来创建线程。
创建线程时,需要注意合理使用线程资源,避免过多的线程造成系统负担。
同步
在多线程环境中,同步是确保多个线程安全地访问共享资源的手段。常见的同步方法包括:
- 互斥锁(Mutex):确保在某一时刻只有一个线程能访问共享资源。
- 信号量(Semaphore):控制同时访问某个特定资源的线程数量。
- 条件变量:允许线程在特定条件下等待或通知其他线程。
通过这些同步机制,可以有效避免数据竞争等问题。
安全问题
在多线程编程中,常见的安全问题包括:
- 数据竞争:多个线程同时修改共享数据,可能导致不一致的结果。
- 死锁:两个或多个线程互相等待对方释放资源,导致程序停滞。
- 活锁:线程在不断尝试执行却无法取得进展的问题。
- 饥饿:某些线程长时间无法获取资源,导致无法执行。
为解决这些问题,开发者需要谨慎设计线程间的交互、合理使用同步机制,并遵循最佳实践以提升程序的安全性和稳定性。
信号量机制
信号量是一种用于控制对共享资源访问的同步机制,主要通过一个计数器来管理并发访问。它允许多个线程或进程之间进行协调,确保安全地使用有限的资源。
在信号量的工作机制中,当一个线程希望访问某个资源时,它会执行 P 操作。如果信号量的计数器大于0,线程就可以继续执行并将计数器减1;如果计数器为0,线程则会进入等待状态,直到有其他线程释放资源。相应地,当线程完成对资源的使用后,会执行 V 操作,将计数器加1,并可能唤醒等待该资源的其他线程。
信号量特别适合限制并发访问,如在数据库连接池中控制最大并发连接数,同时也能用于线程间的协作与通信。通过有效管理线程的访问,信号量能够减少数据竞争和死锁的发生。然而,使用信号量时需要小心设计操作的顺序,以避免潜在的死锁和资源饥饿问题。总之,信号量是多线程编程中一种重要的同步工具。