how to use std::atomic_signal_fence() with semaphore and volatile?

Viewed 199

std::atomic_signal_fence() Establishes memory synchronization ordering ... between a thread and a signal handler executed on the same thread. -- cppreference

  • In order to find an example for this illustration, I looked at bames53's similar question in stackoverflow. However the answer may not suit my x86_64 environment, since x86_64 CPU is strong memory model and forbids Store-Store re-ordering ^1. Its example will correctly execute even without std::atomic_signal_fence() in my x86_64 environment.

  • So I made a Store-Load re-ordering example suitable for x86_64 after Jeff Preshing's post. The example code is not that short, so I opened this question instead of appending onto bames53's similar question.

  • main() and signal_handler() will run in the same thread(i.e. they will share the same tid) in a single core environment. main() can be interrupted at any time by signal_handler(). If no signal_fences are used, in the generated binary X = 1; r1 = Y; will be exchanged their ordering if compiled with g++ -O2(Store(X)-Load(Y) is optimized to Load(Y)-Store(X)). The same with Y = 1; r2 = X;. So if main() is interrupted just after 'Load(Y)', it results r1 == 0 and r2 == 0 at last. Thus in the following code line (C) will assert fail. But if line (A) and (B) are uncommented, it should never assert fail since a signal_fence is used to protect synchronization between main() and signal_handler(), to the best of my understanding.

#include <atomic>
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <semaphore.h>
#include <signal.h>
#include <unistd.h>

sem_t endSema;
// volatile int synchronizer;
int X, Y;
int r1, r2;

void signal_handler(int sig) {
  signal(sig, SIG_IGN);
  Y = 1;
  // std::atomic_signal_fence(std::memory_order_seq_cst);  // (A) if uncommented, assert still may fail
  r2 = X;
  signal(SIGINT, signal_handler);
  sem_post(&endSema);  // if changed to the following, assert never fail
  // synchronizer = 1;
}

int main(int argc, char* argv[]) {
  std::srand(std::time(nullptr));
  sem_init(&endSema, 0, 0);
  signal(SIGINT, signal_handler);
  for (;;) {
    while(std::rand() % std::stol(argv[1]) != 0);  // argv[1] ~ 1000'000
    X = 1;
    // std::atomic_signal_fence(std::memory_order_seq_cst);  // (B) if uncommented, assert still may fail.
    r1 = Y;
    sem_wait(&endSema);  // if changed to the following, assert never fail
    // while (synchronizer == 0); synchronizer = 0;
    std::cout << "r1=" << r1 << " r2=" << r2 << std::endl;

    if (r1 == 0) assert(r2 != 0);  // (C)

    Y = 0; r1 = 0; r2 = 0; X = 0;
  }
  return 0;
}
  • Firstly semaphore is used to synchronize main() with signal_handler(). In this version, the assert always fail after around received 30 SIGINTs with or without the signal fence. It seems that std::atomic_signal_fence() did not work as I expected.

  • Secondly If semaphore is replaced with volatile int synchronizer, the program seems never fail with or without the signal fence.

What's wrong with the code? Did I miss-understand the cppreference doc? Or is there any more proper example code for this topic in x86_64 environment that I can observe the effects of std::atomic_signal_fence?

Below is some relevant info:

  • compiling & running env: CentOS 8 (Linux 4.18.0) x86_64 single CPU core.

  • Compiler: g++ (GCC) 8.3.1 20190507

  • Compiling command g++ -std=c++17 -o ordering -O2 ordering.cpp -pthread

  • Run with ./ordering 1000000, then keep pressing Ctrl-C to invoke the signal handler.

0 Answers
Related