如何防止 get() 和 set() 之间的竞争条件行为?
how to prevent race condition behaviour between get() and set()?
考虑以下全局变量 foo
及其 get()
和 set()
函数:
int32_t foo;
mutexType fooMutex;
int32_t getfoo(void)
{
int32_t aux;
MutexLock(fooMutex);
aux = foo;
MutexUnlock(fooMutex);
return aux;
}
void setfoo(int32_t value)
{
MutexLock(fooMutex);
foo = value;
MutexUnlock(fooMutex);
}
以下任务修改 foo:
void ResetTask()
{
while(1)
{
setfoo(resetvalue);
AccessPeripheral2();
wait(resetPeriod);
}
}
void ActTask()
{
while(1)
{
foocopy = getfoo();
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( foocopy - x);
}
wait(actPeriod);
}
}
accessPeripheralX() 函数内部有临界区,尽管保护的资源不同。
如果满足 if 条件 (foocopy > 0),但 foo 在 ActTask() 中的 getfoo() 和 setfoo() 之间被重置,那么它将在结束时被设置为一个过时的无效值循环,产生错误的计算。
如何防止这种竞争情况?
将 ActTask() 重写为:
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( getfoo() - x);
}
实际上消除了它,但理论上任务仍然可以在获取和设置 foo 之间被抢占。
我还想过在处理foo的时候再引入一个临界区:
[Enter critical section]
foocopy = getfoo();
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( foocopy - x);
}
[Leave critical section]
和
[Enter critical section]
setfoo(resetvalue);
AccessPeripheral2();
wait(resetPeriod);
[Leave critical section]
但我倾向于避免这种选择,因为我发现嵌套关键部分会大大增加复杂性(以及出现错误的可能性)。
你只有一个关键部分(见下面的假设),它从读foo
到写,因为写取决于读值。不是写入值取决于读取值,事实即它是写入的
只要读取的值不影响写入(无论是值还是写入本身),读取本身就没有竞争条件。
例如。读取值然后将其打印到控制台以供人类思考是无害的。
只要要写入的值不依赖于当前值,写入本身就不存在竞争条件。例如。尝试将 5 写入包含 4 的变量是无害的。但是,如果这个想法是在写入之后,变量应该比写入之前高一个,那么你就会遇到任何其他增量访问的竞争条件。
例如。 4 上的两个抢占式增量,两者都应该有效果,即正确的结果应该是 6,如果没有保护,最终可能会变成 5。
我假设 reading/writing 单个变量是原子的。 IE。如果您尝试写入一个给定值,而抢占发生在要写入的不同值上,那么任何一个值都会在变量中结束,而不是两者的混合,例如一个值的高字节,另一个值的低字节。读取相同,即使用抢占式写入访问的读取将产生旧的一致值或新的一致值。
严格来说这是不正确的,即不需要符合标准的编译器来保证这一点。如果您担心这种特殊的原子性问题,请将保护保留在 getter 和 setter 中,或者使用隐式保证这一点的写入机制。
您描述的问题的竞争条件从读取权限开始
foocopy = getfoo();
因为这里读取的值影响写权限
setfoo( foocopy - x);
通过只有在变量包含大于零的值时才应写入新值的意图,从
中可以明显看出
if (foocopy > 0)
即实际要保护的临界区是这个
foocopy = getfoo();
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( foocopy - x);
}
这当然正是您在倒数第二个代码片段中保护的部分。
然而,没有必要保护所有最后的代码片段。只需要在易受攻击的临界区期间阻止写入 foo。这并不是因为写作本身就是一个易受攻击的关键部分;这只是 "attacker",潜在的问题。它需要被锁定,以防止上述易受攻击的关键部分。
综上所述,我提出以下建议:
int32_t foo;
mutexType paranoiaMutex;
/* only needed to protect the unrelated critical section of reading or writing,
separatly, if that is not atomic */
int32_t getfoo(void)
{
int32_t aux;
MutexLock(paranoiaMutex); /* or use atomic mechanism */
aux = foo;
MutexUnlock(paranoiaMutex); /* or use atomic mechanism */
/* note this by the way,
you might have overlooked something in your design here */
return aux; /* not foo */
}
void setfoo(int32_t value)
{
MutexLock(paranoiaMutex); /* or use atomic mechanism */
foo = value;
MutexUnlock(paranoiaMutex); /* or use atomic mechanism */
/* Using fooMutex additionally here is imaginable,
but in order to minimise the confusion of nested mutexes,
I propose to use that mutex on the same "layer" of coding.
Note that only one mutex and one layer of mutex nesting
is occuring inside this code part.
You ARE right about being careful with that...
*/
}
和
mutexType fooMutex;
/* Note that only one mutex and one layer of mutex nesting
is occuring inside this code part. */
void ResetTask()
{
while(1)
{
MutexLock(fooMutex);
setfoo(resetvalue);
MutexUnlock(fooMutex);
AccessPeripheral2();
wait(resetPeriod);
}
}
void ActTask()
{
while(1)
{
MutexLock(fooMutex);
foocopy = getfoo();
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( foocopy - x);
}
MutexUnlock(fooMutex);
wait(actPeriod);
}
}
锁定互斥体的持续时间可能存在优化,即 "expensive"。
此优化假设
AccessPeripheral1()
慢(外设访问多)
- 如果
foo<=0
AccessPeripheral1();
是可以的(可能不需要)
- 早点做
AccessPeripheral1();
就可以了,
特别是在阅读 foo
之前(这可能是不受欢迎的)
这些假设很重要,需要验证,我当然做不到。
但如果假设适用,则如下更改,以获得更短的临界区。
void ActTask()
{
while(1)
{
x = AccessPeripheral1();
MutexLock(fooMutex);
foocopy = getfoo();
if (foocopy > 0)
{
setfoo( foocopy - x);
}
MutexUnlock(fooMutex);
wait(actPeriod);
}
}
考虑以下全局变量 foo
及其 get()
和 set()
函数:
int32_t foo;
mutexType fooMutex;
int32_t getfoo(void)
{
int32_t aux;
MutexLock(fooMutex);
aux = foo;
MutexUnlock(fooMutex);
return aux;
}
void setfoo(int32_t value)
{
MutexLock(fooMutex);
foo = value;
MutexUnlock(fooMutex);
}
以下任务修改 foo:
void ResetTask()
{
while(1)
{
setfoo(resetvalue);
AccessPeripheral2();
wait(resetPeriod);
}
}
void ActTask()
{
while(1)
{
foocopy = getfoo();
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( foocopy - x);
}
wait(actPeriod);
}
}
accessPeripheralX() 函数内部有临界区,尽管保护的资源不同。
如果满足 if 条件 (foocopy > 0),但 foo 在 ActTask() 中的 getfoo() 和 setfoo() 之间被重置,那么它将在结束时被设置为一个过时的无效值循环,产生错误的计算。
如何防止这种竞争情况?
将 ActTask() 重写为:
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( getfoo() - x);
}
实际上消除了它,但理论上任务仍然可以在获取和设置 foo 之间被抢占。
我还想过在处理foo的时候再引入一个临界区:
[Enter critical section]
foocopy = getfoo();
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( foocopy - x);
}
[Leave critical section]
和
[Enter critical section]
setfoo(resetvalue);
AccessPeripheral2();
wait(resetPeriod);
[Leave critical section]
但我倾向于避免这种选择,因为我发现嵌套关键部分会大大增加复杂性(以及出现错误的可能性)。
你只有一个关键部分(见下面的假设),它从读foo
到写,因为写取决于读值。不是写入值取决于读取值,事实即它是写入的
只要读取的值不影响写入(无论是值还是写入本身),读取本身就没有竞争条件。
例如。读取值然后将其打印到控制台以供人类思考是无害的。
只要要写入的值不依赖于当前值,写入本身就不存在竞争条件。例如。尝试将 5 写入包含 4 的变量是无害的。但是,如果这个想法是在写入之后,变量应该比写入之前高一个,那么你就会遇到任何其他增量访问的竞争条件。
例如。 4 上的两个抢占式增量,两者都应该有效果,即正确的结果应该是 6,如果没有保护,最终可能会变成 5。
我假设 reading/writing 单个变量是原子的。 IE。如果您尝试写入一个给定值,而抢占发生在要写入的不同值上,那么任何一个值都会在变量中结束,而不是两者的混合,例如一个值的高字节,另一个值的低字节。读取相同,即使用抢占式写入访问的读取将产生旧的一致值或新的一致值。
严格来说这是不正确的,即不需要符合标准的编译器来保证这一点。如果您担心这种特殊的原子性问题,请将保护保留在 getter 和 setter 中,或者使用隐式保证这一点的写入机制。
您描述的问题的竞争条件从读取权限开始
foocopy = getfoo();
因为这里读取的值影响写权限
setfoo( foocopy - x);
通过只有在变量包含大于零的值时才应写入新值的意图,从
中可以明显看出if (foocopy > 0)
即实际要保护的临界区是这个
foocopy = getfoo();
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( foocopy - x);
}
这当然正是您在倒数第二个代码片段中保护的部分。
然而,没有必要保护所有最后的代码片段。只需要在易受攻击的临界区期间阻止写入 foo。这并不是因为写作本身就是一个易受攻击的关键部分;这只是 "attacker",潜在的问题。它需要被锁定,以防止上述易受攻击的关键部分。
综上所述,我提出以下建议:
int32_t foo;
mutexType paranoiaMutex;
/* only needed to protect the unrelated critical section of reading or writing,
separatly, if that is not atomic */
int32_t getfoo(void)
{
int32_t aux;
MutexLock(paranoiaMutex); /* or use atomic mechanism */
aux = foo;
MutexUnlock(paranoiaMutex); /* or use atomic mechanism */
/* note this by the way,
you might have overlooked something in your design here */
return aux; /* not foo */
}
void setfoo(int32_t value)
{
MutexLock(paranoiaMutex); /* or use atomic mechanism */
foo = value;
MutexUnlock(paranoiaMutex); /* or use atomic mechanism */
/* Using fooMutex additionally here is imaginable,
but in order to minimise the confusion of nested mutexes,
I propose to use that mutex on the same "layer" of coding.
Note that only one mutex and one layer of mutex nesting
is occuring inside this code part.
You ARE right about being careful with that...
*/
}
和
mutexType fooMutex;
/* Note that only one mutex and one layer of mutex nesting
is occuring inside this code part. */
void ResetTask()
{
while(1)
{
MutexLock(fooMutex);
setfoo(resetvalue);
MutexUnlock(fooMutex);
AccessPeripheral2();
wait(resetPeriod);
}
}
void ActTask()
{
while(1)
{
MutexLock(fooMutex);
foocopy = getfoo();
if (foocopy > 0)
{
x = AccessPeripheral1();
setfoo( foocopy - x);
}
MutexUnlock(fooMutex);
wait(actPeriod);
}
}
锁定互斥体的持续时间可能存在优化,即 "expensive"。
此优化假设
AccessPeripheral1()
慢(外设访问多)- 如果
foo<=0
AccessPeripheral1();
是可以的(可能不需要) - 早点做
AccessPeripheral1();
就可以了, 特别是在阅读foo
之前(这可能是不受欢迎的)
这些假设很重要,需要验证,我当然做不到。
但如果假设适用,则如下更改,以获得更短的临界区。
void ActTask()
{
while(1)
{
x = AccessPeripheral1();
MutexLock(fooMutex);
foocopy = getfoo();
if (foocopy > 0)
{
setfoo( foocopy - x);
}
MutexUnlock(fooMutex);
wait(actPeriod);
}
}