我最近在学习名为 Advanced Linux Programming 的书,我遇到了这个问题:书中说你应该使用sig_atomic_t
变量类型来确保如果你在信号处理函数中设置全局标志或计数器,则不会发生上下文切换算术运算(即++
)并将其保存到寄存器中。
我的问题是:如果我们不使用sig_atomic_t
而只使用另一种类型并且发生上下文切换会发生什么?例如,我的意思是该程序稍后会返回并保存它。有人可以给我一个场景,它会使我们的代码不稳定或有问题吗?
我最近在学习名为 Advanced Linux Programming 的书,我遇到了这个问题:书中说你应该使用sig_atomic_t
变量类型来确保如果你在信号处理函数中设置全局标志或计数器,则不会发生上下文切换算术运算(即++
)并将其保存到寄存器中。
我的问题是:如果我们不使用sig_atomic_t
而只使用另一种类型并且发生上下文切换会发生什么?例如,我的意思是该程序稍后会返回并保存它。有人可以给我一个场景,它会使我们的代码不稳定或有问题吗?
在您描述的场景中运行的风险(从内存读取到寄存器、更新寄存器、写入内存以及在任何这些操作之间发生上下文切换)是您可能会丢失在其他上下文中进行的更新。
例如:
main context:
read i (=10) from memory to register R1
add 5 to R1
<interrupt. Switch to interrupt context>
read i (=10) from memory to register R1
add 10 to R1
write R1 to i in memory (i = 20)
<end of interrupt. Back to main context>
write R1 to i in memory (i = 15)
如您所见,来自中断的更新已丢失。
如果您的类型需要多次操作才能将其写入内存并且中断发生在写入操作的中间,则会出现更大的问题。
例如:
main context:
read first half of i (=10) from memory to register R1
read second half of i (=10) from memory to register R2
add 5 to R1/R2 pair
write R1 to first half of i in memory
<interrupt. Switch to interrupt context>
read first half of i (= ??) from memory to register R1
read second half of i (= ??) from memory to register R2
add 10 to R1/R2 pair
write R1 to first half of i in memory
write R2 to second half of i in memory
<end of interrupt. Back to main context>
write R2 to second half of i in memory
在这里,不知道我最终会得到什么价值。
有了sig_atomic_t
,就不会出现第二个问题,因为该类型保证使用原子读/写操作。
这是一个导致不安全行为的示例:
int64_t a = 2^32-1;
void some_signal_handler()
{
++a;
}
void f()
{
if( a == 0 )
printf("a is zero");
}
假设一个 32 位架构。变量 a 实际上存储为 2 个 32 位整数,并以 {0,2^32-1} 开头。首先 f 将 a 的上半部分读取为 0。然后出现一个信号,执行切换到信号处理程序。它将 a 从 2^32-1 增加到 2^32 a 的新值是 {1,0}。信号处理程序完成并且 f 的执行继续。f 将 a 的下半部分读为 0。总共 f 将 a 读为 0,这是从未打算过的。
比从信号处理程序写入变量更好的解决方案是保持管道打开并从信号处理程序向管道写入值。这样做的好处是它可以在没有任何竞争条件的情况下唤醒一个选择(只要您在管道的读取端进行选择),并且使您可以在 main 中对信号进行大部分主要处理选择循环,您可以在其中自由使用所需的任何库函数。