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! ❤️