在c代码的信号处理函数中访问共享原子对象
在c代码的信号处理函数中访问共享原子对象
yujian0231 发表于3年前
在c代码的信号处理函数中访问共享原子对象
  • 发表于 3年前
  • 阅读 33
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云 技术升级10大核心产品年终让利>>>   

摘要: <<Secure Coding in C and C++, Second Edition>>的作者 ​Robert C. Seacord,描述了在信号处理函数中访问被共享的对象会导致竞态条件,使得数据处于不一致状态。从历史上来说,原来仅有的在信号处理函数安全访问共享变量的方式是,读写某个volatile sig_atomic_t 类型的变量。但随着c11标准的到来,原子变量现在成为更好的选择。

The CERT® C Coding Standard, Second Edition: 98 Rules for Developing Safe, Reliable, and Secure Systems, Second Edition 很快就会发布(早出来了,就是没找到电子版的)。它已经更新到c11标准,兼容ISO/IEC TS 17961 c 编码守则,这一版里最让我头疼的是SIG31-C:“在信号处理函数中不要访问共享对象“,规则的存在是因为在信号处理函数中访问共享对象会导致竟态条件,使得数据状态不一致。在这篇文章里,我会超越此规则及书中的例子,提供更多的关于信号处理函数中访问共享对象的背景资料。这个规则曾经出现在第一版《cert 安全编码标准》里,但是由于那本书是在c99的范围内讨论,而且原子对象尚未定义,信号处理函数中访问共享对象,唯一合法的方式是,读写volatile sig_atomic_t 类型的变量。以下程序安装sigint 信号处理handler,设置 volatile sig_atomic_t 类型变量e_flag,然后在程序退出之前,测试handler 是否被调用过。

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#if __STDC_NO_ATOMICS__ != 1
#include <stdatomic.h>
#endif

atomic_flag e_flag = ATOMIC_FLAG_INIT;

void handler(int signum) {
  (void)atomic_flag_test_and_set(&e_flag);
}

int main(void) {

  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Main code loop */

  if (atomic_flag_test_and_set(&e_flag)) {
    puts("SIGINT received.");
  }
  else {
    puts("SIGINT not received.");
  }
  return EXIT_SUCCESS;
}

C11, 5.1.2.3,第五段也定义了允许signal handler 读写免锁原子对象,下面就是一个简单访问原子标志变量的例子(但与标准不相容),atomic_flag 类型提供了经典的test-and-set 功能,它有两种状态,设置,清除,c 标准保证对于atomic_flag 类型的读写操作是免锁的。

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#if __STDC_NO_ATOMICS__ != 1
#include <stdatomic.h>
#endif

atomic_flag e_flag = ATOMIC_FLAG_INIT;

void handler(int signum) {
  (void)atomic_flag_test_and_set(&e_flag);
}

int main(void) {

  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Main code loop */

  if (atomic_flag_test_and_set(&e_flag)) {
    puts("SIGINT received.");
  }
  else {
    puts("SIGINT not received.");
  }
  return EXIT_SUCCESS;
}

倘若原子变量被支持,atomic_flag l类型是仅有的被保证是免锁的,同样也是仅有的signal handler中可访问的类型。然而,这种类型的变量只能通过调用原子函数来有效存取,但是这种调用又不是被允许的。根据c标准7.14.1.1,章节5,signal handler 中调用标准库中函数,除了函数abort,_Exit,quick_exit,第一个参数是引起此次signal handler被调用的signal 编号的signal 函数,调用其他一切函数都会产生未定义行为。

这个限制的存在是由于大多数库函数不一定是异步信号安全的。为了在不改动标准的前提下,解决这个问题,我们需要使用不一样的原子类型重写上述代码,比如atomic_int:

#include <signal.h>
#include <stdlib.h>
#if __STDC_NO_ATOMICS__ != 1
#include <stdatomic.h>
#endif

atomic_int e_flag = ATOMIC_VAR_INIT(0);

void handler(int signum) {
  e_flag = 1;
}

int main(void) {
  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Main code loop */

  if (e_flag) {
    puts("SIGINT received.");
  }
  else {
    puts("SIGINT not received.");
  }
  return EXIT_SUCCESS;
}

这个解决方案只在atomic_int 类型总是免锁的平台上成功。如果原子变量不被支持,或者atomic_int 类型从来都不是免锁的情况下, 下面代码导致编译器输出诊断消息:

#if __STDC_NO_ATOMICS__ == 1
#error "Atomics is not supported"
#elif ATOMIC_INT_LOCK_FREE == 0
#error "int is never lock-free"
#endif

ATOMIC_INT_LOCK_FREE 宏可能被定义是0,表示atomic_int 绝不是免锁的;值 1 表示有时是免锁的;值2表示总是免锁的。如果这个类型是有时免锁的,需要在运行时调用atomic_is_lock_free 函数来确定是否它是免锁的:

#if ATOMIC_INT_LOCK_FREE == 1
  if (!atomic_is_lock_free(&e_flag)) {
    return EXIT_FAILURE;
  }
#endif

atomic 类型出现有时是免锁的情况是,由于某些机器架构,一些处理器支持免锁行为的比较且交换(cas),另一些不支持(80386 vs 80486)。依赖于处理器变种,应用程序可能被绑定到不同的动态库,所以在ATOMIC_INT_LOCK_FREE == 1的情形,有必要引入运行时检查机制。下面程序在atomic_int 是免锁时,运转:

#include <signal.h>
#include <stdlib.h>
#if __STDC_NO_ATOMICS__ != 1
#include <stdatomic.h>
#endif

#if __STDC_NO_ATOMICS__ == 1
#error "Atomics is not supported"
#elif ATOMIC_INT_LOCK_FREE == 0
#error "int is never lock-free"
#endif

atomic_int e_flag = ATOMIC_VAR_INIT(0);

void handler(int signum) {
  e_flag = 1;
}

int main(void) {
#if ATOMIC_INT_LOCK_FREE == 1
  if (!atomic_is_lock_free(&e_flag)) {
    return EXIT_FAILURE;
  }
#endif
  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Main code loop */

  if (e_flag) {
    puts("SIGINT received.");
  }
  else {
    puts("SIGINT not received.");
  }
  return EXIT_SUCCESS;
}

有一个问题,就是为什么e_flag 变量没有使用volatile 修饰,与第一个使用volatile sig_atomic_t 例子不同,对原子对象的load,store 操作有着memory_order_seq_cst(顺序一致)语义.符合顺序一致性的程序的行为就好像构成它的线程之间交错操作结果,每次对象值的计算成为此次交错中最后存入的。原子操作的参数这样本身就具备每次从其地址读取新鲜的值,不在需要volatile修饰。

在支持并发方面,c标准委员会(wg14)追随了c++标准委员会(wg21)脚步。wg21 的意图是使得在c++11标准下的signal handler 用的上免锁的原子变量。不幸的是,wg21 犯了一些错误想争取能在c++14里修复。在c++里指定程序signal handler行为的最新提案是WG21/N3910。这个导致了c++14草案里有了以下描述:

signal handler 被调用,作为调用raise 函数的结果,出现在发起raise 调用的同一线程里。否则,哪个线程执行signal handler 是未定义的。

posix 要求如果信号是为某个进程,或进程里某个特定线程产生,要做出决策。如果信号生成是由于某个线程执行了特别动作,如硬件错误,那么此信号就是为导致这个信号产生的线程生成。如果信号产生关联了进程id,进程组id,或者像终端活动这种异步事件,那么信号就是为进程生成。

存取volatile 对象会根据抽象机器的规则严格计算。volatile 对象上的操作不能被实现优化掉。在原子对象出现以前,volatile为signal handler共享对象提供了最为近似语义。现在原子对象成了更好的选择,因为volatile 没有强制与其他线程之间的可见性规则,使得用它几乎不能提供跨线程工作。所以,volatile sig_atomic_t 仅能用在signal handler 运行在与sig_atomic_t 变量同一个线程的情况。

c标准没有允许在多线程程序里安装signal handler,c11 明确说明了在多线程程序里使用signal 函数行为是未定义的。所以说,对于遵循标准的c 多线程程序,谈论如何处理信号是无意义的。

下面代码是这个程序最具移植性的版本,使用了类型替换,一切在编译时已知。这个例子,免锁原子类型编译时能确定可用,就用atomic type,否则使用 volatile sig_atomic_t 。所以 ,如果 ATOMIC_INT_LOCK_FREE == 1,也被当做是0。

#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#if __STDC_NO_ATOMICS__ != 1
#include <stdatomic.h>
#endif

#if __STDC_NO_ATOMICS__ == 1
  typedef volatile sig_atomic_t flag_type;
#elif ATOMIC_INT_LOCK_FREE == 0 || ATOMIC_INT_LOCK_FREE == 1
  typedef volatile sig_atomic_t flag_type;
#else
  typedef atomic_int flag_type;
#endif

flag_type e_flag;

void handler(int signum) {
  e_flag = 1;
}

int main(void) {
  if (signal(SIGINT, handler) == SIG_ERR) {
    return EXIT_FAILURE;
  }

  /* Main code loop */

  if (e_flag) {
    puts("SIGINT received.");
  }
  else {
    puts("SIGINT not received.");
  }

  return EXIT_SUCCESS;
}

根据c标准,static ,thread local storage 生命周期的对象,0 值初始化,所以e_flag 不必显式初始化。

结语

当前在c/c++ 的signal handler 里访问共享对象存在问题(有希望在c++14 里解决)。当前呼声趋向于修改c标准,使得能在signal handler 里调用原子标志函数,此提案已经提交到wg14。Austin 小组目前在致力于把c11 集成入posix 标准第八次发行(最近的是IEEE Std 1003.1, 2013 Edition, issue 7)。由于在c 的多线程代码里调用signal 函数 是未定义的,posix 可以拓展语言规范,为此未定义行为提供具体定义。长远来看,c/c++ 标准委员会希望抛弃 volatile sig_atomic_t ,因为它不能支持多线程执行,而且原子类型提供了更好的替代。

gcc-4.9 添加了缺失的 stdatomic.h 

标签: signal atomic c
共有 人打赏支持
粉丝 5
博文 12
码字总数 8006
评论 (0)
×
yujian0231
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: