Chapter 5 Question 7 The linux programming Interface

Implement readv() and writev() using read(), write(), and suitable functions from the malloc package (Allocating Memory on the Heap: malloc() and free()).

Implementation:

Note: This answer isn't peer reviewed and just represents me fooling around learning. I don't code in C regularly so convention is probably not correct.


/**
 * The Linux Programming Interface by Michael Kerrisk
 *
 * Chapter 5 Question 7
 *
 * Implement readv() and writev() using read(), write(), and suitable functions from the
 * malloc package (Allocating Memory on the Heap: malloc() and free()).
 *
 * Answer: See code below.
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>

#define BUFFER_SIZE 20

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

/**
 * iovec data structure is made up of a two things:
 *
 * iov_base which is a pointer that points to the base of an array and the second argument, iov_len which is the
 * length of the array
 */
struct iovec {
    void *iov_base; /* Start address of buffer */
    size_t iov_len; /* Number of bytes to transfer to/from buffer */
};

/**
 * The readv() system call performs scatter input: it reads a contiguous sequence of bytes from the file referred to
 * by the file descriptor and places ("scatters") these bytes into the buffers specified by iov. Each of the buffers,
 * starting with the one defined by iov[0] is completely filled before readv() proceeds to the next buffer.
 *
 * An important property of readv() is that it completes atomically; that is, from the point of view of the calling
 * process the kernel performs a single data transfer between the file referred to by fd and user memory. This means,
 * for example, that when reading from a file, we can be sure that the range of bytes read is contiguous, even if
 * another process (or thread) sharing the same file offset attempts to manipulate the offset at the same time
 * as the readv() call.
 *
 * @param fd file descriptor int
 * @param iov a pointer to an array made up of iovec structs
 * @param iovCount the number of elements in the iov array
 * @return Returns number of bytes read, 0 on EOF, or -1 on error
 */
ssize_t readv(int fd, const struct iovec *iov, int iovCount) {
    // Validate at least one array is there to write to
    if (0 >= iovCount) {
        errno = EINVAL;
        return -1;
    }

    /*
     * Use the iovcnt int and the iov_len to figure out the size of the buffer we need
     * then use that length to create some memory to read the bytes from the open file descriptor,
     * then populate the arrays of the iovec struct with this data from memory
     */
    size_t totalLength = 0;
    for (int counter = 0; counter < iovCount; counter++) {
        fprintf(stdout, "Getting size of iov[counter].iov_len: %zu \n", iov[counter].iov_len);
        totalLength = iov[counter].iov_len + totalLength;
    }

    // Make sure the total Length of iovec structure arrays is less than SIZE_MAX
    if (totalLength > SIZE_MAX) {
        errno = EINVAL;
        return -1;
    }

    // use malloc to read the file and store the data
    fprintf(stdout, "Trying to allocate: %d Bytes using malloc.\n", (int) totalLength);
    char *readBytes = malloc(totalLength);
    memset(readBytes, 0, totalLength);

    // Read from the file and print it out
    ssize_t readStatus = read(fd, readBytes, (size_t) totalLength);
    if (0 > rudimentaryErrorHandler((int) readStatus, errno)) return -1;
    fprintf(stdout, "Printing the full malloced read array:\n%s\n", readBytes);

    // use memcpy to copy the contents to the original buffers
    size_t currentMemoryLocation = 0;
    fprintf(stdout, "Size of size_t: %zu bytes\n", sizeof(size_t));
    fprintf(stdout, "Size of int: %zu bytes\n", sizeof(int));
    fprintf(stdout, "Size of char: %zu bytes\n", sizeof(char));

    for (int counter = 0; counter < iovCount; counter++) {
        fprintf(stdout, "Attempting to write sections %d-%d in malloc read array to iov[%d].iov_base.\n",
                (int) currentMemoryLocation, (int) currentMemoryLocation + (int) iov[counter].iov_len, counter);
        /*
         * The memcpy function returns a pointer to a destination
         *
         * Failure to observe the requirement that the memory areas do not overlap has been the source of significant
         * bugs.
         *
         * Arguments:
         * void *memcpy(void *dest, const void *src, size_t n);
         *
         * Note: the second argument is using pointer arithmetic to find the location from the malloced array
         */
        fprintf(stdout, "The currentMemoryLocation is: %zu Bytes\n", currentMemoryLocation);
        memcpy(iov[counter].iov_base,
               readBytes + currentMemoryLocation,
               iov[counter].iov_len);

        currentMemoryLocation += iov[counter].iov_len;
    }

    // Free the bytes acquired through malloc
    free(readBytes);

    // Return success
    return 0;
}

/**
 * The writev() system call performs gather output. It concatenates ("gathers") data from all of the buffers
 * specified by iov and writes them as a sequence of contiguous bytes to the file referred to by the file
 * descriptor fd. The buffers are gathered in array order, starting with the buffer defined by iov[0].
 *
 * writev() completes atomically, with all data being transferred in a single operation from user memory to the file
 * referred to by fd. Thus, when writing to a regular file, we can be sure that all of the requested data is written
 * contiguously to hte file, rather than being interspersed with writes by other processes (or threads).
 *
 * @param fd file descriptor int
 * @param iov a pointer to an array made up of iovec structs
 * @param iovCount the number of elements in the iov array
 * @return Returns number of bytes written, or -1 on error
 */
ssize_t writev(int fd, const struct iovec *iov, int iovCount) {
    // Validate at least one array is there to write to
    if (0 >= iovCount) {
        errno = EINVAL;
        return -1;
    }

    /*
    * Use the iovcnt int and the iov_len to figure out the size of the buffer we need
    * then use that length to create some memory to read the bytes from the open file descriptor,
    * then populate the arrays of the iovec struct with this data from memory
    */
    size_t totalLength = 0;
    for (int counter = 0; counter < iovCount; counter++) {
        fprintf(stdout, "Getting size of iov[counter].iov_len: %zu \n", iov[counter].iov_len);
        totalLength = iov[counter].iov_len + totalLength;
    }

    // Make sure the total Length of iovec structure arrays is less than SIZE_MAX
    if (totalLength > SIZE_MAX) {
        errno = EINVAL;
        return -1;
    }

    // use malloc to read the file and store the data
    fprintf(stdout, "Trying to allocate: %d Bytes using malloc.\n", (int) totalLength);
    char *writeBytes = malloc(totalLength);
    memset(writeBytes, 0, totalLength);

    // use memcpy to copy the contents to the original buffers and put it on our newly allocated memory buffer
    size_t currentMemoryLocation = 0;
    for (int counter = 0; counter < iovCount; counter++) {
        fprintf(stdout, "Attempting to write iov[%d].iov_base buffers to writeByte in locations: %d-%d.\n",
                counter, (int) currentMemoryLocation, (int) currentMemoryLocation + (int) iov[counter].iov_len);
        /*
          * The memcpy function returns a pointer to a destination
          *
          * Failure to observe the requirement that the memory areas do not overlap has been the source of significant
          * bugs.
          *
          * Arguments:
          * void *memcpy(void *dest, const void *src, size_t n);
          *
          * Note: the first argument is using pointer arithmetic to find the location from the malloced array
          */
        memcpy(writeBytes + currentMemoryLocation,
               iov[counter].iov_base,
               iov[counter].iov_len);

        fprintf(stdout, "Current memory write buffer contains: %s\n", writeBytes);

        currentMemoryLocation += iov[counter].iov_len;
    }

    fprintf(stdout, "Successfully got all the data out of the iovec structures!\n");
    fprintf(stdout, "The contents of the write array are: %s\n", writeBytes);
    /*
     * ssize_t write(int fd, const void *buf, size_t count);
     */
    ssize_t writeStatus = write(fd, writeBytes, totalLength);
    if (0 > rudimentaryErrorHandler((int) writeStatus, errno)) return -1;

    /*
     * The free() function frees the memory space pointed to by ptr, which must have been returned by a
     * previous call to malloc(), calloc() or realloc(). Otherwise, or if free(ptr) has already been called before,
     * undefined behavior occurs. If ptr is NULL, no operation is performed.
     */
    fprintf(stdout, "Trying to free the memory allocated for the write.\n");
    free(writeBytes);

    // Return success
    return 0;
}

/*
 * Open two files passed in via argv then use the implemented readv(*) and writev(*) implemented above
 */
int main(int argc, char *argv[]) {
    /*
     * Use a file of known length read from it then write to another file
     *
     * argv[0] = the program name, i.e. cp
     * argv[1] = the file to read from
     * argv[2] = the file to write to, create it if doesn't exist
     */
    if (argc <= 2) {
        fprintf(stderr, "usage: %s readFile writeFile\n", argv[0]);
        return 1;
    }

    // Store a reference pointer in memory to store the file names
    char *readFileName = argv[1];
    char *writeFileName = argv[2];

    /*
     * O_RDONLY open for read only
     */
    fprintf(stdout, "Attempting to open read only file %s\n", readFileName);
    int openFd = open(readFileName, O_RDONLY);
    if (0 > rudimentaryErrorHandler(openFd, errno)) return -1;

    /*
     * O_WRONLY open for write only
     * O_CREAT will create the file if it doesn't exist
     * O_TRUNC overwrite 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 write only file %s\n", writeFileName);
    int writeFd = open(writeFileName, O_CREAT | O_WRONLY | O_TRUNC,
                   S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
    if (0 > rudimentaryErrorHandler(writeFd, errno)) return -1;

    // Set iovec struct to three in length
    struct iovec iov[3];
    int iovCount = 3;

    // Three - eight byte buffers
    char bufferZero[BUFFER_SIZE];
    char bufferOne[BUFFER_SIZE];
    char bufferTwo[BUFFER_SIZE];

    // Set the inner iovec structs equal to the buffer arrays / length above
    iov[0].iov_base = bufferZero;
    iov[0].iov_len = sizeof(bufferZero);
    iov[1].iov_base = bufferOne;
    iov[1].iov_len = sizeof(bufferOne);
    iov[2].iov_base = bufferTwo;
    iov[2].iov_len = sizeof(bufferTwo);

    // Call readv() and check status code
    ssize_t readStatus = readv(openFd, iov, iovCount);
    if (0 > rudimentaryErrorHandler((int) readStatus, errno)) return -1;

    // print out the buffers because we are curious
    fprintf(stdout, "The size of buffer zero is: %zu Bytes, Its Content:\n%s\n", sizeof(bufferZero), bufferZero);
    fprintf(stdout, "The size of buffer one is: %zu Bytes, Its Content:\n%s\n", sizeof(bufferOne), bufferOne);
    fprintf(stdout, "The size of buffer two is: %zu Bytes, Its Content:\n%s\n", sizeof(bufferTwo), bufferTwo);

    // call writev() and check status code
    ssize_t writeStatus = writev(writeFd, iov, iovCount);
    if (0 > rudimentaryErrorHandler((int) writeStatus, errno)) return -1;

    fprintf(stdout, "The file was written to successfully! Closing out all the file Descriptors!\n");
    // Validating closing files successfully if so return 0
    if (0 > close(openFd)) return -1;
    if (0 > close(writeFd)) return -1;

    return 0;
}