文档章节

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

yujian0231
 yujian0231
发布于 2015/02/17 17:10
字数 2154
阅读 43
收藏 0
点赞 0
评论 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 

© 著作权归作者所有

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

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

Im刘亚芳 ⋅ 2014/12/02 ⋅ 0

iOS中assign、copy 、retain等关键字的含义

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

CEOIOS ⋅ 2015/04/17 ⋅ 0

并发二:JAVA内存模型

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

wangjie2016 ⋅ 2017/05/19 ⋅ 0

Java并发编程注意事项

保证线程安全的三种方法: 一开始就将类设计成线程安全的, 比在后期重新修复它,更容易. 编写多线程程序, 首先保证它是正确的, 其次再考虑性能. 无状态或只读对象永远是线程安全的. 不要将一个...

candies ⋅ 2014/02/22 ⋅ 0

【09】Effective Java - 并发

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

xixicat ⋅ 2014/07/14 ⋅ 0

java 内存模型

学习,程晓明的《深入理解Java内存模型》。 摘要 文章内容有并发、内存模型、重排序、内存屏障、happens-before规则、as-if-serial语义、顺序一致性内存模型、volatile、锁、final。 并发 并...

___k先生 ⋅ 2017/11/09 ⋅ 0

Java并发教程-3同步

线程间的通信主要是通过共享域和引用相同的对象。这种通信方式非常高效,不过可能会引发两种错误:线程干扰和内存一致性错误。防止这些错误发生的方法是同步。 不过,同步会引起线程竞争,当...

noday ⋅ 2014/04/25 ⋅ 0

线程安全到底是什么意思?

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

李矮矮 ⋅ 2016/10/17 ⋅ 0

java的内存模型

并发 定义:即,并发(同时)发生。在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机...

疯狂的兔子 ⋅ 2015/01/04 ⋅ 0

多线程安全性:每个人都在谈,但是不是每个人都谈地清

要编写多线程安全的代码,最关键的一点就是需要对于共享的和可变的状态进行访问控制: 所谓共享的,指的是该变量可能同时被多个线程访问; 所谓可变的,指的是该变量在生命周期内其值可能放生...

登高且赋 ⋅ 2017/09/20 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

SpringCloud 微服务 (六) 服务通信 RestTemplate

壹 通信的方式主要有两种,Http 和 RPC SpringCloud使用的是Http方式通信, Dubbo的通信方式是RPC 记录学习SpringCloud的restful方式: RestTemplate (本篇)、Feign 贰 RestTemplate 类似 Http...

___大侠 ⋅ 12分钟前 ⋅ 0

React创建组件的三种方式

1.无状态函数式组建 无状态函数式组件,也就是你无法使用State,也无法使用组件的生命周期方法,这就决定了函数组件都是展示性组件,接收Props,渲染DOM,而不关注其他逻辑。 无状态函数式组...

kimyeongnam ⋅ 19分钟前 ⋅ 0

react 判断实例类型

今天在写组件的时候想通过判断内部子元素不同而在父元素上应用不同的class,于是首先要解决的就是如何判断子元素的类型。 这里附上一个讲的很全面的文章: https://www.cnblogs.com/onepixel...

球球 ⋅ 26分钟前 ⋅ 0

Centos7备份数据到百度网盘

一、关于 有时候我们需要进行数据备份,如果能自动将数据备份到百度网盘,那将会非常方便。百度网盘有较大的存储空间,而且不怕数据丢失,安全可靠。下面简单的总结一下如何使用 bypy 实现百...

zctzl ⋅ 39分钟前 ⋅ 0

开启远程SSH

SSH默认没有开启账号密码登陆,需要再配置表中修改: vim /etc/ssh/sshd_configPermitRootLogin yes #是否可以使用root账户登陆PasswordAuthentication yes #是都开启密码登陆ser...

Kefy ⋅ 42分钟前 ⋅ 0

Zookeeper3.4.11+Hadoop2.7.6+Hbase2.0.0搭建分布式集群

有段时间没更新博客了,趁着最近有点时间,来完成之前关于集群部署方面的知识。今天主要讲一讲Zookeeper+Hadoop+Hbase分布式集群的搭建,在我前几篇的集群搭建的博客中已经分别讲过了Zookeep...

海岸线的曙光 ⋅ 50分钟前 ⋅ 0

js保留两位小数方法总结

本文是小编针对js保留两位小数这个大家经常遇到的经典问题整理了在各种情况下的函数写法以及遇到问题的分析,以下是全部内容: 一、我们首先从经典的“四舍五入”算法讲起 1、四舍五入的情况...

孟飞阳 ⋅ 今天 ⋅ 0

python log

python log 处理方式 log_demo.py: 日志代码。 #! /usr/bin/env python# -*- coding: utf-8 -*-# __author__ = "Q1mi""""logging配置"""import osimport logging.config# 定义三种......

inidcard ⋅ 今天 ⋅ 0

mysql 中的信息数据库以及 shell 查询 sql

Information_schema 是 MySQL 自带的信息数据库,里面的“表”保存着服务器当前的实时信息。它提供了访问数据库元数据的方式。 什么是元数据呢?元数据是关于数据的数据,如数据库名或表名,...

blackfoxya ⋅ 今天 ⋅ 0

maven配置阿里云镜像享受飞的感觉

1.在maven目录下的conf/setting.xml中找到mirrors添加如下内容,对所有使用改maven打包的项目生效。 <mirror> <id>alimaven</id> <name>aliyun maven</name> <url>http://maven.al......

kalnkaya ⋅ 今天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部