用户态和内核态的区别
内核态:在内核态下,CPU可以执行所有的指令和访问所有的硬件资源。
用户态:在用户态下,CPU只能执行部分指令集,无法直接访问硬件资源。
内核态的底层操作主要包括:内存管理、进程管理、设备驱动程序控制、系统调用等。
分为内核态和用户态的原因:
安全性:通过对权限的划分,用户程序无法直接访问硬件资源,从而避免了恶意程序对系统资源的破坏。
稳定性:用户态程序出现问题时,不会影响到整个系统,避免了程序故障导致系统崩溃的风险。
隔离性:内核态和用户态的划分使操作系统内核与用户程序之间有了明确的边界,有利于系统的模块化和维护。
内核态和用户态的划分有助于保证操作系统的安全性、稳定性和易维护性。
线程和进程的区别是什么?
本质区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
开销方面:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有很大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换开销小。
稳定性:进程中某个线程如果崩溃了,可能会导致整个进程都崩溃。而进程中的子进程崩溃,并不会影响其他进程。
内存分配:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所有使用的资源来自其所属进程的资源),进程组之间只能共享资源。
包含关系:没有线程的进程可以看作是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线。
进程、线程、协程的区别是什么?
类型 | 定义 | 资源特点 | 通信方式 | 上下文切换开销 | 优缺点 |
---|---|---|---|---|---|
进程 | 操作系统中进行资源分配和调度的基本单位 | 拥有独立内存空间和系统资源,有独立的堆和栈 | 通过管道、消息队列、信号量等特定机制 | 较大,需保存和恢复整个进程状态 | 稳定性和安全性相对较高,但上下文切换开销大 |
线程 | 进程内的执行单元,CPU调度和分派的基本单位 | 共享进程的内存空间,包括堆和全局变量 | 可直接读写共享内存 | 较小,只需保存和恢复线程上下文 | 通信高效,上下文切换开销小,但存在数据竞争和线程安全问题 |
协程 | 用户态的轻量级线程 | 拥有自己的寄存器上下文和栈,与其他协程共享堆内存 | 可直接读写共享内存 | 非常小,只需保存和恢复协程上下文,无需内核级切换 | 处理大量并发任务效率高,但需程序员显式调度管理,编程模型复杂 |
为什么进程崩溃不会对其他进程产生很大影响?
隔离性:每个进程都有自己独立的内存空间,当一个进程崩溃时,其内存空间会被操作系统回收,不会影响其他进程的内存空间。
独立性:每个进程都是独立运行的,它们之间不会共享资源。
资源:虚拟内存、文件句柄、信号量等
进程之下为什么还要设计进程?
线程之间可以并发运行且可以共享相同的地址空间,可以解决多进程资源共享和创建开销大等问题。
多线程比单线程的优势,劣势
优势:提高程序的运行速率,可以充分利用多核处理器的资源,同时处理多个任务,加快程序的执行速度。
劣势:存在多线程数据竞争访问的问题,需要通过锁机制来保证线程安全,增加了加锁的开销,并且还会有死锁的风险。多线程会消耗更多资源,因为每个线程都需要占用一定内存和处理时间。
线程是不是越多越好,大多会有什么问题?
切换开销:线程的创建和切换会消耗资源。如果创建太多线程,会占用大量的系统资源,导致系统资源负载过高,某个线程崩溃后,可能会导致进程崩溃。
死锁问题:过多的线程相互竞争可能会导致死锁问题。竞争是指多个线程同时访问和修改共享资源,如果没有合适的同步机制,可能会导致数据不一致或错误的结果。而死锁则是多个线程相互等待对方释放资源,导致程序无法继续运行。
进程切换和线程切换的区别?
进程切换:包括整个进程的地址空间、全局变量、文件描述符等。
线程切换:线程切换只涉及到线程的堆栈、寄存器和程序计数器等,不涉及进程级别资源。
线程切换为什么比进程切换快,节省了什么资源?
线程共享同一进程的地址空间和资源,线程切换时只需要切换堆栈和程序计数器等少量信息,而不需要切换地址空间,避免了进程切换时需要切换内存映射表等大量资源的开销,从而节省了时间和系统资源。
线程切换详细过程时怎么样的?上下文保存在哪里?
步骤:
- 上下文保存:保存当前线程的上下文信息。包括寄存器状态、程序计数器、堆栈指针等,用于保存线程的执行状态。
- 切换到调度器:调度器负责选择下一个要执行的线程,并根据调度算法做出决策。
- 上下文恢复:调度器选择了下一个要执行的线程后,它会从该线程保存的上下文信息中恢复线程的执行状态。
- 切换到新进程:调度器将执行权切换到新进程,使其开始执行。
上下文信息的保存通常由操作系统负责管理,具体保存在哪里取决于操作系统的实现方式。一般情况下,上下文信息会保存在线程的控制块(Thread Control Block,TCB)中。
TCB是操作系统用于管理线程的数据结构,包含了线程的状态、寄存器的值、堆栈信息等。当发生线程切换时,操作系统会通过切换TCB来保存和恢复线程的上下文信息。
进程的状态,如何切换?
- NULL -> 创建状态:一个新进程被创建时的第一个状态;
- 创建状态 -> 就绪状态:当进程被创建完成并初始化后,一切就绪准备运行时,变为就绪状态,这个过程是很快的;
- 就绪态 -> 运行状态:处于就绪状态的进程被操作系统的进程调度器选中后,就分配给CPU正式运行该进程;
- 运行状态 -> 结束状态:当进程已经运行完成或出错时,会被操作系统作结束状态处理;
- 运行状态 -> 就绪状态:处于运行状态的进程在运行过程中,由于分配给它的运行时间片用完,操作系统会把该进程变为就绪态,接着从就绪态选中另外一个进程运行;
- 运行状态 -> 阻塞状态:当进程请求某个事件且必须等待时,例如请求I/O事件;
- 阻塞状态 -> 就绪状态:当进程要等待的事件完成时,它从阻塞状态变到就绪状态;
进程间通讯有哪些方式?
通信方式 | 特点 | 适用场景 |
---|---|---|
管道 | 分为匿名管道和命名管道,匿名管道无名字,存于内存,数据无格式、大小受限、单向通信,用于父子进程;命名管道需在文件系统创建设备文件,突破亲缘关系限制 | 适合简单、对通信速度要求不高,且数据量不大的同主机进程间通信 |
消息队列 | 数据以自定义消息体形式存于内核消息链表,解决管道数据无格式问题,但读写需在用户态与内核态拷贝数据 | 适合同主机进程间,对数据格式有要求,通信速度要求相对不极致的场景 |
共享内存 | 直接分配共享空间,进程可直接访问,速度快,但多进程竞争易致数据错乱 | 对通信速度要求极高,同主机进程间需频繁大量数据交互的场景 |
信号量 | 本质是计数器,通过P、V原子操作实现进程互斥与同步,保护共享资源 | 多进程共享资源访问控制场景 |
信号 | 异步通信机制,用于应用进程和内核交互,通知系统事件,部分信号进程无法捕捉忽略 | 用于通知进程系统事件,如进程结束、停止等控制场景 |
socket | 可用于不同主机或本地主机进程通信,基于TCP、UDP协议或本地进程通信方式 | 不同主机间或本地有网络通信需求的进程间通信 |
管道有几种方式?
匿名管道:是一种在父子进程或者兄弟进程之间进行通信的机制,只能用于具有亲缘关系的进程间通信,通常通过pipe系统创建。
命名管道:是一种允许无关的进程间进行通信的机制,基于文件系统,可以在不相关的进程之间进行通信。
信号和信号量有什么区别?
信号:一种处理异步事件的方式。信号是比较复杂的通信方式,用于通知接收进程有某种事件发生,除了用于进程外,还可以发送信号给进程本身。
信号量:进程间通信处理同步互斥的机制。是在多线程环境下使用的一种设施,它负责协调各个线程,以保证它们能够正确、合理地使用公共资源。
公共内存是怎么实现的?
公共内存的机制:不同进程拿出一块虚拟地址空间来,映射到相同的物理内存中。减少了进程通信来回拷贝问题,提高了通信速率。
线程间通讯有什么方式?
名称 | 原理及特性 |
---|---|
互斥锁(Mutex) | 本质是锁,访问共享资源前加锁,完成后释放。加锁后其他线程尝试加锁会阻塞,直至当前线程释放。释放时有多个阻塞线程,第一个变为运行态可加锁,其余等待 |
条件变量(Condition Variables) | 多线程实现“等待 - 唤醒”逻辑常用方法,利用线程间共享全局变量同步,与互斥锁结合。线程改变条件状态前,先锁互斥量,pthread_cond_wait 操作将自己放等待列表并解锁互斥量(原子操作),返回时重锁 |
自旋锁(Spinlock) | 通过 CPU 的 CAS 函数(Compare And Swap),在用户态完成加解锁,不主动产生线程上下文切换。加锁先查看锁状态,空闲则设置为当前线程持有;竞争时加锁失败线程“忙等待”。CAS 函数将两步合为原子指令,“忙等待”可用 while 循环或 CPU 的 PAUSE 指令实现 |
信号量(Semaphores) | 可命名或无名,用于控制资源访问次数,对应整型变量(sem)。有 P、V 两个原子操作函数:P 操作将 sem 减 1,小于 0 则进程/线程阻塞;V 操作将 sem 加 1,小于等于 0 则唤醒等待进程/线程 |
读写锁(Read-Write Locks) | 由读锁和写锁构成,读资源用读锁加锁,写资源用写锁加锁。写锁未被持有时,多个线程可并发持读锁;写锁被持有时,读、写线程获取锁均会阻塞。适用于读多写少场景 |
除了互斥锁你还知道什么锁?分别应用于什么场景?
读写锁:允许多个线程同时读取共享资源,但只允许一个线程进行写操作。适用于读操作频繁、写操作较少的场景,可以提高并发性能。
自旋锁:是一种忙着等待锁,线程在获取锁时不会进入阻塞状态,而是循环忙等待直到获取到锁。适用于临界区很小且锁的持有时间很短的场景,避免线程频繁切换带来的开销。
条件变量:条件变量用于线程间的同步和通信。它通常与互斥锁一起使用,线程可以通过条件变量等待某个条件满足,当条件满足时,其他线程可以通过条件变量发送信号通知等待线程。
信号量:是一种计数器,用于控制对共享资源的访问。它可以用于限制同时访问资源的线程数量,或者用于线程间的同步。
进程调度算法有哪些?
先来先服务
每次从就绪队列选择最先进入队列的进程,然后一致运行,直到进程退出或被阻塞,才会继续从队列中选择第一个进程接着运行。(利于长作业,不利于短作业)。
最短作业优先
优先选择运行时间最短的进程来运行,有利于提高系统的吞吐量。
不利于长作业:长作业可能不断往后推。
高响应优先
每次进行进程调度时,先计算响应比优先级,然后把响应比优先级最高的进程投入运行。
该算法同时兼顾了短作业和长作业:等待时间相同时,要求的服务时间越短,响应比就越高,兼顾了短作业;要求的服务时间相同时,等待时间越长,响应比越高,兼顾了长作业。
时间片轮调度算法:
最古老、最简单、最公平的且使用最广的算法。
每个进程被分配一个时间段,成为时间片,即允许该进程在该时间段中运行。
- 如果时间片用完,进程还在运行,那么将会把此进程从CPU释放出来,并把CPU分配另外一个进程。
- 如果该进程在时间片结束前阻塞或结束,则CPU立即进行切换。
- 如果时间片设的太短会导致过多的进程上下文切换,降低了CPU效率。
- 如果舍得太长又可能引起对短作业进程的响应时间变长。
最高优先级
从就绪队列中选择最高优先级的进程进行运行。(低优先级的进程可能永远不会运行)
-
静态优先级:创建进程时就已经确定了优先级。
-
动态优先级:根据进程的动态变化调整优先级。随着时间的推移增加等待进程的优先级。
-
非抢占式:当就绪队列中出现优先级高的进程,运行完当前进程,再选择优先级高的进程。
-
抢占式:当就绪队列中出现优先级高的进程,当前进程挂起,调度优先级高的进程运行。
多级反馈队列调度算法
-
多级:表示有多个队列,每个队列优先级从高到低,同时优先级越高时间片越短。
-
反馈:表示如果有新的进程加入优先级高的队列时,立刻停止当前正在运行的进程,转而去运行优先级高的队列。
工作原理: -
设置多个队列,赋值每个队列不同的优先级,每个队列优先级从高到低,同时优先级越高时间片越短。
-
新的进程会被放入到第一级队列的末尾,按先来先服务的原则排队等待被调度,如果在第一级队列规定的时间片没运行完成,则将其转入到第二级队列的末尾,依次类推,直到完成。
-
当较高优先级的队列为空,才调度较低优先级的队列中的进程运行。如果进程运行时,有新进程进入较高优先级的队列,则停止当前运行的进程并将其移入到原队列末尾,接着让优先级高的进程运行。
对于短作业可能可以在第一级队列很快就被处理完。
对于长作业,如果在第一级队列处理不完,可以移入下次队列等待被执行,虽然等待时间变长了,但是运行时间也会更长了,所以该算法很好的兼顾了长短作业,同时有较好的响应时间。
为什么并发执行线程要加锁?
- 为了保护共享数据,防止出现”竞争条件“。
- ”竞争条件:多个线程同时访问和操作同一块数据时,最终结果依赖于线程的执行顺序,这可能导致数据的不一致性。
- 通过加锁,可以保证在任何时刻只有一个线程能够访问共享数据,从而避免“竞争条件”,确保数据的一致性和完整性。
自旋锁是什么?应用在哪些场景?
自旋锁加锁失败后,线程会忙等待,直到它拿到锁。
自旋锁是通过CPU提供的CAS函数,在用户态完成加锁和解锁,不会主动产生线程上下文交换。
步骤:
- 查看锁的状态,如果锁是空闲的,执行下一步。
- 将锁设置为当前线程持有。
CAS函数把这两个步骤合并成了一条硬件级指令,形成原子指令。
在单核CPU上,需要抢占式的调度器(即不断通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单个CPU上无法使用,因为一个自旋的线程永远不会放弃CPU。
- 当加锁失败时,互斥锁用线程切换来应对,自旋锁则用忙等待来应对。
- 如果确定被锁住的代码执行时间很短,就不应该用互斥锁,应该使用自旋锁。
死锁发生条件是什么?
- 互斥条件:多个线程不能同时使用同一个资源。
- 持有并等待条件:线程A已经持有了资源1,又想申请资源2,而资源2已经被线程B持有了,所以线程A就会处于等待状态,但是线程A在等待资源2的同时并不会释放自己已经持有的资源1。
- 不可剥夺条件:当线程已经持有了资源,在自己使用完之前不能被其他线程获取。
- 环路等待条件:在死锁发生的时候,两个线程获取资源的顺序构成了环形链。
如何避免死锁?
避免死锁问题就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破坏环路等待条件。
线程总是以相同的顺序申请自己想要的资源。
乐观锁和悲观锁有什么区别?
乐观锁:
- 基本思想:乐观锁假设多个事务之间很少发生冲突,因此在读取数据时不会加锁,而是在更新数据时检查数据的版本(如使用版本号或时间戳),如果版本匹配则执行更新操作,否则认为发生了冲突。
- 使用场景:乐观锁适用于读多写少的场景,可以减少锁的竞争,提高并发性能。例如,数据库中的乐观锁机制可以用于并发更新同一行数据的情况。
悲观锁:
- 基本思想:悲观锁假设多个事务之间会频繁发生冲突,因此在读写数据时会加锁,防止其他事务对数据进行修改,直到当前事务完成操作后才释放锁。
- 适用场景:悲观锁适用于写多的场景,通过加锁保证数据的一致性。例如,数据库中的行级锁机制可以用于处理并发更新同一行数据的情况。
乐观锁适用于读多写少的场景,通过版本控制来处理冲突;而悲观锁适用于写多的场景,通过加锁来避免冲突。
讲一下银行家算法
银行家算法的业务逻辑:
- 不负荷执行:一个进程的最大需求量不超过系统拥有的总资源数,才会接纳执行。
- 可分期:一个进程可以分期请求资源,但总请求数不可超过最大需求量。
- 推迟分配:当系统现有资源数小于进程需求时,对进程的需求可以延迟分配,但总让进程在有限时间内获取资源。
举例:
假设系统中有三类互斥资源R1、R2、R3,可用资源数分别为9,8,5,在指定时刻有P1,P2,P3,P4,P5这五个进程,这三个互斥资源的最大需求量和已分配资源数如下表:
进程 | 最大需求量(分别为R1 R2 R3) | 已分配资源数(分别为R1 R2 R3) |
---|---|---|
P1 | 6 5 2 | 1 2 1 |
P2 | 2 2 1 | 2 1 1 |
P3 | 8 1 1 | 2 1 0 |
P4 | 1 2 1 | 1 2 0 |
P5 | 3 4 4 | 1 1 3 |
第一步:分析
资源R1的剩余可用资源数 = 9 - 1 - 2 - 2 - 1 - 1 = 2。
资源R2的剩余可用资源数 = 8 - 2 - 1 - 1 - 2 - 1 = 1。
资源R3的剩余可用资源数 = 5 - 1 - 1 - 0 - 0 - 3 = 0。
系统剩余可用资源数分别时2,1,0
计算出各个进程需要的资源数如下表:
进程 | 最大需求量 | 已分配资源数 | 首次分配需要的资源数 |
---|---|---|---|
P1 | 6 5 2 | 1 2 1 | 5 3 1 |
P2 | 2 2 1 | 2 1 1 | 0 1 0 |
P3 | 8 1 1 | 2 1 0 | 6 0 1 |
P4 | 1 2 1 | 1 2 0 | 0 0 1 |
P5 | 3 4 4 | 1 1 3 | 2 3 1 |
根据银行家算法不负荷原则(一个进程的最大需求量不超过系统拥有的总资源数,才会被接纳执行),优先给进程P2执行。
第二步:执行P2
执行完成P2后,操作系统剩余可用资源数为4,2,1。
进程 | 最大需求量 | 已分配资源数 | 第二次分配需要的资源数 |
---|---|---|---|
P1 | 6 5 2 | 1 2 1 | 5 3 1 |
P2 | 完成 | 完成 | 完成 |
P3 | 8 1 1 | 2 1 0 | 6 0 1 |
P4 | 1 2 1 | 1 2 0 | 0 0 1 |
P5 | 3 4 4 | 1 1 3 | 2 3 1 |
第三步:执行P4
执行完成P4后,操作系统剩余可用资源数为5,4,1。
进程 | 最大需求量 | 已分配资源数 | 第三次分配需要的资源数 |
---|---|---|---|
P1 | 6 5 2 | 1 2 1 | 5 3 1 |
P2 | 完成 | 完成 | 完成 |
P3 | 8 1 1 | 2 1 0 | 6 0 1 |
P4 | 完成 | 完成 | 完成 |
P5 | 3 4 4 | 1 1 3 | 2 3 1 |
第四步:执行P5
执行完成P5后,操作系统剩余资源可用资源数为 6,5,4
进程 | 最大需求量 | 已分配资源数 | 第三次分配需要的资源数 |
---|---|---|---|
P1 | 6 5 2 | 1 2 1 | 5 3 1 |
P2 | 完成 | 完成 | 完成 |
P3 | 8 1 1 | 2 1 0 | 6 0 1 |
P4 | 完成 | 完成 | 完成 |
P5 | 完成 | 完成 | 完成 |
第五步:执行P1
执行完成P1后,操作系统剩余资源可用资源数为7,7,5
进程 | 最大需求量 | 已分配资源数 | 第三次分配需要的资源数 |
---|---|---|---|
P1 | 完成 | 完成 | 完成 |
P2 | 完成 | 完成 | 完成 |
P3 | 8 1 1 | 2 1 0 | 6 0 1 |
P4 | 完成 | 完成 | 完成 |
P5 | 完成 | 完成 | 完成 |
第六步:执行P3
执行完成P3后,操作系统剩余资源可用资源数为9,8,5
进程 | 最大需求量 | 已分配资源数 | 第三次分配需要的资源数 |
---|---|---|---|
P1 | 完成 | 完成 | 完成 |
P2 | 完成 | 完成 | 完成 |
P3 | 完成 | 完成 | 完成 |
P4 | 完成 | 完成 | 完成 |
P5 | 完成 | 完成 | 完成 |
总结:
银行家算法的核心思想:在分配给进程资源前,首先判断这个进程的安全性。如果系统当前资源能满足其执行,则尝试分配,如果不满足则让该进程等待。
通过不断检查剩余可用资源是否满足某个进程的最大需求,如果可以则加入安全序列,并把该进程当前持有的资源回收;不断重复这个进程,看最后能否实现让所有进程都加入安全序列。
介绍一下操作系统内存管理
操作系统设计了虚拟内存,每个进程都有自己的独立的虚拟内存。
虚拟内存带来的好处:
- 第一,虚拟内存可以使进程对运行内存超过物理内存大小。对于没有被经常访问的内存,可以把它换到物理内存之外,比如硬盘上的swap区域。
- 第二,每个进程的虚拟内存空间是相互独立的(每个进程都有自己的页表)。解决了多进程之间地址冲突的问题。
- 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的bit,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。
- 页表是存储在内存里的,内存管理单元(MMU)将虚拟内存转换成物理内存。
- 当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
什么是虚拟内存和物理内存?
- 虚拟内存:操作系统提供给每个运行中程序的一种地址空间,每个程序在运行时认为自己拥有的内存空间就是虚拟内存,其大小可以远远大于物理内存的大小。虚拟内存通过将程序的地址空间划分成若干个固定大小的页或段,并将这些页或者段映射到物理内存中的不同位置,从而使得程序在运行时可以更高效地利用物理内存。
- 物理内存:物理内存是计算机实际存在地内存,是计算机中地实际硬件部件。
讲一下页表
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间就叫做页。
- 页与页之间是紧密排列的,所以不会有外部碎片。
- 内存分页机制会有内部内存碎片。(内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,最少只能分配一个页)
虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址。这个基地址与页内偏移的组合就形成了物理内存地址。
内存地址转换步骤:
- 把虚拟内存地址,切分成页号和偏移量
- 根据页号,从页表里面,查询对应的物理页号。
- 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。
讲一下段表?
虚拟地址可以通过段表与物理地址进行映射,分段机制会把程序的虚拟地址分为4个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址。
虚拟地址是怎么转化到物理地址的?
当程序访问一个虚拟地址时,MMU会将虚拟地址分解为页号和页内偏移量。然后,MMU会查找页表,根据页号找到对应的页表项。页表项中包含了物理页的地址或页框号。最后,MMU将物理页的地址与页内偏移量组合,得到对应的物理地址。
程序的内存布局是怎么样的?
- 代码段:二进制可执行代码。
- 数据段:已初始化的静态常量和全局常量。
- BSS段:未初始化的静态变量和全局变量。
- 堆段:动态分配的内存,从低地址开始向上增长。
- 文件映射段:动态库、共享内存。
- 栈段:局部变量和函数调用的上下文等。栈的大小是固定的,一般是8MB。
- 保留区:代码段下面的内存空间,防止程序因为出现BUG,导致读或写了一些小内存地址的数据,而使得程序跑飞。
堆和栈的区别?
- 分配方式:堆是动态分配内存,有程序员手动申请和释放内存,通常用于存储动态数据结构和对象。栈是静态分配内存,由编译器自动分配和释放内存,用于存储函数的局部变量和函数调用信息。
- 内存管理:堆需要程序员手动管理内存的分配和释放,如果管理不当可能会导致内存泄漏或内存溢出。栈由编译器自动管理内存,遵循后进先出的原则,变量的生命周期由其作用域决定,函数调用时分配内存,函数返回时释放内存。
- 大小和速度:堆通常比栈大,内存空间大,动态分配和释放内存需要时间开销。栈大小有限,通常比较小,内存分配和释放速度较快,因为是编译器自动管理。
fork会复制哪些东西?
- fork阶段会复制父进程的页表信息(虚拟内存)。
- fork之后,如果发生了写时复制,就会复制物理内存。
介绍cpoy on write(写时复制)
主进程在执行fork的时候,操作系统会把主进程的页表复制一份给子进程,这个页表记录着虚拟地址和物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读。
不过,当父进程或者子进程在向这个内存发起写操作时,CPU就会写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在写保护处理函数里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作这个过程被称为写时复制。
写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止fork创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。
copy on write 节省了什么资源?
节省了物理内存的资源,因为fork的时候,子进程不需要复制父进程的物理内存,避免了不必要的内存复制开销,子进程只需要复制父进程的页表,这时候父进程的页表指向的都是共享的物理内存。
只有当父子进程任何有一方对这片共享的物理内存发生了修改操作,才会触发写时复制机制,这时候才会复制发生修改操作的物理内存。
malloc 1KB和1MB有什么区别?
malloc()源码里面默认定义了一个阈值:
- 如果用户分配的内存小于128KB,则通过brk()申请内存;
- 如果用户分配的内存大于128KB,则通过mmap()申请内存;
介绍一下brk和mmap
brk()函数将堆顶指针向高地址移动,获取新的内存空间。
mmap()系统调用通过私有匿名映射的方式,在文件映射区分配一块内存,也就是从文件映射区“偷”了一块内存。
操作系统内存不足的时候会发生什么?
应用程序通过malloc函数申请内存的时候,实际上申请的是虚拟内存,此时并不会分配物理内存。
当应用程序读写了这块虚拟内存,CPU就会去访问这个虚拟内存,这时会发现这个虚拟内存没有映射到物理内存,CPU就会产生缺页中断,进程会从用户态切换到内核态,并将缺页中断交给内核的Page Fault Handler(缺页中断函数)处理。
缺页中断处理函数会看是否有空闲的物理内存,如果有,就直接分配物理内存,并建立虚拟内存与物理内存之间的映射关系。
如果没有空闲的物理内存,那么内核就会开始进行回收内存的工作,回收的方式主要是两种:直接内存回收和后台内存回收。
- 后台内存回收(kswapd):在物理内存紧张的时候,会唤醒kswapd内核线程来回收内存,这个回收内存的过程异步的,不会阻塞进程的执行。
- 直接内存回收(direct reclaim):如果后台异步回收跟不上进程内存申请的速度,就会开始直接回收,这个回收内存的过程是同步的,会阻塞进程的执行。
如果直接内存回收后,空闲的物理内存仍然无法满足此次物理内存的申请,那么内核就会放最后的大招了——触发OOM(Out of Memory)机制。
OOM Killer机制会根据算法选择一个占用物理内存较高的进程,然后将其杀死,以便释放内存资源,如果物理内存依然不足,OOM Killer会继续杀死占用物理内存较高的进程,直到释放足够的内存位置。
系统内存紧张的时候,就会进行回收内存的工作,那具体哪些内存是可以被回收的呢?
- 文件页(File - backed Page):内核缓存的磁盘数据(Buffer)和内核缓存的文件数据(Cache)都叫作文件页。大部分文件页,都可以直接释放内存,以后有需要时,再从磁盘重新读取就可以了。而那些被应用程序修改过,并且暂时还没写入磁盘的数据(也就是脏页),就得先写入磁盘,然后才能进行内存释放。所以,回收干净页的方式是直接释放内存,回收脏页的方式是先写回磁盘后再释放内存。
- 匿名页(Anonymous Page):这部分内存没有实际载体,不像文件缓存有硬盘文件这样一个载体,比如堆、栈数据等。这部分内存很可能还要再次被访问,所以不能直接释放内存,它们回收的方式是通过Linux的Swap机制,Swap会把不常访问的内存先写到磁盘中,然后释放这些内存,给其他更需要的进程使用。再次访问这些内存时,重新从磁盘读入内存就可以了。
文件页和匿名页的回收都是基于LRU算法,也就是优先回收不常访问的内存。LRU回收算法,实际上维护着active和inactive两个双向链表,其中:
- active_list活跃内存页链表,这里存放的是最近被访问过(活跃)的内存页;
- inactive_list不活跃内存页链表,这里存放的是很少被访问(非活跃)的内存页;
越接近链表尾部,就表示内存页越不常访问。这样,在回收内存时,系统就可以根据活跃程度,优先回收不活跃的内存。
页面置换有哪些算法?
页面置换算法:当出现缺页异常,需调入新页面而内存已满时,选择被置换的物理页面,也就是说选择一个物理页面换出到磁盘,然后把需要访问的页面换入到物理页。
最佳页面置换算法:置换在未来最长时间不访问的页面。
先进先出置换算法:选择在内存驻留时间最长的页面进行置换。
最近最久未使用的置换算法(LRU):选择最长时间没有被访问的页面进行置换。
时钟页面置换算法:把所有的页面都保存在一个类似钟面的环形链表中,一个表针指向最老的页面。
当发生缺页中断时,算法首先检查表针指向的页面:
- 如果它的访问位是0就淘汰该页面,并把新的页面插入这个位置,然后把表针前移一个位置。
- 如果访问位是1就清除访问位,并把表针前移一个位置,重复这个过程直到找到了一个访问位为0的页面为止。
最不常用算法(LFU):当发生缺页中断时,选择访问次数最少的那个页面,并将其淘汰。
它的实现方式是:对每个页面设置一个访问计数器,每当一个页面被访问时,该页面的访问计数器就累加1。在发生缺页中断时,淘汰计数器值最小的那个页面。
什么是中断?
中断:CPU停下当前的工作任务,去处理其他的事情,处理完后回来继续执行刚才的任务。
-
外部中断:
- 可屏蔽中断:通过INTR线向CPU请求的中断,主要来自外部设备。此类中断并不会影响系统运行,可随时处理,甚至不处理。
- 不可屏蔽中断:通过NMI线向CPU请求的中断,如电源掉线,硬件线路故障等。问题太大,无法屏蔽。
-
内部中断:
- 陷阱:一种有意的,预先安排的异常事件。一般是在编写程序时故意设下的陷阱指令,而后执行到陷阱指令后,CPU将会调用特定程序进行相应的处理,处理结束后返回到陷阱指令的下一条指令。
- 故障:在引起故障的指令被执行但尚未执行结束时,CPU 检测到的一类意外事件。出错时交由故障处理程序处理,若能处理修正该错误,就将控制返回到引起故障的指令,即 CPU 重新执⾏这条指令;若不能处理则报错。常见的故障为缺页,当 CPU 引⽤的虚拟地址对应的物理页不存在时,就会发⽣故障。缺页异常是能够修正的,存在专门的缺页处理程序,它会将缺失的物理页从磁盘中重新调进主存,之后再次执⾏引起故障的指令便能顺利进⾏。
- 终止:执行指令的过程中发生了致命错误,不可恢复,程序无法继续执行,只能终止,通常会是一些硬件的错误。终止处理程序不会将控制返回给原程序,而是直接终止原程序。
讲讲中断的流程
- 发生中断:当外部设备或者软件程序需要处理器的注意或者响应时,会发出中断信号。处理器在接收到中断信号后,会停止当前执行的指令,保存当前执行现场,并跳转到中断处理程序执行。
- 中断响应:处理器接收到中断信号后,会根据中断向量表找到对应的中断处理程序的入口地址。处理器会保存当前现场(如程序计数器、寄存器状态等),以便在中断处理完成后能够恢复执行。
- 中断处理:处理器跳转到中断处理程序的入口地址开始执行中断处理程序。中断处理程序会根据中断类型进行相应的处理,可能涉及到保护现场、处理中断事件、执行特定任务等。
中断的类型有哪些?
分类 | 细分类型 | 来源 | 特点及说明 |
---|---|---|---|
外部中断 | 可屏蔽中断(maskable interrupt) | CPU外部硬件产生 | 通过INTR信号线通知CPU,不影响系统运行,CPU可通过设置中断屏蔽寄存器的IF位选择屏蔽 |
不可屏蔽中断(non - maskable interrupt) | 通过NMI信号线通知CPU,代表影响系统运行的严重错误,不可屏蔽,因屏蔽无意义,系统已无法运行 | ||
内部中断 | 软中断 | 处理器内部,由软件主动发起 | 常用于系统调用(system call) |
异常 | 指令执行期间CPU内部产生错误引起 | 不受eflags寄存器的IF位影响,影响系统正常运行,如除0、越界访问等 |
中断的作用是什么?
中断使得计算机系统具备对处理突发事件的能力,提高了CPU的工作效率,如果没有中断系统,CPU就只能按照原来的程序编写的先后顺序,对各个外设进行查询和处理,即轮询工作方式,轮询方法貌似公平,但实际工作效率却很低,却不能及时响应紧急事件。