I'm working through the book the linux programming interface by Michael Kerrisk.
The following source code is provided as a solution to Exercise 5-3. The comments describe the purpose of the exercice and the program and its usage.
/*************************************************************************\
* Copyright (C) Michael Kerrisk, 2017. *
* *
* This program is free software. You may use, modify, and redistribute it *
* under the terms of the GNU General Public License as published by the *
* Free Software Foundation, either version 3 or (at your option) any *
* later version. This program is distributed without any warranty. See *
* the file COPYING.gpl-v3 for details. *
/* Solution for Exercise 5-3 */
/* atomic_append.c
Demonstrate the difference between using nonatomic lseek()+write()
and O_APPEND when writing to a file.
Usage: file num-bytes [x]
The program write 'num-bytes' bytes to 'file' a byte at a time. If
no additional command-line argument is supplied, the program opens the
file with the O_APPEND flag. If a command-line argument is supplied, the
O_APPEND is omitted when calling open(), and the program calls lseek()
to seek to the end of the file before calling write(). This latter
technique is vulnerable to a race condition, where data is lost because
the lseek() + write() steps are not atomic. This can be demonstrated
by looking at the size of the files produced by these two commands:
atomic_append f1 1000000 & atomic_append f1 1000000
atomic_append f2 1000000 x & atomic_append f2 1000000 x
*/
#include <sys/stat.h>
#include <fcntl.h>
#include "lib/tlpi.h"
int
main(int argc, char *argv[])
{
int numBytes, j, flags, fd;
Boolean useLseek;
if (argc < 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s file num-bytes [x]\n"
" 'x' means use lseek() instead of O_APPEND\n",
argv[0]);
useLseek = argc > 3;
flags = useLseek ? 0 : O_APPEND;
numBytes = getInt(argv[2], 0, "num-bytes");
fd = open(argv[1], O_RDWR | O_CREAT | flags, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("open");
for (j = 0; j < numBytes; j++) {
if (useLseek)
if (lseek(fd, 0, SEEK_END) == -1)
errExit("lseek");
if (write(fd, "x", 1) != 1)
fatal("write() failed");
}
printf("%ld done\n", (long) getpid());
exit(EXIT_SUCCESS);
}
After running the commands atomic_append f1 1000000 & atomic_append f1 1000000
and atomic_append f2 1000000 x & atomic_append f2 1000000 x
, the author writes that ls -l f1 f2
should be similar to
-rw------- 1 mtk users 2000000 Jan 9 11:14 f1
-rw------- 1 mtk users 1999962 Jan 9 11:14 f2
That is, without using lseek(2)
, all the bytes are written without any data loss. No data is lost because write(2)
is atomic, and since the O_APPEND
flag is used when open(2)
ing the files, both processes do not overwrite each other's output.
However, after running this exact source code on my machine running macOS High Sierra 10.13.1, both commands lead to some bytes being overwritten, suggesting that write(2)
is not atomic. On my machine, ls -l f1 f2
outputs
-rw------- 1 user staff 1983995 Jun 7 21:38 f1
-rw------- 1 user staff 1964984 Jun 7 21:37 f2
which leaves me extremely confused. Why is a write(2)
on a file open(2)
ed with the O_APPEND
flag seemingly not atomic?
I have carefully executed the above commands on clean files using the above program. The results were always reproduced.
I can provide more context concerning the exercise or my setup if needed.
This was marked as a duplicate of this question. The discussion there provides some insight, namely that:
- Certain filesystems (in my case OSX's filesystem) do not guarantee atomicity. This is the most plausible answer. But, how exactly does OSX fail in the context of the above program ?