- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
KBEngine(kbeengine)的服务器端(dbmgr、baseapp、cellapp 等进程)与配套工具链都运行在其自带的 Python 运行时之上(该仓库中嵌入的 CPython 版本为 3.7.3,见 patchlevel.h)。faulthandler正是 CPython 3.3 引入的标准模块,专门解决"进程段错误、死锁或卡死时,Python 层发生了什么"这一诊断难题。本文以 faulthandler.rst 文档为主体,结合仓库内 C 实现 faulthandler.c 与测试用例 test_faulthandler.py 展开,讲清enable()/disable()/dump_traceback()/dump_traceback_later()/register()/unregister()六个核心 API 的语义、底层信号处理机制与典型陷阱,帮助你在 C++/Python 混合的 MMO 服务器进程中落地一套"崩溃必留现场、卡死可留快照"的排障手段。
一、模块概览:在灾难现场仍能写出 Traceback 的 C 扩展
faulthandler提供四类能力:
- 显式转储:随时调用
faulthandler.dump_traceback()把当前线程(或所有线程)的 Python 调用栈写出去; - 致命信号转储:
faulthandler.enable()会为SIGSEGV、SIGFPE、SIGABRT、SIGBUS、SIGILL安装信号处理器,发生致命错误时自动转储 traceback; - 超时转储:
dump_traceback_later(timeout, ...)基于一个 watchdog 线程,在超时后(或每隔超时周期)转储,用于发现死锁与卡死; - 用户信号转储:
register(signum)把某个自定义信号(如SIGUSR1)绑定为"一键 dump"开关。
启用方式有两种编程/启动途径:
- 启动时设置环境变量
PYTHONFAULTHANDLER,或使用-X faulthandler命令行选项。两者的解析逻辑见 main.c 中config_read_complex_options():
if (config->faulthandler < 0) { if (config_get_env_var("PYTHONFAULTHANDLER") || config_get_xoption(config, L"faulthandler")) { config->faulthandler = 1; } }- 在代码中显式调用
faulthandler.enable()。
文档特别强调了几点设计约束,理解它们是使用好该模块的前提:
- 与系统级故障处理器兼容:如 Linux 的 Apport 或 Windows 的故障处理器;
enable()只会"链式"接管,不会破坏原有 handler(源码中faulthandler_fatal_error()在转储后会恢复并触发前一个 handler)。 - 备用信号栈(sigaltstack):若平台支持
sigaltstack,模块会使用备用栈运行信号处理器,因此在栈溢出场景下依然能转储 traceback。初始化代码在_PyFaulthandler_Init()(faulthandler.c 第 1302 行起):分配一块SIGSTKSZ大小的栈并通过sigaltstack()注册,安装sigaction时加上SA_ONSTACK标志。 - 只能用信号安全函数:处理器运行在灾难现场,不能从堆上分配内存,因此转储内容是"精简版"的。文档列出的限制在源码中都能一一对应到宏定义(traceback.c):
| 文档描述的限制 | 源码对应 |
|---|---|
仅支持 ASCII,编码错误采用backslashreplace | _Py_DumpTraceback*系列只写原始字节,不经过 codec |
| 每个字符串最长 500 字符 | #define MAX_STRING_LENGTH 500(traceback.c 第 18 行) |
| 只显示文件名、函数名、行号(不含源码行) | _Py_DumpTraceback()逐帧只输出File "...", line N in func |
| 最多 100 帧、100 线程 | #define MAX_FRAME_DEPTH 100、#define MAX_NTHREADS 100(traceback.c 第 19-20 行) |
| 顺序反转:最近的调用在最前面 | 帧链自内向外遍历输出 |
默认输出目标是sys.stderr,因此要看到 traceback 需要进程运行在终端(或重定向了 stderr);也可以把日志文件传给faulthandler.enable()。由于整个模块用 C 实现,即使解释器已经死锁,watchdog 线程仍然可以完成转储。
二、显式转储:dump_traceback()
faulthandler.dump_traceback(file=sys.stderr, all_threads=True)- 将(所有线程或当前线程的)traceback 写入file;
all_threads=False时只转储当前线程。 - 自 Python 3.5 起,file可以直接传**文件描述符(int)**而不只是文件对象。
从 C 实现看,其处理流程很直接(faulthandler.c 第 250 行faulthandler_dump_traceback_py()):
faulthandler_get_fileno()解析file参数:None→ 取sys.stderr;int→ 直接作为 fd;其他对象 → 调用其fileno()并flush();- 获取当前线程的
PyThreadState,随后调用_Py_DumpTracebackThreads()(全线程)或_Py_DumpTraceback()(单线程),把帧链逐层写入 fd。
一个典型的显式转储用法(比如在长耗时操作前留快照):
import faulthandler def heavy_init(): faulthandler.dump_traceback() # 输出到 stderr,标记"进入初始化" ...需要注意:dump_traceback()本身是普通的 Python 级调用,它要求解释器此刻"还活着";而enable()之后的信号路径则不依赖解释器存活——这是两者的本质区别。
三、致命信号处理器状态管理:enable / disable / is_enabled
enable(file=sys.stderr, all_threads=True)
为SIGSEGV、SIGFPE、SIGABRT、SIGBUS、SIGILL安装处理器,故障发生时转储 traceback。参数语义:
- all_threads:
True(默认)转储所有运行中的线程,False只转储当前线程; - file:输出目标;文档警告——该文件必须保持打开直到 fault handler 被禁用(文件描述符陷阱见下文"五");
- 版本沿革:3.5 起支持直接传 fd;3.6 起在 Windows 上额外安装 Vectored Exception Handler(
AddVectoredExceptionHandler,见源码faulthandler_enable()第 474-477 行),以捕获 access violation 等 Windows 异常。
安装细节上,faulthandler_enable()对每个信号都使用sigaction()并设置SA_NODEFER——允许处理器内部再次收到同名信号,保证转储过程中若发生二次故障,前一个 handler 仍会被调用;如果启用了备用栈,则同时加SA_ONSTACK。
disable() 与 is_enabled()
disable():卸载enable()安装的信号处理器(Windows 上同时移除 vectored exception handler),并释放保存的文件引用;is_enabled():返回当前是否已启用。
从源码结构看,每个信号 handler 都保存了"前一个 handler"(fault_handler_t.previous),禁用时通过sigaction(signum, &previous)精确恢复,因此多次 enable/disable 不会破坏外部(例如 C++ 主程序)注册过的信号处理链。
文档给出的 Linux 段错误示例
文档末尾用一段 shell 会话对比了开/关 fault handler 时的输出,这里完整保留:
$ python3 -c "import ctypes; ctypes.string_at(0)" Segmentation fault $ python3 -q -X faulthandler >>> import ctypes >>> ctypes.string_at(0) Fatal Python error: Segmentation fault Current thread 0x00007fb899f39700 (most recent call first): File "/home/python/cpython/Lib/ctypes/__init__.py", line 486 in string_at File "<stdin>", line 1 in <module> Segmentation fault可以观察到输出格式的三个特征:以Fatal Python error: <信号名>开头的头行(对应源码中faulthandler_handlers[]表里的 name,如 "Segmentation fault"、"Bus error"、"Illegal instruction"、"Floating point exception"、"Aborted")、按线程分组(Current thread 0x... (most recent call first):)、帧自内向外排列且不含源码行。对 C++/Python 混合进程尤其关键的一点:traceback 只覆盖到 Python 帧,C 侧崩溃位置需要结合 core dump 或系统调试器继续定位,faulthandler 的价值在于把 Python 侧的调用上下文固定下来。
四、超时转储:dump_traceback_later / cancel_dump_traceback_later
dump_traceback_later(timeout, repeat=False, file=sys.stderr, exit=False)
在timeout秒后转储所有线程的 traceback;repeat为True时则每隔timeout秒重复转储;exit为True时转储后调用_exit(1)立即终止进程(注意_exit不做任何清理,如刷新文件缓冲区)。语义要点(均来自文档原文):
- 函数被调用两次时,新调用替换旧参数并重置计时器;
- 计时器具有亚秒级分辨率(源码中 timeout 被换算为微秒
timeout_us存储); - 基于 watchdog 线程实现,因此在禁用线程编译的 Python 中不可用;
- 3.5 起支持传 fd。
底层实现(faulthandler.cfaulthandler_thread(),第 553 行起)值得细看:
- watchdog 线程启动后先用
pthread_sigmask()屏蔽所有信号(sigfillset+SIG_SETMASK),确保转储动作不被中断; - 它把"取消"事件建模为锁:主线程持有
thread.cancel_event,watchdog 线程用PyThread_acquire_lock_timed(cancel_event, timeout_us)带超时等待——超时即到期,拿到锁则说明被取消; - 到期后先写格式化好的
Timeout (H:MM:SS.ffffff)!头行(format_timeout()),再调用_Py_DumpTracebackThreads();exit=True时_exit(1); repeat=True时循环重复,直到cancel_dump_traceback_later()释放锁。
cancel_dump_traceback_later()的取消过程是一个精确的锁协议:释放cancel_event(通知 watchdog)→ 等待 watchdog 释放running锁(join 语义)→ 主线程重新持锁并清理 file/header 引用。dump_traceback_later()再次调用时内部会先执行这套取消,因此"重复调用即替换参数并重置计时器"的行为是天然成立的。
一个典型的"防止服务器启动卡死"用法:
import faulthandler, sys faulthandler.enable() # 崩溃兜底 faulthandler.dump_traceback_later(30, repeat=True, file=sys.stderr) try: startup_and_load_data() # 例如加载 EntityDef、导航网格、数据库连接 finally: faulthandler.cancel_dump_traceback_later()若启动流程超过 30 秒,stderr 上会持续出现Timeout (...)!加全线程快照,可以清晰看到是哪个脚本模块、哪一行在持锁等待。
五、用户信号转储:register / unregister
register(signum, file=sys.stderr, all_threads=True, chain=False)
为signum信号安装处理器,收到该信号时把(所有线程或当前线程的)traceback 写入file;chain为True时会继续调用上一个 handler。文档明确说明Windows 上不可用——源码中用#ifndef MS_WINDOWS定义了FAULTHANDLER_USER宏,注释解释:Windows 下进程只能处理SIGSEGV/SIGABRT/SIGILL,且这些信号只能走enable()通道,register()没有意义。
实现上的两个值得注意的细节(faulthandler_register(),第 729 行起):
- 默认使用
SA_RESTART标志:如果信号到达时线程正在系统调用中,内核会重启系统调用而不是返回EINTR,减少转储动作对业务代码的干扰; - 当
chain=True时改用SA_NODEFER:dump 完成后先临时恢复旧 handler、raise(signum)交给旧 handler 执行,再重新装回 faulthandler 的 handler,整个过程对业务透明。
同时check_signum()做了两层校验:传入致命信号(SIGSEGV 等)会报RuntimeError: signal N cannot be registered, use enable() instead;信号号超出[1, NSIG)范围会报ValueError: signal number out of range。
unregister(signum)
卸载由register()安装的处理器;返回True表示该信号之前确实被注册过,否则返回False。同样不可用于 Windows。
运维实战:远端"一键快照"
对常驻服务器进程,把SIGUSR1注册为快照信号是经典用法:
import faulthandler, signal with open("/var/log/kbe_snap.txt", "a") as log: faulthandler.register(signal.SIGUSR1, file=log, all_threads=True)之后kill -USR1 <pid>即可在不中断进程的前提下拿到全线程 Python 调用栈。对 C++ 宿主进程而言,只要该信号没被宿主程序占用,就能与 Python 逻辑共存(handler 保存/恢复链条由源码保证)。
六、文件描述符陷阱(文档专节:Issue with file descriptors)
文档用专节警告了一个容易被忽略的坑,原文要点如下:
enable()、dump_traceback_later()和register()保存的是file参数的文件描述符(fd 整数),而非文件对象本身;- 如果该文件被关闭、其 fd 被新打开的文件复用,或有人用
os.dup2()替换了该 fd,traceback 会被写进另一个文件; - 结论:每次文件被替换时,都必须重新调用这三个函数。
从 C 代码看这一警告的根据:fatal_error、thread、user_signals[]三个状态结构体里保存的都是int fd(fatal_error.fd、thread.fd、user->fd),而faulthandler_get_fileno()在注册时刻就把文件对象解析成了 fd 并只保留 fd 用于写(PUTS(fd, str)即_Py_write_noraise(fd, ...))。对使用管道重定向、日志轮转(logrotate)的服务器进程,这意味着轮转后必须重新enable()/register(),否则崩溃现场会写进被复用给其他用途的 fd。
七、在 KBEngine 代码库中的落位与验证方式
- 模块编译开关:faulthandler 作为标准扩展模块通过 Setup.dist 声明(
faulthandler faulthandler.c,第 135-136 行),随内嵌 CPython 一起构建,服务器各 Python 进程(如kbe/res/scripts/下的服务器脚本、kbe/tools/下的 webconsole/pycommon 工具)导入该模块即可使用,无需额外链接 C++ 侧代码; - 崩溃抑制辅助:
_read_null()、_sigsegv()、_sigabrt()、_sigfpe()、_stack_overflow()等下划线前缀函数是测试专用接口,faulthandler_suppress_crash_report()会同时处理 Windows WER 弹窗(SEM_NOGPFAULTERRORBOX)、禁用 core dump(RLIMIT_CORE)以及 MSVC 的 abort 消息——这解释了为什么测试进程里触发段错误时不会弹出系统对话框; - 回归验证:仓库自带 test_faulthandler.py(800 余行),覆盖了信号转储格式校验(
expected_traceback()用正则比对File "..."行)、fd 复用、dump_traceback_later的超时/重复/取消语义、chain=True的行为等,可作为理解每个 API 边界行为的第一手资料。
八、使用建议小结
- 进程启动最早期调用
faulthandler.enable(file=专用日志),并让该日志在进程生命周期内保持打开,避免 fd 复用陷阱; - 对启动/重载等可能卡死的阶段,用
dump_traceback_later(timeout, repeat=True)兜底,完成后cancel_dump_traceback_later(); - 把
SIGUSR1一类信号register()为在线快照开关,作为不重启进程的最后一道诊断手段; - 记住输出是精简的(无源码行、500 字符截断、100 帧/100 线程上限),它回答的是"Python 层卡/崩在哪",C++ 侧现场仍需配合系统调试手段;
- 在 Windows 平台上
register()/unregister()不可用,致命错误转储则由信号 handler + Vectored Exception Handler 双通道保证。
以上所有行为均以本仓库内嵌的 CPython 3.7.3 实现为准:接口文档见 faulthandler.rst,核心实现见 faulthandler.c,帧转储限长常量见 traceback.c 开头的MAX_STRING_LENGTH/MAX_FRAME_DEPTH/MAX_NTHREADS宏定义,测试见 test_faulthandler.py。
- 游戏开发
- 后端
【免费下载链接】kbengine
A MMOG engine of server.
相关推荐
CPython faulthandler 完全指南:崩溃转储、超时看门狗与用户信号触发的 Python/C 栈回溯
CPython faulthandler 完全指南:崩溃转储、超时看门狗与用户信号触发的 Python/C 栈回溯 faulthandler 是 CPython
编程语言语言运行时解释器标准库KBEngine 内嵌 Python 3.7.3 运行时 datetime 模块全解:日期时间类型、时区协议与格式化机制
KBEngine 内嵌 Python 3.7.3 运行时 datetime 模块全解:日期时间类型、时区协议与格式化机制 在 KBEngine 的服务器端脚本资
游戏开发后端KBEngine 内嵌 Python 的 ast 模块实战指南:抽象语法树解析、遍历与转换
KBEngine 内嵌 Python 的 ast 模块实战指南:抽象语法树解析、遍历与转换 本文以 KBEngine 仓库内置 Python 3.7.3 标准库
游戏开发后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考