4

I've been reading about EINTR on write(2) etc, and trying to determine whether I need to check for it in my program. As a sanity check, I tried to write a program that would run into it. The program loops forever, writing repeatedly to a file.

Then, in a separate shell, I run:

while true; do pkill -HUP test; done

However, the only output I see from test.c is the .s from the signal handler. Why isn't the SIGHUP causing write(2) to fail?

test.c:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>
#include <errno.h>

#include <sys/types.h>

void hup_handler(int sig)
{
    printf(".");
    fflush(stdout);
}

int main()
{
    struct sigaction act;
    act.sa_handler = hup_handler;
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);

    sigaction(SIGHUP, &act, NULL);

    int fd = open("testfile", O_WRONLY);

    char* buf = malloc(1024*1024*128);

    for (;;)
    {
        if (lseek(fd, 0, SEEK_SET) == -1)
        {
            printf("lseek failed: %s\n", strerror(errno));
        }
        if (write(fd, buf, sizeof(buf)) != sizeof(buf))
        {
            printf("write failed: %s\n", strerror(errno));
        }
    }
}
Rodrigo Queiro
  • 1,324
  • 8
  • 15
  • Since this `write` does no I/O, it is impossible to interrupt the I/O. You're just modifying pages in the cache. Try writing to a socket, writing to an NFS server, or writing to a file from another file that's `mmap`ed using a write size larger than your RAM. – David Schwartz Aug 07 '12 at 11:04

3 Answers3

10

Linux tends to avoid EINTR on writes to/reads from files; see discussion here. While a process is blocking on a disk write, it may be placed in an uninterruptible sleep state (process code D) which indicates that it cannot be interrupted at that time. This depends on the device driver; the online copy of Linux Device Drivers, 3rd Edition is a good reference for how this appears from the kernel side.

You still need to handle EINTR for other platforms which may not behave the same, or for pipes and sockets where EINTR definitely can occur.

Note that you're only writing sizeof(void *) bytes at a time:

char* buf = malloc(1024*1024*128);

    if (write(fd, buf, sizeof(buf)) != sizeof(buf))

This should be

const size_t BUF_SIZE = 1024*1024*128;
char* buf = malloc(BUF_SIZE);

    if (write(fd, buf, BUF_SIZE) != BUF_SIZE)
Community
  • 1
  • 1
ecatmur
  • 152,476
  • 27
  • 293
  • 366
  • If I change it to a FIFO, then the write doesn't finish, although it doesn't set EINTR: the output looks like ".write failed: Success\n" repeatedly. However, I imagine that I could get EINTR to happen at some point. – Rodrigo Queiro Aug 07 '12 at 11:07
  • to have EINTR, you'd really have to have the process interrupted in the short time between entering the `write` call and when the first bytes are placed in the interal buffers, a very unlikely event. Achieving this with a kill loop from a shell is very unlikely. – Jens Gustedt Aug 07 '12 at 11:19
  • Note also that you never need to handle `EINTR` unless you're installing interrupting signal handlers (i.e. missing `SA_RESTART` flag) or trying to work around bugs on very old Linux versions. – R.. GitHub STOP HELPING ICE Aug 07 '12 at 13:27
5

There are 2 possibilities:

  • You're writing very few bytes, since you're misusing the sizeof operator. Thus the write happens instantaneously and it never gets interrupted - you're only writing 4 or 8 bytes at a time

  • Somehow the syscall gets restarted, as if you applied SA_RESTART to sigaction


In your code, since buf is a pointer, sizeof(buf) yields the size of the pointer on your machine, not the (much bigger) allocated space

cnicutar
  • 178,505
  • 25
  • 365
  • 392
  • Well spotted on the sizeof() mistake: I originally had buf on the stack, but forgot to change that. However, fixing this doesn't change the overall behaviour (other than making things slower). – Rodrigo Queiro Aug 07 '12 at 11:02
1

If you check the manual page for EINTR

The call was interrupted by a signal before any data was written

Also from the signal(7) manual page:

read(2), readv(2), write(2), writev(2), and ioctl(2) calls on "slow" devices. A "slow" device is one where the I/O call may block for an indefinite time, for example, a terminal, pipe, or socket. (A disk is not a slow device according to this definition.) If an I/O call on a slow device has already transferred some data by the time it is interrupted by a signal handler, then the call will return a success status (normally, the number of bytes transferred).

Taking these two together, if writing to a file on a disk, and write has started to write (even if only one single byte has been written) the return from that write call will be a success.

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • My code checks that *all* the data is written, so it would detect this case. – Rodrigo Queiro Aug 07 '12 at 11:00
  • @RodrigoQueiro Yes, but it's still not an _error_, and the reason it specifically doesn't result in `EINTR`. And as noted by cnicutar you use the `sizeof` operator wrong so the `write` call only writes four or eight bytes (depending on if you are on a 32 or 64 bit platform). – Some programmer dude Aug 07 '12 at 11:02