CPU
- 中央处理器
- 代码是 静态文件, 编译之后变成可执行的 二进制文件, CPU 的作用就是执行这些二进制文件

CPU 位数
- 32 位, 64 位 CPU 指的是 CPU 一次可以计算的数据量
- 32 位 CPU 一次可以处理 32 bits, 及 4 字节
- 64 位 CPU 一次可以处理 8 字节
CPU 位数的影响
- 32 位的 CPU 意味着这个 CPU 的 总线宽度 是 32 位, 也就是一次最多只能处理 32 bits 的数据
- → 总线就是上图的 bus, 有数据总线, 内存地址总线, 控制总线
- 32 位的总线说明这个 CPU physically 一次只能有 32 bit 的数据传入、传出
- 这也意味着, 在 32 位的 CPU 上, 虚拟内存地址的 长度 是 32 位 (本身 内存地址 也是数据)
- 也就是说, 32 位 CPU 能够支持的最多地址数量是 2^32 (最多可以寻址2^32个地址空间)
- 一般内存上, 一个地址对应的存储空间能够存储 1 字节 的数据
- 1 字节/地址 x 2^32 个地址 = 4G
- 也就是说, 每个进程能够分配到的虚拟内存空间的 上限 是 4G
- 进程被分配到的虚拟内存空间, 通常又会被划分成 用户空间 和 内核空间
- 32 位的 CPU, 每个进程分配的虚拟内存上限是 4G, 3G 是用户内存, 1G 是内核内存
CPU 位数和 虚拟内存地址的上限 的关系是什么?
- 地址总线是 32 位这么宽, 说明 每个拟内存地址的长度 是 32 bits
- 32 个二进制数来表示所有的内存地址, 那 所有的内存地址的数量 最多就是
2^32, 也就是 4GB
为什么 2^32 个地址是 4G? 2^32 bits 不是等于 0.5 GB 吗??
- 之前理解错了…
- 2^32 的地址总线, 相当于有 2^32 个 index
- 每个 index 指向内存上的一个地址
- 在内存上,
0x00000001是个内存地址; 每个内存地址可以储存 1 字节 的数据- 这一点是理解这个概念的关键
- 所以 地址数量 有 2^32 这么多个, 所占用的 空间大小 是 2^32 * 1 byte = 4G
gpt: - 地址宽度决定的是地址本身能够表示的范围,而不是说每个地址占用的字节数。 - 在32位地址空间中,一个地址是32位(4个字节),但这个地址指向一个1字节大小的存储位置。 - 因此,一个32位系统可以直接寻址的空间是 2^32 个字节,即4 GB。
32bit / 64bit program
- 在说一个 program 的位数的时候, 是说这个 program 是怎么样被 compiled 的
- 64-bit 的机器可以跑 32bit compiled 的程序, backward compatibility; 但 32bit 的机器不能跑 64bit compiled 的程序
# compile different program
gcc -m32 prog.c
gcc -m64 prog.cCPU 的核
- CPU的核是CPU的处理核心
- 多核CPU → 多个处理核心 → 能同时处理多个进程、线程, 做到真正的 并行
multi-processor
source CSAPP
- multiprocessors 被分成 multi-core 和 hyper-threaded
- 这俩不是同一个东西
- 但一个 CPU 可以有多个 core, 每个 core 都是 hyper-threaded; 即, 这个 CPU 既是 multicore 又是 hyper-threaded
multi-core
source CSAPP
- 同一个 CPU 上有多个 core
- 从图上看, 每个 core 有一定的独立性 (自己独立的 L1 L2 cache) (图中的 L1 cache 被分成两个: “one to hold recently fetched instructions and one to hold data.” - CSAPP) 那像是 ALU 这些 CPU 上的其他 component 呢? 也是有多个吗 → 根据 gpt, 应该是都有多个的
hyper-threading
- 也叫 simultaneous multi-threading
“(hyper-threading) is a technique that allows a single CPU to execute multiple flows of control. It involves having multiple copies of some of the CPU hardware, such as program counters and register files, while having only single copies of other parts of the hardware, such as the units that perform floating-point arithmetic.
→ CSAPP, emphasis added
“让每个物理核心模拟出两个逻辑核心(Logical Cores),使操作系统看到的核心数量翻倍“
→ GPT
- “multiple copies of some of the CPU hardware” → 多个 program counter、寄存器 等; 每个逻辑核心都有自己的 PC 和 register
- 一个逻辑核心正在处理的 process 进入等待的时候, ALU 资源可以马上转移到另一个逻辑核心上, 执行另一个逻辑核心上的 process
CPU 内部都有什么
- 控制单元 control unit → 控制 CPU
- 逻辑运算单元 Arithmetic Logic Unit → 负责计算 二进制 的数据
- 寄存器 → 有多种寄存器, 主要负责 存储 计算时需要用到的 数据 (比内存更快)
- 通用寄存器, 存储运算时的数据, 比如存储 1 + 1 这个运算的两个1
- 程序计数器, 存储 CPU 要执行的下一条命令的内存地址 (下一条命令还在内存上)
- 指针寄存器, 存储当前 正在执行的指令
- 还有好多不同的寄存器
- (有的地方会把这些东西叫做 CPU的上下文)
- Memory Management Unit
- buses 总线
- 硬件组件之间进行数据、地址和控制信号传输的物理通道
- 还有其他东西, 还不懂在干啥:
- cache (CPU 上也有缓存哈, CPU 直接 可以访问的是 CPU 上的缓存, 不是内存)
- clock

寄存器 register
-
CPU寄存器:这是CPU内部的 高速存储单元, 访问速度最快,但容量极小
- 寄存器是 CPU内部的元件
- 寄存器是 CPU内部的信息储存
- 寄存器是 在CPU上的, 所以比 RAM 读取还快
-
寄存器中的数据是CPU正在处理的数据, 这些数据包括:
- 程序计数器、堆栈指针、程序状态
- 寄存器的大小一般为 8、16、32、64 bits
- 一个 8 bits 的寄存器代表 这个CPU 一次最多只能处理 8 bits 的数据
-
CPU 寄存器的大小是由 CPU 的位数决定的, 32位 CPU 的寄存器的大小就是 32 位

Note
进程是 操作系统 分配资源 (内存、文件描述符、I/O 设备) 的 最小单位
线程是 操作系统 调度任务 的 最小单位.
hyperthreading
It(hyperthreading) involves having multiple copies of some of the CPU hardware, such as program counters and register files, while having only single copies of other parts of the hardware, such as the units that perform floating-point arithmetic. → CSAPP
→ 我理解是, 有的硬件有多份 (如 PC), 而真正执行 arithmetic computation 的硬件只有一份; 在执行的时候, computing 的硬件直接读不同的 PC 就可以了, 不需要 context switching?
user & kernel
Note
linux kernel 其实就是一套 software.
- “The kernel is software residing in memory that tells the CPU where to look for its next task.” → how linux works.
- “The kernel is the portion of the operating system code that is always resident in memory.” → CSAPP.
- 在说 用户态、内核态——这个概念的时候, 涉及两个点:
- 一个是 CPU 运行的两种模式
- 一个是操作系统的两种状态 和权限机制
- CPU 运行的模式 直接影响 到 操作系统 的状态
- 这个概念是为了 隔离、保护操作系统的核心组件和应用程序
CPU 本身:
- 在 内核态和用户态下, CPU 有不同的权限和指令集
- 本身 CPU 是处理器嘛, 什么硬件、内存、I/O 这些都是在和 CPU 打交道
- 所以是说, CPU 在切换到 ring 0 内核态的时候, 不会有限制, 能够进行这些操作
Note
“内核态”和“用户态”是 操作系统 的一种状态划分,但具体是 **基于 CPU ** 的运行模式来实现的。
它们的核心区别在于CPU执行指令时的权限级别和功能范围。
内核态 对应 CPU 上的 ring 0, 用户态对应 CPU 上的 ring 3 stackoverflow
→ 这两种 态 是操作系统的抽象逻辑状态,但实现是由CPU硬件支持的。
用户态: 权限受限, 可执行操作受限
- 普通程序运行的模式, 比如浏览器等应用, 运行在用户态下
- 用户态的程序不能直接访问硬件资源, 不能修改其他进程的内存等
- 用户态的程序需要访问资源时(读写文件), 要发起system call, 通过内核才能完成和资源的交互
- 这里实现的是内核态和用户态的切换, os会切换到内核态, 读取硬件资源后返回给浏览器并且切换回用户态
- 安全性: 一个进程崩溃了, 不影响其他进程
内核态: 完全权限, 无操作限制
- 操作系统的核, 直接与硬件(网卡等)交互、操作内存、CPU
- 区分的原因: 安全性(恶意应用程序)、稳定性(一个应用崩溃)、隔离(隔离不同应用, 防止相互干扰)
- 虚拟内存也被分为用户内存空间和内核内存空间
- kernel mode 可以 access 用户态内存空间和内核态内存空间
内核空间: 说 kernel space 指的是 内存!!
- 指的是 内存 上一段专门给内核使用的区域
- 32位的CPU上, 一个进程可以分配 4G (2^32) 虚拟内存; 通常是 1G 给内核, 3G 给用户
内核态用户态相互转换
用户态 → 内核态:
- 系统调用 (不过好像 syscall 也是触发了 中断? go deep 系统调用的流程 )
- 中断 interrupt
内核态 → 用户态:
- 系统调用执行完毕
- 中断处理完毕
进程和线程
Note
一个CPU在一个时间只能执行一个进程或一个线程!!
目前单核CPU里多个进程执行的方式是 并发(concurrent):多个进程分别分配CPU的时间来执行,像是同时在执行,其实不是!多核CPU才能实现 并行(parallel):多个进程同时执行
→ 实际上现在也有 hyperthreading 这种技术了
I/O 操作是个啥
发生了什么
简单来说:
- 应用程序发出文件读写请求
- 操作系统内核接收请求
- 内核与文件系统交互,定位文件
- 文件系统与磁盘控制器和驱动程序协作
- 数据在磁盘、磁盘控制器、系统总线、内存之间传输
展开说说:
- 用户发起 system call, 进行 I/O 操作
- 操作系统触发 上下文切换, 切换到 内核态
- 进入内核态之后, 对应的 I/O 操作被交到操作系统对应的 module (内核的子系统)
- 如, 文件 I/O 是 filesystem, 网络 I/O 是 网络栈
- 对应的 内核系统 会把请求发到对应的 硬件设备驱动程序 driver
- 操作系统调度发起 I/O 的进程:
- 如果是 阻塞性 I/O, 则把进程挂起
- 如果是 非阻塞 I/O, 进程则继续进行, 正常被调度
- 硬件设备驱动程序 会处理 I/O 请求, 和对应的硬件打交道 (网卡、磁盘等)
- 这个就是耗时的地方
- I/O 完成之后, 硬件设备驱动程序 会发一个 中断 给 CPU (或者说 操作系统内核)
- CPU 响应中断, 处理数据 (把数据放到缓冲区啥的), 重新调度进程
阻塞 I/O 是在等什么?
- 比如说 文件 I/O 操作阻塞, 是需要 硬盘 将数据读取到设备缓冲区中,然后数据从设备缓冲区传输到内存。
- 阻塞 I/O 就是在等待 数据 在 磁盘、磁盘 driver、cpu buses、内存 之间传输 (物理层面的传输)
内存 Memory
- 内存是个储存, 用来储存数据的;
- 内存的储存区域是 线性 的
- 内存就是一般指的是 RAM; ROM 也是内存,但通常说内存的时候指的都是 RAM
- 存储的基本单位是 字节 byte, 1 byte = 8 bits (8 位), 一个内存地址指向的内存空间大小就是一字节 (所以字节是基本单位)
虚拟内存和物理内存
虚拟地址映射到页表, 页表项指向 物理内存中的页框 或者包含 磁盘上的页位置信息 → 页表实际上是个index table (索引表) → 页表记录的是虚拟页和物理页的映射关系 → 页表里是页面号码 → 不太清楚这个 页位置信息 是啥, 页表到底是存磁盘的地址还是不存磁盘的地址..?
堆栈
- 堆栈是内存中两个不同的区域
进程的虚拟内存

线程的虚拟内存
- 从下图可以看到, 线程之间共享 heap, 但是有自己的 stack

进程
什么是进程
- 编写的 代码 是个磁盘中存储的 静态文件, 代码 编译 过后会生成可执行的 二进制文件
- 这个 二进制文件 执行起来, 这个文件就会被装载到 内存 上, CPU 根据程序中的指令执行
- 这个被执行的程序就是个 进程 → 进程是 linux 运行程序的 **实例
Note
a process is the operating system’s abstraction for a running program.
→ CSAPP
其他相关:
- 浏览器的一个标签页就是一个进程,一个应用程序可以有多个进程
- 一个进程的 cpu 时间: 进程真正在cpu上执行的时间, 不包含 I/O 操作的等待时间、被挂起的时间、等待的时间
- 一个进程的cpu时间也分 用户态时间 和 内核态时间: 进程执行时,这两个执行状态所占用的时间
- 一个进程如果用户态8ms, 内核态2ms, 说明这个进程主要都是在执行自身的代码,只有少量系统调用

进程的状态
5种基本状态
- 运行 running
- 正在占用 CPU
- 一次只能有一个进程占有 CPU (拥有 CPU 的控制权), aka, 一次只能有一个进程处于运行状态
- 堵塞 blocked
- 执行了, 但是被事件阻塞了, 如等待 I/O;
- 阻塞的意思是, 即便给了这个进程 CPU 控制权, 这个进程也 执行不了
- 阻塞进程不再参与 CPU 调度!!!!
- 就绪 ready
- 等着被执行
- 因为有其他进程正在执行, 所以这个进程处于就绪 三个状态之间能够相互转换 另外两个状态:
- 创建状态 new
- 一个新进程被创建时的第一个状态
- 结束状态 exit
- 从系统中消失

- 从系统中消失
挂起 suspend
- 场景: 有大量的进程被阻塞了, 这些进程也会占用内存, 这样浪费资源显然不好
- 这时候 操作系统 会把这些进程的内存 swap 到 磁盘 里, 这些进程就被 挂起 了
- 被挂起的进程 不占用物理内存
- 不确定挂起是不是进程的状态之一, 待确定 进程还有其他的方式进入挂起
- 进程自己 有 sleep, 主动进入挂起状态
- 用户 触发, 比如 linux 上 ctrl z 会让进程进入挂起状态
PCB process controlling block
是什么
- PCB 是个数据结构
- PCB 是 操作系统 用来存 进程信息 的, 一个进程对应一个 PCB
PCB 包含以下信息
- 进程的 PID, UID
- 进程的状态
- 进程的优先级 (CPU 调度)
- 进程虚拟内存信息
- 进程打开的文件信息, I/O 信息
- 进程寄存器的值
PCB 是数据结构
- PCB 以 链表 linked list 的形式存在
- PCB 按照 进程状态 组成 链表, 有 就绪列表、阻塞列表 等
Note
PCB 不是文件, 是一种 结构化的数据 (有一定结构的数据)

PCB 存在哪?
- PCB 以 task_struct 的数据结构, 存储在 内核空间 里
Note
进程的 上下文信息 以 PCB 的形式储存在 内存 (内核空间) 上
PCB 的作用
- context switching 进程切换 的时候记录进程的信息
- 进程调度, 根据优先级, 操作系统选择下一个要执行的进程
- 系统调用修改进程的状态
访问权限
- 用户态无法访问
- 可以通过系统调用访问, 如
ps
进程控制
创建
创建进程的过程如下:
- 创造一个新的 PCB, 在 PCB 上写上 PID 等信息
- 分配进程运行所需要的资源, 内存等
- 把 PCB 插入 就绪队列
Note
Other than init, all new user processes on a Linux system start as a result of
fork(), and most of the time, you also runexec()to start a new program instead of running a copy of an existing process.
→ how linux works
终止
终止方式:
- 正常结束: 进程自己 exit()
- 异常结束: 外界干扰 (kill 等信号)
操作系统干了什么
- 操作系统释放资源, 如 文件描述符、内存、CPU (在谈释放 CPU 这个资源的时候, 说的是 把 CPU 的控制权转移走)
- 清理物理内存
- 关闭 I/O, 如打开的网络、文件等
- 销毁进程的 PCB
- 通知父进程, 操作系统向 父进程 发送 SIGCHLD
阻塞
- 阻塞: 进程执行不了了, 给他 CPU 的控制权他也执行不了
- 需要等待某些条件被满足才能执行 (条件是 I/O、锁、信号等)
- 进程被阻塞之后是不能自己恢复的 (它在等 条件被满足, 给它 CPU 它也跑不了, 所以它也不能自己恢复自己)
操作系统干了什么
- 阻塞的进程 不再参与 CPU 调度, 操作系统把进程从 CPU 调度队列 中移除
- 操作系统将 PCB 中进程的状态转换为 阻塞
- 操作系统保存进程的 执行状态, 也就是 上下文信息 (寄存器、程序计数器等) 保存在 PCB 中; 给进程恢复的时候使用
- 把进程放到对应的 等待队列, 比如 I/O 等待队列、互斥锁等待队列
唤醒阻塞进程
进程阻塞是在等待 条件被满足, 比如 I/O、信号、锁等 比如, I/O 完成之后, 会有一个 中断 发给 操作系统, 操作系统会:
- 改变进程状态, 从 阻塞 到 就绪
- 把进程从 等待队列 转移到 就绪队列
- 进程在 就绪队列 里等待调度
进程切换 (context switching)
是什么
- 操作系统切换进程的时候, aka, 把 CPU 的控制权交给不同进程的时候, 会进行上下文切换
- layman term: CPU 执行了一个进程, 要执行第二个进程的时候, 要把第一个进程的 执行状态 保存起来, 并且加载第二个进程的 执行状态
- 进程是由内核调度和管理的, 因此进程切换只发生在内核态
啥时候触发上下文切换:
- 一个进程 CPU 时间用完的时候, 进程从 运行 变成 就绪, 等待下一次被调度
- 系统资源不足 (如内存不足) 的时候, 进程会被 挂起, 其他进程被调度
- 进程调用 sleep 自己主动 请求挂起 自己 (实际上还是内核把它挂起的哈)
- 高优先级的进程插入, 系统会优先执行
- 硬件中断
- 进程阻塞: 比如进程在等待 I/O操作, 给它 CPU 控制权它也执行不了
- 某个进程结束
context switching 是在 switch 什么东西
Note
本身 context switching 是个 CPU 的执行权在 不同进程 之间交换 的过程, 所以所有 进程纬度 的信息都要切换
先搞清楚 进程切换 的时候, 上下文切换是在切换的是什么吧
- 本身切换进程, 是 CPU 从执行一个进程切换到另一个进程, 重点是 执行
- 执行 这件事情本身是 CPU 在做, 所以不需要切换 内存 上面的什么东西 (除了内存不够、数据在磁盘上等情况)
- 所以上下文切换, 切换的是 CPU 上的内容
- CPU 上的内容是 寄存器 和 程序计数器 -》 所以进程切换, 切换的是 CPU 上存储的数据, 也就是 寄存器 和 程序计数器 的内容!
CPU 寄存器 register & 程序计数器 program counter
- CPU 寄存器 是 CPU 上 的 存储, 因为就在 CPU 上, 所以 非常快, 但也 非常小
- CPU 寄存器的大小是由 CPU 的位数决定的, 32位 CPU 的寄存器的大小就是 32 位
- 有好几种寄存器, 有不同的目的, 都存储的是不同的内容
- 通用寄存器: 当前 CPU 正在处理的数据
- 指令寄存器: 当前正在执行的指令的内存地址
- 程序计数器: 储存 CPU 要执行的 下一条指令的内存地址
Note
寄存器是个 广义概念 哈, 一个 CPU 里有 8~32 个甚至更多 寄存器 (甚至上百个..?), 每个寄存器都有自己的 purpose
program counter 也是一种寄存器!
context switching 还有什么要 switch 的
所有进程纬度的信息: 切换的时候, 更新一系列的寄存器里存储的数据, 这些寄存器分别指向以下内容:
- 内存页表、内存段表 (用户内存)
- 进程也会有自己的 内核内存
- 文件描述符
- 信号
- PCB 信息 → context switching 不会 mess with 内存, 但是 内存页表、内存段表 需要切换 (每个进程都有自己的内存页表)
咋样切换的, context switching 的流程是咋样的:
- context switching 被触发后, 第一步是保存当前进程的状态, 包括
- 寄存器、程序计数器、栈指针、程序控制块(PCB)
- 调度器选择下一个进程
- 加载上一个进程的状态: 寄存器、程序计数器、栈指针、程序控制块
context switching 带来的好处
多任务执行
- 日常使用电脑时, 用户可以同时浏览网页、听歌等
- 从用户的体验上, 感觉浏览网页和听歌是同时进行的
- 实际上这些任务是分时占用CPU的 高效利用CPU资源
- 有时候进程会陷入等待, 比如 I/O 操作 (读写文件等)
- OS 在这种情况会切换到另一个进程, 来提高CPU的使用率 优先响应某些进程
- 有些进程的优先级比较高,需要OS快速响应,需要立即获得CPU资源 多核CPU的负载均衡
- 某个核的负载过高了,需要把某些进程放到其他核上执行,这时候也需要就进程切换
子进程
是什么
- 进程调用
fork(),clone()这两个 system call, 然后内核会把这个进程 复制 一份 - 复制:
- 子进程 继承 父进程的 大部分环境 (文件描述符、环境变量、信号处理等) → 简单理解: 就是和 复制文件 一样; 文件内容是一样的, 但是两个文件是 各自独立 的
- 内存: 子进程继承父进程的 虚拟内存页表, 但是是 copy on write, 在只读的时候, 子进程读的是和父进程一样的内存 (因为内存地址都是直接复制过来的); 在写的时候, 会触发 page fault, 写到自己独立的内存里
- 子进程的 PPID 指向父进程, 父进程可以调用
wait()或waitpid()等待子进程结束并收集 子进程的 exit 信息 (父进程对子进程有控制权的表现、父进程和子进程生命周期上的关联性)
子进程和父进程资源共享
- 父子进程共享一样的 代码、数据、内存, 但 子进程 也有 独立的资源
- 调用 fork() 过后, 父子进程的 虚拟内存空间 是一样的 (但是是 copy on write 哈)
- 父子进程的 文件描述符 是一样的, 文件指针 是一样的 → 子进程可以继续读写父进程读写的文件 (unnamed pipe 就是通过这个特性实现的)
- 子进程继承父进程的 环境变量、工作目录、信号处理
- ==子进程是独立的进程: 有自己的 CPU 寄存器、程序计数器、栈== → 因此子进程和父进程可以并发
子进程和父进程通信
- 所有的 IPC
- 有亲缘关系的进程实现 匿名管道、共享内存 会比较简单
zombie & orphan
-
孤儿进程
- 孤儿 → 没有 parent
- 子进程还在运行, 但是父进程被终止了
- 子进程的 PPID 会变成 1 (也就是 init 进程)
- 操作系统如何处理孤儿进程:
- 子进程会被系统收养, 继续运行
-
僵尸进程
- 僵尸 → 进程已经结束了, 但还没被回收
- 进程结束了, 但是没有 父进程 的
wait()或waitpid()来收集进程的退出信息 - 导致进程一直停留在 进程表 里, 浪费资源
- 操作系统如何处理僵尸进程:
- 保留进程的最小信息 (PID、退出状态、资源使用信息等)
- 等待父进程通过 wait/waitpid 系统调用来获取这些信息
- 父进程如果没有 wait, 等父进程终止后, 僵尸子进程就变成了孤儿进程, 然后就被 init 进程 收养并处理了
实际上僵尸进程占用的资源有限:
- 进程表项:
- 僵尸进程占用一个进程表中的位置(slot),即使它已经不再执行任何代码。每个进程在系统中都有一个唯一的标识符(PID),僵尸进程仍然保留这个标识符,直到其父进程处理完毕。
- 这意味着如果系统中存在大量僵尸进程,将会 耗尽可用的PID,导致无法创建新进程
- 内存使用:
- 僵尸进程几乎 不占用内存,因为它们已经完成了执行,且没有可执行代码。它们仅保留一些必要的信息,如退出状态和资源使用情况(如CPU时间),这些信息需要父进程来读取。
- 系统性能:
- 虽然单个僵尸进程的影响微乎其微,但如果有大量僵尸进程存在,可能会导致系统性能下降,甚至引发系统崩溃,因为这会阻止新的进程被创建
Note
- 父进程调用 wait() 或 waitpid() 时会被 阻塞(挂起?), 直到某个子进程的状态发生变化 (如正常终止或被信号中断)
- 当子进程通过 exit() 终止时, 操作系统会向父进程发送 SIGCHLD 信号, 通知其子进程状态变化
- 父进程在处理 SIGCHLD 信号或从 wait()/waitpid() 返回后, 继续执行后续代码
Copy On Write
- copy on write 是指:
- 创建子进程的时候, 系统复制了一份父进程的 页表, 但只有在子进程 写 操作的时候, 系统才会为这个 虚拟内存 分配 物理内存 展开说说
- 本身 fork() 创建子进程的时候, 是相当于复制了一份父进程, 代码、数据、虚拟内存 啥的都是一样的
- 页表 里的 虚拟内存和物理内存的映射关系 也是一样的, 子进程的页表会标记 只读 , 在子进程 写 一个标记了 只读 的内存的时候, 就会丢出 page fault, 重新分配新的物理内存给子进程, 然后进行内存复制的操作
- 如果是 读 的操作, 那子进程读直接读父进程的内存就够了
- 只有 写 操作的时候, 才需要为子进程的 虚拟内存 分配 物理内存
为什么要父子进程
优点
- 父子进程的沟通更简单、高效 (unnamed pipe, shared memory)
- 父进程可以监控子进程 (wait, waitpid)
父子进程常用模式:
- 父进程监听端口, 子进程执行请求对应的任务
缺点:
- 稳定性: 所有子进程都依赖父进程, 父进程倒了的话所有进程就倒了
- scalability: 所有子进程都依赖父进程, 很难拓展
子进程实际使用
例子: 在 terminal 里执行 ls
- terminal 的进程 (例如 bash、zsh) 调用
fork()创建子进程 - 终端进程继续运行, 由子进程来执行命令
- 子进程会调用
exec()来加载并执行新的程序 exec()会指定新的程序 替换 当前进程 (即子进程)exec()会替换子进程 所有用户空间的内容 (代码、数据、堆栈等)
- 但是 子进程的 ID 不变, PPID 不变 (子进程还是那个进程, 只是进程执行的内容、用户态的内容被替换了)
- 子进程和 内核相关 的内容也不会变, 如文件描述符, 所以子进程还能把 ls 的返回写到副进程的 stdout 里
子进程执行 exec()
子进程执行 exec() 之后, 以下内容被替换:
- 正文段 (.text segment): 存放程序代码的区域; 执行
exec()后, 新代码替换老代码 - 数据段 (.data segment): 存放全局变量和静态变量的区域,
- 堆栈 (heap stack): 堆栈都会被替换
- 程序计数器 (program counter): 指向下一条指令的内存地址 不变的内容:
- 程序的 PID, PPID, group id;
- 子进程只是 内容被替换, 本身还是原来那个进程
- 程序在调用
exec()之前的一些资源还保持有效, 如 文件描述符; 除非子进程里 显示地关闭 (explicitly close)
在 shell 里执行命令发生了什么?

- image credit - how linux works
父进程 fork() 创建 子进程, 子进程 exec()执行 代码
- 解析命令
- 找到命令对应的 可执行文件 executable
- 是外部文件那就根据 $PATH 来找
- 不然在 /bin 里找
- shell 调用 fork() 创建子进程 (shell 在这里是父进程)
- 子进程中调用 exec() 执行其他程序, 其他程序会把这个子进程的 用户空间 的内容 替换, 替换内容包含 代码、内存空间 等
- 替换过后, 子进程不再执行父进程的代码; 但子进程还是那个子进程, 还是那个 PID
- exec() 不会创建新的进程, 而是在现有进程中执行程序
- 父进程会调用 wait(), 等待子进程结束
- 子进程结束后, 父进程 (shell) 会得到子进程的退出状态 (退出状态为整数, 0 或 非0; 可通过这个整数获取终止子进程的信号等信息)
- 子进程退出后, 父进程恢复 (父进程调用了 wait() 会等待子进程执行结束)
Note
子进程调用 exec() 后, 执行其他程序, 代码、数据、栈 被替换, 不再和父进程共享以上内容;
但有些系统级的信息会被保留, 子进程和父进程之间还有关系:
- 子进程的 ppid 还是指向 父进程, 还继承父进程的 进程组ID
- 父进程还是等待子进程结束, 等着获取子进程的退出状态 (没有wait()的话, 子进程会变成僵尸进程)
- 文件描述符 也有保留 (如 shell 执行 ls, 子进程直接把 ls 的 stdout 写到父进程的 stdout)
系统层面上, 子进程的 pid 一直没有改变, 只不过是 执行的代码 和 执行时使用的资源 不同了
线程
是什么
- 线程是 程序执行的最小单元, CPU 任务调度 的最小单元, 最小的独立 执行路径
- 操作系统会为线程维护 自己的 堆栈
- 执行路径怎么理解?
- 我的理解: 线程相当于是在执行一个二进制文件里的不同部分 (比如说, 代码里的不同 function), 每个线程执行的过程都是 独立 的
- 线程是独立的执行路径 → 操作系统为每个线程维护 CPU 寄存器的状态、程序计数器的状态 → 每个线程都有自己的上下文
Note
是不是换个角度理解会好一点:
- “不要从线程是进程的一部分” 的角度来理解
- 从 “线程相当于是共享一些资源的进程” 的角度理解是不是更好?
- 因为本身线程和进程一样, 具备很多独立的东西, 上下文啥的
线程讲究的是多个线程之间的共享关系
- 线程是任务执行的最小单位, 是CPU分配的最小单位
- 多线程会更消耗资源 (一个进程里的线程越多, 这个进程就需要越多 CPU 时间)
- 不同线程之间 共享代码, 但是每个线程有自己的 stack, 因此多核CPU能够同时处理一个进程里的不同线程
- 一个进程里的不同线程 拥有相同的代码, 共享数据, 共享heap, 但有自己的 stack
- (本身 stack 存的是局部变量啥的, 根据函数调用自动变化的, 不同线程有不同的代码执行路径; 线程执行到哪, stack就是怎么样)
怎么理解线程是CPU调度的最小单位
- CPU一个时间只能执行 一个线程 或 一个进程; 对于多线程的进程, CPU一次也只能执行其中的一个线程; 所以CPU在分资源的时候, 是按照这个进程中的 each 线程单独分配的
- 多核的CPU,在执行一个多线程的进程的时候,可以把不同的线程分配到不同的核上,根据线程来分配资源
- 更深刻的原因:CPU执行任务的执行粒度 → CPU的核心功能是执行代码,这些代码组成的最小任务就是进程(这样理解对吗?)。
线程 & 进程 对比
- 不同进程不共用内存,同一进程的不同线程共用内存 → 不同线程共享 虚拟内存页表
- 进程是资源 (包括内存、打开的文件等) 分配的单位, 线程是CPU 调度的单位;
- 进程拥有一个完整的资源平台, 而线程只独享必不可少的资源, 如寄存器和栈;
- 线程同样具有就绪、阻塞、执行三种基本状态, 同样具有状态之间的转换关系;
- 线程能减少并发执行的时间和空间开销; -》线程就是轻量级, 因为很多资源都共享了, 所以创造线程快 (需要创造的资源少), 终止线程快 (需要释放的资源少), 同一进程中切换线程快 (需要切换的资源少)
- 线程之间要沟通, 直接通过共享内存啥的; 进程之间要沟通, 还需要经过内核
Note
==线程之间共用内存, 但每个线程也有自己的栈, 用来储存局部变量和执行情况==
线程的实现
小林这里还讲了线程具体在内核和操作系统上是怎么实现的, 就先不学习了 link
协程
协程就是个轻量级的线程
- 协程的执行是由 程序 控制的, 也就是在代码里显性实现的
- 不涉及内核, 完全是 用户态的行为 (那是不是说, 协程都是共用资源的? 因为切换资源要涉及到内核?)
- 协程自主决定何时挂起自己,将控制权交给其他协程。 并发:
- 协程和线程、进程一样, CPU 一次只能执行一个, 看似并发, 实际上也是交错运行
- 在单线程中,协程轮流使用CPU,而不是同时运行。
- 多核CPU不能同时执行一个线程里的不同协程 协程之间共用:
- 线程的资源, 线程id, 线程网络, 线程的CPU时间,
- 栈
- 全局变量 协程独立:
- 局部变量
- 栈指针 (每个协程在线程的栈下有自己的一小块)
Note
Go 在语言 runtime 层面实现了一个可以在 用户态 的 单个线程的内部 进行自主管理的协程调度机制,让一个协程的代码执行完之后,不需要进行上下文切换,就可以执行下一个协程,无需读写内存,在操作系统看来,这个线程没有任何的变化
→ 高并发哲学
→ 为什么cpu调度的最小单位不是协程?
- 每个协程也会把对应进程的CPU时间给分了, 每个协程也有自己的CPU时间
- 协程是由程序自己控制的, 我们这里说的CPU调度是由操作系统控制的
Note
- 协程是 用户态 的调度, 不依赖系统 内核 的调度
什么时候用进程什么时候用线程
任务类型
I/O密集型任务:
- 大部分时间都在进行 I/O 操作, 等待时间长 CPU密集型任务:
- 大部分时间都在进行 CPU 计算,
文件下载管理器:
- 属于 I/O 密集型任务, 多线程可以有效地利用 I/O 等待的时间
- 需要做几件事: 1. 下载, 2. 处理下载数据, 3. 储存到磁盘
- 多线程共享内存, 所以 1 下载一下, 等待 I/O 的时候 2 就可以去处理数据 3 可以去写到磁盘 GUI应用
- 主线程处理 UI, 其他线程处理后台, 保持页面的响应性
进程 context switching 的成本大, 所以选择多进程的时候是需要 pros 远远 outweigh cons, pros 是保持进程之间的独立性, 一个进程崩溃不会影响其他进程, 而一个线程崩溃会影响其他线程
nginx:
- 多进程, 为每个请求创造一个进程来处理, 提高并发能力和稳定性
问题
- 操作系统是怎么处理僵尸进程和孤儿进程的
- 僵尸进程是处于什么状态, 终止状态吗
- 内核 空间 是指什么? 系统内核是什么, 操作系统的内核态吗
- 进程在等待 I/O 的时候被阻塞, 其他进程开始执行; 那这时候是谁在执行第一个进程的 I/O ??
- 怎么理解 “挂起”, 和 “阻塞” 的差别是什么? 挂起的时候, 进程占用的内存一定会被swap吗?