What is shmid_ds?
Every System V shared memory segment has a kernel-maintained metadata block called shmid_ds. The kernel updates this structure automatically whenever processes attach, detach, or modify the segment. You read it with shmctl(shmid, IPC_STAT, &buf) and partially update it with shmctl(shmid, IPC_SET, &buf).
Understanding shmid_ds is essential for monitoring, auditing, and debugging shared memory in multi-process applications.
/* Defined in <sys/shm.h> */
struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment in bytes */
time_t shm_atime; /* Time of last shmat() call */
time_t shm_dtime; /* Time of last shmdt() call */
time_t shm_ctime; /* Time of last shmctl() change */
pid_t shm_cpid; /* PID of the creator process */
pid_t shm_lpid; /* PID of process that last called */
/* shmat() or shmdt() */
shmatt_t shm_nattch; /* Number of currently attached */
/* processes */
};
/* The nested ipc_perm structure (also used by msgid_ds and semid_ds) */
struct ipc_perm {
key_t __key; /* Key passed to shmget() */
uid_t uid; /* Effective UID of owner (changeable) */
gid_t gid; /* Effective GID of owner (changeable) */
uid_t cuid; /* Effective UID of creator (fixed) */
gid_t cgid; /* Effective GID of creator (fixed) */
unsigned short mode; /* Permission bits + SHM_DEST/SHM_LOCKED */
unsigned short __seq; /* Sequence number (kernel-internal) */
};
/* shmatt_t: unsigned integer type for the attach count field */
| Field | Type | Set By | IPC_SET? | Meaning |
|---|---|---|---|---|
| shm_perm.uid | uid_t | shmget / IPC_SET | ✅ Yes | Effective UID of current owner. Initially set to creator’s EUID. |
| shm_perm.gid | gid_t | shmget / IPC_SET | ✅ Yes | Effective GID of current owner. |
| shm_perm.cuid | uid_t | shmget only | ❌ No | EUID of the creator. Fixed forever after creation. |
| shm_perm.mode | unsigned short | shmget / IPC_SET | ✅ Yes (lower 9 bits) | Permission bits (rwxrwxrwx). Upper bits hold SHM_DEST and SHM_LOCKED flags — always mask with 0777 when reading permissions. |
| shm_segsz | size_t | shmget only | ❌ No | Size in bytes as given to shmget(). Fixed forever. Physical allocation is rounded up to next page boundary. |
| shm_atime | time_t | shmat() | ❌ No | Timestamp of the most recent shmat() call. 0 until first attach. |
| shm_dtime | time_t | shmdt() | ❌ No | Timestamp of the most recent shmdt() call. 0 until first detach. |
| shm_ctime | time_t | shmget / IPC_SET | ❌ No | Timestamp of segment creation or last IPC_SET. Not updated by shmat/shmdt. |
| shm_cpid | pid_t | shmget only | ❌ No | PID of the process that called shmget(). Set once, never changes. |
| shm_lpid | pid_t | shmat / shmdt | ❌ No | PID of the process that most recently called shmat() or shmdt(). 0 until first attach. |
| shm_nattch | shmatt_t | shmat++ / shmdt– | ❌ No | Count of processes currently attached. IPC_RMID fires actual deletion when this reaches 0. |
The mode field is 16 bits. The lower 9 bits are the standard Unix permission bits. The upper bits contain kernel state flags. Always mask with 0777 when reading permissions to avoid misinterpreting the flag bits.
/*
* shm_perm.mode bit layout:
*
* bits 15..11 reserved
* bit 10 SHM_LOCKED — set while SHM_LOCK is active
* bit 9 SHM_DEST — set after IPC_RMID is called (pending deletion)
* bits 8..0 rwxrwxrwx — standard Unix permission bits
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/shm.h>
/* Linux flag constants (define if not in your headers) */
#ifndef SHM_DEST
#define SHM_DEST 01000 /* 0x0200 — segment pending deletion */
#endif
#ifndef SHM_LOCKED
#define SHM_LOCKED 02000 /* 0x0400 — segment locked in RAM */
#endif
void check_flags(int shmid, const char *label)
{
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
unsigned short m = ds.shm_perm.mode;
printf("[%s] perms=%04o SHM_DEST=%s SHM_LOCKED=%s\n",
label,
m & 0777,
(m & SHM_DEST) ? "YES" : "no",
(m & SHM_LOCKED) ? "YES" : "no");
}
int main(void)
{
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0644);
check_flags(shmid, "fresh segment ");
/* Attach so segment survives IPC_RMID */
char *addr = shmat(shmid, NULL, 0);
shmctl(shmid, IPC_RMID, NULL);
check_flags(shmid, "after IPC_RMID (attached)");
if (shmctl(shmid, SHM_LOCK, NULL) == 0) {
check_flags(shmid, "after SHM_LOCK ");
shmctl(shmid, SHM_UNLOCK, NULL);
check_flags(shmid, "after SHM_UNLOCK ");
}
shmdt(addr);
return 0;
}
/*
* Output:
* [fresh segment ] perms=0644 SHM_DEST=no SHM_LOCKED=no
* [after IPC_RMID (attached)] perms=0644 SHM_DEST=YES SHM_LOCKED=no
* [after SHM_LOCK ] perms=0644 SHM_DEST=YES SHM_LOCKED=YES
* [after SHM_UNLOCK ] perms=0644 SHM_DEST=YES SHM_LOCKED=no
*/
| shmget() | shm_ctime = now shm_atime = 0 shm_dtime = 0 |
shmat() | shm_atime = now shm_lpid = current PID shm_nattch++ |
| shmdt() | shm_dtime = now shm_lpid = current PID shm_nattch– |
IPC_SET | shm_ctime = now |
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <sys/shm.h>
static void ts(time_t t, const char *name)
{
if (t == 0) printf(" %-30s : never\n", name);
else printf(" %-30s : %s", name, ctime(&t));
}
static void dump_times(int shmid, const char *label)
{
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
printf("\n[%s]\n", label);
ts(ds.shm_ctime, "shm_ctime (created/changed)");
ts(ds.shm_atime, "shm_atime (last shmat) ");
ts(ds.shm_dtime, "shm_dtime (last shmdt) ");
printf(" %-30s : %lu\n", "shm_nattch", (unsigned long)ds.shm_nattch);
printf(" %-30s : %d / %d\n", "cpid / lpid",
(int)ds.shm_cpid, (int)ds.shm_lpid);
}
int main(void)
{
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0660);
dump_times(shmid, "After shmget");
char *addr = shmat(shmid, NULL, 0);
sleep(1); /* make timestamps distinct */
dump_times(shmid, "After shmat");
shmdt(addr);
sleep(1);
dump_times(shmid, "After shmdt");
/* IPC_SET updates shm_ctime */
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
ds.shm_perm.mode = 0640;
shmctl(shmid, IPC_SET, &ds);
dump_times(shmid, "After IPC_SET");
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
shm_nattch is incremented by every shmat() and decremented by every shmdt(). A manager process can poll this field to know when all workers have finished and detached.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/shm.h>
#include <sys/wait.h>
#define NUM_WORKERS 4
int main(void)
{
int shmid;
int *counter; /* shared integer counter */
shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0660);
counter = shmat(shmid, NULL, 0);
*counter = 0;
printf("[Manager] Spawning %d workers\n", NUM_WORKERS);
for (int i = 0; i < NUM_WORKERS; i++) {
if (fork() == 0) {
int *w = shmat(shmid, NULL, 0);
sleep(1 + i % 3); /* simulate variable workload */
(*w)++;
printf("[Worker %d] incremented counter, detaching\n", i);
shmdt(w);
exit(0);
}
}
/* Poll shm_nattch until all workers have detached (nattch == 1, only manager) */
struct shmid_ds ds;
do {
sleep(1);
shmctl(shmid, IPC_STAT, &ds);
printf("[Manager] shm_nattch = %lu\n", (unsigned long)ds.shm_nattch);
} while (ds.shm_nattch > 1);
printf("[Manager] All workers done. counter = %d\n", *counter);
for (int i = 0; i < NUM_WORKERS; i++) wait(NULL);
shmctl(shmid, IPC_RMID, NULL);
shmdt(counter);
return 0;
}
/*
* Output:
* [Manager] Spawning 4 workers
* [Manager] shm_nattch = 5
* [Worker 0] incremented counter, detaching
* [Worker 1] incremented counter, detaching
* [Manager] shm_nattch = 3
* [Worker 2] incremented counter, detaching
* [Worker 3] incremented counter, detaching
* [Manager] shm_nattch = 1
* [Manager] All workers done. counter = 4
*/
shm_segsz records the exact byte count you passed to shmget(). But the kernel allocates memory in whole pages (typically 4096 bytes). So a 1-byte segment still occupies one full page in RAM.
#include <stdio.h>
#include <sys/shm.h>
#include <unistd.h>
int main(void)
{
long page_size = sysconf(_SC_PAGESIZE);
size_t sizes[] = {1, 100, 4095, 4096, 4097, 8192, 10000};
printf("Page size: %ld bytes\n\n", page_size);
printf("%-12s %-18s %s\n",
"Requested", "shm_segsz stores", "Physical pages used");
printf("%-12s %-18s %s\n",
"---------", "----------------", "-------------------");
for (int i = 0; i < 7; i++) {
int shmid = shmget(IPC_PRIVATE, sizes[i], IPC_CREAT | 0600);
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
/* Round up to next page boundary */
size_t actual = (ds.shm_segsz + page_size - 1)
& ~(page_size - 1);
long pages = actual / page_size;
printf("%-12zu %-18zu %ld page(s) = %zu bytes\n",
sizes[i], ds.shm_segsz, pages, actual);
shmctl(shmid, IPC_RMID, NULL);
}
return 0;
}
/*
* Output (4096-byte pages):
* Page size: 4096 bytes
*
* Requested shm_segsz stores Physical pages used
* --------- ---------------- -------------------
* 1 1 1 page(s) = 4096 bytes
* 100 100 1 page(s) = 4096 bytes
* 4095 4095 1 page(s) = 4096 bytes
* 4096 4096 1 page(s) = 4096 bytes
* 4097 4097 2 page(s) = 8192 bytes
* 8192 8192 2 page(s) = 8192 bytes
* 10000 10000 3 page(s) = 12288 bytes
*/
/*
* shm_dump.c — display all shmid_ds fields for a given shmid
* Usage: ./shm_dump <shmid>
* Compile: gcc -o shm_dump shm_dump.c
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/shm.h>
#ifndef SHM_DEST
#define SHM_DEST 01000
#endif
#ifndef SHM_LOCKED
#define SHM_LOCKED 02000
#endif
static void print_ts(const char *label, time_t t)
{
if (t == 0)
printf(" %-28s : (never)\n", label);
else {
char buf[64];
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", localtime(&t));
printf(" %-28s : %s\n", label, buf);
}
}
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "Usage: %s \n", argv[0]);
return 1;
}
int shmid = atoi(argv[1]);
struct shmid_ds ds;
if (shmctl(shmid, IPC_STAT, &ds) == -1) {
perror("shmctl IPC_STAT");
fprintf(stderr, "Segment shmid=%d does not exist or no permission\n", shmid);
return 1;
}
long page = sysconf(_SC_PAGESIZE);
size_t phys = (ds.shm_segsz + page - 1) & ~(page - 1);
printf("==========================================================\n");
printf(" shmid_ds Dump — shmid = %d\n", shmid);
printf("==========================================================\n");
printf(" PERMISSIONS & OWNERSHIP\n");
printf(" %-28s : %u\n", "Owner UID", ds.shm_perm.uid);
printf(" %-28s : %u\n", "Owner GID", ds.shm_perm.gid);
printf(" %-28s : %u\n", "Creator UID", ds.shm_perm.cuid);
printf(" %-28s : %u\n", "Creator GID", ds.shm_perm.cgid);
printf(" %-28s : %04o\n", "Permissions", ds.shm_perm.mode & 0777);
printf(" %-28s : %s\n", "SHM_DEST (IPC_RMID called)",
(ds.shm_perm.mode & SHM_DEST) ? "YES" : "no");
printf(" %-28s : %s\n", "SHM_LOCKED (SHM_LOCK active)",
(ds.shm_perm.mode & SHM_LOCKED) ? "YES" : "no");
printf("----------------------------------------------------------\n");
printf(" SEGMENT INFO\n");
printf(" %-28s : %zu bytes\n", "Logical size (shm_segsz)", ds.shm_segsz);
printf(" %-28s : %zu bytes (%zu page(s))\n",
"Physical allocation", phys, phys / page);
printf(" %-28s : %lu\n",
"Attached count (nattch)", (unsigned long)ds.shm_nattch);
printf("----------------------------------------------------------\n");
printf(" PROCESS INFO\n");
printf(" %-28s : %d\n", "Creator PID (shm_cpid)", (int)ds.shm_cpid);
printf(" %-28s : %d\n", "Last-op PID (shm_lpid)", (int)ds.shm_lpid);
printf("----------------------------------------------------------\n");
printf(" TIMESTAMPS\n");
print_ts("Created/changed (ctime)", ds.shm_ctime);
print_ts("Last shmat() (atime)", ds.shm_atime);
print_ts("Last shmdt() (dtime)", ds.shm_dtime);
printf("==========================================================\n");
return 0;
}
/*
* Sample run:
* $ ./make_shm & # program that creates and holds a segment, prints shmid
* Created shmid=5
* $ ./shm_dump 5
* ==========================================================
* shmid_ds Dump — shmid = 5
* ==========================================================
* PERMISSIONS & OWNERSHIP
* Owner UID : 1000
* Owner GID : 1000
* Creator UID : 1000
* Creator GID : 1000
* Permissions : 0660
* SHM_DEST ... : no
* SHM_LOCKED ... : no
* ----------------------------------------------------------
* SEGMENT INFO
* Logical size : 8192 bytes
* Physical allocation : 8192 bytes (2 page(s))
* Attached count : 1
* ----------------------------------------------------------
* PROCESS INFO
* Creator PID : 12345
* Last-op PID : 12345
* ----------------------------------------------------------
* TIMESTAMPS
* Created/changed : 2026-06-10 10:30:00
* Last shmat() : 2026-06-10 10:30:00
* Last shmdt() : (never)
* ==========================================================
*/
/* List all shared memory segments (basic view) */
$ ipcs -m
/*
* ------ Shared Memory Segments --------
* key shmid owner perms bytes nattch status
* 0x00000000 0 ravi 660 4096 2
* 0x00000000 1 ravi 660 8192 0 dest
*
* "dest" in status = IPC_RMID was called (SHM_DEST flag set)
*/
/* Show timestamps (shm_atime, shm_dtime, shm_ctime) */
$ ipcs -m -t
/* Show PID info (shm_cpid, shm_lpid) */
$ ipcs -m -p
/* Show everything at once */
$ ipcs -m -a
/* Remove a specific orphaned segment (e.g. after crash) */
$ ipcrm -m 0 /* by shmid */
$ ipcrm -M 0x1234 /* by key */
/* Remove all orphaned shm segments owned by current user (cleanup script) */
$ ipcs -m | awk 'NR>3 && $3==ENVIRON["USER"] {print $2}' | xargs -I{} ipcrm -m {}
Answer: It is the kernel-maintained metadata block for a System V shared memory segment. It records the segment’s size, owner/creator UIDs and GIDs, permission mode, the three operation timestamps (attach/detach/change), creator PID, last-operation PID, and current attach count. Applications read it with IPC_STAT to monitor or audit the segment and use IPC_SET to change owner/group/permissions.
Answer: shm_cpid is the PID of the process that called shmget() to create the segment — set once at creation, never changes. shm_lpid is the PID of the process that most recently called shmat() or shmdt() — updated on every attach or detach. In a busy system, shm_lpid changes frequently.
Answer: It is the count of processes currently attached to the segment. The kernel increments it on each shmat() and decrements it on each shmdt(). For resource management: after IPC_RMID is called, the segment is not physically deleted until shm_nattch drops to 0. A manager process can poll this field to synchronize — waiting until all workers have detached before proceeding with cleanup or shutdown.
Answer: shm_atime (“access time”) is updated every time any process calls shmat(). shm_ctime (“change time”) is updated when the segment is created and when IPC_SET changes its metadata. shm_ctime does not change on attach or detach operations.
Answer: shm_segsz stores the exact requested byte count from shmget(). The kernel allocates in whole pages (typically 4096 bytes), so the actual physical footprint is rounded up. A 1-byte segment uses a full 4096-byte page. The actual allocation is: (shm_segsz + PAGE_SIZE - 1) & ~(PAGE_SIZE - 1).
Answer: Yes. ipc_perm is the common ownership/permissions block shared by all three System V IPC mechanisms: it is the first member of shmid_ds (shared memory), msqid_ds (message queues), and semid_ds (semaphore sets). This common structure enables uniform permission checking across all three mechanisms.
Answer:
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
if (ds.shm_perm.mode & SHM_DEST)
printf("Segment is marked for deletion\n");
The SHM_DEST flag (octal 01000) is set in the mode field when IPC_RMID has been called. From the command line, ipcs -m shows “dest” in the status column for such segments.
Answer: The calling process does not have read permission on the segment. IPC_STAT requires at minimum read permission (the ‘r’ bit set for the appropriate user/group/other category based on the process’s effective UID/GID vs the segment’s owner/group). Either the permissions must be changed by the owner (via IPC_SET) or the process must run with appropriate credentials.
You now understand the virtual memory layout of shared memory processes, safe pointer storage, all shmctl() operations, and the complete shmid_ds metadata structure.
