Understanding `EINTR`

This thread has been locked by a moderator; it no longer accepts new replies.

I’ve talked about EINTR a bunch of times here on DevForums. Today I found myself talking about it again. On reading my other explanations, I didn’t think any of them were good enough to link to, so I decided to write it up properly.

If you have questions or comments, please put them in a new thread here on DevForums. Use the App & System Services > Core OS topic area so that I see it.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


Understanding EINTR

Many BSD-layer routines can fail with EINTR. To see this in action, consider the following program:

import Darwin

func main() {
    print("will read, pid: \(getpid())")
    var buf = [UInt8](repeating: 0, count: 1024)
    let bytesRead = read(STDIN_FILENO, &buf, buf.count)
    if bytesRead < 0 {
        let err = errno
        print("did not read, err: \(err)")
    } else {
        print("did read, count: \(bytesRead)")
    }
}

main()

It reads some bytes from stdin and prints the result. Build this and run it in one Terminal window:

% ./EINTRTest
will read, pid: 13494

Then, in other window, stop and start the process by sending it the SIGSTOP and SIGCONT signals:

% kill -STOP 13494
% kill -CONT 13494

In the original window you’ll see something like this:

% ./EINTRTest
will read, pid: 13494
zsh: suspended (signal)  ./EINTRTest
% 
did not read, err: 4

[1]  + done       ./EINTRTest

When you send the SIGSTOP the process stops and the shell tells you that. But looks what happens when you continue the process. The read(…) call fails with error 4, that is, EINTR. The read man page explains this as:

[EINTR] A read from a slow device was interrupted before any data arrived by the delivery of a signal.

That’s true but unhelpful. You really want to know why this error happens and what you can do about it. There are other man pages that cover this topic in more detail — and you’ll find lots of info about it on the wider Internet — but the goal of this post is to bring that all together into one place.

Signal and Interrupts

In the beginning, Unix didn’t have threads. It implemented asynchronous event handling using signals. For more about signals, see the signal man page.

The mechanism used to actually deliver a signal is highly dependent on the specific Unix implementation, but the general idea is that:

  • The system decides on a specific process (or, nowadays, a thread) to run the signal handler.

  • If that’s blocked inside the kernel waiting for a system call to complete [1], the system unblocks the system call by failing it with an EINTR error.

Thus, every system call that can block [2] might fail with an EINTR. You see this listed as a potential error in the man pages for read, write, usleep, waitpid, and many others.

[1] There’s some subtlety around the definition of system call. On traditional Unix systems, executables would make system calls directly. On Apple platforms that’s not supported. Rather, an executable calls a routine in the System framework which then makes the system call. In this context the term system call is a shortcut for a System framework routine that maps to a traditional Unix system call.

[2] There’s also some subtlety around the definition of block. Pretty much every system call can block for some reason or another. In this context, however, a block means to enter an interruptible wait state, typically while waiting for I/O. This is what the above man page quote is getting at when it says slow device.

Solutions

This is an obvious pitfall and it would be nice if we could just get rid of it. However, that’s not possible due to compatibility concerns. And while there are a variety of mechanism to automatically retry a system call after a signal interrupt, none of them are universally applicable. If you’re working on a large scale program, like an app for Apple’s platforms, you only good option is to add code to retry any system call that can fail with EINTR.

For example, to fix the program at the top of this post you might wrap the read(…) system call like so:

func readQ(_ d: Int32, _ buf: UnsafeMutableRawPointer!, _ nbyte: Int) -> Int {
    repeat {
        let bytesRead = read(d, buf, nbyte)
        if bytesRead < 0 && errno == EINTR { continue }
        return bytesRead
    } while true
}

Note In this specific case you’d be better off using the read(into:retryOnInterrupt:) method from System framework. It retries by default (if that’s not appropriate, pass false to the retryOnInterrupt parameter).

You can even implement the retry in a generic way. See the errnoQ(…) snippet in QSocket: System Additions.

Library Code

If you’re writing library code, it’s important that you handle EINTR so that your clients don’t have to. In some cases it might make sense to export a control for this, like the retryOnInterrupt parameter shown in the previous section, but it should default to retrying.

If you’re using library code, you can reasonably expect it to handle EINTR for you. If it doesn’t, raise that issue with the library author. And you get this error back from an Apple framework, like Foundation or Network framework, please file a bug against the framework.

Boost
Understanding `EINTR`
 
 
Q