Problem 5.3 Linux Programming Interface by Michael Kerrisk

This exercise is designed to demonstrate why the atomicity guaranteed by opening a file with the O_APPEND flag is necessary. Write a program that takes up to three command-line arguments:


$ atomic_append filename num-bytes [x]

This program should open the specified filename (creating it if necessary) and append num-bytes bytes to the file by using write() to write a byte at a time. By default, the program should open the file with the O_APPEND flag, but if a third command-line argument (x) is supplied, then the O_APPEND flag should be omitted, and instead the program should perform an lseek(fd, 0, SEEK_END) call before each write(). Run two instances of this program at the same time without the x argument to write 1 million bytes to the same file:


$ atomic_append f1 1000000 & atomic_append f1 1000000

Repeat the same steps, writing to a different file, but this time specifying the x argument:


$ atomic_append f2 1000000 x & atomic_append f2 1000000 x

List the sizes of the files f1 and f2 using ls -l and explain the difference.

Here is the solution that I worked out:


/**
 * The Linux Programming Interface by Michael Kerrisk
 *
 * Chapter 5 Question 3
 * This exercise is designed to demonstrate why the atomicity guaranteed by opening a file with the O_APPEND
 * flag is necessary. Write a program that takes up to three command-line arguments:

  *  $ atomic_append filename num-bytes [x]
  *
  * This program should open the specified filename (creating it if necessary) and append num-bytes bytes
  * to the file by using write() to write a byte at a time. By default, the program should open the
  * file with the O_APPEND flag, but if a third command-line argument (x) is supplied, then the O_APPEND
  * flag should be omitted, and instead the program should perform an lseek(fd, 0, SEEK_END) call before each write().
  * Run two instances of this program at the same time without the x
  * argument to write 1 million bytes to the same file:
  *
  *  $ atomic_append f1 1000000 & atomic_append f1 1000000
  *
  * Repeat the same steps, writing to a different file, but this time specifying the x argument:
  *
  * $ atomic_append f2 1000000 x & atomic_append f2 1000000 x
  *
  * List the sizes of the files f1 and f2 using ls -l and explain the difference.
  *
  * Solution with code below ran:
  *
  * mike@phobos:~/CLionProjects/atomicAppend/cmake-build-debug$ ./atomicAppend test.txt 1000000 \
  *   & ./atomicAppend test.txt 1000000
  *  [1] 23299
  *  Using filename: test.txt
  *  Using Number of bytes: 1000000
  *  Attempting to open/create file test.txt
  *  Using filename: test.txt
  *  Using Number of bytes: 1000000
  *  Attempting to open/create file test.txt
  *  [1]+  Done                    ./atomicAppend test.txt 1000000
  *
  * mike@phobos:~/CLionProjects/atomicAppend/cmake-build-debug$ ./atomicAppend test2.txt 1000000 x \
  *    & ./atomicAppend test2.txt 1000000 x
  * [1] 23351
  * Using filename: test2.txt
  * Using Number of bytes: 1000000
  * Do not use the append flag passed, not appending using lseek
  * Attempting to open/create file test2.txt
  * Using filename: test2.txt
  * Using Number of bytes: 1000000
  * Do not use the append flag passed, not appending using lseek
  * Attempting to open/create file test2.txt
  * [1]+  Done                    ./atomicAppend test2.txt 1000000 x
  *
  *  mike@phobos:~/CLionProjects/atomicAppend/cmake-build-debug$ ls -al
  *  -rw-r--r-- 1 mike mike 1023284 Jun 28 00:42 test2.txt
  *  -rw-r--r-- 1 mike mike 2000002 Jun 28 00:42 test.txt
  *
  * The files are much different when using lseek obviously nearly half the size. The first process executing the code
  * is interrupted between the lseek() and write() clals by a second process doing the same thing,
  * then both processes will set their file offset to the same location before writing,
  * and when the first process is rescheduled it will overwrite the data already written by the second process.
  *
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

// buffer size is 1 as defined by the problem
#define BUF_SIZE 1

/**
 * Rudimentary error handler used throughout these example problems
 *
 * @param errnoSaved errno value
 * @return int -1 for error handling
 */
int rudimentaryErrorHandler(int errnoSaved) {
    fprintf(stderr, "%s \n", strerror(errnoSaved));
    return -1;
}

int chapterFiveQuestionFour(char *filename, int numberOfBytes, int useAppendFlag) {
    /*
    * ssize_t is used for functions whose return value could either be a valid size or a negative value to
    * indicate an error, it is guaranteed to have the reange [-1, SSIZE_MAX], SSIZE_MAX system dependent
    */
    ssize_t writeStatus;

    int writeFd, errnoSaved;

    // Initialize a char array of size 1 byte and set it to 0
    char buffer[BUF_SIZE];
    memset(buffer, 0, sizeof(buffer));

    /*
     * O_RDONLY open for read only
     * O_CREAT will create the file if it doesn't exist
     * O_NONBLOCK open file in nonblocking mode
     * O_APPEND append to the end of the file
     * S_IRUSR read permission bit for the owner of the file 0400
     * S_IWUSR write permissions bit for the owner 0200
     * S_IGRRP read permission bit for teh group owner of the file usually 040
     * S_IWGRP write permission bit for the group owner of the file usually 020
     * S_IROTH read permission bit for other users usually 04
     * S_IWOTH write permission bit for other users usually 02
     */
    fprintf(stdout, "Attempting to open/create file %s\n", filename);
    writeFd = open(filename, O_CREAT | O_RDWR | O_NONBLOCK | (useAppendFlag ? O_APPEND: 0),
            S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
    errnoSaved = errno;

    /*
    * Will return negative one if open system call fails
    * errno man page: http://man7.org/linux/man-pages/man3/errno.3.html
    */
    if (0 > writeFd) {
        fprintf(stderr, "There was an issue opening the file to write\n");
        return rudimentaryErrorHandler(errnoSaved);
    }

    int numberOfBytesWritten = 0;
    while (numberOfBytesWritten <= numberOfBytes) {

        if (0 == useAppendFlag) {
            /*
            * Change the file offset to the beginning of the file.
            *
            * For each open file, the kernel records a file offset, sometimes also called the read-write
            * offset or pointer. This is the location in the file at which the next read() or write() will commence.
            * The file offset is expressed as an ordinal byte position relative to the start of the file.
            * The first byte of the file is at offset 0.
            * lseek(fd, 0, SEEK_SET);     Start of file
            * lseek(fd, 0, SEEK_END);     Next byte after the end of the file
            * lseek(fd, -1, SEEK_END);    Last byte of file
            * lseek(fd, -10, SEEK_CUR);   Ten bytes prior to current location
            * lseek(fd, 10000, SEEK_END); 10001 bytes past last byte of file
            */
            if (-1 == lseek(writeFd, 0, SEEK_END)) {
                fprintf(stderr, "There was an issue trying to seek to the end of the file.\n");
                return rudimentaryErrorHandler(errnoSaved);
            }
        }

        writeStatus = write(writeFd, buffer, BUF_SIZE);

        errnoSaved = errno;
        if (-1 == writeStatus) {
            fprintf(stderr, "There was an issue trying to write to the file.\n");
            return rudimentaryErrorHandler(errnoSaved);
        }

        numberOfBytesWritten++;
    }

    // Close the file and ensure it closes successfully
    writeStatus = close(writeFd);
    errnoSaved = errno;
    if (-1 == writeStatus) {
        return rudimentaryErrorHandler(errnoSaved);
    }
}

int main(int argc, char *argv[]) {
    /*
     * Don't run anything if the filename isn't passed in via command line argument
     *
     * argv[0] = the program name, i.e. cp
     * argv[1] = the file to write to
     * argv[2] = the number of bytes to write
     * argv[3] = whether or not to omit the append flag
     */
    if (argc <= 2) {
        fprintf(stderr, "usage: %s fileName numberOfBytesToWrite [Omit Append Flag: x] \n", argv[0]);
        return 1;
    }

    // Store a reference pointer in memory to store the filename
    char *fileName = argv[1];
    fprintf(stdout, "Using filename: %s\n", fileName);

    /*
     * The strtol() function converts the initial part of the string in nptr to a long integer value
     * according to the given base, which must be between 2 and 36 inclusive, or be the special value 0.
     *
     * The string may begin with an arbitrary amount of white space (as determined by isspace(3))
     * followed by a single optional '+' or '-' sign. If base is zero or 16, the string may
     * then include a "0x" prefix, and the number will be read in base 16; otherwise, a zero base
     * is taken as 10 (decimal) unless the next character is '0', in which case it is taken as 8 (octal).
     */
    int numberOfBytesToWrite = strtol(argv[2], NULL, 10);
    fprintf(stdout, "Using Number of bytes: %d\n", numberOfBytesToWrite);

    // If the x is passed in then optionalAppend is true so include the append flag
    int useAppendFlag = 1;
    if (NULL != argv[3]) {
        fprintf(stdout, "Do not use the append flag passed, not appending using lseek\n");
        useAppendFlag = 0;
    }

    return chapterFiveQuestionFour(fileName, numberOfBytesToWrite, useAppendFlag);
}