【问题标题】:Implementation of a semaphore in C在 C 中实现信号量
【发布时间】:2019-02-27 05:20:59
【问题描述】:

我正在用 C 语言实现信号量的简单实现,当我的实现工作(作为二进制信号量)时,我对其有效性有疑问。

我的担忧源于我对等待函数的定义:

void Wait(int semid) {
    char *shmPtr;

    shmPtr = shmat(semid, NULL, 0);

    if(shmPtr == (void *) -1){
        printf("Could not attach to semaphore...\n");
        exit(1);
    } 

    //Wait for the value in shared memory to 
    //equal 0, then set it equal to 1,
    //detach and return
    while( (*shmPtr) != 0);

    (*shmPtr) = 1;

    if(shmdt(shmPtr) < 0) {
        printf("Cannot detach from semaphore...");
    }

    return;

}

我的问题在于 while( (*shmPtr) != 0) 环形。假设我们有 2 个进程正在等待。第三个过程将信号量的值更改为等于 0(忽略这是信号量的二进制实现这一事实)。

我担心的是,如果进程 1 评估 while 循环的条件为假,然后 CPU 上下文在将信号量值设置为 1 之前切换到进程 2,则两个进程都会进入临界区。

有没有更好的方法来实现等待功能?我见过很多人使用 pthread_cond_wait,但它使用了互斥体,这基本上违背了信号量实现的目的。

谢谢

编辑:添加*在 C 中的 TestAndSet 实现以在 cmets 中引用

#define LOCKED 1

int TestAndSet(int* lockPtr) {
    int oldValue;

    oldValue = *lockPtr;
    *lockPtr = LOCKED;
    // -- End of atomic segment --

    return oldValue;
}

【问题讨论】:

  • 这称为竞争条件。您需要一些您没有的原子测试和设置操作之王。否则,两个进程都可以将其视为 0,然后它们都将尝试将其更改为 1,每个进程都认为自己是第一个这样做的。基本上,您无法知道它在将其视为零和将其设置为一之间没有改变。
  • 正是我担心的。这种实现似乎可以作为二进制信号量正常工作,因为两个进程不会同时等待。我不知道在同一个语句中检查值和增加值的方法,你有什么想法吗?
  • 你没有实现信号量。您使用一个(借助一些外部库)
  • 在某个时候有人实现了它。了解你的代码在最低层次上做了什么会让你成为一个更好的程序员,而不是仅仅接受某件事有效并继续前进。
  • while( (*shmPtr) != 0); 您的编译器有权完全优化此循环。

标签: c ipc semaphore


【解决方案1】:

正如 Tom 评论的那样,要使信号量正确,您需要原子测试和设置或比较和交换(比较交换)。

但这还不是全部。由于您使用的是共享内存(即通过多个进程),因此 C11 (Link) 提供的原子操作是不够的。

由于您无论如何都在调用 Posix 函数,我假设您可以访问 Posix 信号量。

“POSIX 信号量允许进程和线程同步它们的动作。” (Link)

【讨论】:

  • 这只是一个练习,我通常在需要同步时使用 sys/sem.h。但是,我在帖子中添加了 wikipedia 的 testAndSet 实现,对此我有疑问。如果我用调用 testAndSet 替换了 while 循环中的条件,会不会出现同样的问题?如果两个线程在等待并且信号量变为 0,假设第一个调用 testAndSet 的进程在读取 oldValue 后切换上下文。这不会导致与我开始时完全相同的问题吗?
  • 是的,同样的问题也会发生。在 Wikipedia 上找到的“实现”仅用于说明,并且具有相同的竞争条件。
【解决方案2】:

我不知道如何在 PC 上执行此操作(如果您发现了,请返回并发布您自己的答案),但您需要的是我所说的“原子访问保护。”换句话说,您需要一种机制来强制在设定的时间内对给定变量进行原子访问。这意味着您实际上强制所有线程/进程暂停片刻,而只有 1 个且只有 1 个线程可以访问变量。然后它对变量执行它的操作(例如:读取、修改、写入它),然后在完成后重新启用其他线程。这样,您保证在这些操作期间该线程对该变量的原子访问。现在,所有的竞争条件都解决了。

我相信在 C 中,这是高度依赖于体系结构的,并且依赖于通过 __asm 关键字等用内联汇编代码编写的 C 函数,和/或它依赖于将特定硬件寄存器中的位设置为为了强制执行某些行为。使用 __asm 关键字的示例:http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.100748_0606_00_en/ddx1471430827125.html

包含在 C 函数中的内联汇编代码示例:

int add(int i, int j)
{
  int res = 0;
  __asm ("ADD %[result], %[input_i], %[input_j]"
    : [result] "=r" (res)
    : [input_i] "r" (i), [input_j] "r" (j)
  );
  return res;
}

一旦您拥有“atomic access guard”函数来为您提供原子访问,您可以执行以下操作:

// atomic access guard ON

// Do whatever you want here: it's all atomic now!
// Read, modify, write, etc.
// - CAUTION: NO OTHER THREADS CAN RUN DURING THIS TIME, SO GET OUT OF THIS QUICKLY

// atomic access guard OFF

在单核系统上,例如我熟悉的微控制器(STM32 和 AVR/Arduino),只需关闭所有中断即可确保原子访问。例如:在 ARM 内核的 STM32 微控制器上,使用必要的 CMSIS(ARM 提供的)函数执行以下操作:

// Read PRIMASK register, check interrupt status before you disable them 
// Returns 0 if they are enabled, or non-zero if disabled 
uint32_t prim = __get_PRIMASK();

// Disable interrupts 
__disable_irq();

// Do some stuff here which can not be interrupted 

// Enable interrupts back, but only if they were previously enabled (prevents nesting problems)
if (!prim) 
{
    __enable_irq();
}

来源:https://stm32f4-discovery.net/2015/06/how-to-properly-enabledisable-interrupts-in-arm-cortex-m/

如果使用FreeRTOS (Free Real-Time Operating System),请执行以下操作:

taskENTER_CRITICAL() // This supports nested calls, and ends up calling `portDISABLE_INTERRUPTS()` anyway.

// do your atomic access here

taskEXIT_CRITICAL() 

见:https://www.freertos.org/taskENTER_CRITICAL_taskEXIT_CRITICAL.html

如果使用 AVR 内核微控制器,例如 ATmega328(基本 Arduino Uno 处理器),请执行以下操作:

uint8_t SREG_bak = SREG; //save global interrupt state
cli(); //clear (disable) interrupts

//atomic variable access guaranteed here

SREG = SREG_bak; //restore interrupt state

在这里查看我的答案:https://*.com/a/39693278/4561887

所以现在你需要去做一些研究(并请回帖),关于如何在给定的操作系统和/或架构上执行 C 中的这样一个原则,和/或通过对内核的一些特殊调用或某物。这甚至可能需要您编写自己的内联程序集来执行此操作,然后将其包装在 C 函数中以进行调用。

我期待看到你如何完成它。

更新:我刚刚对 FreeRTOS 源代码进行了一些挖掘,以了解它们如何禁用中断,这是我在使用 GCC 时发现的 ARM Cortex M3 处理器(例如 STM32 微控制器)的内容编译器:

来自“FreeRTOSv9.0.0/FreeRTOS/Source/portable/GCC/ARM_CM3/portmacro.h”:

#define portDISABLE_INTERRUPTS() vPortRaiseBASEPRI()

portFORCE_INLINE static void vPortRaiseBASEPRI( void )
{
uint32_t ulNewBASEPRI;

    __asm volatile
    (
        "   mov %0, %1                                              \n" \
        "   msr basepri, %0                                         \n" \
        "   isb                                                     \n" \
        "   dsb                                                     \n" \
        :"=r" (ulNewBASEPRI) : "i" ( configMAX_SYSCALL_INTERRUPT_PRIORITY )
    );
}

【讨论】: