sigbus信号

SIGBUS (信号编号 7) 是一个在 Unix 和类 Unix 系统(如 Linux, macOS, BSD)中由内核发送给进程的信号,表示该进程尝试执行了一次无效的内存访问,但这种无效性更多地与底层硬件或系统对内存访问方式的限制有关,而不是简单的访问了未分配的内存(后者通常是 SIGSEGV)。

核心含义:总线错误

“总线”指的是计算机内部用于在 CPU、内存和其他硬件组件之间传输数据的物理通道。SIGBUS 本质上意味着进程试图执行一个物理上不可能或不被硬件支持的内存操作,违反了内存总线访问的规则。

常见触发 SIGBUS 的原因:

  1. 未对齐的内存访问:

    • 这是最常见的原因之一。许多 CPU 架构要求对特定数据类型(如 int, long, double, 结构体)的访问必须在特定的内存边界(通常是其自身大小的倍数)上进行。
    • 例子: 在要求 4 字节对齐的架构上,尝试从地址 0x1001(不是 4 的倍数)读取一个 4 字节的 int 可能会导致 SIGBUS
    • 原因: 硬件可能无法在一个总线周期内获取未对齐的数据,或者架构规范直接禁止这种访问。
  2. 访问超出内存映射文件末尾的内存:

    • 当你使用 mmap() 将一个文件映射到进程的地址空间后,访问这个映射区域内的地址是合法的。
    • 但是,如果在映射之后,底层文件被另一个进程截断(ftruncate)或缩短了,而你尝试访问映射区域中现在位于文件新结尾之后的那部分内存,就会触发 SIGBUS
    • 原因: 内核无法为这部分“消失”的文件内容提供有效的物理页面。你访问的地址虽然曾经在映射范围内且有效,但底层支撑它的文件内容已经不存在了。
  3. 访问不存在的物理地址或硬件错误:

    • 程序尝试访问一个物理上根本不存在的内存地址(虽然这种情况在用户态程序中相对少见,更多由内核或驱动问题引起)。
    • 内存硬件(RAM)本身出现故障(位翻转、坏块等)。
    • 原因: 硬件总线控制器检测到对无效物理位置的访问请求或数据传输过程中发生了无法纠正的错误。
  4. 访问仅部分有效的内存映射区域:

    • 一些特殊的内存映射情况,例如映射硬件设备的 I/O 寄存器或映射具有特殊访问权限的区域,如果访问方式不符合硬件要求(如写入只读寄存器、读取未初始化的状态寄存器等),也可能导致 SIGBUS
    • 原因: 硬件设备或内核通过总线错误信号拒绝了这个非法访问。
  5. 堆栈溢出(某些架构/配置下):

    • 在极少数情况下,当进程的堆栈空间被完全耗尽并尝试访问“保护页”(用于捕获栈溢出的特殊不可访问页面)之外的地址时,一些系统可能会发送 SIGBUS 而不是 SIGSEGV
    • 原因: 系统实现细节。

SIGBUSSIGSEGV 的区别:

虽然两者都表示内存访问错误,但侧重点不同:

  • SIGSEGV (段错误): 主要关注虚拟内存地址的有效性。它发生在进程试图访问一个未被映射到其地址空间(未分配、已释放、无权限访问)的虚拟内存地址时。核心问题是“这个虚拟地址无效或无权访问”。
  • SIGBUS (总线错误): 主要关注有效虚拟地址访问的物理实现限制。进程访问的虚拟地址本身是已映射且可访问的,但硬件或底层系统无法按请求的方式完成这次访问(数据没对齐、支撑文件的页面没了、物理地址无效、硬件故障)。核心问题是“这个地址是有效的,但你要做的事情在物理层面上行不通或有错误”。

调试和处理 SIGBUS

  1. 使用调试器: 最有效的方法是使用 gdb 运行程序。当 SIGBUS 发生时,gdb 会中断执行,让你检查:
    • 程序计数器 (rip/eip/pc):查看崩溃时正在执行的指令。
    • 寄存器和堆栈:查看正在操作的内存地址 ($rax, $rdi 等常存放地址) 和函数调用链。
    • 崩溃地址:info registersx/i $pc 查看指令,然后检查该指令操作的内存地址。
    • 关键: 检查该内存地址是否对齐?该地址是否位于 mmap 的文件区域且文件可能被截断了?
  2. 检查内存操作: 查看崩溃点附近的代码,特别是涉及指针操作、直接内存访问、类型转换 (reinterpret_cast in C++, C-style casts)、结构体访问、访问数组越界但“恰好”落在有效页但未对齐的位置。
  3. 检查文件操作: 如果使用了 mmap
    • 确保在访问映射内存期间,底层文件不会被其他进程截断
    • 使用文件锁 (flock, fcntl) 或其他同步机制来保护文件不被意外修改。
    • 考虑在访问映射区域前检查文件大小(注意同步问题)。
  4. 使用内存检查工具: valgrind (特别是 memcheck 工具) 有时可以捕捉到未对齐访问(如果配置了相应选项)或某些内存管理错误,但它在检测 mmap 文件截断问题方面能力有限。
  5. 检查硬件: 如果 SIGBUS 频繁发生且难以定位软件错误,运行内存诊断工具(如 memtest86+)排除物理内存故障。
  6. 捕获信号(谨慎使用): 进程可以注册信号处理函数 (signal, sigaction) 来捕获 SIGBUS,但这通常只适用于特定场景(如优雅地处理预期的 mmap 文件截断)。捕获后程序状态可能已损坏,难以安全恢复。主要用于记录错误信息或清理资源后退出。不要简单地忽略它。

总结:

SIGBUS 是一个指示严重内存访问错误的信号,其根源在于违反了硬件或系统对有效内存地址进行物理访问的约束(未对齐、支撑页面消失、硬件故障)。调试时需重点关注内存对齐、指针操作的正确性以及 mmap 文件映射的生命周期管理。理解其与 SIGSEGV 的区别有助于更快定位问题本质。使用 gdb 捕获崩溃现场是诊断的关键第一步。

内核触发sigbus的场景

内核触发 SIGBUS 信号给用户态进程的场景,本质上都是内核在处理内存管理相关硬件异常(主要是由 MMU 触发的 Bus Error)时,判定该异常是由用户态进程的非法访问引起的,因此通过发送 SIGBUS 信号来通知进程。以下是内核触发 SIGBUS 的主要场景及其背后的机制:

  1. 处理硬件异常:MMU 产生的 Bus Error

    • 核心机制: 当 CPU 执行用户态进程的指令尝试访问内存时,如果该访问违反了硬件层面的内存访问规则(物理总线约束),内存管理单元 (MMU) 会检测到错误并触发一个 硬件异常(例如,x86 上的 #PF Page Fault 异常,但其错误码表明是总线错误;ARM 上的 Prefetch AbortData Abort,特定状态码表示总线错误)。
    • 内核介入: CPU 捕获到这个硬件异常后,会切换到内核态,由内核的异常处理程序(如 do_page_fault, do_translation_fault, do_DataAbort 等)来处理。
    • 内核判断: 内核的异常处理程序会分析异常的原因(通过错误码、访问的地址、当前进程的上下文等)。如果内核确定:
      • 该异常发生在用户态进程的上下文中。
      • 该异常的原因是违反了硬件总线访问规则(而非简单的权限错误或未映射错误,后者通常触发 SIGSEGV)。
    • 发送信号: 内核就会构造一个 siginfo_t 结构体(包含错误类型 BUS_ADRALNBUS_ADRERR 等、故障地址等),并调用 force_sig_info() 或类似函数,向触发该异常的进程发送 SIGBUS 信号。
  2. 具体场景 (内核视角):

    • 用户态未对齐访问:

      • 进程执行了一条需要对齐访问的指令(如 x86MOVAPS 要求 16 字节对齐;ARM 上访问 int 要求 4 字节对齐)。
      • 指令操作的目标内存地址不符合该架构/指令的对齐要求。
      • MMU 检测到未对齐访问,触发总线错误异常。
      • 内核异常处理程序分析确认是用户态未对齐访问。
      • 内核发送 SIGBUS (si_code 通常为 BUS_ADRALN - Alignment Error) 给该进程。
    • 访问被截断的内存映射文件 (mmap):

      • 进程 A 使用 mmap() 将文件 F 的一部分映射到其地址空间 [Addr_Start, Addr_End]
      • 进程 B(或 A 自身)随后调用 ftruncate() 或等效操作缩短了文件 F 的大小,导致映射区域 [Addr_Start, Addr_End] 的一部分([New_File_Size, Addr_End])不再对应有效的文件内容。
      • 进程 A 尝试读取或写入位于 [New_File_Size, Addr_End] 区域内的地址 Addr_X
      • 当 CPU 访问 Addr_X 时,MMU 触发缺页异常 (#PF / Page Fault) 要求内核提供物理页。
      • 内核的缺页处理程序 (handle_mm_fault 等) 发现 Addr_X 在进程 A 的虚拟地址空间内(属于有效映射),但其对应的文件偏移已经超出了当前文件 F 的大小。
      • 内核无法为这个地址提供有效的物理页面(因为文件内容不存在了)。
      • 内核判定这是一个由文件截断引起的、无法解决的错误访问。
      • 内核发送 SIGBUS (si_code 通常为 BUS_ADRERR - Non-existent physical address 或特定于文件的 BUS_OBJERR - Object-specific hardware error) 给进程 A。这是内核在缺页中断处理路径中触发 SIGBUS 的典型例子。
    • 访问不存在的物理内存 (硬件错误):

      • 用户进程访问一个有效的虚拟地址 VA
      • MMU 成功将 VA 翻译成了物理地址 PA
      • 但物理地址 PA 在硬件上不存在(例如,超出系统安装的物理内存范围,或者该物理内存条/DIMM 已被移除/故障)。
      • 内存控制器或总线检测到对无效物理地址 PA 的访问,触发总线错误异常。
      • 内核异常处理程序确认物理地址 PA 无效。
      • 内核发送 SIGBUS (si_code 通常为 BUS_ADRERR) 给该进程。这种情况相对少见,通常指向严重的硬件问题。
    • 访问映射的硬件设备内存 (违反设备约束):

      • 进程通过 mmap (如映射 /dev/mem 或特定设备文件) 将硬件设备的寄存器或内存区域映射到用户空间。
      • 进程尝试以设备不支持的方式访问该映射区域(例如,向一个只读的硬件寄存器写入数据,从一个需要特定访问宽度/序列的寄存器读取,访问保留/未实现的寄存器区域)。
      • 硬件设备检测到非法访问,通过系统总线报告错误(触发总线错误异常)。
      • 内核异常处理程序(可能涉及设备驱动)确认是用户态对设备映射区域的非法访问。
      • 内核发送 SIGBUS (si_code 可能是 BUS_OBJERRBUS_ADRERR) 给该进程。
    • 堆栈溢出 (某些系统/架构):

      • 进程耗尽了为其分配的栈空间。
      • 继续压栈会触及为捕获栈溢出而设置的**保护页 (Guard Page)**。
      • 保护页通常被映射为不可访问
      • 访问保护页触发 MMU 异常。
      • 在某些系统实现中,内核的异常处理程序可能判定这是一个栈溢出错误。
      • 虽然更常见的是发送 SIGSEGV,但在特定架构或内核配置下,也可能发送 SIGBUS 来指示栈溢出。 (需查阅特定系统文档确认行为)。
  3. 内核发送 SIGBUS 的关键点总结:

    • 源头是硬件异常: 总是由 CPU MMU 检测到的总线错误或特定类型的缺页错误触发。
    • 内核作为中介: 内核的异常/中断处理程序是实际接收硬件异常、分析原因并决定如何处理(包括是否发送信号)的实体。
    • 判定责任在用户进程: 内核分析异常上下文(发生在用户态、地址属于用户进程空间、错误类型符合 SIGBUS 语义)后,才会将信号发给用户进程。
    • 构造并发送信号: 内核填充 siginfo_t 结构(包含关键信息如 si_signo=SIGBUS, si_code, si_addr 故障地址),然后通过 force_sig_info() 等机制将信号排入目标进程的信号队列。
    • 用户态进程响应: 最终,当目标用户进程下次被内核调度执行、并在返回用户态之前,内核会检查其挂起的信号。如果发现 SIGBUS 且进程没有忽略或捕获它,内核会执行该信号的默认动作(通常是终止进程并生成 Core Dump)。如果进程注册了信号处理函数,则跳转到该函数执行。

SIGSEGV 触发的区别:

内核触发 SIGSEGV 的流程与 SIGBUS 非常相似(同样源于硬件异常->内核处理->判定责任->发送信号)。核心区别在于内核异常处理程序对错误原因的分类

  • SIGSEGV 内核判定错误原因是虚拟地址无效(无映射 VM_FAULT_BADMAP、无权限 VM_FAULT_BADACCESS、保护位错误等)。错误在于“这个地址你不能碰”。
  • SIGBUS 内核判定错误原因是虚拟地址有效,但物理访问失败或违反硬件约束(未对齐 VM_FAULT_BADALIGN、支撑页不存在 VM_FAULT_NOPAGE 且非匿名映射、硬件报告总线错误等)。错误在于“这个地址你能碰,但你碰的方式物理上行不通或出错了”。

理解内核如何将底层的硬件总线错误转化为发送给用户进程的 SIGBUS 信号,是深入调试此类问题的关键。调试工具如 gdb 和解析 Core Dump 可以帮助定位触发异常的指令和内存地址,从而找出代码中的根本原因。

Linux内核根据vma获取mm

在 Linux 内核中,vm_area_struct(简称 VMA)表示进程虚拟地址空间中的一个内存区域(如代码段、数据段、堆、栈或内存映射区域)。每个 VMA 都归属于一个内存描述符 mm_struct(简称 mm),后者管理整个进程的地址空间。

获取方式

通过 VMA 获取其所属的 mm_struct 非常简单,因为 vm_area_struct 结构体中直接包含指向所属 mm_struct 的指针

1
2
struct vm_area_struct *vma;  // 已有的 VMA 指针
struct mm_struct *mm = vma->vm_mm; // 直接获取对应的 mm

关键点解析

  1. 字段位置

    • vm_area_struct 结构体中,vm_mm 成员直接指向其所属的 mm_struct
    • 源码定义(Linux 内核源码):
      1
      2
      3
      4
      struct vm_area_struct {
      struct mm_struct *vm_mm; // 指向所属的内存描述符
      // 其他成员...
      };
  2. 使用场景示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 示例:在缺页异常处理中获取 mm
    static vm_fault_t handle_pte_fault(struct vm_fault *vmf) {
    struct vm_area_struct *vma = vmf->vma;
    struct mm_struct *mm = vma->vm_mm; // 从 VMA 获取 mm

    // 使用 mm 进行操作(例如统计计数)
    mm->fault_stat[address]++;
    // ...
    }
  3. 引用计数管理

    • 短期使用:如果仅在当前函数中使用 mm(例如检查状态),无需额外管理引用计数。
    • 长期持有:若需将 mm 存储到其他结构或跨函数传递,必须增加引用计数:
      1
      2
      3
      mmget(mm);    // 增加 mm->mm_users 计数(防止 mm 被释放)
      // ...使用 mm...
      mmput(mm); // 使用完毕后减少计数

典型使用场景

  1. 内存管理操作

    • 在缺页异常(handle_mm_fault)、内存映射(mmap 处理)等路径中,通过 VMA 获取 mm 以操作进程地址空间。
  2. 统计与调试

    • 访问 mm_struct 中的统计字段(如 rsstotal_vm)或调试信息。
  3. 跨进程操作

    • ptraceprocfs 等机制中,通过目标进程的 VMA 获取其 mm,以访问或修改其内存。

注意事项

  1. 并发安全

    • 操作 mm 时通常需要持有 mm->mmap_lock(读写锁),防止并发修改地址空间:
      1
      2
      3
      down_read(&mm->mmap_lock);  // 读锁定
      // ...访问 VMA 或页表...
      up_read(&mm->mmap_lock);
  2. 生命周期

    • mm_struct 的生命周期由引用计数 mm->mm_users 控制:
      • 当进程退出时,mm->mm_users 降为 0 后,mm 会被销毁。
      • 使用 mmget()/mmput() 确保安全访问。
  3. 内核线程的特殊性

    • 内核线程没有用户地址空间,其 task_struct->mmNULL
    • 此时可通过 task_struct->active_mm 获取借用自前一个用户进程的 mm(需谨慎使用)。

完整示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void inspect_vma(struct vm_area_struct *vma) {
struct mm_struct *mm = vma->vm_mm;

// 锁定 mm 防止并发修改
down_read(&mm->mmap_lock);

pr_info("Process %d's VMA [%lx-%lx] in mm %px\n",
current->pid, vma->vm_start, vma->vm_end, mm);

// 访问 mm 的统计信息
pr_info("Total VM: %lu kB, RSS: %lu kB\n",
mm->total_vm << (PAGE_SHIFT - 10),
get_mm_rss(mm) << (PAGE_SHIFT - 10));

up_read(&mm->mmap_lock);
}

总结

操作 代码 说明
从 VMA 获取 mm mm = vma->vm_mm; 直接访问结构体成员
增加引用计数 mmget(mm); 长期持有时必须调用
减少引用计数 mmput(mm); mmget() 配对使用
锁定内存描述符 down_read(&mm->mmap_lock); 确保并发安全

通过 vma->vm_mm 获取 mm_struct 是内核内存管理中的基础操作,正确管理引用计数和并发锁是保证系统稳定的关键。