进程#
进程与线程#
进程是什么#
进程是操作系统进行资源分配和调度的基本单位之一,一个进程通常拥有:
- 独立的虚拟地址空间
- 代码段
- 数据段
- 堆
- 栈
- 打开的文件描述符
- 信号处理信息
- 当前工作目录
- 用户 ID / 权限信息
- 至少一个线程
进程之间默认相互隔离,进程 A 不能直接访问进程 B 的内存。例如:
进程 A 地址 0x1000
进程 B 地址 0x1000
这两个地址看起来一样,但实际映射到的物理内存可能完全不同,这种隔离提高了系统安全性和稳定性。如果进程之间要通信,需要使用 IPC 机制,例如:
- pipe
- socket
- shared memory
- message queue
- signal
- mmap
- eventfd
线程是什么#
线程是进程内部的执行流,一个进程可以有一个或多个线程。可以理解为:进程是资源容器,线程是真正执行代码的单位。线程共享同一个进程的大部分资源,例如:
- 虚拟地址空间
- 全局变量
- 堆
- 文件描述符
- 当前工作目录
- 代码段
但每个线程也有自己独立的部分,例如:
- 线程 ID
- 栈
- 寄存器状态
- 程序计数器 PC
- 线程局部存储 TLS
- 调度状态
进程和线程的区别#
| 对比项 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 拥有独立资源 | 共享进程资源 |
| 地址空间 | 独立 | 同进程内共享 |
| 创建开销 | 较大 | 较小 |
| 切换开销 | 较大 | 较小 |
| 通信方式 | IPC,较复杂 | 共享内存,较简单 |
| 崩溃影响 | 通常影响自身进程 | 一个线程崩溃可能导致整个进程崩溃 |
| 安全隔离 | 强 | 弱 |
上下文切换#
上下文切换是什么#
上下文切换指 CPU 从执行一个任务切换到执行另一个任务时,操作系统保存和恢复执行现场的过程。这里的“任务”可以是:
- 进程
- 线程
- 内核任务
CPU 同一时刻一个核心只能执行一个执行流。如果有多个线程要运行,操作系统会让它们轮流使用 CPU。例如:
时间片 1:线程 A 运行
时间片 2:线程 B 运行
时间片 3:线程 A 继续运行
当从线程 A 切到线程 B 时,就发生了上下文切换。
上下文切换什么时候发生#
常见场景:
- 时间片用完
操作系统为了公平调度,会给每个线程一定运行时间。时间片到期后,当前线程可能被切走。
- 线程主动阻塞
例如调用:
read(fd, buf, size);
如果没有数据,线程可能阻塞,CPU 切去执行别的线程。
- 等待锁
线程等待 mutex、semaphore、condition variable 时可能被挂起。
- I/O 等待
磁盘、网络、管道等 I/O 没准备好时,线程会睡眠。
- 更高优先级任务就绪
如果高优先级线程变为可运行,调度器可能抢占当前线程。
- 系统调用或中断
系统调用、中断、异常也可能触发调度。
上下文切换的代价#
上下文切换不是免费的,它的成本包括:
- 保存当前任务状态
- 恢复下一个任务状态
- CPU cache 命中率下降
- TLB 失效
- 分支预测状态受影响
- 内核调度开销
如果系统中上下文切换过多,会导致:CPU 忙着切换任务,而不是执行真正业务代码。 这就是常说的调度开销过大。
用户态和内核态#
用户态和内核态是什么#
现代操作系统通常把 CPU 执行状态分成不同权限级别。最常见的是:用户态,内核态。
用户态
普通应用程序运行在用户态。例如:
- 浏览器
- 数据库
- Web 服务器
- 命令行程序
- 普通业务代码
用户态权限较低,不能直接操作硬件,也不能直接访问内核内存。用户态程序不能直接做这些事:
- 直接读写磁盘硬件
- 直接操作网卡
- 修改页表
- 关中断
- 访问任意物理内存
- 调度其他进程
这样可以防止普通程序破坏整个系统。
内核态
操作系统内核运行在内核态。内核态权限很高,可以:
- 管理进程和线程
- 管理内存
- 访问硬件设备
- 执行文件系统操作
- 管理网络协议栈
- 处理中断
- 进行 CPU 调度
- 管理权限和安全
例如 Linux 内核就在内核态运行。
用户态如何请求内核服务#
用户态程序不能直接操作硬件,但它可以通过 系统调用 请求内核服务帮忙。
read(fd, buf, size);
write(fd, buf, size);
open(path, flags);
socket(...);
fork();
mmap(...);
这些最终都会进入内核。过程大致是:
用户态程序
|
| 系统调用
v
内核态
|
| 内核执行操作
v
返回用户态
例如读文件:
应用程序调用 read()
进入内核态
内核检查 fd、权限、缓存、磁盘
把数据复制到用户缓冲区
返回用户态
应用程序继续执行
内存#
虚拟内存#
虚拟内存 是操作系统给每个进程提供的一套“看起来连续、独立”的地址空间。每个进程看到的地址类似这样:
0x00000000 ----------------
代码段
数据段
堆
mmap 区
栈
0xffffffff ----------------
但这些地址不一定直接对应真实物理内存,进程访问的是 虚拟地址。CPU 和操作系统通过页表把它翻译成 物理地址。也就是:
进程访问虚拟地址
|
v
页表
|
v
物理内存
虚拟内存的作用主要有:
- 进程隔离
进程 A 不能直接访问进程 B 的内存。
- 简化编程模型
每个进程都觉得自己拥有一大片连续内存。
- 按需分配
申请了虚拟地址,不代表立刻占用物理内存。
- 支持内存映射
例如:
- mmap 文件
- 共享库
- 匿名内存
- 共享内存
- 支持换页 / swap
内存不足时,可以把部分页换出到磁盘。
虚拟内存不等于物理内存, 例如程序执行:
char *p = malloc(1024 * 1024 * 1024); // 申请 1GB
这通常只是申请了 1GB 虚拟地址空间。如果程序没有实际写入这些内存,物理内存可能并没有真的占用 1GB。
当真正访问时,例如:
p[0] = 1;
系统才可能分配对应的物理页。这叫 按需分配 / demand paging
页#
虚拟内存按固定大小的块管理,这个块叫 page,即 页。常见页大小是 4KB,也可能有大页 2MB、 1GB。
虚拟地址空间和物理内存都按页管理,例如:
虚拟页 1 -> 物理页 A
虚拟页 2 -> 物理页 B
虚拟页 3 -> 尚未分配
当进程访问一个还没有真实物理页的虚拟地址时,会触发 page fault,也就是缺页异常。
注意:缺页异常不一定是错误,它经常是正常机制。
页缓存#
页缓存,也叫 page cache,是 Linux 用物理内存缓存磁盘文件内容的一种机制。当程序读文件:
read(fd, buf, size);
内核可能会先把文件内容读入页缓存,然后再复制给用户空间。下一次再读同一个文件时,如果数据还在页缓存里,就不用访问磁盘了。
流程大概是:
第一次读文件:
磁盘 -> 页缓存 -> 用户缓冲区
第二次读文件:
页缓存 -> 用户缓冲区
页缓存的意义是可以显著提升文件 I/O 性能,因为内存比磁盘快很多。
Linux 会尽量使用空闲内存做缓存。你可能看到 Linux 上 free 显示内存“用掉很多”,这不一定是坏事,因为 Linux 会把空闲内存用于:
- page cache
- dentry cache
- inode cache
- buffer cache
这些缓存可以在内存紧张时被回收。所以判断可用内存时,不能只看 free,还要看 available。
页缓存和进程内存的关系: 页缓存属于内核管理的物理内存,它可能被多个进程共享,例如多个进程读取同一个动态库/lib/x86_64-linux-gnu/libc.so
内核不需要给每个进程都加载一份物理内存,而是可以共享同一份页缓存。
进程 A 虚拟地址 -> libc 物理页
进程 B 虚拟地址 -> libc 物理页
进程 C 虚拟地址 -> libc 物理页
所以共享库、mmap 文件等经常会导致“虚拟内存看起来很大,但实际独占物理内存没那么大”。
VSS、RSS、PSS、USS 对比#
| 指标 | 含义 | 是否真实占用物理内存 | 是否包含共享页 |
|---|---|---|---|
| VSS / VSZ | 虚拟地址空间大小 | 不一定 | 是 |
| RSS | 驻留物理内存大小 | 是 | 是,且会重复算 |
| PSS | 按比例分摊后的驻留内存 | 是 | 是,但均摊 |
| USS | 独占驻留内存 | 是 | 否 |
简单理解:
VSS: 进程“画了多大的地盘”
RSS: 进程“当前有多少地盘真的在内存里”
PSS: 共享部分摊薄后的 RSS
USS: 进程自己独占的内存
VSS#
VSS 是 Virtual Set Size,也常叫 VSZ。它表示:一个进程拥有的虚拟地址空间大小,也就是进程“映射了多少虚拟内存”。包括:
- 代码段
- 数据段
- 堆
- 栈
- 共享库
- mmap 区
- 已申请但未实际使用的内存
- 文件映射
- 可能还没分配物理页的区域
例如 VSS = 2 GB 不代表这个进程真的用了 2GB 物理内存,它只是说明该进程的虚拟地址空间里映射了 2GB。
VSS 偏大的常见原因
- malloc 申请大块内存但没有实际访问
- mmap 大文件
- 加载多个共享库
- 线程多,每个线程预留栈空间
- JVM、Go runtime、数据库预留大地址空间
- 所以 VSS 一般不能直接用于判断真实内存压力。
RSS#
RSS 是 Resident Set Size。它表示:当前实际驻留在物理内存中的页面大小,也就是进程当前有多少页真的在 RAM 中。包括:
- 进程实际使用的匿名内存
- 已加载进物理内存的代码页
- 共享库页
- mmap 文件页
- 部分页缓存映射
例如 RSS = 300 MB 表示该进程当前大约有 300MB 页面驻留在物理内存中。
RSS 也不等于进程独占内存,因为 RSS 包含共享页。例如 10 个进程都映射了同一个 libc.so,每个进程的 RSS 可能都把这部分算进去。如果简单把所有进程 RSS 相加,会重复计算共享内存。所以:
总 RSS 相加 >= 实际物理内存使用
PSS 和 USS#
PSS 是 Proportional Set Size,它会把共享页按进程数均摊。例如一个 100MB 共享库被 4 个进程共享,每个进程算25MB。所以 PSS 更适合估算进程“公平占用”的物理内存。
USS 是 Unique Set Size,表示进程独占的物理内存。如果杀掉这个进程,大约能立即释放的内存更接近 USS。
OOM#
OOM 是 Out Of Memory,内存不足。当 Linux 内核发现内存不够,并且无法通过回收页缓存、回收 slab、swap、压缩等方式获得足够内存时,可能触发 OOM。这时内核会选择一个或多个进程杀掉,以释放内存。这个机制叫 OOM killer。
Linux 内存不足时会先做什么#
一般不是一缺内存就杀进程,内核会尝试:
- 回收 page cache
- 回收 slab cache 3。 写回脏页
- swap out 匿名页
- 内存压缩 / compact
- 直接回收
- 如果还是不够,触发 OOM
大致过程:
内存紧张
|
v
回收缓存 / 写回 / swap
|
v
仍无法满足分配
|
v
OOM Killer
OOM Killer 怎么选进程#
Linux 会给进程计算一个 OOM 分数。
相关文件:
/proc/<pid>/oom_score
/proc/<pid>/oom_score_adj
oom_score 越高,越容易被杀。影响因素包括:
- 进程占用内存多少
- 是否有特权
- oom_score_adj 设置
- cgroup 限制
- 内核策略
oom_score_adj 范围通常是 -1000 到 1000。含义:
- -1000:尽量不被 OOM Killer 杀
- 0:默认 0 1000:非常容易被杀
系统 OOM 和 cgroup OOM#
现代 Linux,尤其容器环境中,要区分两类 OOM。
系统 OOM: 整台机器内存不足,内核从全局选择进程杀掉。
cgroup OOM: 某个 cgroup 或容器超过自己的内存限制。
例如 Docker 容器限制 memory limit = 512MB。即使宿主机还有很多空闲内存,容器超过 512MB 也可能触发 cgroup OOM,这时通常杀容器里的进程。
页缓存和 OOM 的关系#
很多人看到free memory 很少 ,buff/cache 很大,会以为要 OOM。但实际上 page cache 是可回收的。例如:
$ free -h
total used free buff/cache available
Mem: 16G 12G 500M 6G 5G
虽然 free 只有 500MB,但 available 有 5GB,通常还不算危险。
更应该关注:
- available
- swap 使用
- 内存回收压力
- major page fault
- OOM 日志
- cgroup memory.current