Applying C - File Descriptors
Written by Harry Fairhead   
Monday, 24 August 2020
Article Index
Applying C - File Descriptors
Permissions & Random Access
fcntl
The Reader

fcntl

The fcntl i.e. file descriptor control function provides a way of working with a file descriptor to find out the status of the file and to modify the way it is used. It is very similar to the ioctl function, see later, but it is specifically targeted at devices that have a file descriptor.

The general form of the function is:

int fcntl(fd, cmd, ... );

where fd is a file descriptor, cmd is an int specifying what you want the function to do and there is often a third parameter according to the cmd.

For example:

newFd=fcntl(fd,F_DUPFD,10);

this does the same job as dup but you can specify the lowest file descriptor to be used i.e. 10 in this case.

A common use for fcntl is to read and set descriptor and file flags. The difference is that descriptor flags are set on the descriptor and file flags are shared between all descriptors referencing the same file. There are four status commands:

F_GETFD - get file descriptor flags

F_SETFD - set file descriptor flags

F_GETFL - get file flags

F_SETFL - set file flags.

At the moment there is only one file descriptor flag FC_CLOEXEC which if set closes the file after any exec functions. The default is for files to stay open after the exec.

The file status flags are:

O_APPEND - set append mode

O_NONBLOCK - no delay

O_DSYNC - synchronize data

O_SYNC - synchronize data and the file

O_TEMP - temporary file I/O

O_CACHE - cache data.

and the access modes are:

O_RDONLY - Open for reading only

O_WRONLY - Open for writing only

O_RDWR - Open for reading and writing.

You can read all of the flags using F_GETFL and you can set all but the access modes using F_SETFL.

For example, to read the status flags and unset the O_APPEND flag:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char** argv) {
    int fd = open("/tmp/fd.txt", O_RDWR | O_CREAT | O_TRUNC | 
                                                O_APPEND, 0644);
    int flag = fcntl(fd, F_GETFL);
    printf("%x\n", flag);
    printf("%d\n", flag & O_APPEND);
    fcntl(fd, F_SETFL, flag&~O_APPEND);
    flag = fcntl(fd, F_GETFL);
    printf("%x\n", flag);
    printf("%d\n", flag & O_APPEND);
    return (EXIT_SUCCESS);
}

Notice that if you want to leave the other status bits unaltered you have to read them and then use bit manipulation to modify just the bits you want to change.

Sharing Files – Locking

Files, or more generally streams, are sometimes used as a communication channel between different processes, different programs and even different machines. The idea is that a file can be read and written by more than one agent. If files are shared then there is the problem of simultaneous update. The solution is to use a lock to restrict who can access it while it is being changed or read.

There are usually two types of lock - a reader lock and a writer lock. A reader lock allows other readers to lock the same area of the file but blocks a writer lock from changing the file while it is being read. A writer lock stops any reader lock being acquired while that portion of the file is being changed. Notice that while a read lock allows sharing the file with other processes, it blocks any changes while it is held. In all cases a lock should be held for the shortest possible time.

There are locking functions provided by POSIX but they aren’t reliable and a much better portable solution is to use general resource locking mechanisms, see Chapter 12. A more reliable solution is provided by Linux but it goes beyond the POSIX standard.

You will also encounter the idea of a lock file. This is just a dummy file that is tested for to determine if another process has the file open already.

There are four well established file locking facilities under Linux-based - BSD locks, POSIX lockf function, POSIX record locks and Open file descriptor locks. BSD locks are less capable than POSIX record locks and the lockf function is just a repackaging of the POSIX record lock to make it slightly easier to use at the expense of some features. The file description lock is a Linux-only feature and it is basically a modification of the POSIX record lock - so it makes sense to start with this.

All of the file locking mentioned so far is "advisory" in the sense that it only works if processes call the appropriate locking functions before accessing a file. Linux does support mandatory locking which stops all access to a file but this, at the time of writing, is not reliable and best avoided.

Locks are only allowed on files and not on directories and you cannot lock a file that is available to any user. All locks are removed when the process exits.

The POSIX record lock supports locking a byte range within a file, reader and writer locks and it guarantees that a lock is acquired in an atomic way - see Chapter 13.

Acquiring a lock is a matter of using the fcntl function with the commands:

F_SETLK - acquire or release lock non-blocking, returns -1 if not acquired

F_SETLKW - as F_SETLK but blocking, returns -1 if a signal interrupts it

F_GETLK - test to see if there is a lock of the type specified.

Each of these uses an flock struct to determine the details of the lock:

struct flock {
   l_type; /* Type of lock: F_RDLCK,  F_WRLCK, F_UNLCK */
   l_whence;  /* How to interpret l_start:
                         SEEK_SET, SEEK_CUR, SEEK_END */
   l_start;   /* Starting offset for lock */
   l_len;     /* Number of bytes to lock */
   l_pid;     /* PID of process blocking our lock
                    (set by F_GETLK and F_OFD_GETLK) */
};

Obviously to hold a read lock the file must be open for reading, for a write lock open for writing, and for both open for read/write. A process can hold multiple overlapping locks. In general, a reader or a writer process would lock the logical equivalent of a record, usually implemented as a struct to make sure that no other changes can be made to the record while it is being read or written.

Here is a simple example of a write process which repeatedly writes two different bit patterns 0x55555555 and 0xaaaaaaaa to part of a file:

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc, char** argv) {
    struct flock lockwrite={0};
    lockwrite.l_type = F_WRLCK;
    lockwrite.l_whence = SEEK_SET;
    lockwrite.l_start = 10;
    lockwrite.l_len = 4;
    int value = 0x55555555;
 
    int fd = open("/tmp/fd.txt", O_RDWR, 0600);
    for (;;) {
        lseek(fd, 10, SEEK_SET);
        
        lockwrite.l_type = F_WRLCK;
        fcntl(fd, F_SETLKW, &lockwrite);
        
        value=~value;
        write(fd, &value, 4);
        
        lockwrite.l_type = F_UNLCK;
        fcntl(fd, F_SETLK, &lockwrite);
    }
    close(fd);
    return (EXIT_SUCCESS);
}

Notice that the program seeks to byte 10, gets a write lock, writes a 4-byte bit pattern and then releases the lock. The call to fcntl is blocking, so if any other process has the same four bytes locked the process will wait.



Last Updated ( Monday, 24 August 2020 )