2

I'm trying to do an exercise from Advanced Programming in the UNIX Environment where I need to:

  • Open a file that contains a counter.
  • Fork a process.
  • Have the parent and child increment the counter alternately.

I solved it using two semaphores without much trouble. With semaphores, I can put them in shared memory so that the initial state is the same for both parent and child.

Now I'm wondering if I can mimic the same behavior using only advisory locking (e.g., via fcntl). However, no matter what I try, I'm plagued by race conditions.

Since locks are inherited only by the parent, I need an extra step to ensure that the locks are properly set up in both processes. My general idea was to start, after both parent and child finish their initialization, with two locks: one for the parent and one for the child. But then I run into issues; if one process releases its lock and immediately starts waiting for it, there's no guarantee which process will obtain it first. With semaphores, there's a natural ordering (often FIFO), but with plain locks I just see chaos.

I wish I could provide a neat code sample, but right now I'm stuck in a tangle of race conditions and unpredictable behavior.

Semaphor version:

#include "apue.h"
#include <fcntl.h>
#include <semaphore.h>
#include <sys/mman.h>
#include <sys/shm.h>

#define NLOOPS 1000
#define SIZE sizeof(long) /* size of shared memory area */

static int
update(long *ptr)
{
    return ((*ptr)++); /* return value before increment */
}

int
main(void)
{
    int fd, i, counter, semid;
    pid_t pid;
    void *area;
    if ((fd = shmget(IPC_PRIVATE, SIZE, IPC_CREAT | 0600)) == -1) {
        perror(NULL);
    }
    if ((area = shmat(fd, 0, 0)) == (void *)-1) {
        perror(NULL);
    }
    int fd_sems;
    void *sems_area;
    if ((fd_sems = shmget(IPC_PRIVATE, 2 * sizeof(sem_t),
         IPC_CREAT | 0600)) == -1) {
        perror(NULL);
    }
    if ((sems_area = shmat(fd_sems, 0, 0)) == (void *)-1) {
        perror(NULL);
    }

    sem_t *sem_par, *sem_chd;
    sem_par = sems_area;
    sem_chd = sems_area + sizeof(sem_t);
    if (sem_init(sem_par, 1, 1) == -1) {
        perror("semget");
    }
    if (sem_init(sem_chd, 1, 0) == -1) {
        perror("semget");
    }

    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid > 0) { /* parent */
        for (i = 0; i < NLOOPS; i += 2) {
            int sem_chd_val;
            sem_wait(sem_par);
            printf("Parent counter read %d\n", *(int *)area);

            if ((counter = update((long *)area)) != i)
                err_quit("parent: expected %d, got %d", i,
                    counter);
            sem_post(sem_chd);
        }
    } else { /* child */
        for (i = 1; i < NLOOPS + 1; i += 2) {
            sem_wait(sem_chd);
            printf("Child counter read %d\n", *(int *)area);

            if ((counter = update((long *)area)) != i)
                err_quit("child: expected %d, got %d", i,
                    counter);
            sem_post(sem_par);
        }
    }

    exit(0);
}

Advisory locking version:

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/shm.h>
#include <unistd.h>

#define NLOOPS 1000
#define SIZE sizeof(long) /* size of shared memory area */
int
lock_byte(int fd, off_t byte)
{
    struct flock fl;
    fl.l_type = F_WRLCK;    // F_WRLCK for exclusive lock,
    fl.l_whence = SEEK_SET; // SEEK_SET, SEEK_CUR, or SEEK_END
    fl.l_start = byte;      // Offset from l_whence
    fl.l_len = 1;           // 0 means lock to EOF
    return fcntl(fd, F_SETLKW, &fl);
}
int
unlock_byte(int fd, off_t byte)
{
    struct flock fl;
    fl.l_type = F_UNLCK;    // F_WRLCK for exclusive lock,
    fl.l_whence = SEEK_SET; // SEEK_SET, SEEK_CUR, or SEEK_END
    fl.l_start = byte;      // Offset from l_whence
    fl.l_len = 1;           // 0 means lock to EOF
    return fcntl(fd, F_SETLK, &fl);
}
int
chk_lock_byte(int fd, off_t byte)
{
    struct flock fl;
    fl.l_type = F_WRLCK;    // F_WRLCK for exclusive lock,
    fl.l_whence = SEEK_SET; // SEEK_SET, SEEK_CUR, or SEEK_END
    fl.l_start = byte;      // Offset from l_whence
    fl.l_len = 1;           // 0 means lock to EOF
    fcntl(fd, F_GETLK, &fl);

    if (fl.l_type == F_UNLCK) {
        return 0;
    }
    return 1;
}

static int
update(long *ptr)
{
    return ((*ptr)++); /* return value before increment */
}

int
main(void)
{
    int fd, i, counter, semid;
    pid_t pid;
    void *area;
    if ((fd = shmget(IPC_PRIVATE, SIZE, IPC_CREAT | 0600)) == -1) {
        perror(NULL);
    }
    if ((area = shmat(fd, 0, 0)) == (void *)-1) {
        perror(NULL);
    }
    int fd_file = open("adv_lock", O_CREAT | O_TRUNC | O_RDWR, S_IRWXU);
    ftruncate(fd_file, 3);

    lock_byte(fd_file, 0);
    lock_byte(fd_file, 1);

    if ((pid = fork()) < 0) {
        exit(EXIT_FAILURE);
    } else if (pid > 0) { /* parent */
        while (chk_lock_byte(fd_file, 2) == 0)
            ; // wait for child to init
        for (i = 0; i < NLOOPS; i += 2) {
            lock_byte(fd_file, 1);
            lock_byte(fd_file, 0);
            unlock_byte(fd_file, 0);
            printf("Parent counter read %d\n", *(int *)area);
            if ((counter = update((long *)area)) != i) {
                fprintf(stderr, "parent: expected %d, got %d\n",
                    i, counter);
                exit(EXIT_FAILURE);
            }
            unlock_byte(fd_file, 1);
            lock_byte(fd_file, 1);
        }
    } else {                       /* child */
        lock_byte(fd_file, 2); // child init
        for (i = 1; i < NLOOPS + 1; i += 2) {
            lock_byte(fd_file, 0);
            lock_byte(fd_file, 1);
            unlock_byte(fd_file, 1);
            printf("Child counter read %d\n", *(int *)area);
            if ((counter = update((long *)area)) != i) {
                fprintf(stderr, "child: expected %d, got %d\n",
                    i, counter);
                exit(EXIT_FAILURE);
            }
            unlock_byte(fd_file, 0);
            lock_byte(fd_file, 0);
        }
    }

    exit(0);
}

My aim is to use only locking and unlocking—without resorting to busy-wait lock-checking during the child's initialization phase, which feels like an unpleasant concession. In the parent process, I attempt a lock on the 0th byte of the file to determine if it's the parent's turn to increment the counter. However, between unlocking the 0th byte and re-locking it, I need to ensure that the child has had enough time to perform its own lock/unlock cycle on that same byte. There's no guarantee that the child will be fast enough to do so before the parent reaches the top of its loop, leading to a race condition.

6
  • 3
    You should probably show what you've tried in a minimal reproducible example even though it's not neat and not working. Commented Mar 1 at 15:03
  • Yep, even scrappy code is fine. Show your working version with semaphores, and then your attempt to change it to advisory locks. Commented Mar 1 at 15:42
  • 1
    Hi no matter what I try what have you tried? and immediately starts waiting for it, there's no guarantee which process will obtain it first how is that a problem? This is expected. You want a FIFO? there's a natural ordering (often FIFO) "often"? either there is FIFO or not. I doubt there is ordering guarantee. I do not see ordering guarantee in any documentation of sem_wait. Are you using sem_wait? What exact functions are you calling? I just see chaos. Please post "chaos". What exactly do you see? Your interpretation is great but might just be misleading, both you and us. Commented Mar 1 at 16:07
  • Not having a guarantee of who will grab the lock next is natural, expected behavior. If you want something like a flag indicating whose turn it is to go next, so a process can release the lock and wait if it grabbed the lock while it's someone else's turn, you need to actually implement that. Nothing about the specification for advisory locking provides a contrary guarantee -- the goal is to ensure that there's only one process holding the lock at a given time, not that there's any particular ordering to who holds it. Commented Mar 1 at 17:18
  • @CharlesDuffy I understand that advisory locking is not designed as a synchronization mechanism, so what I'm attempting is suboptimal and artificial. I'm just curious whether it's possible to implement alternating counter increments using advisory locking alone—it's purely an exercise (maybe a silly one?), not production-quality code. Commented Mar 2 at 11:53

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.