我正在尝试理解由另一个程序员编写的代码。它使用I²C通信在STM32微控制器的EEPROM上写入数据。
一般来说,我理解他的代码是如何工作的,但我无法理解为什么他使用了HAL_LOCK
和HAL_UNCLOCK
函数。
以下是这些方法的代码:
typedef enum
{
HAL_UNLOCKED = 0x00U,
HAL_LOCKED = 0x01U
} HAL_LockTypeDef;
#if (USE_RTOS == 1)
/* Reserved for future use */
#error "USE_RTOS should be 0 in the current HAL release"
#else
#define __HAL_LOCK(__HANDLE__) \
do{ \
if((__HANDLE__)->Lock == HAL_LOCKED) \
{ \
return HAL_BUSY; \
} \
else \
{ \
(__HANDLE__)->Lock = HAL_LOCKED; \
} \
} while (0)
#define __HAL_UNLOCK(__HANDLE__) \
do{ \
(__HANDLE__)->Lock = HAL_UNLOCKED; \
} while (0)
在哪些情况下可以使用这些方法?
答案 0 :(得分:7)
有人尝试使用这些宏实现基本的,咨询性的,非递归的mutex(有时称为“锁定”或“关键部分”,但这些术语具有其他含义;“互斥”是明确的)。我的想法是你写这样的代码:
int frob_handle(handle_t hdl)
{
__HAL_LOCK(hdl);
hdl->frob_counter += 1;
__HAL_UNLOCK(hdl);
return 0;
}
然后,一次只有一个执行线程可以执行语句hdl->frob_counter += 1
。 (在一个真实的程序中,那里可能会有相当多的代码。)它是“建议性的”,因为没有什么可以阻止你在需要时忘记使用互斥锁,并且它是“非递归的”因为你无法调用如果您已将其锁定,则第二次__HAL_LOCK
。这些都是互斥锁具有相对正常的属性。
在对这个问题的评论中,我说这些宏是“灾难性的错误”,“我相信你最好不要使用它们。”最重要的问题是__HAL_LOCK
不是atomic。
// This code is incorrect.
if ((__HANDLE__)->Lock == HAL_LOCKED)
return HAL_BUSY;
else
(__HANDLE__)->Lock = HAL_LOCKED;
想象一下,两个执行线程试图在同一时间获取锁。它们将同时从内存中获取__HANDLE__->Lock
,因此它们都会将其值视为HAL_UNLOCKED
,并且它们将继续执行仅应由一个线程执行的代码一次。如果我写出可能生成的汇编语言,则可能更容易看到问题:
; This code is incorrect.
; r1 contains the __HANDLE__ pointer
load.b r0, Lock(r1)
test.b r0
bnz .already_locked
inc.b r0
store.b Lock(r1), r0
...
.already_locked:
mov.b r0, #HAL_BUSY
ret
没有什么能阻止两个线程同时执行加载指令,因此都会观察互斥锁被解锁。即使只有一个CPU,当线程1位于加载和存储之间时,也会触发中断,导致上下文切换并允许线程2在线程1执行存储之前执行加载。
要使互斥锁完成其工作,您必须以某种方式确保不可能两个并发线程从HAL_UNLOCKED
加载__HANDLE__->Lock
,这是不可能完成的与普通的C.实际上,用普通的机器语言无法做到;您需要使用特殊说明,例如compare-and-swap。
如果您的编译器实现C2011,那么您可以使用atomic types的新功能获取这些特殊说明,但我不知道如何做到这一点,并且我不打算写出可能有问题的东西。否则,您需要使用编译器扩展或手写程序集。
第二个问题是__HAL_LOCK
没有实现通常称为“锁定”的操作。 “Lock”应该等待如果它无法立即获取锁定,但__HAL_LOCK
做的是失败。该操作的名称是“try-lock”,并且应该相应地命名宏。此外,可能导致调用函数返回的宏被认为是不好的做法。