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.
no matter what I trywhat have you tried?and immediately starts waiting for it, there's no guarantee which process will obtain it firsthow 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 ofsem_wait. Are you usingsem_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.