为什么程序会崩溃:从段错误、非法访问到 OOM

程序一崩,很多人第一反应是“代码炸了”。这话没错,但只说了一半。更准确地说,程序崩溃通常是操作系统在替你做边界检查:你越过了不该碰的地址,或者机器已经没有足够内存继续撑下去,于是内核把这件事掐断了。

如果把程序放进虚拟内存这张地图里看,很多“莫名其妙的崩溃”其实都很有逻辑。

用户态程序
  -> 访问一个虚拟地址
  -> MMU/页表检查
  -> 地址合法且有权限:继续
  -> 地址不存在或权限不对:触发 page fault
  -> 用户态:SIGSEGV
  -> 内存压力过大:OOM killer 介入

Linux 在 RISC-V 上的异常入口里,读/写/取指 page fault 会先进入架构相关的故障处理路径;在 arch/riscv/mm/fault.c 里,内核会先判断是不是用户态、是不是权限错误,再决定是补页、报错,还是直接给进程一个致命信号。

段错误到底是什么

段错误不是玄学。它的本质是:进程访问了一个不该访问的虚拟地址。

最常见的几种情况很朴素:

  • 空指针解引用
  • 访问已经释放的内存
  • 数组越界,把别的对象踩坏了
  • 试图往只读页里写
  • 栈太深,栈空间被耗尽

这类错误之所以会变成 SIGSEGV,是因为页表告诉 CPU:这块地址要么根本没映射,要么权限不对。Documentation/mm/page_tables.rst 里说得很直接,page fault 既可能来自 lazy allocation、Copy-on-Write,也可能来自代码 bug 或恶意构造的地址。如果是在用户态,内核就发 Segmentation Fault

你可以把它理解成门禁系统:

地址存在且有权限 -> 放行
地址不存在 -> 你踩空了
地址存在但没权限 -> 你试图进不该进的房间

前者通常继续执行,后两种就很容易看到崩溃。

为什么“非法访问”有时不是立刻崩

这里容易误解。程序并不是一碰到内存就死,它先触发的是 page fault。很多 page fault 是正常的,比如第一次访问某段懒分配内存,或者第一次写入 Copy-on-Write 页。

真正决定生死的,是 fault 能不能被内核修复。

如果地址属于进程的合法虚拟空间,只是这页还没在内存里,内核可能去把数据补回来。比如文件映射会从磁盘把页读进来,匿名页可能在需要时才分配。

如果地址根本不属于这个进程,或者权限不对,arch/riscv/mm/fault.c 里最终会走到 bad_area*() 这类路径,用户态看到的就是 SIGSEGV。

所以你在日志里常见的“Segmentation fault”,往往不是程序自己喊疼,而是内核在说:这次访问我不认。

OOM 和段错误不是一回事

这两个名字经常被混在一起,但它们解决的是完全不同的问题。

段错误是“你访问错了地址”。 OOM 是“系统已经没法再分配内存了”。

Linux 不会无限制地给你画饼。内存紧张时,内核会先回收缓存、写回脏页、换出不活跃页面;实在腾不出来,就会进入 OOM 流程。Documentation/admin-guide/sysctl/vm.rst 里把这件事写得很清楚:oom_kill_allocating_taskoom_dump_taskspanic_on_oom 都是在描述内存耗尽时内核怎么选人、怎么记录、要不要直接崩整机。

这时候被杀掉的,通常不是“最坏”的程序,而是“最适合被杀”的程序。内核会根据内存占用、oom_score_adj、任务行为等因素挑一个牺牲品。目的不是惩罚,而是让系统活下来。

两种崩溃,体感却很像

用户看到的现象有时很接近:

| 现象 | 更像什么 | | --- | --- | | 运行到一半突然退出,终端提示 Segmentation fault | 非法访问 / 权限错误 | | 进程直接消失,dmesg 里有 OOM 记录 | 内存耗尽,被内核杀掉 | | 整机开始卡,最后某个程序被杀 | 持续内存压力,进入回收和 OOM |

这也是为什么排障时别只盯着“崩了”。先看它是怎么崩的,比盯着崩溃本身更有用。

char *p = NULL;
*p = 'x';      /* 大概率直接 SIGSEGV */

char *q = malloc(1UL << 30);
if (!q) {
    /* 这里不是崩溃,是分配失败 */
}

第一种是地址错了,第二种是资源不够。症状都可能是“程序没了”,根因完全不同。

最后一句

程序崩溃并不神秘。段错误说明你越过了虚拟内存边界,非法访问说明页表和权限不允许,OOM 说明系统已经没法再优雅地分配内存了。

把这三件事分开看,很多“代码怎么一运行就挂”的问题就会清楚得多。你下次如果看到崩溃日志,先找的是 SIGSEGV 还是 OOM killer,这一步通常就已经把排查方向缩小了一半。

吉ICP备2024014750号-1 备案图标 京公网安备11011202100605号