学习笔记
计算机操作系统基础
计算机操作系统基础及常见面试题
一、进程、线程、协程
1.1 进程和线程的区别
核心要点:概念、内存区别、上下文切换、通信方式
面试口头回答:
好的,我从四个维度来回答这个问题。
第一,概念上:进程是资源分配的最小单位,线程是CPU调度的最小单位。一个进程可以包含多个线程。
第二,内存和创建开销:每个进程拥有独立的内存空间,进程之间相互隔离,所以创建开销比较大;而线程共享同一个进程的内存空间和资源,创建开销小很多。
第三,上下文切换:进程切换开销大,因为操作系统需要保存寄存器状态、页表、文件描述符等大量信息;线程切换开销相对小,只需要保存线程私有的数据,比如寄存器、栈指针、程序计数器这些。当然,如果是跨进程的线程切换,那还是得切换页表,开销就上去了。
第四,通信方式:进程内存独立,通信需要借助操作系统提供的机制,比如管道、消息队列、共享内存,通信效率比较低;线程之间因为共享内存,可以直接读写共享数据,但需要通过锁、信号量等机制保证同步,避免数据竞争。
总结一下,进程更适合需要强隔离的场景,线程适合需要频繁协作共享数据的场景。
1.2 线程和协程的区别?
核心要点:调度方式、资源消耗、并发能力、同步安全、典型应用场景
面试口头回答:
线程和协程都是实现并发的方式,但本质上差别很大,我主要从调度方式、资源消耗和适用场景来说。
调度方式上:线程是操作系统内核级别的调度,属于抢占式调度,操作系统会根据时间片强制切换线程,这涉及用户态到内核态的切换,开销比较大;协程是用户态的轻量级线程,由程序自己调度,是协作式的,协程主动让出CPU时才切换,比如遇到
yield或await,切换完全在用户态完成,开销极小。资源消耗上:线程通常需要独立的栈空间,比如1到8MB,系统能创建的线程数量有限,一般就几千到几万;协程栈空间可以动态调整,初始只有几KB,单个线程里可以跑几十万甚至上百万个协程。
并发能力上:多线程可以利用多核CPU真正并行执行;但单个线程内的协程是伪并发,同一时间只有一个协程在运行,无法并行,所以协程特别适合IO密集型任务,通过高效切换来规避IO等待。
同步安全上:多线程共享进程资源,存在数据竞争风险,需要加锁保护;而单线程内的协程运行在同一个执行序列,不存在数据竞争,通常不需要锁。
实际应用中,像Go的goroutine、Python的async/await、Java的虚拟线程,都是协程的实现。
1.3 一个进程崩溃了,再恢复上下文还在吗?
面试口头回答:
不在的。进程崩溃后,内核会直接销毁这个进程,回收它的虚拟内存、PCB、寄存器上下文,还有所有打开的文件句柄,所以原来的上下文是保留不下来的。
如果要"恢复",只能通过外部的监控机制重新启动一个新的进程。如果需要恢复到之前的状态,那只能依赖应用层自己做的持久化,比如保存到数据库或者文件里,新启动的进程再把这些数据加载回来,而不是靠操作系统保留原来的进程上下文。
1.4 编写一个需要调用多次的函数,什么情况下使用进程,什么情况下改用线程?
面试口头回答:
这个问题主要得看任务是IO密集型还是CPU密集型。
如果是IO密集型,比如网络请求、文件读写这些操作,大部分时间都在等IO,线程切换开销小,适合用多线程来提升性能,线程在等待IO的时候可以让出CPU去做别的事。
如果是CPU密集型,比如图像处理、矩阵计算,特别是Python里面,因为GIL全局解释器锁的存在,多线程没法真正并行,这时候应该用多进程,每个进程独立运行,充分利用多核CPU。
总结就是:IO密集选线程,CPU密集选进程。当然实际选型还要综合考虑开销、数据共享需求和上下文切换成本。
1.5 两个线程,一个OOM另一个能正常运行吗?
面试口头回答:
在Java里,一个线程发生OOM,另一个线程在大多数情况下是可以继续运行的。
因为OOM虽然是JVM进程级别的内存问题,但抛出异常的是具体某个线程。JVM会终止抛出OOM的那个线程,但不会立刻终止整个进程。其他不受影响的线程可以继续执行,前提是它们还有足够的内存可用。
不过有几种特殊情况要注意:
- 如果OOM发生在共享资源的操作里,比如静态变量初始化、全局缓存分配,可能会导致资源处于不一致状态,间接影响其他线程;
- 极端情况是堆内存完全耗尽,其他线程后续分配内存时也会陆续OOM,最终导致整个进程崩溃。
所以结论是:单个线程OOM不一定导致其他线程终止,但得看内存整体状态和资源依赖关系。
二、上下文切换
2.1 进程上下文切换
核心要点:保存现场、恢复现场、切换时机
面试口头回答:
进程上下文切换主要分两步:保存现场和恢复现场。
保存现场时:CPU把当前进程的寄存器、程序计数器、栈指针等保存到进程的PCB里。如果发生了内核态切换,还要保存内核栈和页表基址。
恢复现场时:从待运行进程的PCB里取出之前保存的寄存器、程序计数器、栈指针,恢复页表基址保证内存映射正确,然后CPU跳转到新的程序计数器继续执行。
切换时机主要有几个:时间片用完了、调用sleep、有更高优先级的进程要运行、或者发生硬件中断。
2.2 线程上下文切换
面试口头回答:
线程上下文切换也分保存现场和恢复现场。
保存现场:CPU把当前线程的寄存器、程序计数器、栈指针保存到线程的TCB里。如果线程因为系统调用进入内核,还会保存内核栈指针这些内核态信息。
恢复现场:调度器选择下一个要运行的线程,从它的TCB里取出之前保存的状态。这里有个关键点:如果目标线程属于同一个进程,那不需要切换页表;如果是不同进程的线程,还得更新页表基址。
切换时机包括:时间片耗尽、线程调用阻塞操作比如IO或sleep、更高优先级线程到来发生抢占、多核CPU的负载均衡导致线程迁移。
2.3 协程上下文切换
面试口头回答:
协程切换完全在用户态进行,不涉及内核态,所以开销非常小。
切换时主要保存当前协程的关键寄存器和栈信息,包括指令指针、栈顶指针、栈帧指针、通用寄存器,还有协程独立的用户栈。
流程是这样的:当前协程调用让出操作时,主动保存自身的寄存器和栈指针信息,把控制权转移给调度器;调度器选择下一个待运行协程;然后把那个协程上次保存的指令指针、栈信息、寄存器重新加载到CPU;最后继续执行。
因为是用户态协作式调度,没有抢占,切换速度通常是纳秒级,比线程的微秒级快很多。
三、通信方式
3.1 进程的通信方式(重点)
核心要点:进程有独立用户空间,不能直接访问对方内存,必须借助内核空间通信
面试口头回答:
进程间通信主要有以下几种方式:
第一是共享内存:多个进程映射同一块内存区域,通过读写共享内存来实现高速数据交换。不过需要配合信号量等同步机制来避免冲突。
第二是消息传递:分为直接通信和间接通信。直接通信就是发送进程直接向接收进程发消息;间接通信是通过中间实体,比如消息队列、信箱来传递信息。
第三是共享文件:通过文件系统让多个进程读写同一个文件来实现通信。
第四是管道通信:管道是一个特殊的内核缓冲区,允许有亲缘关系的进程通过对管道文件进行读写,完成半双工的数据交换。
第五还有信号量、信号、套接字等:信号量和信号主要用于进程间的同步与通知,套接字还可以跨设备通信,是网络通信的基础。
3.2 线程的通信方式
面试口头回答:
线程属于同一个进程,天然共享进程的地址空间,所以不需要复杂的通信机制,主要需要同步机制来保证共享数据的一致性。
最直接的方式就是共享内存,线程之间直接读写同一片内存区域。
在Java里,线程通信是基于Java内存模型JMM的。每个线程有自己的栈空间,存放局部变量和方法调用信息,这些是线程私有的。而共享数据比如对象字段、静态变量、数组存放在堆内存里。当一个线程访问或修改共享数据时,为了保证可见性和有序性,需要依赖JMM的规则和同步机制,比如synchronized、volatile、Lock这些。
另外,Object类的wait()、notify()、notifyAll()方法也用于线程间的协调和通信。
3.3 协程的通信方式
协程运行于用户态,拥有自己独立的栈和寄存器上下文。
通信方式主要有三种:
- 共享内存:因为协程通常位于同一个线程内,能直接访问同一块内存空间,通常通过简单的变量或数据结构共享数据。
- 消息传递或事件驱动:协程间有时使用消息队列、Channel等机制实现协作,特别是在异步编程框架里很常见。
- 异步控制结构:利用等待和唤醒机制实现协作调度,不需要复杂的同步原语。
因为单线程内的协程不存在数据竞争,所以通常不需要加锁保护。
四、进程内存结构与生命周期
4.1 进程内存结构
面试口头回答:
操作系统用进程控制块PCB来描述进程,PCB是进程存在的唯一标识。PCB里主要包含几类信息:
第一是进程描述信息:包括进程标识符PID,用来唯一标识各个进程;还有用户标识符,表示进程归属的用户。
第二是进程状态和优先级:进程当前的状态,比如新建、就绪、运行、等待、阻塞这些;还有进程的优先级,决定抢占CPU时的优先程度。
第三是资源分配情况:包括内存地址空间或虚拟地址空间的信息,打开的文件列表,以及使用的IO设备信息。
第四是CPU相关信息:CPU中各个寄存器的值。当进程被切换时,CPU的状态信息都会保存到PCB里,这样进程重新执行时能从断点处继续执行。
4.2 进程五种状态/生命周期
面试口头回答:
进程的五种状态分别是:
第一是初始化状态:进程正在被创建,但还没有进入就绪状态。
第二是就绪状态:进程已经具备了除了处理器之外的所有运行条件,一旦获得CPU时间片就可以开始执行。
第三是执行状态:进程正在处理器上运行。
第四是阻塞状态,也叫等待状态:进程因为等待某个事件,比如获取锁失败或者等待IO资源,而暂停执行。
第五是终止状态:进程从系统中退出。
这五个状态之间会有转换,比如就绪到运行是调度器分配CPU,运行到阻塞是等待资源,阻塞到就绪是等待的事件发生了,运行到就绪是时间片用完了。
五、用户态和内核态
5.1 用户态和内核态是什么?什么时候切换?
面试口头回答:
用户态和内核态是操作系统的两种运行模式,主要作用是隔离用户进程和系统核心资源,确保系统的安全性和稳定性。
用户态是普通应用程序运行的状态,程序只能访问受限的系统资源,不能直接操作硬件。
内核态是操作系统内核运行的状态,拥有最高权限,能够直接访问CPU、内存和外设。
切换时机主要有三种:
第一是通过系统调用:应用程序主动向内核请求资源,比如文件操作、进程管理,会执行特权指令触发CPU切换到内核态。内核根据中断向量表找到对应的内核函数执行,完成后再返回用户态。
第二是异常处理:程序在用户态发生非法操作,比如除零错误,会触发异常,CPU进入内核态查找异常处理程序。处理完毕后根据情况返回用户态,如果错误严重就终止进程。
第三是硬件中断:外设比如网卡、磁盘完成任务后向CPU发信号,迫使CPU进入内核态处理中断,处理完后恢复到用户态继续执行原来的程序。
简单来说,应用程序运行在用户态,调用系统API时会通过系统调用进入内核态执行,再切回用户态。切换过程涉及寄存器保存和栈切换,有一定的性能开销。
六、内存管理
6.1 内存管理策略?
面试口头回答:
操作系统的内存管理主要是四个方面:内存分配、地址映射、内存保护和置换策略。
第一,内存分配方式:早期用连续分配,每个进程占用一块连续空间,包括固定分区和可变分区,简单但容易产生碎片。现代系统一般采用非连续分配:
- 分页:把内存和进程空间都划分成固定大小的页和页框,通过页表映射,解决外部碎片问题;
- 分段:根据逻辑结构划分,比如代码段、数据段,更符合程序设计;
- 实际很多系统采用段页式结合,先分段再分页,既能逻辑隔离又能减少碎片。
第二,虚拟内存与页面置换:虚拟内存让进程感觉自己有连续的大空间,实际只在需要时加载部分页面到物理内存。内存不足时操作系统执行页面置换,常见算法有FIFO先进先出、LRU最近最少使用、Clock算法是LRU的高效近似,还有理论上最优的OPT算法但只用于评估。
第三,内存保护与共享:通过基址寄存器和界限寄存器保证进程访问合法内存;页表里设置读写执行权限;代码段等可以在多个进程间共享节省内存。
第四,常见优化机制:写时复制COW提高进程创建效率;大页内存减少TLB缺失;内存映射文件mmap加速文件访问;内存池减少频繁申请释放。
总结一句话:操作系统通过分页、分段实现高效内存分配,通过虚拟内存和置换算法扩展可用空间,通过保护与共享机制保证安全和性能。
6.2 虚拟内存是什么?和物理内存有什么区别?怎么映射的?
面试口头回答:
虚拟内存是操作系统给进程提供的逻辑地址空间,让每个进程都以为自己拥有独立的、连续的内存空间,而实际上它们共享物理内存。
物理内存就是实际的硬件RAM。
区别上:虚拟内存是逻辑的、每个进程独立的;物理内存是真实的硬件资源,所有进程共享。
映射方式:虚拟内存和物理内存的映射通常通过分页技术实现。虚拟内存被划分成多个页面,每个页面映射到物理内存中的页框。操作系统通过页表来管理虚拟地址和物理地址之间的映射关系。如果物理内存不够用了,操作系统会把部分虚拟页面交换到硬盘的交换空间里。
我打个比方:物理内存就像图书馆实际的书架,虚拟内存就像每个人的书单,大家不需要知道具体书架在哪。映射就是管理员负责确保书单上的书最终出现在书架上,或者从仓库里取出来,保证每个人都能拿到自己需要的书。
七、零拷贝与数据传输优化
7.1 如何减少数据拷贝次数优化传输?
面试口头回答:
减少数据拷贝主要有四种技术:
第一是零拷贝技术:通过操作系统提供的sendfile、mmap等系统调用,避免多次从用户空间到内核空间的拷贝,直接把数据从内存传输到网络接口。比如sendfile可以直接从文件描述符向网络连接发送数据,不需要把文件数据拷贝到用户空间。
第二是共享内存:多个进程直接访问同一块内存区域,无需数据拷贝,在同一台机器上传输数据效率很高。
第三是高效序列化格式:选择protobuf、MessagePack、flatbuffers这些二进制格式,代替JSON或XML,不仅数据更紧凑,还能减少序列化时间和传输时的拷贝。
第四是直接内存访问DMA:DMA允许硬件直接访问内存,绕过CPU干预,减少数据传输中的内存拷贝和CPU负担。在支持DMA的网卡上,数据可以直接从内存传输到网络接口。
7.2 介绍一下什么是零拷贝(Zero-Copy)?
面试口头回答:
零拷贝是一种通过减少数据传输过程中的内存拷贝操作来提高性能、降低资源消耗的技术。
传统数据传输要经过多次拷贝:从磁盘到内核缓冲区,从内核缓冲区到用户空间,再从用户空间到网络或存储设备。零拷贝的目标就是避免这些不必要的拷贝。
主要实现方式有三种:
- mmap:通过内存映射文件直接访问文件内容;
- sendfile:直接将文件数据从内核发送到网络套接字;
- splice:用于在文件描述符之间直接传输数据。
零拷贝的优点很明显:提高性能、减少内存拷贝、提升吞吐量;减少延迟;节省内存,避免不必要的内存分配。
应用场景主要是大文件传输、高性能网络服务比如CDN和视频流、数据库存储优化这些场景。
八、Java IO与线程映射
8.1 Java线程和Linux内核线程的映射关系是什么?
面试口头回答:
Java线程和Linux内核线程通常采用1:1的映射关系,也就是说每个Java线程对应一个Linux内核线程。
在现代的JVM实现中,Java线程的创建、调度和终止都依赖操作系统的线程模型。JVM使用操作系统提供的线程库,比如Linux上的pthread来创建线程,并通过操作系统的调度机制来执行。
Linux内核负责对所有线程的调度和管理,所有线程包括Java线程都由Linux内核进行调度分配CPU资源。这种1:1映射使得Java线程的并发执行能够充分利用多核处理器的优势。
8.2 讲一下AIO/BIO
面试口头回答:
BIO和AIO是Java网络编程中两种不同的IO模型。
BIO是同步阻塞模型,特点是"一个连接对应一个线程"。当客户端有连接请求时,服务端必须启动一个线程来处理,如果这个连接不做任何事情,线程就会一直阻塞在read或write操作上,造成严重的资源浪费。虽然可以用线程池优化,但在高并发场景下线程切换的开销依然很大。所以BIO主要适用于连接数比较少而且固定的架构。
AIO是异步非阻塞模型,核心机制是基于"事件和回调"。用户线程发起IO请求后立即返回继续执行其他任务,底层的IO操作完全由操作系统内核完成。当内核准备好数据并拷贝到用户缓冲区后,会主动通知应用程序或者调用预先注册的回调函数。
简单来说,BIO是"数据没来我就死等",AIO是"数据来了你再叫我"。
不过在Linux环境下,AIO的底层实现不如NIO选择的epoll模式成熟稳定,所以目前互联网高并发开发中,主流框架比如Netty依然更倾向于使用NIO。但在Windows环境下,基于IOCP实现的AIO性能是非常出色的。
评论区