profile picture

Michael Stapelberg

The C write() pattern (2013)

published 2013-01-19, last modified 2018-03-18
Edit Icon

When writing data to a file descriptor (file, socket, …) in C, it is recommended to use a loop to write the entire buffer and keep track of how many bytes write() could actually write to the file descriptor. This is how to write data to a file in C in a naive way:

#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <err.h>
#include <errno.h>

int main() {
    int fd = open("/tmp/pattern.example",
                  O_CREAT | O_TRUNC | O_RDWR,
		  S_IRUSR | S_IWUSR);
    if (fd == -1)
        err(EXIT_FAILURE, "Could not open() file");

    const char *data = "This data illustrates my point.";
    /* This is WRONG, don’t do that: */
    write(fd, data, strlen(data));

    close(fd);
    return 0;
}

…and here is how to write data with the aforementioned pattern:

const char *data = "This data illustrates my point.";
int written = 0;
int n;
while (written < strlen(data)) {
    if ((n = write(fd, data + written, strlen(data) - written)) < 0) {
        err(EXIT_FAILURE, "Could not write() data");
    }

    written += n;
}

In case it is not entirely obvious what happens here: write() returns the amount of bytes it wrote, and that might be less than you specified. Therefore, we keep track of how many bytes were written and try to write the rest, until finally all data was written successfully. Be careful, though: a return value of -1 signals an error, so you need to handle these carefully.

The reason I am writing about this pattern is to illustrate it with real-world examples. We recently received a bug report for i3 (ticket #896, direct link omitted due to spam bots) which stated that i3bar would crash in a certain setup when switching workspaces. This report was only reproducible on OpenBSD, which tends to use conservative buffer sizes for many things.

It turned out that the cause for the crash was an error in our write code, which would fail to properly call write() multiple times. This never came to our attention previously because the data we send upon workspace switches got larger only recently and the buffer sizes on Linux still fit all of the data in a single write() call.

Another interesting behavior of some system calls is that they might return an error which means that you should just repeat that call. Two such error codes come to mind: EAGAIN and EINTR. The former is only relevant for non-blocking file descriptors, and means that performing that write() would block the process. EINTR means the system call was interrupted by a signal.

The same piece of code which contained the bug I talked about earlier was also not prepared to handle EAGAIN: when you switched workspaces often enough, the scheduler might give i3 so much CPU time — and none to i3bar — that i3 filled up the socket buffer and write() returned -1 with errno set to EAGAIN.

In conclusion, the correct write pattern looks like this:

const char *data = "This data illustrates my point.";
int written = 0;
int n;
while (written < strlen(data)) {
    if ((n = write(0, data + written, strlen(data) - written)) < 0) {
        if (errno == EINTR || errno == EAGAIN)
            continue;
        err(EXIT_FAILURE, "Could not write() data");
    }

    written += n;
}

I run a blog since 2005, spreading knowledge and experience for almost 20 years! :)

If you want to support my work, you can buy me a coffee.

Thank you for your support! ❤️