操作系统——学习笔记
部分内容容来自,javaGuide 和小林Code
介绍
操作系统(Operating System, OS)是管理计算机硬件和软件资源、给应用程序和用户提供底层抽象的一种系统软件。
操作系统的作用
(同时也是我们写操作系统的时候需要实现的功能模块)
-
用户接口:向上,操作系统提供了用户与计算机系统之间的交互界面。这个界面可以是图形用户界面(GUI)或者命令行界面(CLI),使用户能够方便地操作计算机系统。
-
系统服务和应用程序支持:操作系统提供了一系列的系统服务和应用程序支持,包括设备驱动程序、系统工具、应用程序接口(API)等。这些服务和支持使得应用程序能够更轻松地访问计算机的硬件资源和系统功能。
-
进程管理:操作系统负责管理运行在计算机上的应用程序(进程)。它负责进程的创建、调度、终止以及进程间通信等功能。操作系统通过进程管理来保证计算机系统的稳定运行和资源的公平分配。
-
内存管理:操作系统负责管理计算机的主存储器(RAM)。内存管理包括内存分配、回收、虚拟内存管理等功能。操作系统通过内存管理来确保系统资源的高效利用。
-
文件管理:操作系统提供了一个文件系统,用于组织、存储和管理用户的数据文件。文件系统允许用户创建、删除、读取和修改文件,并提供了文件保护、权限管理等功能。
应用程序、内核、CPU

用户态和内核态
进程在系统上运行时所具有的权限不同,可以分为用户态和内核态
用户态
用户态(User Mode) : 拥有较低的权限,用户态运行的进程可以直接读取用户程序的数据。当应用程序需要执行某些需要特殊权限的操作,例如读写磁盘、网络通信等,就需要向操作系统发起系统调用请求,进入内核态。
内核态
内核态(Kernel Mode):拥有非常高的权限,内核态运行的进程几乎可以访问计算机的任何资源包括系统的内存空间、设备、驱动程序。当操作系统接收到进程的系统调用请求时,就会从用户态切换到内核态,执行相应的系统调用,并将结果返回给进程,最后再从内核态切换回用户态。
用户态到内核态的切换需要上下文切换等一系列操作,耗费一定的资源。
用户态切换到内核态的 3 种方式:
系统调用(Trap)
什么是系统调用
系统调用(System Call)是应用程序请求操作系统内核提供服务的接口。具体来说 我们运行的用户态程序,如果需要调用系统内核提供的服务(如文件管理、进程控制、内存管理等),需要向OS发起系统调用来完成。
系统调用的过程
应用程序调用系统调用时,会触发从用户态到内核态的切换,具体步骤如下:
- 应用程序发起请求:通过高级语言中的函数(如 C 语言的printf()底层调用write()系统调用)触发系统调用,传递参数(如文件名、操作类型)。
- 陷入内核态:CPU 通过 “软中断”(如 x86 架构的int 0x80指令或syscall指令)暂停当前用户态程序,切换到内核态,并保存当前进程的上下文(如寄存器数据)。
- 内核处理请求:操作系统内核根据系统调用号(每个系统调用有唯一编号)找到对应的处理函数(如sys_write()),执行具体操作(如向磁盘写入数据)。
- 返回用户态:处理完成后,内核将结果返回给应用程序,恢复进程上下文,切换回用户态,应用程序继续执行后续代码。
例如:应用程序想要发起TCP连接,就需要通过系统调用使用TCP协议栈发起连接请求。
异常
异常(Exception):当 CPU 在执行运行在用户态下的程序时,发生了某些事先不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态。通知 CPU 程序运行出现异常状态(如错误、调试需求),需要内核介入处理以避免崩溃或实现特殊功能。
中断
中断是由外部硬件设备触发的异步事件,用于通知 CPU “需要立即处理的外部请求”,与当前正在执行的程序无关,具有随机性。
系统中断过程
- 硬件设备产生中断信号,通过中断控制器(如 APIC)发送给 CPU。
- CPU 暂停当前程序,保存上下文(如寄存器状态),切换到内核态。
- 内核根据中断类型(通过中断号)调用对应的中断服务程序(ISR)(如读取键盘输入、处理网络数据包)。
- 处理完成后,恢复上下文,返回被中断的程序继续执行。
进程、线程和协程 (※)
JAVA内存逻辑图
点击查看代码
┌─────────────────────────────────────────────────────────────────┐
│ 线程共享区域(JVM进程级别) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 方法区 │ │ 堆(Heap) │ │ 运行时常量池 │ │
│ │ (Method Area) │ │ │ │(Runtime Constant Pool)│
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 线程私有区域(线程级别) │
│ (每个线程独立拥有,线程创建时分配,线程销毁时回收) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ 程序计数器 │ │ 虚拟机栈 │ │ 本地方法栈 │ │
│ │(Program Counter Register) │(Native Method Stack)│
│ │ │ (VM Stack) │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
堆(Heap) 线程共享
功能:Java 内存中最大的区域,用于存储对象实例和数组(几乎所有new出来的对象都在这里分配内存)。
垃圾回收的主要区域(“GC 堆”)
多线程并发访问时需通过同步机制(如锁)保证线程安全
方法区(Method Area) 线程共享
功能:存储已被 JVM 加载的类信息(类名、父类、接口、方法信息等)、常量、静态变量、即时编译器编译后的代码等。
逻辑上属于堆的一部分(JVM 规范未强制要求实现方式)
与堆的关系:类的静态变量存储在方法区,而非堆。
运行时常量池(Runtime Constant Pool) 线程共享
功能:方法区的一部分,存储类的常量池信息(编译期生成的字面量、符号引用),以及运行时动态生成的常量(如String.intern()产生的常量)。
虚拟机栈、本地方法栈以及程序计数器: 线程私有
总结
- 一个进程在执行过程中可以产生多个线程;
- 基本上各个进程是相互独立的,但是线程则不然;同一个进程的线程,则由于共享进程的某些资源,而产生相互影响。例如:当多个线程对共享资源进行非原子性(non-atomic)的读写操作时,由于操作系统对线程的调度顺序不确定,就会引发竞争条件。因此一般采用同步机制(例如互斥锁,信号量,条件变量等方法)
- 线程的上下文切换开销小,但是不利于资源的开销和保护。
进程
进程是操作系统进行资源分配和调度的基本单位,正在运行的一个程序实例。每个进程拥有独立的内存空间、文件描述符、寄存器状态等资源,彼此之间相互隔离。
为了提高CPU的利用效率,例如某进程A进行IO操作时CPU等待,那这个等待的时间中CPU可以切换到另一个进程B来执行,从而利用这段等待时间。当A所需要的数据读取完成后,回向CPU发起中断,继续执行A进程。
这种CPU上多个进程交替执行的思想,叫做并发。(其实是不同进程被分配了CPU的时间片,本质上还是交替串行)
进程控制块
PCB(Process Control Block) 即进程控制块,是操作系统中用来管理和跟踪进程的数据结构,每个进程都对应着一个独立的 PCB。
。。。
进程的状态 (来自javaguid)

为了支持虚拟内存和中程调度(内存交换),五状态模型被扩展为七状态模型。它引入了两个挂起 (Suspend) 状态,当内存不足时,操作系统可以将一些进程换出到外部存储(如硬盘),以释放内存空间。
新增的两个状态:
-
就绪挂起态 (Ready, Suspend): 进程已经准备好运行,但由于内存不足,其数据被换出到外存。当内存可用时,它需要先被调入内存才能进入就绪态。
-
阻塞挂起态 (Blocked, Suspend): 进程正在等待某个事件,并且其数据也被换出到了外存。它必须同时满足两个条件才能恢复:等待的事件发生 + 被调入内存。
进程间的通信方式
- 管道 (Pipe) 通常只用于父进程和子进程,或者两个兄弟进程之间。当进程关系终止时,管道也随之消失。内核缓冲: 数据由内核进行缓冲,无需程序员管理。
- 命名管道 (Named Pipe / FIFO):克服亲缘限制: 它是一个存在于文件系统中的特殊文件(First-In, First-Out),因此任何两个知道该文件路径的进程都可以通过它进行通信,即使它们没有任何亲缘关系。
- 消息队列 (Message Queue) :消息队列是保存在内核中的一个消息链表。它允许一个或多个进程向其写入或读取消息。发送方将消息放入队列后即可继续执行,无需等待接收方读取。接收方同样可以在需要时去队列里取消息。摆脱了先进先出限制: 每个消息都可以被赋予一个类型,接收方可以按类型来读取消息,而不是必须遵循先进先出的顺序。
4。 共享内存 (Shared Memory):共享内存是速度最快的IPC方式,因为它直接省去了数据在内核和用户空间之间来回复制的开销。操作系统在物理内存中开辟一块区域,然后将其分别映射到多个进程的虚拟地址空间中。这样,这些进程就可以像访问自己的私有内存一样,直接读写这块共享区域。正因为多个进程可以直接操作同一块内存,这会引发线程冲突中提到的“竞争条件”。 - 信号量 (Semaphore):信号量本质上不是用来传输数据的,而是用于进程间同步和互斥的机制。它通常作为“锁”来保护对共享资源的访问,比如和共享内存配合使用。
- 套接字 (Socket):套接字是目前应用最广泛的IPC机制,因为它不仅可以用于同一台主机上的进程间通信,更可以用于不同主机之间通过网络的通信。
- 信号 (Signal):信号是一种非常轻量级的、异步的通知机制,用于告知一个进程发生了某个事件。信息量少: 它本身不携带复杂数据,只能用来传递一个“信号”(一个整数值)。异步通知: 进程可以在任何时候被一个信号中断,转而去执行对应的信号处理函数。
僵尸进程和孤儿进程 (被腾讯面试官拷打过...)
僵尸进程 (Zombie Process)
一句话定义:子进程死了,但父进程没来为它“收尸”,导致子进程的“尸体”(PCB信息)仍旧留在系统中。
- 形成原因:
1 子进程已经执行完毕,并调用了 exit() 系统调用。
2 在调用 exit() 后,内核会释放子进程的所有资源(内存、文件句柄等),但会保留它的进程控制块(PCB)。
3 这个PCB中包含了进程的ID、退出状态、资源使用统计等重要信息。保留它的目的是为了让父进程能够通过调用 wait() 或 waitpid() 来获取这些信息,从而知道子进程是以什么状态结束的(是正常完成还是出错了)。
4 如果父进程一直不调用 wait() 或 waitpid(),那么这个保留的PCB就会一直残留在内核的进程表中,占着一个进程ID。
5 这个已经死亡但PCB仍在的进程,就处于“僵尸”状态。
-
核心特征:
-
- 资源: 不占用任何内存、CPU或其他硬件资源。它唯一占用的就是内核进程表中的一个条目(PCB)。
-
- 危害: 少量的僵尸进程本身无害。但如果大量出现,它们会耗尽进程表中的可用ID,导致系统无法创建新的进程,从而引发严重问题。
-
- 如何识别: 在Linux/Unix下使用 ps aux 命令,僵尸进程的状态(STAT)通常显示为 Z 或 Z+,并且其 CMD 会显示为
。
- 如何识别: 在Linux/Unix下使用 ps aux 命令,僵尸进程的状态(STAT)通常显示为 Z 或 Z+,并且其 CMD 会显示为
-
- 如何“杀死”: 僵尸进程已经是“死”的,所以你无法用 kill -9 来杀死一个僵尸进程。唯一的解决方法是杀死它的父进程。当父进程被杀死后,这个僵尸进程会变成一个孤儿进程,然后被 init 进程(PID为1)接管并清理。
孤儿进程:
一句话定义:父进程死了,但子进程还在“活着”,这个子进程就成了“孤儿”。
-
形成原因:
-
- 一个父进程创建了子进程。
-
- 父进程在子进程结束之前就提前退出了(或者意外崩溃了)。
-
- 此时,子进程还在正常运行,但它已经没有父进程了。
-
核心特征:
-
- 状态: 仍然在运行(Running)、睡眠(Sleeping)或就绪(Ready)。
-
- 系统接管 (Adoption): 为了防止孤儿进程在未来结束时变成无人回收的僵尸进程,操作系统设计了一个“收养”机制。在Unix/Linux系统中,所有孤儿进程都会被一个特殊的进程——init 进程(PID为1)所收养。
-
- init 进程的角色: init 进程是所有进程的“祖先”,它会周期性地调用 wait() 来检查并清理它收养的所有子进程(也就是那些已经成为孤儿的进程)留下的PCB。
-
- 危害: 孤儿进程本身是无害的。它只是一个正常的、正在运行的进程,只不过它的父进程变成了 init 进程。这是一种正常的、被操作系统妥善处理了的情况。
-
- 如何识别: 可以通过 ps -ef 查看进程的父进程ID(PPID)。如果一个普通进程的PPID是 1,那么它很可能就是一个孤儿进程。
线程
线程是进程内的一个执行单元,是 CPU 调度的基本单位。一个进程可以包含多个线程,所有线程共享该进程的内存空间、文件资源等,但拥有独立的栈和寄存器状态。
协程
协程是用户态的轻量级线程,由程序(用户)自行调度,而非操作系统内核。协程依赖于语言或框架实现,不直接受 CPU 调度,其切换完全在用户态完成,无需内核参与。
有了进程为什么还需要线程?
- 线程解决了进程切换成本过高的问题
- 线程让进程内的 “并发任务” 更高效;线程允许一个进程内存在多个 “并行执行流”:当某个线程因 IO 阻塞时,其他线程可继续利用 CPU,避免资源浪费。
- 线程优化了多核心 CPU 的利用率。线程作为 CPU 调度的最小单位,可被分配到不同核心并行执行(真正的 “并行” 而非 “并发”),让多核资源得到充分利用。
- 线程简化了进程内的通信与协作。而同一进程内的线程共享地址空间和文件资源:
线程的上下文切换
因为进程是资源分配的基本单位,而线程则是CPU调度的基本单位:因此操作系统的任务调度,实际上调度的对象是线程,而进程则是提供虚拟内存、全局变量等资源。当进程只有一个线程的时候,可以认为这个线程就相当于进程。
同一个进程的线程进行上下文切换时,不需要切换这些共享的资源(虚拟内存、全局变量等),只需要切换私有数据即可。
多线程冲突
在多线程并发的环境中,如果没有上锁,那么就可能会产生线程冲突。
互斥 (核心是安全)
如果多线程执行一段代码,这段代码操作共享变量可能会导致竞争状态,因此我们将这段代码称为临界区。(它是访问共享资源的代码片段,不能让多线程同时执行),我们希望这段代码对与多线程时互斥的,一个线程在临界区执行的时候,其他线程不能进入临界区。(当然多进程竞争共享资源的时候也可以使用互斥的方式避免资源混乱)
同步 (核心时有序)
多线程执行时不一定是顺序执行的,但如果我们希望某些线程之间存在某种顺序,那就需要额外的操作(互相等待和互通消息)。
锁
加锁可以解决并发线程/进程的互斥问题;信号量则更强,可以实现线程/进程间的互斥和同步。
想进入临界区的线程需要执行加锁操作,加锁成功才能进入临界区,完成操作后释放锁。
信号量通过P,V操作来管理资源。
- 互斥锁 (Mutex - Mutual Exclusion)
核心特点: 这是最基础的锁。在任何时刻,只允许一个线程持有。当一个线程获取了互斥锁后,其他任何尝试获取该锁的线程都会被阻塞(睡眠),直到锁被释放。线程被阻塞时,CPU会切换去执行其他任务,不会空转。
使用场景: 保护临界区,用于绝大多数需要保证互斥访问的通用场景。
互斥锁的设计目标非常纯粹:在任何时刻,只允许一个线程持有锁。它的上锁过程 lock() 是一个精心设计的两阶段过程。
阶段一:快速路径 (Fast Path) - 乐观尝试
当一个线程调用 lock() 时,它首先会乐观地认为锁是空闲的。
它会直接使用一条CPU原子指令(通常是 Compare-and-Swap, CAS)去尝试修改锁的状态。
阶段二:慢速路径 (Slow Path) - 请求内核帮助
如果快速路径失败了(即CAS操作发现锁的值已经是1了),说明锁正被其他线程持有。这时,线程不会傻傻地空转(像自旋锁那样)。
陷入内核:线程会执行一个系统调用(System Call),从用户态切换到内核态,请求操作系统的帮助。
进入等待队列,等待CPU调度。
- 信号量 (Semaphore)
信号量本质上是一个受保护的计数器,它不仅仅用于互斥,更主要用于管理一组有限的资源。它的核心操作是 P 和 V(也常被称为 wait 和 signal)。
V操作 (signal/release): 释放一个资源,将信号量计数器原子性地加一。
P操作 (wait/acquire): 申请一个资源,将信号量计数器原子性地减一。
这两个操作的原子性同样是由操作系统内核来保证的。它们不是简单的 count++ 或 count--,而是被实现为系统调用。
当一个线程调用 P(S) 时:
陷入内核:执行系统调用,进入内核态。
内核原子操作:在内核中,会执行以下受保护的逻辑:
检查计数值:查看信号量 S 的当前值。
如果 S > 0:说明有可用资源。
将 S 的值减一 (S--)。
系统调用返回,线程成功获取资源,继续在用户态执行。
如果 S <= 0:说明没有可用资源。
将该线程放入该信号量的等待队列中。
将线程状态设置为“阻塞态”。
触发CPU调度,切换到其他线程。

浙公网安备 33010602011771号