File Locking is generally useful in an environment where multiple applications or processes may want to access(read/write) the same file. In such cases, it becomes important to protect integrity of file data and provide consistent view of data to all the processes.

This post studies only advisory file locks with Go on Unix based systems such as Linux, FreeBSD.

Lock types

There are two types of locks:

  • Read or Shared locks
  • Write or Exclusive locks

Read or Shared locks

Multiple reader (processes, threads, goroutines, ...) can hold read locks on the same handler (such as file, programming object) unless any exclusive lock held.

This is like sync.RWMutex.RLock() in go.

Write or Exclusive locks

At a time only one writer can hold a write lock on the handler. So no other handler should be able to acquire a read or write lock on a handler already write locked by a different reader or writer.

This is like sync.RWMutex.Lock() in go.

Advisory File Locking

Advisory File Locking requires cooperation from all the involved processes. Once a process(say P1) has taken a read/shared lock on a file “F”, there is really nothing done by Operating System to prevent a process(say P2) from going ahead and issuing a write system call. As mentioned, this method requires the participating processes to obey some locking protocol/API and hence cooperate with each other. As long as these processes first make use of the provided locking protocol, and respect its result before issuing read or write system calls, advisory locking scheme is going to work.

There are two types of file locking in Unix based OSes.

  • BSD Lock (flock)
  • Posix Lock

BSD Lock (flock)

BSD locks are associated with individual file descriptors and not to processes. So, all the different file descriptors acquired by the process for a particular file are treated independently. For example, a process opens a file “F” and takes a write lock on it. It now opens the same file again, and gets another descriptor. Any lock attempt by the process on this file descriptor will result in a “Lock-Conflict” error.

flock is originally implemented in 4.2BSD Operating System. In Linux; since kernel 2.0, flock() is implemented as a system call in its own right rather than being emulated in the GNU C library as a call to fcntl(2). Locks set by flock system call are usually referred to as BSD locks. flock supports only advisory locking. Also, it does not provide byte-range locks, instead all locks are full-file locks.

import(
    "fmt"
    "os"
    "syscall"
)

func Lock(path string, exclusive bool) (f *os.File, err error) {
    f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        return nil, fmt.Errorf("open error: %w", err)
    }
    flags := syscall.LOCK_NB
    if exclusive {
        flags |= syscall.LOCK_EX
    } else {
        flags |= syscall.LOCK_SH
    }
    err = syscall.Flock(int(f.Fd()), flags)
    if err != nil {
        f.Close()
        if err != syscall.EWOULDBLOCK {
            return nil, fmt.Errorf("syscall error: %w", err)
        }
        return nil, nil
    }
    return
}

Posix lock

On most Unixes, advisory locking is the default scheme provided to applications.

Locks can be placed over file in two different forms:

  • Full-File locks are placed over the entire file. It implies that entire region starting from first byte offset to the largest byte offset of the file is locked by the process.
  • Byte-Range locks are placed over specific byte-offset ranges in the file.

fcntl system call is the most popular way of using advisory locks on Unix systems. By default, locks set by fcntl are advisory in nature. Locks set by fcntl system call are usually referred to as POSIX locks as it adheres to POSIX standards of file locking, and is available on most compliant operating systems.

import(
    "fmt"
    "os"
    "syscall"
)

func Lock(path string, exclusive bool) (f *os.File, err error) {
    locksMu.Lock()
    defer locksMu.Unlock()
    if _, ok := locks[path]; ok {
        return nil, nil
    }
    f, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        return nil, fmt.Errorf("open error: %w", err)
    }
    typ := syscall.F_RDLCK
    if exclusive {}
        typ = syscall.F_WRLCK
    }
    flockT := &syscall.Flock_t{
        Type:   typ,
        Whence: 0,
        Start:  0,
        Len:    0,
    }
    err = syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, flockT)
    if err != nil {
        f.Close()
        if err != syscall.EWOULDBLOCK {
            return nil, fmt.Errorf("syscall error: %w", err)
        }
        return nil, nil
    }
    locks[path] = f
    return
}

func Unlock(path string) {
    locksMu.Lock()
    defer locksMu.Unlock()
    if f, ok := locks[path]; ok {
        f.Close()
        delete(locks, path)
    }
}

var locks = make(map[string]*os.File)
var locksMu sync.Mutex

You can see the above example has an internal locking with locks map. Because posix locks don't lock the file descriptor as bsd locks. It locks processes against the file.