shmat() & shmdt() — Attach & Detach

 

shmat() & shmdt() — Attach & Detach
Chapter 48, Section 48.3 – TLPI | System V Shared Memory
Part 3
shmat / shmdt Deep Dive
SHM_RDONLY
Read-Only Attach
SHMLBA
Low Boundary Addr

What Do shmat() and shmdt() Do?

After shmget() gives you a segment identifier, shmat() attaches the segment — it maps it into your process’s virtual address space and returns a pointer. From that point on you use the segment exactly like normal memory: dereference the pointer, cast it to your struct, memcpy into it, whatever you need.

When you are done, shmdt() detaches the segment — it removes the mapping from your virtual address space. The segment itself still exists; only your process loses access to it. Detach happens automatically at process exit even if you forget to call it.

Key Terms for This Section

shmat() shmdt() shmaddr SHM_RDONLY SHM_RND SHM_REMAP SHMLBA Virtual Address Space Page Table SIGSEGV Multiple Attaches

shmat() — Function Signature
#include <sys/types.h>   /* For portability */
#include <sys/shm.h>

void *shmat(int shmid, const void *shmaddr, int shmflg);

/* Returns: address at which segment is attached on success,
            or (void *) -1 on error */
Parameter Purpose
shmid The segment identifier returned by shmget().
shmaddr Desired attach address in virtual memory. Pass NULL to let the kernel choose (strongly recommended).
shmflg Control flags: 0 for read-write, SHM_RDONLY, SHM_RND, SHM_REMAP.

shmdt() — Function Signature
#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

/* Returns: 0 on success, or -1 on error */

shmaddr must be the value previously returned by shmat(). Passing any other address causes EINVAL. After this call the pointer is invalid — any further dereference causes a segfault.

How Attaching Works — Virtual Memory Diagram

When you call shmat(), the kernel adds new entries to the calling process’s page table. These entries map some range of virtual addresses to the physical frames of the shared segment. Two processes can map the same physical frames at different virtual addresses — the pointer values will differ but they point to the same RAM.

Process A
0x7fff… Stack
0x7f3a… ← shm ptr A
heap
text/data

page table

Physical RAM
Shared
Segment
Frames
4096 bytes
one physical copy

page table

Process B
0x7fff… Stack
0x7f8c… ← shm ptr B
heap
text/data

Notice: ptr A (0x7f3a…) ≠ ptr B (0x7f8c…) — different virtual addresses, same physical frames. Never store raw pointers inside a shared memory struct — the virtual addresses differ between processes!

The Three shmaddr Options

Option 1
shmaddr = NULL (RECOMMENDED)

Let the kernel choose a suitable address. Always portable, never conflicts with existing mappings. This is what you should use 99% of the time.

void *shm = shmat(shmid, NULL, 0);
/* shm is now a kernel-chosen address, guaranteed valid */

Option 2
shmaddr != NULL, SHM_RND not set

Attach at exactly the address you specify. It must be a multiple of the page size or you get EINVAL. Fails if that range is already in use. Not portable.

/* Must be page-aligned */
void *addr = (void *)0x40000000;   /* must be multiple of 4096 */
void *shm  = shmat(shmid, addr, 0);
if (shm == (void *)-1) perror("shmat fixed addr");

Option 3
shmaddr != NULL, SHM_RND set

Address is rounded down to the nearest SHMLBA boundary. SHMLBA (Shared Memory Low Boundary Address) is a multiple of the page size — on x86 it equals the page size itself. Only relevant on architectures where misaligned segment attaches cause cache inconsistencies (e.g., some RISC architectures).

#include <sys/shm.h>

void *desired = (void *)0x40001234;  /* not aligned */
/* SHM_RND rounds down to nearest SHMLBA boundary */
void *shm = shmat(shmid, desired, SHM_RND);
printf("SHMLBA = %ld\n", SHMLBA);

Read-Only Attach with SHM_RDONLY

Pass SHM_RDONLY in shmflg to attach the segment for reading only. Attempting to write to a read-only attachment causes a SIGSEGV (segmentation fault), the same signal you get from writing to a const pointer or a write-protected page.

To attach read-only, the process needs only read permission on the segment. Without SHM_RDONLY, both read and write permissions are required.

/* Read-only attach */
const char *shm_ro = shmat(shmid, NULL, SHM_RDONLY);
if (shm_ro == (void *)-1) { perror("shmat rdonly"); exit(1); }

printf("Data: %s\n", shm_ro);   /* OK — reading */

/* shm_ro[0] = 'X';  — WRONG! This would cause SIGSEGV */

shmdt(shm_ro);
/* Practical: producer attaches read-write, consumer read-only */
/* Producer (writer): */
char *buf = shmat(shmid, NULL, 0);            /* read-write */

/* Consumer (reader): */
const char *buf = shmat(shmid, NULL, SHM_RDONLY);  /* read-only */

Attaching the Same Segment Multiple Times

A single process can call shmat() on the same segment multiple times. Each call creates an independent mapping at a different virtual address. All mappings point to the same physical RAM.

You can mix read-write and read-only attachments in the same process. This is unusual but valid — you might attach read-only for most code and keep one read-write pointer for a privileged update function.

/* Multiple attaches — same segment, different virtual addresses */
char *rw_ptr = shmat(shmid, NULL, 0);          /* read-write */
char *ro_ptr = shmat(shmid, NULL, SHM_RDONLY); /* read-only */

/* Both point to the same physical memory */
printf("rw_ptr = %p\n", (void *)rw_ptr);
printf("ro_ptr = %p\n", (void *)ro_ptr);
/* These addresses will differ, but *rw_ptr == *ro_ptr */

strcpy(rw_ptr, "hello");
printf("via ro: %s\n", ro_ptr);   /* prints: hello */

/* Must detach both independently */
shmdt(rw_ptr);
shmdt(ro_ptr);

SHM_REMAP — Replacing Existing Mappings

Normally, attaching at an address range already occupied by another mapping fails with EINVAL. The Linux-specific SHM_REMAP flag overrides this: it replaces any existing mapping in the target range with the new shared memory segment.

SHM_REMAP requires shmaddr to be non-NULL. It is a non-standard Linux extension — do not use it in portable code.

/* SHM_REMAP: replace whatever is at 'addr' with this segment */
void *addr = (void *)0x60000000;

/* First attach something at that address */
void *old = shmat(other_shmid, addr, 0);

/* Now replace it — without SHM_REMAP this would fail with EINVAL */
void *new = shmat(shmid, addr, SHM_REMAP);
if (new == (void *)-1) {
    perror("shmat SHM_REMAP");
}
/* old mapping is gone, 'addr' now maps to shmid */

Correct Use of shmdt()

shmdt() is straightforward but has important rules:

Always pass the shmat() return value
Any pointer arithmetic on the shmat address is fine during use, but you must pass the original address back to shmdt().
Do NOT pass a modified address
If you do ptr++ after shmat(), you cannot pass ptr to shmdt(). Save the original.
/* CORRECT: save original pointer */
char *shm_base = shmat(shmid, NULL, 0);   /* original */
char *p        = shm_base;                /* working pointer */

while (*p) p++;   /* advance p */

/* ... */

shmdt(shm_base);   /* pass original — correct */
/* shmdt(p);       would be WRONG if p != shm_base */
/* WRONG pattern — don't do this */
char *shm = shmat(shmid, NULL, 0);
shm += 100;   /* moved shm away from base */
shmdt(shm);   /* EINVAL — wrong address! */

shmat() shmflg Flags Reference
Flag Effect Notes
0 Attach read-write. Kernel chooses address if shmaddr=NULL. Most common.
SHM_RDONLY Attach read-only. Writes cause SIGSEGV. Only read permission needed on segment.
SHM_RND Round shmaddr down to SHMLBA. Requires shmaddr != NULL. Rarely needed; mainly for non-x86 architectures.
SHM_REMAP Replace existing mapping at shmaddr. Requires shmaddr != NULL. Linux only — non-standard extension.

Critical Pitfall: Never Store Pointers Inside Shared Memory

Because two processes attach the same segment at different virtual addresses, a pointer that is valid in Process A (pointing into the shared segment) will point to the wrong place in Process B — or crash it outright.

WRONG — Pointer stored in shared memory
Process A sees Shared Memory Process B sees
attached at 0x7f30 ptr field = 0x7f30 + 100 attached at 0x7f80 → ptr 0x7f30+100 = INVALID!

Use offsets from the base address instead of absolute pointers:

/* WRONG: storing absolute pointer in shared memory */
struct bad_shm {
    char  data[512];
    char *next;        /* absolute pointer — process-specific! */
};

/* CORRECT: use offset from base */
struct good_shm {
    char   data[512];
    size_t next_offset;   /* offset within the segment */
};

/* Writer (Process A): */
struct good_shm *shm = shmat(shmid, NULL, 0);
shm->next_offset = sizeof(struct good_shm);  /* offset, not pointer */

/* Reader (Process B): */
struct good_shm *shm = shmat(shmid, NULL, 0);  /* different virtual addr */
struct good_shm *next = (struct good_shm *)((char *)shm + shm->next_offset);
/* This works regardless of attach address */

Complete Example: Shared Struct with Mutex Synchronization

This example puts a struct directly in shared memory along with a POSIX mutex (configured for process sharing) to prevent race conditions.

/* shared_struct.h — put this in shared memory */
#ifndef SHARED_STRUCT_H
#define SHARED_STRUCT_H

#include <pthread.h>

#define MAX_MSG 256

typedef struct {
    pthread_mutex_t mutex;      /* MUST be process-shared */
    int             count;
    char            message[MAX_MSG];
    int             ready;      /* flag: 1 = new data available */
} shared_data_t;

#endif
/* producer.c — creates segment and writes data */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <pthread.h>
#include "shared_struct.h"

#define KEY_PATH "/tmp"
#define KEY_ID   'P'

int main(void)
{
    key_t          key    = ftok(KEY_PATH, KEY_ID);
    int            shmid  = shmget(key, sizeof(shared_data_t),
                                   IPC_CREAT | IPC_EXCL | 0660);
    if (shmid == -1) { perror("shmget"); exit(1); }

    shared_data_t *shm = shmat(shmid, NULL, 0);
    if (shm == (void *)-1) { perror("shmat"); exit(1); }

    /* Initialize mutex for cross-process sharing */
    pthread_mutexattr_t attr;
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
    pthread_mutex_init(&shm->mutex, &attr);
    pthread_mutexattr_destroy(&attr);

    shm->count = 0;
    shm->ready = 0;

    for (int i = 1; i <= 5; i++) {
        pthread_mutex_lock(&shm->mutex);

        shm->count = i;
        snprintf(shm->message, MAX_MSG, "Message #%d from PID %d", i, getpid());
        shm->ready = 1;

        pthread_mutex_unlock(&shm->mutex);

        printf("[producer] wrote: %s\n", shm->message);
        sleep(1);
    }

    /* Signal done */
    pthread_mutex_lock(&shm->mutex);
    shm->ready = -1;   /* sentinel: done */
    pthread_mutex_unlock(&shm->mutex);

    /* Cleanup after consumer has finished */
    sleep(2);
    pthread_mutex_destroy(&shm->mutex);
    shmdt(shm);
    shmctl(shmid, IPC_RMID, NULL);
    printf("[producer] segment deleted\n");
    return 0;
}
/* consumer.c — reads from existing segment */
#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <pthread.h>
#include "shared_struct.h"

#define KEY_PATH "/tmp"
#define KEY_ID   'P'

int main(void)
{
    key_t          key   = ftok(KEY_PATH, KEY_ID);
    int            shmid = shmget(key, 0, 0);   /* open existing */
    if (shmid == -1) { perror("shmget"); exit(1); }

    shared_data_t *shm = shmat(shmid, NULL, 0);
    if (shm == (void *)-1) { perror("shmat"); exit(1); }

    int last_count = 0;
    while (1) {
        pthread_mutex_lock(&shm->mutex);
        int done  = (shm->ready == -1);
        int fresh = (shm->ready == 1 && shm->count != last_count);

        if (fresh) {
            printf("[consumer] read: %s\n", shm->message);
            last_count = shm->count;
        }
        pthread_mutex_unlock(&shm->mutex);

        if (done) break;
        usleep(100000);   /* poll every 100ms */
    }

    printf("[consumer] producer finished\n");
    shmdt(shm);
    return 0;
}
gcc -o producer producer.c -lpthread
gcc -o consumer consumer.c -lpthread

# Terminal 1:
./producer

# Terminal 2:
./consumer

Checking Attachment Count with shmctl()

The shmid_ds structure maintained by the kernel contains shm_nattch — the number of processes currently attached to the segment. You can query it with shmctl(IPC_STAT).

#include <sys/shm.h>
#include <stdio.h>

void print_shm_info(int shmid)
{
    struct shmid_ds ds;

    if (shmctl(shmid, IPC_STAT, &ds) == -1) {
        perror("shmctl IPC_STAT");
        return;
    }

    printf("Segment size:    %zu bytes\n",  ds.shm_segsz);
    printf("Attach count:    %lu\n",         (unsigned long)ds.shm_nattch);
    printf("Owner UID:       %u\n",           ds.shm_perm.uid);
    printf("Creator PID:     %d\n",           (int)ds.shm_cpid);
    printf("Last attach PID: %d\n",           (int)ds.shm_lpid);
}

int main(void)
{
    int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
    if (shmid == -1) { perror("shmget"); return 1; }

    printf("Before attach:\n");
    print_shm_info(shmid);

    void *p = shmat(shmid, NULL, 0);

    printf("\nAfter attach:\n");
    print_shm_info(shmid);    /* nattch = 1 */

    shmdt(p);
    printf("\nAfter detach:\n");
    print_shm_info(shmid);    /* nattch = 0 */

    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

Common shmat() / shmdt() Errors
Function errno Cause & Fix
shmat EACCES No read/write permission on segment. Check segment perms.
shmat EINVAL Bad shmid, or shmaddr not page-aligned (without SHM_RND), or range in use without SHM_REMAP.
shmat ENOMEM Not enough virtual address space to attach. Common in 32-bit processes with large segments.
shmdt EINVAL shmaddr is not the value returned by shmat() (e.g., pointer was incremented).

Interview Questions — shmat() & shmdt()
Q1. What does shmat() return and how do you use the return value?

shmat() returns a void * pointing to the start of the shared memory segment in the calling process’s virtual address space. You cast it to a pointer-to-struct (or char*, etc.) and then read/write through it just like any normal memory. On error it returns (void *) -1 — not NULL — so always check if (ptr == (void *)-1).

Q2. Why should you always pass NULL for shmaddr in shmat()?

Passing NULL lets the kernel select a free, aligned address — portable across architectures and guaranteed not to conflict with existing mappings. Specifying a fixed address is not portable (valid on one OS/arch, invalid on another), and will fail with EINVAL if that range is already in use (e.g., by a library already loaded at that address).

Q3. What happens if you write to a segment attached with SHM_RDONLY?

The process receives a SIGSEGV (segmentation fault). This is because the kernel marks the page table entries as read-only; any write triggers a hardware protection fault which the kernel converts to SIGSEGV and delivers to the process.

Q4. Can the same process attach the same segment twice? What are the implications?

Yes. Each shmat() call creates an independent mapping at a different virtual address, all backed by the same physical RAM. You can mix read-write and read-only. Each attachment must be individually released with shmdt(). This is useful if you need to pass the segment to code that expects to receive pointers via different mechanisms.

Q5. Why should you never store raw pointers inside a shared memory struct?

Different processes attach the segment at different virtual addresses. A pointer value recorded by Process A refers to an address in A’s virtual space — it will be wrong (or outright invalid) in Process B. Always store offsets from the base of the segment, then reconstruct pointers as (char *)shm_base + offset.

Q6. What is SHMLBA and when is SHM_RND flag relevant?

SHMLBA (Shared Memory Low Boundary Address) is a multiple of the system page size. SHM_RND rounds the user-supplied attach address down to the nearest SHMLBA boundary. On x86/x86-64 SHMLBA equals the page size, so alignment is trivial. On some RISC architectures (SPARC, MIPS), cache aliasing means segments must be attached at SHMLBA-aligned addresses to ensure all attachments have a consistent view in the CPU cache.

Q7. What is shmdt() and what happens if you forget to call it?

shmdt() removes the segment’s mapping from the calling process’s virtual address space. If you forget it, the mapping is automatically removed when the process exits. The segment itself is NOT deleted — it persists until shmctl(IPC_RMID) is called. The only consequence of missing shmdt() is a minor virtual address space leak during the process lifetime.

Q8. How would you use a POSIX mutex for synchronization inside a shared memory segment?

Place a pthread_mutex_t inside the shared struct. Before initializing it, create a pthread_mutexattr_t, call pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED), then pthread_mutex_init(&shm->mutex, &attr). The PTHREAD_PROCESS_SHARED attribute makes the mutex usable across process boundaries as long as it lives in shared memory visible to both processes.

Quick Reference: shmat / shmdt Cheat Sheet
Task Code
Attach read-write void *p = shmat(shmid, NULL, 0);
Attach read-only void *p = shmat(shmid, NULL, SHM_RDONLY);
Check for error if (p == (void *)-1) { perror("shmat"); }
Cast to struct my_struct_t *s = (my_struct_t *)shmat(shmid, NULL, 0);
Detach shmdt(original_ptr);
Check nattch shmctl(shmid, IPC_STAT, &ds); ds.shm_nattch;
Delete segment shmctl(shmid, IPC_RMID, NULL);

Series Navigation

System V Shared Memory – Chapter 48 | EmbeddedPathashala

Leave a Reply

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