Stream: t-lang/wg-unsafe-code-guidelines

Topic: Signal handlers and Rust


Lokathor (Jan 05 2020 at 09:12, on Zulip):

I have a terminal application and it would like to track the terminal size. On Windows this is communicated with normal event polling, but with unix systems a change in terminal size sends a signal to your process. By default the signal is ignored, so you have to set a custom signal handler.

My question is, "does Rust have any specific rules about signal handlers? or is it just follow the C rules?"

Particularly, can the signal handler just write an AtomicBool for "you need to check the size again", which the main code then checks every "frame" of input gathering? What aromic ordering would be sufficient here to interact that data between the main code and the signal handler, Or would even just a static Cell<bool> be correct?

rkruppe (Jan 05 2020 at 10:47, on Zulip):

a static Cell<usize> would certainly not be correct, for the same reasons a non-volatile global in C isn't (ordinary load-store optimizations of sequential programs can break the communication with a signal handler)

Amanieu (Jan 05 2020 at 17:39, on Zulip):

Yes AtomicBool should be fine for communicating with a signal handler.

Amanieu (Jan 05 2020 at 17:40, on Zulip):

In terms of ordering, you don't need anything since you are not transmitting any data other than the flag. So Relaxed is fine.

Amanieu (Jan 05 2020 at 17:41, on Zulip):

If you need anything more (i.e. sending additional data about the signal through an UnsafeCell guarded by an atomic flag), then you would need to use compiler_fence on the main thread side to ensure the flag is checked before you access the protected data.

Lokathor (Jan 05 2020 at 21:30, on Zulip):

Hmm, so as long as it's only the atomic in question, just basic reads and writes are fine? I could also use an AtomicU32 to store a [u16; 2] of the dimensions or something like that.

Amanieu (Jan 05 2020 at 22:57, on Zulip):

Yea if you data is within the atomic itself then relaxed load/store is fine.

Lokathor (Jan 06 2020 at 19:58, on Zulip):

<https://github.com/Lokathor/vanadium/blob/master/src/bin/vanadium.rs> it seems to be working well

RalfJ (Jan 07 2020 at 20:45, on Zulip):

I'd treat the signal handler like a separate thread (which it sounds like you're already doing)

RalfJ (Jan 07 2020 at 20:46, on Zulip):

@Amanieu when using release/acquire atomics, I suppose no fence is needed? what's good enough for inter-thread communication should also suffice for communication with the signal handler?

RalfJ (Jan 07 2020 at 20:47, on Zulip):

(I guess using full-spectrum atomics is overkill, but then I'd be surprised if signal handlers were perf-critical)

Amanieu (Jan 07 2020 at 20:49, on Zulip):

@RalfJ For signal handlers you can use Acquire/Release but that's overkill. In practice you just need Relaxed + compiler_fence.

Amanieu (Jan 07 2020 at 20:50, on Zulip):

Also you only need the fence on the main thread side, not the signal handler, since returning from the signal handler acts as an implicit fence.

RalfJ (Jan 07 2020 at 20:53, on Zulip):

you need an acquire fence in the signal handler though I suppose, to avoid reordering the signal load and the data load?

RalfJ (Jan 07 2020 at 20:54, on Zulip):

I find it much easier to treat these as separate threads than trying to optimize for how signal handlers differ from threads^^

Lokathor (Jan 07 2020 at 20:58, on Zulip):

if both the handler and the main code use Relaxed, will that reach an "eventual" consistency? That is, assuming the main thread is very frequently checking the signal handler's AtomicBool, I'm comfortable with the handler running and then the main code not seeing that change on the very next loop, only some fuzzy number of loops later.

Lokathor (Jan 07 2020 at 20:59, on Zulip):

Such as with this:
main loop reads the flag before getting any other input: <https://github.com/Lokathor/vanadium/blob/master/src/bin/vanadium.rs#L142>
and the signal handler just flips the flag on every time it's triggered <https://github.com/Lokathor/vanadium/blob/master/src/bin/vanadium.rs#L202>

Lokathor (Jan 07 2020 at 21:01, on Zulip):

To be clear, the demo does run and work as intended, I'm just kinda unclear if it's an accidentally sort of working or if this is an expected sort of working.

Amanieu (Jan 07 2020 at 21:01, on Zulip):

It works as expected.

Lokathor (Jan 07 2020 at 21:01, on Zulip):

okay cool

Lokathor (Jan 07 2020 at 21:02, on Zulip):

I've never ever touched C++ so a lot of the time when Rust says "it works kinda how C++ works because that's what LLVM does" it can be harder.

Lokathor (Jan 07 2020 at 21:03, on Zulip):

(I hope that in the future we'll move towards a knowledge base that doesn't assume the user knows any previous programming languages.)

Amanieu (Jan 07 2020 at 21:07, on Zulip):

@Lokathor You only need acquire/release if you need to transfer data outside the atomic. For example:

static mut DATA: i64 = 0;
static FLAG: AtomicBool = AtomicBool::new(false);

pub fn sig_handler() {
    // Release ensures FLAG is stored after DATA
    unsafe { DATA = 5; }
    FLAG.store(true, Ordering::Release);
}

pub fn main_thread() {
    // Acquire ensures FLAG is loaded before DATA
    if FLAG.load(Ordering::Acquire) {
        println!("{}", unsafe { DATA });
    }
}
Amanieu (Jan 07 2020 at 21:07, on Zulip):

This is the generic version where the signal handler is modeled as a separate thread.

Lokathor (Jan 07 2020 at 21:09, on Zulip):

oh so they only matter if the atomic is a pseudo-lock sort of thing on some other non-atomic data?

Amanieu (Jan 07 2020 at 21:09, on Zulip):

However you can do better since you know the main thread is suspended while the signal handler is running:

static mut DATA: i64 = 0;
static FLAG: AtomicBool = AtomicBool::new(false);

pub fn sig_handler() {
    // Since the main thread is suspended, the
    // entire signal handler is an atomic operation
    // from the point of view of the thread.
    unsafe { DATA = 5; }
    FLAG.store(true, Ordering::Relaxed);
}

pub fn main_thread() {
    // Acquire ensures FLAG is loaded before DATA
    if FLAG.load(Ordering::Relaxed) {
        compiler_fence(Ordering::Acquire);
        println!("{}", unsafe { DATA });
    }
}
Lokathor (Jan 07 2020 at 21:10, on Zulip):

does the Acquire need a compiler_fence Release to match it?

Amanieu (Jan 07 2020 at 21:10, on Zulip):

Not in this case because there is an implicit one when the signal handler ends.

Amanieu (Jan 07 2020 at 21:11, on Zulip):

Or to put it another way, it doesn't matter if FLAG is stored before DATA because the thread is suspended by the signal handler.

Amanieu (Jan 07 2020 at 21:11, on Zulip):

So from the thread's point of view they both happen at the same time.

Lokathor (Jan 07 2020 at 21:12, on Zulip):

i meant in the main thread, it doesn't need a release to match the acquire?

Lokathor (Jan 07 2020 at 21:12, on Zulip):

the ordering docs made it sound like you should always use matched pairs

Amanieu (Jan 07 2020 at 21:13, on Zulip):

You mean in the first example? The Release in the signal handler is matched with the Acquire in the main thread.

Lokathor (Jan 07 2020 at 21:23, on Zulip):

In this one:

static mut DATA: i64 = 0;
static FLAG: AtomicBool = AtomicBool::new(false);

pub fn sig_handler() {
    // Since the main thread is suspended, the
    // entire signal handler is an atomic operation
    // from the point of view of the thread.
    unsafe { DATA = 5; }
    FLAG.store(true, Ordering::Relaxed);
}

pub fn main_thread() {
    // Acquire ensures the load instruction for FLAG happens before the load instruction for DATA
    if FLAG.load(Ordering::Relaxed) {
        compiler_fence(Ordering::Acquire);
        println!("{}", unsafe { DATA });
    }
}

main_thread has Acquire but no Release

Lokathor (Jan 07 2020 at 21:24, on Zulip):

if that's fine it's no problem, but the docs for the atomic Ordering values make it sound like Acquire and Release must always have matching pairs

Amanieu (Jan 07 2020 at 21:50, on Zulip):

Well, that is true in the general case.

Amanieu (Jan 07 2020 at 21:50, on Zulip):

For example this code:

static mut DATA: i64 = 0;
static FLAG: AtomicBool = AtomicBool::new(false);

pub fn thread_a() {
    // Since the main thread is suspended, the
    // entire signal handler is an atomic operation
    // from the point of view of the thread.
    unsafe { DATA = 5; }
    compiler_fence(Ordering::Release);
    FLAG.store(true, Ordering::Relaxed);
}

pub fn thread_b() {
    // Acquire ensures the load instruction for FLAG happens before the load instruction for DATA
    if FLAG.load(Ordering::Relaxed) {
        compiler_fence(Ordering::Acquire);
        println!("{}", unsafe { DATA });
    }
}
Amanieu (Jan 07 2020 at 21:52, on Zulip):

Will work correctly on a uniprocessor system that simulates multiprocessing with preemption.

Amanieu (Jan 07 2020 at 21:53, on Zulip):

But that's because you have 2 threads running and execution could switch to the other at any time.

RalfJ (Jan 07 2020 at 21:54, on Zulip):

though if you use thread::spawn this will still be UB, uniprocessor or not. so, "work correctly" is relative.

RalfJ (Jan 07 2020 at 21:54, on Zulip):

(assuming you actually mean threads here. the comments sound more like signal handlers.)

Amanieu (Jan 07 2020 at 21:55, on Zulip):

I'm pretty sure my code will work on an uniprocessor system.

Amanieu (Jan 07 2020 at 21:56, on Zulip):

Since you can think of the whole system only having a single thread, and task switching being done in a signal handler.

RalfJ (Jan 07 2020 at 21:58, on Zulip):

if it was assembly I'd agree

RalfJ (Jan 07 2020 at 21:58, on Zulip):

but it has UB in Rust, so none of these arguments matter

RalfJ (Jan 07 2020 at 21:58, on Zulip):

this is the same argument as in this blog post

RalfJ (Jan 07 2020 at 21:59, on Zulip):

the compiler is totally allowed to optimize your code to garbage because of the data race it has

RalfJ (Jan 07 2020 at 22:01, on Zulip):

I may sound like a broken record, but I think this is an important point, which is why I keep making it^^. You can only apply hardware/OS-level reasoning after you established that the program doesn't have UB.

Amanieu (Jan 07 2020 at 22:17, on Zulip):

@RalfJ But you agree the code is correct if I use fence instead of compiler_fence?

Lokathor (Jan 07 2020 at 22:23, on Zulip):

i would like to note that a signal handler can also be triggered on any thread, not just the main thread

RalfJ (Jan 08 2020 at 09:04, on Zulip):

@Amanieu yes. (Assuming all main does is just run those two functions in parallel.)

Last update: Jan 21 2020 at 08:40UTC