System V Semaphores Part 3: semtimedop() — Timed Semaphore Operations

 

System V Semaphores
Chapter 47 – Part 3: semtimedop() — Timed Semaphore Operations
47.6
TLPI Section
Linux 2.6+
Availability
ns
Precision

What you will learn in this file

Sometimes you want to attempt a semaphore operation but give up if it cannot complete within a certain time — avoiding indefinite blocking. Linux provides semtimedop() for exactly this purpose. This tutorial explains its behavior, the timespec structure, how it compares to the older setitimer() + semop() trick, and gives practical code examples.

Key Terms

semtimedop() struct timespec EAGAIN _GNU_SOURCE setitimer() Linux 2.4.22 Deadline Timeout Semaphore

1. semtimedop() — Function Signature
#define _GNU_SOURCE         /* Required — this is Linux-specific */
#include <sys/types.h>     /* For portability */
#include <sys/sem.h>

int semtimedop(int semid, struct sembuf *sops, unsigned int nsops,
               struct timespec *timeout);
/* Returns 0 on success, -1 on error */
Parameter Meaning
semid Semaphore set ID (same as semop)
sops Array of sembuf operations (same as semop)
nsops Number of operations (same as semop)
timeout Maximum time to wait. Pass NULL to behave identically to semop(). Pass a zero-valued timespec for immediate fail (like IPC_NOWAIT).

The first three arguments are identical to semop(). The only difference is the fourth argument: a pointer to a maximum wait duration.

2. struct timespec — Expressing the Timeout Duration
struct timespec {
    time_t tv_sec;   /* Seconds (whole seconds part) */
    long   tv_nsec;  /* Nanoseconds (0 to 999,999,999) */
};

The timeout is a relative duration, not an absolute clock time. The kernel starts counting from the moment semtimedop() is called. If the semaphore becomes available within this duration, the call succeeds. If the duration expires first, the call fails with errno == EAGAIN.

Desired timeout tv_sec tv_nsec
5 seconds 5 0
500 milliseconds 0 500,000,000
100 microseconds 0 100,000
1 nanosecond (minimum) 0 1
Immediate fail (no wait) 0 0
Block forever (like semop) Pass NULL as timeout pointer

Note: The timeout value is a duration, not a deadline. Every call to semtimedop() gets a fresh countdown from the current time. If you want “retry until wallclock time X”, you must compute the remaining duration before each call.

3. Behavior Summary – semtimedop() vs. semop()

semop()
Blocks indefinitely until operation completes
Or fails immediately with EAGAIN (IPC_NOWAIT)
No timeout between “forever” and “immediate”
semtimedop()
Blocks up to a specified duration
Fails with EAGAIN if duration expires
timeout=NULL behaves exactly like semop()

4. Why semtimedop() is Better than setitimer() + semop()

Before semtimedop() existed, programmers implemented timeouts using setitimer() to deliver SIGALRM after a delay, which would interrupt semop() with EINTR. This approach has several problems:

Issue setitimer() + semop() semtimedop()
Thread safety SIGALRM is process-wide — affects all threads Timeout is per-call, fully thread-safe
Complexity Requires signal handler + careful handler code Single function call
Interference May interfere with other uses of SIGALRM No signal interference
Performance Signal delivery overhead on every timeout Kernel handles entirely in-kernel (faster)
Portability Available on all POSIX systems Linux-only (not in SUSv3)

The performance advantage matters particularly for database systems that perform many semaphore operations per second. Avoiding signal delivery on each timeout saves measurable CPU time at scale.

5. Code Example – Basic semtimedop() with 5-second Timeout
/* svsem_timed.c
 * Demonstrates semtimedop() — try to acquire a semaphore
 * but give up after 5 seconds if it remains locked.
 * Compile: gcc -D_GNU_SOURCE svsem_timed.c -o svsem_timed
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>

union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
};

int main(void)
{
    int semid;
    union semun arg;

    semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }

    /* Initialize to 0 — semaphore is locked (no resource) */
    arg.val = 0;
    semctl(semid, 0, SETVAL, arg);

    pid_t pid = fork();
    if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }

    if (pid == 0) {
        /* Child: holds the lock for 8 seconds, then releases */
        printf("Lock-holder: sleeping for 8 seconds before release...\n");
        sleep(8);
        struct sembuf release = {0, 1, 0};
        semop(semid, &release, 1);
        printf("Lock-holder: released!\n");
        exit(0);
    }

    /* Parent: try to acquire with 5-second timeout */
    struct sembuf acquire = {0, -1, 0};
    struct timespec timeout;
    timeout.tv_sec  = 5;   /* Wait up to 5 seconds */
    timeout.tv_nsec = 0;

    printf("Waiter: trying to acquire (timeout = 5s)...\n");

    int ret = semtimedop(semid, &acquire, 1, &timeout);

    if (ret == 0) {
        printf("Waiter: acquired successfully!\n");
        /* Use the resource ... */
        struct sembuf rel = {0, 1, 0};
        semop(semid, &rel, 1);
    } else {
        if (errno == EAGAIN) {
            printf("Waiter: TIMED OUT after 5 seconds — lock still held\n");
        } else if (errno == EINTR) {
            printf("Waiter: interrupted by signal\n");
        } else {
            perror("semtimedop");
        }
    }

    wait(NULL);
    semctl(semid, 0, IPC_RMID);
    return 0;
}
/* Expected output:
 * Lock-holder: sleeping for 8 seconds before release...
 * Waiter: trying to acquire (timeout = 5s)...
 * Waiter: TIMED OUT after 5 seconds — lock still held
 * Lock-holder: released!
 */

6. Code Example – Retry Loop with Absolute Deadline

Since semtimedop() takes a relative duration, you must recompute the remaining time before each retry if you want a fixed wall-clock deadline:

/* svsem_deadline.c
 * Acquires semaphore with retry, giving up at an absolute deadline.
 * Uses clock_gettime() to compute remaining time.
 * Compile: gcc -D_GNU_SOURCE svsem_deadline.c -o svsem_deadline -lrt
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <time.h>

union semun {
    int              val;
    struct semid_ds *buf;
    unsigned short  *array;
};

/* Acquire semaphore within deadline_secs seconds from now.
 * Returns 0 on success, -1 on timeout or error. */
int sem_acquire_deadline(int semid, int deadline_secs)
{
    struct timespec deadline, now, remaining;
    struct sembuf acquire = {0, -1, 0};
    int ret;

    /* Compute absolute deadline */
    clock_gettime(CLOCK_REALTIME, &deadline);
    deadline.tv_sec += deadline_secs;

    while (1) {
        /* Compute remaining time */
        clock_gettime(CLOCK_REALTIME, &now);
        remaining.tv_sec  = deadline.tv_sec  - now.tv_sec;
        remaining.tv_nsec = deadline.tv_nsec - now.tv_nsec;

        if (remaining.tv_nsec < 0) {
            remaining.tv_sec--;
            remaining.tv_nsec += 1000000000L;
        }

        /* Deadline already passed */
        if (remaining.tv_sec < 0)
            return -1;

        ret = semtimedop(semid, &acquire, 1, &remaining);

        if (ret == 0)
            return 0;  /* Success */

        if (errno == EAGAIN)
            return -1; /* Timed out */

        if (errno == EINTR)
            continue;  /* Signal interrupted — retry with updated remaining */

        return -1;     /* Real error */
    }
}

int main(void)
{
    int semid;
    union semun arg;

    semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }

    arg.val = 0;  /* Start locked */
    semctl(semid, 0, SETVAL, arg);

    printf("Trying to acquire within 3-second deadline...\n");

    /* Resource will remain locked — we expect timeout */
    if (sem_acquire_deadline(semid, 3) == 0) {
        printf("Acquired!\n");
        struct sembuf rel = {0, 1, 0};
        semop(semid, &rel, 1);
    } else {
        printf("Could not acquire within deadline — giving up\n");
    }

    semctl(semid, 0, IPC_RMID);
    return 0;
}

7. Code Example – Millisecond-Precision Timeouts

For tighter timing (e.g., in database or real-time systems), use sub-second precision:

/* Helper: build a timespec from milliseconds */
#define _GNU_SOURCE
#include <sys/sem.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

struct timespec ms_to_timespec(long milliseconds)
{
    struct timespec ts;
    ts.tv_sec  = milliseconds / 1000;
    ts.tv_nsec = (milliseconds % 1000) * 1000000L;
    return ts;
}

/* Try to acquire semaphore, waiting up to ms milliseconds */
int sem_try_acquire_ms(int semid, long ms)
{
    struct sembuf acquire = {0, -1, 0};
    struct timespec timeout = ms_to_timespec(ms);

    int ret = semtimedop(semid, &acquire, 1, &timeout);
    if (ret == 0) return 1;  /* Got it */
    if (errno == EAGAIN) return 0;  /* Timed out */
    return -1;  /* Error */
}

int main(void)
{
    union semun { int val; struct semid_ds *buf; unsigned short *array; } arg;
    int semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }

    arg.val = 0;
    semctl(semid, 0, SETVAL, arg);

    /* Try with 100ms timeout */
    printf("Trying 100ms timeout...\n");
    int r = sem_try_acquire_ms(semid, 100);
    if (r == 1)       printf("Acquired!\n");
    else if (r == 0)  printf("Timed out after 100ms\n");
    else              perror("semtimedop error");

    /* Try with 500ms timeout */
    printf("Trying 500ms timeout...\n");
    r = sem_try_acquire_ms(semid, 500);
    if (r == 1)       printf("Acquired!\n");
    else if (r == 0)  printf("Timed out after 500ms\n");
    else              perror("semtimedop error");

    semctl(semid, 0, IPC_RMID);
    return 0;
}

8. Kernel Version and Portability Notes
Detail Value
First appeared in Linux 2.6 (also back-ported to Linux 2.4 starting with 2.4.22)
In SUSv3? No — Linux-specific extension. Not available on macOS, AIX, or Solaris.
Compile flag required -D_GNU_SOURCE (or #define _GNU_SOURCE before includes)
Check for availability #ifdef __linux__ guard if writing portable code
Error if expired errno == EAGAIN (same as IPC_NOWAIT)

For embedded Linux work (Thundersoft, Qualcomm platforms): Since you are targeting modern Linux kernels, semtimedop() is always available. The _GNU_SOURCE define is typically already set in embedded Linux BSPs.

9. Real-World Pattern – Watchdog with Timeout

A common embedded pattern: a watchdog checks if a worker has completed within a deadline. The worker signals completion via semaphore; the watchdog uses semtimedop() with a timeout:

/* svsem_watchdog.c
 * Watchdog process waits for a worker to signal done.
 * If worker takes too long, watchdog logs a fault.
 * Compile: gcc -D_GNU_SOURCE svsem_watchdog.c -o svsem_watchdog
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/sem.h>
#include <unistd.h>
#include <sys/wait.h>
#include <time.h>

union semun { int val; struct semid_ds *buf; unsigned short *array; };

int main(void)
{
    int semid;
    union semun arg;

    semid = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
    if (semid == -1) { perror("semget"); exit(EXIT_FAILURE); }

    arg.val = 0;  /* Worker has NOT completed yet */
    semctl(semid, 0, SETVAL, arg);

    pid_t worker_pid = fork();
    if (worker_pid == 0) {
        /* --- WORKER: simulate doing work (may take variable time) --- */
        int work_time = 3;  /* Change to 6 to simulate a late/stuck worker */
        printf("Worker [%d]: starting work (will take %ds)\n",
               getpid(), work_time);
        sleep(work_time);

        /* Signal completion */
        struct sembuf done_sig = {0, 1, 0};
        semop(semid, &done_sig, 1);
        printf("Worker [%d]: signaled DONE\n", getpid());
        exit(0);
    }

    /* --- WATCHDOG: wait up to 5 seconds for worker to finish --- */
    struct sembuf wait_done = {0, -1, 0};
    struct timespec timeout = {5, 0};  /* 5-second watchdog timeout */

    printf("Watchdog: waiting for worker (max 5s)...\n");

    int ret = semtimedop(semid, &wait_done, 1, &timeout);
    if (ret == 0) {
        printf("Watchdog: worker completed on time! System healthy.\n");
    } else if (errno == EAGAIN) {
        printf("Watchdog: FAULT! Worker did not complete within 5 seconds!\n");
        printf("Watchdog: taking recovery action...\n");
        /* In real system: kill worker, restart subsystem, log fault, etc. */
    } else {
        perror("semtimedop");
    }

    wait(NULL);
    semctl(semid, 0, IPC_RMID);
    return 0;
}

10. Interview Questions & Answers
Q1. What is semtimedop() and how does it differ from semop()?

semtimedop() is a Linux-specific variant of semop() that accepts a fourth argument — a pointer to a struct timespec specifying a maximum wait duration. If the semaphore operation cannot complete within this duration, the call fails with errno == EAGAIN. When NULL is passed as the timeout, it behaves identically to semop().

Q2. What errno value is returned when the semtimedop() timeout expires?

EAGAIN — the same error returned when semop() fails due to IPC_NOWAIT. This makes it consistent: both “won’t wait at all” (IPC_NOWAIT) and “waited too long” (semtimedop timeout) report the same error, meaning “resource not available right now.”

Q3. Is the timeout in semtimedop() relative or absolute?

Relative — it specifies a duration counted from when semtimedop() is called, not a wall-clock timestamp. If you need an absolute deadline, you must compute the remaining time using clock_gettime() before each call. This is different from, say, pthread_cond_timedwait() which takes an absolute time.

Q4. Why is semtimedop() preferable to setitimer() + semop() for timeouts?

The setitimer() approach delivers SIGALRM which is process-wide — it affects all threads, can interfere with other signal handling, and requires a signal handler. semtimedop() is entirely in-kernel, per-call, and thread-safe. It is also more efficient because no signal needs to be delivered; the kernel simply wakes the blocking syscall after the timeout expires.

Q5. What compile-time definition is needed for semtimedop()?

You must define _GNU_SOURCE before including any headers, either by adding #define _GNU_SOURCE at the top of your source file or by compiling with -D_GNU_SOURCE on the command line. Without this, the prototype for semtimedop() is not visible and the compiler produces a warning or error.

Q6. When semtimedop() is interrupted by a signal (EINTR), does the timeout restart?

No. When semtimedop() returns EINTR, the timeout clock does not reset. If you want to retry after a signal, you must recompute the remaining time (by subtracting elapsed time from the original timeout) and call semtimedop() again with the reduced timeout. Otherwise, repeated signal delivery could cause you to wait much longer than intended.

Q7. What is the struct timespec field range for tv_nsec?

tv_nsec must be in the range 0 to 999,999,999 (inclusive). If you set it to 1,000,000,000 or more, semtimedop() will fail with errno == EINVAL. Always compute nanoseconds carefully: 1 millisecond = 1,000,000 nanoseconds; 1 microsecond = 1,000 nanoseconds.

Q8. What happens if timeout = {0, 0} (both fields zero) in semtimedop()?

A zero-value timeout means “do not wait at all” — semantically equivalent to passing IPC_NOWAIT on all operations. If the semaphore operation cannot be performed immediately, semtimedop() returns -1 with errno == EAGAIN without blocking.

11. Chapter 47 Quick Reference – All Three Parts
Topic Key Function File
Safe init (race condition fix) semget + semctl + semop(no-op) Part 1
Semaphore operations semop(semid, sops, nsops) Part 2
Timed operations semtimedop(semid, sops, nsops, timeout) Part 3

Chapter 47 Complete!

You now understand System V Semaphore initialization, operations, and timed operations.

← Part 2: semop() ← Part 1: Initialization Home

Leave a Reply

Your email address will not be published. Required fields are marked *