The C Standard Library Part 2


You can access the slides 🖼️ for this lecture.All the code samples given here can be found online, alongside instructions on how to bring up the proper environment to build and execute them here. You can download the entire set of slides and lecture notes in PDF from the home page.

Here we continue to discuss the standard library. We cover reading and writing to files, as well as error management.

File I/O: Basic Access Functions

open

Before reading or writing from a file, we first need to obtain a handle to that file with open:

int open(const char *pathname, int flags, mode_t mode);

This handle is named a file descriptor. Open takes several parameters:

  • pathname is the path of the file, for example /home/pierre/test.
  • flags specifies how the file will be used. For that parameter several flags can be specified by piping particular keywords. The access mode indicates if we will only read the file (O_RDONLY), only write the file (O_WRONLY) or both (O_RDWR). We can indicate to create the file if it does not exist with O_CREAT. We can truncate the file size to 0 if it exists with O_TRUNC. There are more possible values for flags, described in open's manual page^[https://linux.die.net/man/2/open].
  • The final argument mode indicates file permissions if it is created, see in the man page the accepted values.

Open returns the file descriptor in the form of an int.

read

Once we have the file descriptor we can access the file. Use the read function to read from the file:

ssize_t read(int fd, void *buf, size_t count);

read takes 3 parameters:

  • The first parameter is the file descriptor to read from.
  • The second is the address of a buffer that will receive the data that is read.
  • The third is the amount of bytes to read.

The read function returns the amount of bytes that were actually read. That value can be used to check for errors. Note that it is not an error if the number of bytes read is smaller than what is requested, for example the end of the file could have been reached. An actual error is indicated by a return value equal to -1.

write

The write function is used to write in the file, it works similarly to read:

ssize_t write(int fd, const void *buf, size_t count);

Its parameters are:

  • The file descriptor to write to.
  • The address of a buffer containing the data we wish to write.
  • The amount of bytes to write.

write returns the amount of bytes that were actually written. Same as with read, it is not an error if the number of bytes written is smaller than what is requested, for example the disk could be full. However, it's an error when it returns -1.

close

Once the operations on a file are finished, the programmer must free the file descriptor using close:

int close(int fd);

Example: Writing to a File

Consider the following example:

#include <stdio.h>
#include <sys/types.h>  // needed for open
#include <sys/stat.h>   // needed for open
#include <fcntl.h>      // needed for open
#include <unistd.h>     // needed for read and write
#include <string.h>

int main(int argc, char **argv) {
    int fd1;
    char *buffer = "hello, world!";

    fd1 = open("./test", O_WRONLY | O_TRUNC | O_CREAT, S_IRUSR | S_IWUSR);
    if(fd1 == -1) {
        printf("error with open\n");
        return -1;
    }

    /* write 'hello, world!' in the file */
    if(write(fd1, buffer, strlen(buffer)) != strlen(buffer)) {
        printf("issue writing\n");
        close(fd1); return -1;
    }

    /* write it again */
    if(write(fd1, buffer, strlen(buffer)) != strlen(buffer)) {
        printf("issue writing\n");
        close(fd1); return -1;
    }

    close(fd1);
    return 0;
}

Here we first open a file. With the flags O_WRONLY | O_TRUNC | O_CREAT we specify that we will perform on write operations, that if the file exist we want its size to be reduced to 0 upon open (this effectively destroys all the content of the file if there was any), and also that we want to create the file if it does not exist. If the file needs to be created, its permission will be read and write permissions for the current user (S_IRUSR | S_IWUSR).

For this example, we can illustrate the content of the (empty) file on disk and of the relevant part of the program's memory after the call to open as follows:

Next, we perform a write operation in the file. We have the file descriptor, and we write from buffer that contains "hello world". We set the amount of bytes to write to be the size of that buffer. For simplicity, we exit if we cannot fully write the buffer. And then we perform again the same write operation. Next we close the file and exit.

This is the content of the file test after execution of that program:

hello, world!hello, world!

When the file is opened, associated with the file descriptor there is an internal offset value that is set at the beginning of the file on disk, that is the address 0 in the file:

When we perform the first write the buffer is written in the file starting from the offset, then the offset is placed right after what was written:

Then we write a second time the buffer in the file starting at the offset, and we shift the offset at the end of what was written:

Example: Reading from a File

Things work very similarly for read operations. Consider the following example:

// Here ee Assume the local file "test" was previously created
// with previous (write) example program

char buffer2[10];
int fd2 = open("./test", O_RDONLY, 0x0);
int bytes_read;

if(fd2 == -1) { printf("error open\n"); return -1; }

/* read 9 bytes */
if(read(fd2, buffer2, 9) != 9) {
    printf("error reading\n"); close(fd2); return -1;
}

/* fix the string and print it */
buffer2[9] = '\0';
printf("read: '%s'\n", buffer2);

/* read 9 bytes again */
bytes_read = read(fd2, buffer2, 9);
if(bytes_read != 9) {
    printf("error reading\n"); close(fd2); return -1;
}

/* fix the string and print it */
buffer2[9] = '\0';
printf("read: '%s'\n", buffer2);

close(fd2);

We open the file we previously created in read-only mode. We read 9 bytes from it inside buffer2 and we display the content of buffer2. We do this operation twice. Note how we manually write the string termination character in the last byte of buffer2, right after what was read. This program outputs the following:

read: 'hello, wo'
read: 'rldhello,'

When the file descriptor is created, the file offset is initialised to 0:

The first read operation of 9 bytes reads the first 9 bytes of the file into the first 9 bytes of buffer2 and shifts the offset right after that:

A second read operation reads the next 9 bytes from the file and shifts the buffer again:

Random Number Generation

The rand function returns a random integer ranging from 0 to a large constant named RAND_MAX:

int rand(void);

If we want to constrain the number to fall within a particular interval we can use the modulo operator. In this example we'll get only numbers ranging between 0 and 99:

for(int i=0; i<10; i++)
    printf("%d ", rand()%100);

Running this program one may notice that the sequence is always the same among several runs. This is not a very random behaviour. It is due to the way the numbers are generated, it is done in sequence based on a value called the seed. A given seed will always yield the same sequence. So to get variable sequences among multiple execution, we can initialise the seed based on the current time:

srand(time(NULL));  // init random seed
for(int i=0; i<10; i++)
    printf("%d ", rand()%100);

Note that in some cases it is good to have a fixed seed to ensure reproducible results.

Error Management

When a function from the C standard library fails, a variable maintained by the C library is set with an error code. This variable is errno. Consider this code:

#include <errno.h>  // needed for errno and perror

int main(int argc, char **argv) {
    int fd = open("a/file/that/does/not/exist", O_RDONLY, 0x0);

    /* Open always returns -1 on failure, but it can be due to many different reasons */
    if(fd == -1) {
        printf("open failed! errno is: %d\n", errno);

        /* errno is an integer code corresponding to a given reason. To format
         * it in a textual way use perror: */
        perror("open");
    }
    return 0;
}

Here we try to open a file that does not exist and open fails, it returns -1. We can print the value of errno, it's an integer, so it's not very helpful by itself. We can get a better description of the error with the function perror. It internally looks at errno and prints a textual description of the error on the terminal.