You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程进程中SIGSEGV信号未被自定义处理函数捕获问题

Vulkan层mprotect内存保护多线程场景下SIGSEGV信号未被自定义处理函数捕获问题

我正在开发一款拦截所有Vulkan调用、将调用记录写入文件的Vulkan层。针对映射到用户空间的设备内存,层通过mprotect给内存区域加保护,检测用户侧对内存的读写操作。

现有实现逻辑

  • 当用户调用vkMapMemory请求映射内存时,层首先通过mmap分配带PROT_READ | PROT_WRITE标志的内存
  • 通过sigaction注册SIGSEGV信号处理函数
  • 再通过mprotect将该内存区域设置为禁止读写状态,最后把内存基址返回给用户
  • 用户对该区域的任何访问都会触发SIGSEGV,由信号处理函数完成后续处理逻辑,单线程场景下该逻辑运行正常

故障现象

存在2个线程分配并访问上述受保护内存区域时,其中一个线程访问内存触发的SIGSEGV不会被路由到自定义处理函数,而是直接触发段错误导致应用终止,另一个线程的信号触发与处理逻辑运行正常。
绑定单核运行(强制所有逻辑串行执行)时问题不会复现。

普通运行场景崩溃日志

[59588] AddExceptionHandler() -> sigaction SIGSEGV *******************
[59588] SetMemoryProtection() mprotect: ptr: 0x7fec0819d000 - 0x7fec0839d000 size: 2097152 mask: 0x0
[59588] MapMemory <-- *ppData: 0x7fec0819d000
[59588] PageGuardExceptionHandler()
  [59588] HandleGuardPageViolation() address: 0x7fec0819d000 is_write: 1 clear_guard: 1
  [59588] 1107:HandleGuardPageViolation() -> SetMemoryProtection()
  [59588] SetMemoryProtection() mprotect: ptr: 0x7fec0819d000 - 0x7fec0819e000 size: 4096 mask: 0x3
[59588] PageGuardExceptionHandler() <-- (handled: true)
[59588] MapMemory(memory: 0x5575f2d5cb20 size: 2097152)
[59588] mmap: 0x7fec013fb000 - 0x7fec015fb000 size: 2097152
[59588] SetMemoryProtection() mprotect: ptr: 0x7fec013fb000 - 0x7fec015fb000 size: 2097152 mask: 0x0
[59588] MapMemory <-- *ppData: 0x7fec013fb000
[59588] PageGuardExceptionHandler()
  [59588] HandleGuardPageViolation() address: 0x7fec013fb000 is_write: 1 clear_guard: 1
  [59588] 1107:HandleGuardPageViolation() -> SetMemoryProtection()
  [59588] SetMemoryProtection() mprotect: ptr: 0x7fec013fb000 - 0x7fec013fc000 size: 4096 mask: 0x3
[59588] PageGuardExceptionHandler() <-- (handled: true)
[59588] AllocateMemory(size: 4194304) *pMemory: 0x5575f2f5c080
[59588] AllocateMemory(size: 537600) *pMemory: 0x5575f2f5c710

Test case 'dEQP-GLES31.functional.shaders.opaque_type_indexing.sampler.dynamically_uniform.geometry.samplercubearray'..
[59607] AllocateMemory(size: 2097152) *pMemory: 0x7febe80016c0
[59607] MapMemory(memory: 0x7febe80016c0 size: 2097152)
[59607] mmap: 0x7fec007fa000 - 0x7fec009fa000 size: 2097152
[59588] PageGuardExceptionHandler()
  [59588] HandleGuardPageViolation() address: 0x7fec0143b9e0 is_write: 1 clear_guard: 1
  [59588] 1107:HandleGuardPageViolation() -> SetMemoryProtection()
  [59588] SetMemoryProtection() mprotect: ptr: 0x7fec0143b000 - 0x7fec0143c000 size: 4096 mask: 0x3
[59588] PageGuardExceptionHandler() <-- (handled: true)
[59588] PageGuardExceptionHandler()
  [59588] HandleGuardPageViolation() address: 0x7fec0829d000 is_write: 1 clear_guard: 1
  [59588] 1107:HandleGuardPageViolation() -> SetMemoryProtection()
  [59588] SetMemoryProtection() mprotect: ptr: 0x7fec0829d000 - 0x7fec0829e000 size: 4096 mask: 0x3
[59588] PageGuardExceptionHandler() <-- (handled: true)
[59607] SetMemoryProtection() mprotect: ptr: 0x7fec007fa000 - 0x7fec009fa000 size: 2097152 mask: 0x0
[59607] MapMemory <-- *ppData: 0x7fec007fa000
[59607] util_copy_rect() 1 src: 0x5575f2e6c468 dst: 0x7fec007fa000 size: 4
Segmentation fault (core dumped)

从日志末尾可见,第二个线程启动后申请受保护内存区域时,被第一个线程触发的SIGSEGV信号处理流程中断,后续第二个线程完成新内存区域的mprotect设置后,访问该区域时信号处理函数未被调用,直接触发段错误。

Valgrind运行场景崩溃日志

[59840] PageGuardExceptionHandler()
  [59840] HandleGuardPageViolation() address: 0x14e08000 is_write: 1 clear_guard: 1
  [59840] 1107:HandleGuardPageViolation() -> SetMemoryProtection()
  [59840] SetMemoryProtection() mprotect: ptr: 0x14e08000 - 0x14e09000 size: 4096 mask: 0x3
[59840] PageGuardExceptionHandler() <-- (handled: true)
[59840] PageGuardExceptionHandler()
  [59840] HandleGuardPageViolation() address: 0x14ac8000 is_write: 1 clear_guard: 1
  [59840] 1107:HandleGuardPageViolation() -> SetMemoryProtection()
  [59840] SetMemoryProtection() mprotect: ptr: 0x14ac8000 - 0x14ac9000 size: 4096 mask: 0x3
[59840] PageGuardExceptionHandler() <-- (handled: true)
[59881] AllocateMemory(size: 2097152) *pMemory: 0xe695c80
[59881] MapMemory(memory: 0xe695c80 size: 2097152)
[59881] mmap: 0x161c9000 - 0x163c9000 size: 2097152
[59881] SetMemoryProtection() mprotect: ptr: 0x161c9000 - 0x163c9000 size: 2097152 mask: 0x0
[59881] MapMemory <-- *ppData: 0x161c9000
[59881] util_copy_rect() 1 src: 0xec7a0d8 dst: 0x161c9000 size: 4
==59840== 
==59840== Process terminating with default action of signal 11 (SIGSEGV)
==59840==  Bad permissions for mapped region at address 0x161C9000
==59840==    at 0x4842B33: memmove (in /usr/lib/x86_64-linux-gnu/valgrind/vgpreload_memcheck-amd64-linux.so)
==59840==    by 0x5EFB4CE: util_copy_box (u_surface.c:78)
==59840==    by 0x611CA0B: u_default_texture_subdata (u_transfer.c:71)
==59840==    by 0x5F05BCC: tc_call_texture_subdata (u_threaded_context.c:2529)
==59840==    by 0x5F01031: tc_batch_execute (u_threaded_context.c:213)
==59840==    by 0x584334B: util_queue_thread_func (u_queue.c:313)
==59840==    by 0x5842F5A: impl_thrd_routine (threads_posix.h:87)
==59840==    by 0x49A7608: start_thread (pthread_create.c:477)
==59840==    by 0x4E51132: clone (clone.S:95)

该场景下第二个线程未被第一个线程的信号处理流程中断,但访问受保护内存时仍直接触发SIGSEGV的默认终止行为。

环境说明

当前运行环境涉及mesa库,日志来自运行在zink上的gles应用(CTS测试用例),zink会将gles调用翻译为Vulkan调用,场景中的多线程由mesa创建。


问题根因与解决方案

核心根因

问题本质是Linux信号投递规则、sigaction注册配置不匹配多线程场景,加上信号处理函数执行期间的信号掩码问题共同导致:

  1. SIGSEGV属于进程级同步信号,默认投递到触发异常的线程,但注册信号处理函数时如果没有设置SA_NODEFER标志,信号处理函数执行期间内核会自动屏蔽同类型信号,此时其他线程触发的SIGSEGV如果投递时信号处于屏蔽状态,就会直接触发默认终止动作。另外如果只在第一次调用vkMapMemory时注册信号处理函数,此前已经创建完成的线程可能无法正确同步处理函数配置。
  2. 信号处理函数不满足异步信号安全要求。从日志看信号处理函数里除了mprotect之外还有日志打印、自定义业务逻辑,这些都不是异步信号安全函数,如果信号处理执行期间触发新异常、或者重入非安全函数,内核会判定信号处理无法正常返回,直接触发默认终止动作,多线程下这个触发概率会被大幅放大。

单核串行执行时不会出现两个线程同时触发SIGSEGV、或者信号处理执行期间其他线程触发异常的场景,因此问题不会复现,和观察到的现象完全匹配。

修复步骤

  1. 修正sigaction注册配置,保证所有线程正确捕获信号
    • 注册信号处理函数时,必须添加SA_SIGINFO | SA_NODEFER | SA_ONSTACK三个标志:
      • SA_SIGINFO:使用三参数信号处理函数,正确获取触发异常的地址、触发原因等信息
      • SA_NODEFER:信号处理函数执行期间不屏蔽SIGSEGV信号,避免其他线程触发的同类型信号被阻塞导致进程终止
      • SA_ONSTACK:使用提前通过sigaltstack分配的独立信号栈,避免信号触发时当前线程栈溢出导致处理函数无法执行
    • 不要在第一次vkMapMemory时才注册信号处理函数,在层初始化入口(vkCreateInstance阶段)就完成信号处理函数注册,确保所有后续创建的线程都能继承正确的信号处理配置。
    • 主线程和所有新创建的线程都要显式调用pthread_sigmask,清空SIGSEGV的信号屏蔽位,确保没有线程屏蔽该信号。
  2. 严格保证信号处理函数的异步信号安全性
    • 信号处理函数里只能调用异步信号安全的函数:允许使用mprotect、直接调用write写文件描述符做极简日志,绝对不能调用printf、malloc、free、C++标准库容器操作、锁操作这类非异步信号安全函数。
    • 信号处理逻辑尽量精简:只需要判断触发异常的地址是否属于自己管理的受保护内存区域,如果是就把对应页的权限改成PROT_READ|PROT_WRITE后直接返回;如果不属于管理的内存范围,必须恢复原有信号处理配置,重新触发异常交给原有逻辑处理,不要拦截所有SIGSEGV。
  3. 处理多线程并发竞态问题
    • 存储受保护内存区域的全局结构时,使用无锁结构或者原子变量标记更新状态,保证信号处理函数读取内存区间列表时不会出现并发读写问题,绝对不能在信号处理函数里加锁。
    • 复杂的日志记录、内存访问记录逻辑不要放在信号处理函数里,可以延后到下一次Vulkan API调用时再执行,信号处理函数只做最基础的页权限恢复操作。
  4. 可选稳定性优化
    如果上述修复后仍有偶发稳定性问题,可以替换为Linux 5.1之后引入的userfaultfd机制,直接在用户态处理内存缺页事件,完全避免信号处理的多线程兼容性问题,目前RenderDoc、Mesa内置的内存跟踪模块都已经切换到该方案。

内容的提问来源于stack exchange,提问作者hiddenbit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 15:51:41