【学习笔记】非异步安全函数(禁止在信号处理中调用)

非异步安全函数(禁止在信号处理中调用)

一、测试

在信号处理函数(Signal Handler)中,只有异步信号安全函数(async-signal-safe functions) 可以安全调用。这类函数的特点是:不使用全局状态、不依赖内部锁、不调用其他非安全函数,且能被信号中断后安全恢复。

非异步安全函数(禁止在信号处理中调用)

以下是常见的非异步安全函数分类及示例(覆盖大部分常用函数):

1. 内存分配 / 释放函数

这类函数依赖全局堆管理结构和内部锁,信号处理中调用可能导致堆损坏或死锁。

  • mallocfreerealloccalloc
  • posix_memalignaligned_alloc
2. 标准 I/O 函数

标准 I/O 库(stdio.h)函数使用全局缓冲区和锁,信号中断可能导致缓冲区状态不一致。

  • 输入输出:printffprintfsprintfsnprintfvprintfvfprintfvsprintfvsnprintf
  • 文件操作:fopenfclosefreadfwritefseekfflushfgetsfputs
  • 其他:perror(内部调用strerrorfprintf)、fflush
3. 字符串 / 字符处理函数(依赖静态缓冲区)

这类函数使用静态缓冲区存储结果,信号中断可能导致缓冲区内容被覆盖。

  • strtok(使用静态缓冲区存储分割状态)
  • strerror(部分实现使用静态缓冲区存储错误信息)
  • ctimeasctime(使用静态缓冲区存储时间字符串)
  • getpwuidgetpwnam(部分实现使用静态缓冲区存储用户信息)
  • getgrgidgetgrnam(部分实现使用静态缓冲区存储组信息)
4. 线程 / 同步函数

线程相关函数依赖全局锁或线程私有数据,信号处理中调用可能导致死锁。

  • 互斥锁:pthread_mutex_lockpthread_mutex_unlockpthread_mutex_trylock
  • 条件变量:pthread_cond_waitpthread_cond_signalpthread_cond_broadcast
  • 线程管理:pthread_createpthread_joinpthread_cancel
  • 线程私有数据:pthread_getspecificpthread_setspecific
5. 环境变量 / 全局状态函数

这类函数访问或修改全局状态(如环境变量、locale),信号中断可能导致状态不一致。

  • 环境变量:getenvsetenvunsetenvputenv
  • 区域设置:setlocale
  • 进程信息:getuidgetgid(部分实现可能安全,但建议避免)、getpid(通常安全,但 POSIX 未明确)
6. 其他非安全函数
  • 时间函数:localtimegmtime(使用静态缓冲区存储时间结构)
  • 信号相关:signal(非可重入,建议用sigaction替代)
  • 系统调用包装:system(内部创建子进程并调用 shell,依赖全局状态)

为什么这些函数不安全?

非异步安全函数通常存在以下问题:

  1. 使用全局锁 :如mallocprintf内部有全局锁,若信号处理函数在主程序持有锁时调用,会导致死锁。
  2. 依赖静态缓冲区 :如strtokctime,信号中断可能导致缓冲区数据被覆盖,主程序恢复后读取错误数据。
  3. 修改全局状态 :如setenvsetlocale,信号处理中修改全局状态可能导致主程序逻辑混乱。

安全替代方案

信号处理函数中如需完成复杂操作,应仅通过异步安全函数 做最小化处理(如设置volatile sig_atomic_t标志),再由主程序轮询标志并执行非安全操作。

常见的异步安全函数 包括:write_exitsigprocmaskpthread_sigmaskgetpidgetppid等(完整列表可参考 POSIX 标准man 7 signal-safety)。

总结:信号处理函数中严禁调用上述非异步安全函数,核心原则是 "仅执行最小化、无状态操作",避免依赖全局资源或锁机制。

举例子

在定时器每秒触发信号的场景下,安全通知线程执行操作的核心是:避免在信号处理函数中直接调用非异步安全接口,而是通过 "信号处理函数→安全通信机制→线程" 的间接方式传递通知。

利用管道(pipe)作为信号和线程之间的安全通信桥梁(write 是异步安全函数),线程通过监听管道数据触发操作。

实现步骤:
  1. 创建管道:用于信号处理函数和工作线程的通信。
  2. 信号处理函数 :收到信号后,向管道写 1 字节数据(仅调用 write,异步安全)。
  3. 工作线程:阻塞在管道读端,读取到数据后执行每秒操作。