跳过正文
  1. 所有文章/

进程与内存

·3799 字·8 分钟
目录

进程
#

进程与线程
#

进程是什么
#

进程是操作系统进行资源分配和调度的基本单位之一,一个进程通常拥有:

  • 独立的虚拟地址空间
  • 代码段
  • 数据段
  • 打开的文件描述符
  • 信号处理信息
  • 当前工作目录
  • 用户 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 时,就发生了上下文切换。

上下文切换什么时候发生
#

常见场景:

  1. 时间片用完

操作系统为了公平调度,会给每个线程一定运行时间。时间片到期后,当前线程可能被切走。

  1. 线程主动阻塞

例如调用:

read(fd, buf, size);

如果没有数据,线程可能阻塞,CPU 切去执行别的线程。

  1. 等待锁

线程等待 mutex、semaphore、condition variable 时可能被挂起。

  1. I/O 等待

磁盘、网络、管道等 I/O 没准备好时,线程会睡眠。

  1. 更高优先级任务就绪

如果高优先级线程变为可运行,调度器可能抢占当前线程。

  1. 系统调用或中断

系统调用、中断、异常也可能触发调度。

上下文切换的代价
#

上下文切换不是免费的,它的成本包括:

  • 保存当前任务状态
  • 恢复下一个任务状态
  • 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
    物理内存

虚拟内存的作用主要有:

  1. 进程隔离

进程 A 不能直接访问进程 B 的内存。

  1. 简化编程模型

每个进程都觉得自己拥有一大片连续内存。

  1. 按需分配

申请了虚拟地址,不代表立刻占用物理内存。

  1. 支持内存映射

例如:

  • mmap 文件
  • 共享库
  • 匿名内存
  • 共享内存
  1. 支持换页 / swap

内存不足时,可以把部分页换出到磁盘。

虚拟内存不等于物理内存, 例如程序执行:

char *p = malloc(1024 * 1024 * 1024); // 申请 1GB

这通常只是申请了 1GB 虚拟地址空间。如果程序没有实际写入这些内存,物理内存可能并没有真的占用 1GB。

当真正访问时,例如:

p[0] = 1;

系统才可能分配对应的物理页。这叫 按需分配 / demand paging

#

虚拟内存按固定大小的块管理,这个块叫 page,即 。常见页大小是 4KB,也可能有大页 2MB1GB

虚拟地址空间和物理内存都按页管理,例如:

虚拟页 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 内存不足时会先做什么
#

一般不是一缺内存就杀进程,内核会尝试:

  1. 回收 page cache
  2. 回收 slab cache 3。 写回脏页
  3. swap out 匿名页
  4. 内存压缩 / compact
  5. 直接回收
  6. 如果还是不够,触发 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