【问题标题】:How does sig_atomic_t actually work?sig_atomic_t 实际上是如何工作的?
【发布时间】:2014-07-24 10:39:25
【问题描述】:

编译器或操作系统如何区分 sig_atomic_t 类型和普通的 int 类型变量,并确保操作是原子的?使用两者的程序具有相同的汇编代码。使操作原子化需要多加注意?

【问题讨论】:

    标签: c linux signals


    【解决方案1】:

    sig_atomic_t 不是原子数据类型。它只是允许您在信号处理程序的上下文中使用的数据类型,仅此而已。所以最好把这个名字读成“相对于信号处理的原子”。

    为了保证与信号处理程序的通信,只需要原子数据类型的一个属性,即读取和更新将始终看到一致的值这一事实。其他数据类型(可能是long long)可以用几个汇编指令来编写较低和较高的部分,例如sig_atomic_t保证一次读写。

    因此,平台可以选择任何整数基类型作为sig_atomic_t,它可以保证volatile sig_atomic_t 可以安全地用于信号处理程序。许多平台为此选择了int,因为他们知道int 是用一条指令编写的。

    最新的 C 标准 C11 具有原子类型,但它们是完全不同的东西。其中一些(“无锁”)也可以用于信号处理程序,但这又是完全不同的故事了。

    【讨论】:

    • @Satchit,他们没有区别对待。为此使用普通int 的平台可以保证volatile int 足以用于信号处理程序。这通常不是您作为用户应该或可能知道的,但编译器实现者必须查找并为此选择正确的基类型。再说一次 sig_atomic_t 不是原子类型
    • 那么在多线程应用中它没有任何意义吗?将全局变量声明为 volatile int a 与声明 volatile sig_atomic_t a 相同?
    • @Satchit,C11 之前的 C 甚至没有线程模型,因此 C 标准的 sig_atomic_t 类型根本没有假设这是否适合多线程。是的,通常它不适合线程间通信,请勿将其用于此目的。 volatile 也与线程安全没有太大关系。正如我所说,引入线程的 C11 也引入了适当的原子数据类型。
    • sig_atomic_t 不是原子数据类型sig_atomic_t 的定义是:“定义的类型是 sig_atomic_t,它是可以作为原子实体访问的对象的(可能是 volatile 限定的)整数类型,即使存在异步中断也是如此。”该标准明确规定“sig_atomic_t ... 是 ... 一个可以作为原子实体访问的对象。”在多线程上下文中仍然存在可见性问题(volatile 对此无济于事),但访问本身似乎需要是原子的。
    • @AndrewHenle,实际上原子一词在这种情况下并没有得到很好的定义。它可能意味着不同的东西,在这里它只是意味着“不可分割”。这个定义不同于后来在 C11 标准中添加的 atomic 的定义。
    【解决方案2】:

    请注意,sig_atomic_t 不是线程安全的,只有异步信号安全。

    原子涉及两种类型的障碍:

    1. 编译器屏障。它确保编译器不会相对于对其他变量的读取和写入对原子变量的读取/写入重新排序。这就是 volatile 关键字的作用。
    2. CPU 屏障和可见性。它确保 CPU 不会重新排序读取和写入。在 x86 上,所有对对齐的 1、2、4、8 字节存储的加载和存储都是原子的。可见性确保商店对其他线程可见。同样,在 Intel CPU 上,由于cache coherence and memory coherence protocol MESI,存储对其他线程立即可见。但这在未来可能会改变。有关详细信息,请参阅英特尔® 64 和 IA-32 架构软件开发人员手册第 3A 卷中的第 8.1 节锁定原子操作。

    对于综合治疗的主题观看atomic Weapons: The C++ Memory Model and Modern Hardware

    【讨论】:

    • 不相关。即使sig_atomic_t 的名称中有 atomic,它也不是 atomic 数据类型。
    • @JensGustedt 您将原子性与可见性混淆了。
    • 怎么样?我不认为我是一个混淆事物的人。我只是提醒您注意 C 标准中的一个事实,即sig_atomic_t 不是原子数据类型。它只是为信号处理程序提供了某些保证。请看我的回答。
    • @JensGustedt The C11 standard 始终将 sig_atomic_t 定义为原子对象:“定义的类型是 sig_atomic_t,它是(可能是 volatile 限定的)整数类型的对象,可以即使存在异步中断,也可以作为原子实体访问。”
    • @MaximEgorushkin 您误解了定义。 sig_atomic_t始终是原子的。定义的“即使存在异步中断”部分并没有将原子性限制为异步中断,它只是强调原子性的要求包括它们。请注意,我并不是说sig_atomic_t 是线程安全的——这确实需要使用屏障。但是,原子性确实意味着 entire 值可以在没有互斥锁之类的东西的情况下访问以保证完整性。仍然需要解决线程安全所需更改的可见性
    【解决方案3】:

    sig_atomic_t 通常只是一个typedef(对于某些系统特定的整数类型,通常是intlong)。并且使用volatile sig_atomic_t(不仅仅是sig_atomic_t)非常重要。

    当你添加volatile关键字时,编译器不得不避免很多优化。

    最近添加了C11 标准_Atomic<stdatomic.h>。您需要最近的GCC(例如4.9)才能获得支持。

    【讨论】:

    • Pelles C IDE 具有完整的 c11 和 stdatomic.c 支持。
    • IDE 无关紧要。 _Atomic & C11 需要 compiler (不是 IDE) 支持。而且 Pelles C 似乎无法在 Linux 上运行。
    • 与任何其他 en.wikipedia.org/wiki/Integrated_development_environment 一样,Pelles 包含一个编译器:smorgasbordet.com/pellesc
    • No IDEs 不 include 而只是 使用interface 到编译器。从 IDE 的角度来看,编译器是一些外部工具。 IDE 只是美化的编辑器(例如 Eclipse CDT 使用 gcc)。您可以使用vimemacs;顺便说一句,您到 Pelles 的链接说只支持 Windows(不支持 Linux),并且他们使用 lcc 的某些变体
    • 是的,它使用的是lcc版本,完全支持c11。
    【解决方案4】:

    使用两者的程序具有相同的汇编代码。使操作原子化需要多加注意?

    虽然这是一个老问题,但我认为仍然值得专门解决这部分问题。在 Linux 上,sig_atomic_t 由 glibc 提供。 glibc 中的sig_atomic_tint 的类型定义,没有特殊处理(截至本文)。 glibc docs 地址:

    在实践中,您可以假设 int 是原子的。你也可以假设 指针类型是原子的;这很方便。这两个 假设在 GNU C 库的所有机器上都是正确的 支持我们知道的所有 POSIX 系统。

    也就是说,在 glibc 支持的所有平台上,普通的int 已经满足了sig_atomic_t 的要求,不需要特殊支持。尽管如此,C 和 POSIX 标准还是强制要求 sig_atomic_t,因为我们可能希望在某些特殊机器上实现 C 和 POSIX,而 int 不满足 sig_atomic_t 的要求。

    【讨论】:

    • 在野外似乎有不同的“原子”含义。 C11 原子类型是线程安全的原子类型,在运行时使用内存屏障。 sig_atomic_t “原子性”并没有提供同样的保证。
    【解决方案5】:

    这种数据类型似乎是原子的。
    来自here

    24.4.7.2 原子类型 为了避免中断对变量的访问的不确定性,您可以使用特定的数据类型来访问 总是原子的:sig_atomic_t。读写这种数据类型是 保证在一条指令中发生,所以没有办法 处理程序在访问的“中间”运行。

    类型 sig_atomic_t 始终是整数数据类型,但它是哪一种 是,它包含多少位,可能因机器而异。

    数据类型:sig_atomic_t 这是一个整数数据类型。这个的对象 类型总是以原子方式访问。

    在实践中,您可以假设 int 是原子的。你也可以假设 指针类型是原子的;这很方便。这两个 假设在 GNU C 库的所有机器上都是正确的 支持我们知道的所有 POSIX 系统。

    【讨论】:

      【解决方案6】:

      研究一些内核开发级别的内存模型是值得的……

      无论如何,sig_atomic_t 原子的。 atomic 的正常定义是您无法获得“部分”结果,例如由于并发写入,或并发读写。将任何其他属性附加到“原子”是危险的,并且会导致此处看到的混淆类型。

      因此,当您执行任何类型的 sig_atomic_t 存储时,您可以保证获得旧值,或者在某些内容回读时获得新值——无论是在该存储之前、期间或之后。

      回答您关于“它是如何工作的”的直接问题:编译器将使用基础类型大小并在需要时发出额外的机器指令,以向 CPU 发出它必须执行原子存储和原子读取的信号。

      说了这么多,重要的是要注意,当您尝试读取像 sig_atomic_t 这样的原子变量时,您真的不能说您会得到旧值还是新值。您所知道的是,您不会得到两个相互竞争的不同存储的混合,也不会在存储与您的读取同时发生时混合旧值和新值。

      在 C 语言中,您通常还需要将变量声明为“volatile sig_atomic_t”,否则编译器没有理由不缓存它,并且您使用旧值的时间可能比预期的要长:编译器没有理由强制如果它已经在寄存器中具有先前读取的旧值,则重新读取内存。 “volatile”告诉编译器在需要获取变量的值时总是执行新的内存读取。

      请注意,“volatile”和“sig_atomic_t”都不是足够强大的“编译器屏障”,以确保它不会被编译器优化器重新排序,更不用说 CPU 本身了(这需要内存屏障,而不仅仅是编译器屏障)。如果您需要任何可见性约束。在执行 MMIO 时,其他线程、处理器甚至硬件,您需要“额外的东西”(编译器屏障和内存屏障)。

      这就是 C11 _Atomic 和 C11 内存模型发挥作用的地方。它们不仅仅是关于“原子”读取和存储,它们还包括许多可见性规则和约束。其他实体(MMIO 设备、其他执行线程、其他处理器)。

      【讨论】:

        猜你喜欢
        • 2021-01-21
        • 2011-09-27
        • 2021-12-16
        • 2013-03-14
        • 2021-03-23
        • 2011-02-11
        • 2017-07-31
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多