文档章节

在c代码的信号处理函数中访问共享原子对象

yujian0231
 yujian0231
发布于 2015/02/17 17:10
字数 2154
阅读 60
收藏 0

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 

本文转载自:http://www.informit.com/articles/article.aspx?p=2204014

共有 人打赏支持
yujian0231
粉丝 4
博文 17
码字总数 16174
作品 0
昌平
私信 提问
iOS中assign、copy 、retain等关键字的含义

assign: 简单赋值,不更改索引计数 copy: 建立一个索引计数为1的对象,然后释放旧对象 retain:释放旧的对象,将旧对象的值赋予输入对象,再提高输入对象的索引计数为1 Copy其实是建立了一...

CEOIOS
2015/04/17
0
0
iOS中assign、copy 、retain等关键字的含义

assign: 简单赋值,不更改索引计数 copy: 建立一个索引计数为1的对象,然后释放旧对象 retain:释放旧的对象,将旧对象的值赋予输入对象,再提高输入对象的索引计数为1 Copy其实是建立了一...

Im刘亚芳
2014/12/02
0
0
并发二:JAVA内存模型

Java Memory Model JAVA内存模型(Java Memory Model 简称JMM),定义了程序中各个共享变量的访问规则。 Java Memory Model 程序中的变量都存储在主内存中,而每个线程拥有自己的工作内存用来...

wangjie2016
2017/05/19
0
0
【09】Effective Java - 并发

1、同步访问共享的可变数据 (1)同步的两个作用 A、可以阻止一个线程看到对象处于不一致的状态之中 B、保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前所有的修改效...

xixicat
2014/07/14
0
0
线程安全到底是什么意思?

本文转发自Jason’s Blog,原文链接 http://www.jasongj.com/java/thread_safe/ 多线程编程中的三个核心概念 原子性 这一点,跟数据库事务的原子性概念差不多,即一个操作(有可能包含有多个...

李矮矮
2016/10/17
89
0

没有更多内容

加载失败,请刷新页面

加载更多

zookeeper配置与使用

一.登录官网下载 不要带后缀的,那是公侧版本,下稳定版,比如3.4.9 二.安装与使用 解压后bin里是启动程序 配置文件:在conf下 复制zoo_sample.cfg改名为为zoo.cfg,打开zoo修改文件...

小兵胖胖
24分钟前
1
0
spring源码阅读笔记(一)

ClassPathXmlApplicationContext 与 FileSystemXmlApplicationContext 用了这么久的框架,是时候搞一下源码了,一般最初接触spring 从以下步骤开始 创建一个bean类 并创建 ooxx.xml之类的spr...

NotFound403
48分钟前
2
0
MySQL主从配置

12月14日任务 17.1 MySQL主从介绍 17.2 准备工作 17.3 配置主 17.4 配置从 17.5 测试主从同步 MySQL主从介绍 MySQL主从又叫做Replication、AB复制。简单将就是A/B两个服务器做主从后,在A上写...

robertt15
50分钟前
8
0
我的Linux系统九阴真经

在今天,互联网的迅猛发展,科技技术也日新月异,各种编程技术也如雨后春笋一样,冒出尖来了。各种创业公司也百花齐放百家争鸣,特别是针对服务行业,新型互联网服务行业,共享经济等概念的公...

问题终结者
今天
22
0
Java 使用 gson 对 json 根据 key 键进行排序

引入Google的gson jar <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.0</version>......

yh32
今天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部