1. 可重入与异步安全
1.1 可重入
可重入函数, 也可以称为是异步信号安全的(async-signal safe), 两者是同一个概念. 可重入函数必定是线程安全的, 而线程安全的函数却不一定可重入.
因为
只有当线程安全函数也可能被信号处理程序调用, 如果信号处理程序的调用也是安全的, 此时, 才能说函数是异步信号安全的(可重入).
可重入 = 异步安全 = 线程安全 + 信号处理函数调用安全
1.2 不可重入
相反, 如果一个函数不是可重入的, 也就是另外的线程或信号处理程序同时调用函数, 是不安全的(可能导致脏数据, 数据不一致, 未定义行为等), 就称函数是不可重入的.
常见的不可重入原因:
- 使用静态数据结构;
- 调用malloc/free;
- 标准I/O函数, 标准I/O库很多实现都以不可重入方式使用全局数据结构;
比如库函数printf就是不可重入的, 因为printf自带库缓存, 信号处理程序可能中断主程序printf调用, 然后在信号处理程序中调用printf. 这样, 库缓存中的数据完整性可能会被破坏, 也就无法保证产生期望的结果.
如果一个线程安全函数使用了静态数据结构, 那么如何保证信号处理程序对静态数据结构的使用是安全的呢?
比如errno, 每个线程都会维护一个副本, 信号处理程序也可能会访问, 如果信号处理程序可能会修改errno值, 但显然会导致对应线程errno也修改, 产生不安全行为, 因为中断处可能另外一个函数正在访问errno值.
可以这样做, 避免不安全行为:
- 进入信号处理程序时, 保存errno值;
- 退出信号前, 恢复errno值;
如果使用了锁的线程安全函数, 调用了不可重入的函数如I/O库函数printf, 信号处理程序如果也要调用printf, 如何处理?
如果直接在信号处理程序调用该上锁的线程安全函数, 很可能会导致死锁, 因为信号处理程序中断了函数的执行, 但函数未释放锁, 信号处理程序会阻塞等待锁, 无法正常返回中断处.
当然可以使用递归锁来解决同一线程重复上锁导致的死锁问题, 但是无法保证printf库缓存不被破坏, 产生预期一致输出.
通常的解决办法是, 在特定区域屏蔽信号响应.
阻塞/屏蔽SIGINT信号, 以屏蔽由Ctrl + C产生的SIGINT为例, 屏蔽信号方法:
-
调用signal()设置, 屏蔽SIGINT信号处理
-
通过sigprocmask()修改进程的signal mask (单线程)
-
通过pthread_sigmask()修改线程的signal mask(多线程)
2. 线程安全
什么是线程安全?
APUE 10.6提到,
如果一个函数在相同的时间点可以被多个线程安全地调用, 就称该函数是线程安全的.
什么样的函数是线程安全的?
1)没有任何全局/静态数据结构等外部变量的访问, 这种函数通常也是可重入的;
2)有访问了全局/静态数据结构等共享资源, 但是对其访问前进行了加锁, 访问后释放锁, 通常有互斥锁, 读写锁, 文件锁等, 确保同一时间点, 不会有2个线程同时访问资源.