What is shmctl()?
After creating and attaching a shared memory segment, you need a way to manage its lifecycle — delete it when done, inspect its current metadata, change its permissions, or lock its pages into RAM for performance. The shmctl() system call is the control interface for all of this.
Think of it as the “admin panel” for a System V shared memory segment.
#include <sys/types.h> /* for portability */
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
/*
* Returns: 0 on success, -1 on error (errno is set)
*
* shmid — identifier returned by shmget()
* cmd — the operation to perform (see table below)
* buf — required for IPC_STAT (kernel writes into it)
* required for IPC_SET (you write values into it)
* pass NULL for IPC_RMID, SHM_LOCK, SHM_UNLOCK
*/
IPC_RMID marks the segment for deletion. What happens immediately depends on the current attach count:
Deletion is immediate. The segment’s memory is freed and the shmid is invalidated right away.
The segment is marked for deletion (SHM_DEST flag is set in mode) but stays alive. It is finally removed when the last process calls shmdt() and nattch drops to 0. All currently-attached processes can still use it normally.
shmat() on the segment if nattch > 0. Most other UNIX implementations forbid new attaches on a deleted segment. This Linux behavior is non-portable — do not rely on it in portable code.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/shm.h>
int main(void)
{
int shmid;
char *addr;
shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0660);
addr = shmat(shmid, NULL, 0);
strcpy(addr, "Hello");
printf("Before IPC_RMID: shmid=%d data='%s'\n", shmid, addr);
/*
* Best practice: call IPC_RMID immediately after all processes have
* attached — even before doing any actual work with the segment.
* This way, if the program crashes, the kernel still cleans up.
* (Without IPC_RMID, a crash leaves an orphaned segment that must
* be removed manually with: ipcrm -m )
*
* Analogous to: fd = open(...); unlink(path); use(fd); close(fd);
*/
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID");
exit(EXIT_FAILURE);
}
/* Segment is still accessible through 'addr' — data is intact */
printf("After IPC_RMID: data='%s' (still accessible!)\n", addr);
strcpy(addr, "Bye");
printf("Still writable: data='%s'\n", addr);
/* Detach — this triggers actual physical removal since IPC_RMID was set */
shmdt(addr);
/* Confirm: trying to attach again fails */
if (shmat(shmid, NULL, 0) == (void *)-1)
printf("Confirmed: segment gone after shmdt() (as expected)\n");
return 0;
}
/*
* Compile: gcc -o rmid_demo rmid_demo.c
* Output:
* Before IPC_RMID: shmid=3 data='Hello'
* After IPC_RMID: data='Hello' (still accessible!)
* Still writable: data='Bye'
* Confirmed: segment gone after shmdt() (as expected)
*/
/* PATTERN: mark for deletion immediately after all processes attach */
int shmid = shmget(key, size, IPC_CREAT | 0660);
char *p_addr = shmat(shmid, NULL, 0); /* process 1 attaches */
/* ... fork or communicate shmid to process 2 ... */
char *q_addr = shmat(shmid, NULL, 0); /* process 2 attaches (in another fork/process) */
shmctl(shmid, IPC_RMID, NULL); /* mark NOW — both are attached so nothing dies yet */
/* Both p_addr and q_addr remain fully usable.
Segment disappears automatically when both call shmdt(). */
IPC_STAT fills a shmid_ds buffer you provide with a snapshot of the segment’s current metadata. Requires read permission on the segment.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/shm.h>
void show_info(int shmid, const char *label)
{
struct shmid_ds ds;
if (shmctl(shmid, IPC_STAT, &ds) == -1) { perror("IPC_STAT"); return; }
printf("\n--- %s ---\n", label);
printf(" Segment size : %zu bytes\n", ds.shm_segsz);
printf(" Permissions : %04o\n", ds.shm_perm.mode & 0777);
printf(" Owner UID/GID : %u/%u\n", ds.shm_perm.uid, ds.shm_perm.gid);
printf(" Creator PID : %d\n", (int)ds.shm_cpid);
printf(" Last-op PID : %d\n", (int)ds.shm_lpid);
printf(" Attach count : %lu\n", (unsigned long)ds.shm_nattch);
printf(" Last shmat() : %s", ds.shm_atime ? ctime(&ds.shm_atime) : "never\n");
printf(" Last shmdt() : %s", ds.shm_dtime ? ctime(&ds.shm_dtime) : "never\n");
printf(" Last changed : %s", ctime(&ds.shm_ctime));
}
int main(void)
{
int shmid = shmget(IPC_PRIVATE, 8192, IPC_CREAT | 0640);
char *addr;
show_info(shmid, "Right after shmget");
addr = shmat(shmid, NULL, 0);
show_info(shmid, "After shmat");
shmdt(addr);
show_info(shmid, "After shmdt");
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
/*
* Output snippets:
* --- Right after shmget ---
* Segment size : 8192 bytes
* Attach count : 0
* Last shmat() : never
*
* --- After shmat ---
* Attach count : 1
* Last shmat() : Wed Jun 10 10:30:00 2026
*
* --- After shmdt ---
* Attach count : 0
* Last shmdt() : Wed Jun 10 10:30:01 2026
*/
IPC_SET updates the owner UID, owner GID, and permission bits of the segment. You must first read the current values with IPC_STAT, modify the fields you want to change, then call IPC_SET. Only the owner, creator, or root can do this.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/shm.h>
int main(void)
{
int shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
struct shmid_ds ds;
/* Step 1: read current values */
shmctl(shmid, IPC_STAT, &ds);
printf("Before IPC_SET: mode = %04o\n", ds.shm_perm.mode & 0777);
/* Step 2: modify the field(s) you want to change */
ds.shm_perm.mode = 0660; /* allow group read+write */
/* ds.shm_perm.uid = new_owner_uid; (to change owner) */
/* ds.shm_perm.gid = new_group_gid; (to change group) */
/* Step 3: apply with IPC_SET */
if (shmctl(shmid, IPC_SET, &ds) == -1) {
perror("shmctl IPC_SET");
exit(EXIT_FAILURE);
}
/* Step 4: verify */
shmctl(shmid, IPC_STAT, &ds);
printf("After IPC_SET: mode = %04o\n", ds.shm_perm.mode & 0777);
/*
* Fields you CAN change with IPC_SET:
* shm_perm.uid — owner user ID
* shm_perm.gid — owner group ID
* shm_perm.mode — lower 9 permission bits
*
* Fields you CANNOT change:
* shm_segsz — size is fixed at shmget() time forever
* shm_cpid — creator PID, kernel-maintained read-only
* shm_nattch — kernel-maintained attach count
*/
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
/* Output:
* Before IPC_SET: mode = 0600
* After IPC_SET: mode = 0660
*/
Under memory pressure, the kernel may swap out memory pages to disk. When a process accesses a swapped-out page, a page fault occurs and the kernel must reload it from disk — adding unpredictable latency. For real-time or performance-critical applications this is unacceptable. SHM_LOCK prevents the kernel from ever swapping out pages of the segment.
SHM_LOCK sets a property of the segment, not of the calling process. Even if the calling process detaches, the pages remain locked. They stay locked until SHM_UNLOCK is called or the segment is deleted. Also, calling SHM_LOCK does not immediately bring all pages into RAM — pages are locked in one-by-one as they are first accessed (faulted in). After being faulted in, they never swap out again.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/resource.h>
#define SHM_SIZE (1024 * 1024) /* 1 MB */
int main(void)
{
int shmid;
char *addr;
/*
* Since Linux 2.6.10, unprivileged processes can use SHM_LOCK
* if RLIMIT_MEMLOCK is large enough. Raise the soft limit first.
*/
struct rlimit rl;
getrlimit(RLIMIT_MEMLOCK, &rl);
printf("RLIMIT_MEMLOCK: soft=%lu hard=%lu bytes\n",
(unsigned long)rl.rlim_cur, (unsigned long)rl.rlim_max);
rl.rlim_cur = rl.rlim_max; /* raise soft to match hard (no root needed) */
if (setrlimit(RLIMIT_MEMLOCK, &rl) == -1)
perror("setrlimit (continuing anyway)");
shmid = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0660);
addr = shmat(shmid, NULL, 0);
/* Lock the segment */
if (shmctl(shmid, SHM_LOCK, NULL) == -1) {
perror("SHM_LOCK failed — need CAP_IPC_LOCK or higher RLIMIT_MEMLOCK");
} else {
printf("SHM_LOCK: segment locked into RAM\n");
/*
* Touch every page to fault them all in.
* After this no page in the segment will ever be swapped.
* We access one byte per 4096-byte page to trigger the fault.
*/
for (size_t i = 0; i < SHM_SIZE; i += 4096)
addr[i] = 0;
printf("All %zu pages faulted in and permanently resident\n",
SHM_SIZE / 4096);
/* Do latency-critical work here — no swap delays */
memset(addr, 0xAB, SHM_SIZE);
printf("Wrote 1 MB — no page-fault delays\n");
/* Unlock when no longer needed */
if (shmctl(shmid, SHM_UNLOCK, NULL) == -1)
perror("SHM_UNLOCK");
else
printf("SHM_UNLOCK: pages may be swapped again\n");
}
shmctl(shmid, IPC_RMID, NULL);
shmdt(addr);
return 0;
}
/*
* Privilege rules for SHM_LOCK / SHM_UNLOCK:
*
* Before Linux 2.6.10:
* Requires CAP_IPC_LOCK capability (root only).
*
* Since Linux 2.6.10 (unprivileged process may call if):
* 1. Effective UID matches the owner OR creator UID of the segment, AND
* 2. Total bytes to be locked <= RLIMIT_MEMLOCK resource limit.
*
* Check limit from shell:
* $ ulimit -l (shows RLIMIT_MEMLOCK in KB, default often 64KB)
*
* Raise for current shell session:
* $ ulimit -l unlimited (only if the hard limit permits)
*
* Note: SHM_LOCK/UNLOCK are Linux-specific. Not in SUSv3/POSIX.
* Not available on all UNIX implementations.
*/
| cmd | buf | Who Can Call | Effect | POSIX? |
|---|---|---|---|---|
| IPC_RMID | NULL | Owner, creator, or root | Mark for deletion. Deleted immediately if nattch==0; otherwise when last shmdt() happens. | ✅ Yes |
| IPC_STAT | shmid_ds * (output) | Read permission on segment | Copy current shmid_ds into buf. Inspect size, perms, timestamps, nattch. | ✅ Yes |
| IPC_SET | shmid_ds * (input) | Owner, creator, or root | Update uid, gid, mode from buf. Cannot change size. | ✅ Yes |
| SHM_LOCK | NULL | Root, or owner/creator with sufficient RLIMIT_MEMLOCK (Linux ≥ 2.6.10) | Set lock flag on segment. Pages locked in RAM as they are faulted in. Property of segment, not process. | ❌ Linux only |
| SHM_UNLOCK | NULL | Same as SHM_LOCK | Remove lock flag. Kernel may now swap out pages. | ❌ Linux only |
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <sys/types.h>
#include <sys/shm.h>
static void stat_print(int shmid, const char *label)
{
struct shmid_ds ds;
shmctl(shmid, IPC_STAT, &ds);
printf("[%s] mode=%04o nattch=%lu size=%zu\n",
label,
ds.shm_perm.mode & 0777,
(unsigned long)ds.shm_nattch,
ds.shm_segsz);
}
int main(void)
{
int shmid;
char *addr;
struct shmid_ds ds;
/* Create */
shmid = shmget(IPC_PRIVATE, 4096, IPC_CREAT | 0600);
printf("Created: shmid=%d\n", shmid);
/* IPC_STAT before attach */
stat_print(shmid, "IPC_STAT before shmat");
/* Attach */
addr = shmat(shmid, NULL, 0);
strcpy(addr, "Hello shmctl demo");
stat_print(shmid, "IPC_STAT after shmat ");
/* IPC_SET: widen permissions */
shmctl(shmid, IPC_STAT, &ds);
ds.shm_perm.mode = 0660;
shmctl(shmid, IPC_SET, &ds);
stat_print(shmid, "IPC_STAT after IPC_SET");
/* SHM_LOCK */
if (shmctl(shmid, SHM_LOCK, NULL) == 0) {
printf("[SHM_LOCK ] locked into RAM\n");
/* SHM_UNLOCK */
if (shmctl(shmid, SHM_UNLOCK, NULL) == 0)
printf("[SHM_UNLOCK] unlocked\n");
} else {
perror("[SHM_LOCK ]");
}
/* IPC_RMID */
shmctl(shmid, IPC_RMID, NULL);
printf("[IPC_RMID ] marked for deletion\n");
stat_print(shmid, "IPC_STAT after IPC_RMID");
/* Detach — triggers actual removal */
shmdt(addr);
printf("shmdt() called — segment fully removed\n");
return 0;
}
Answer: It marks the segment for deletion. If shm_nattch == 0 (no process has it attached), deletion is immediate. If one or more processes are attached, the kernel sets the SHM_DEST flag and defers actual removal until shm_nattch drops to 0 — i.e., until the last process calls shmdt().
Answer: As a safety cleanup pattern. If the program crashes before reaching a cleanup routine, an orphaned System V shared memory segment remains in the system consuming memory. It must then be removed manually with ipcrm -m shmid. By calling IPC_RMID right after all processes attach, you ensure the kernel removes the segment automatically when all processes detach — even on abnormal termination. This is the same idiom as open() + unlink() for temporary files.
Answer: Only three: shm_perm.uid (owner user ID), shm_perm.gid (owner group ID), and shm_perm.mode (permission bits). The segment size (shm_segsz), creator PID (shm_cpid), attach count (shm_nattch), and timestamps are all kernel-maintained and cannot be set via IPC_SET.
Answer: Both lock memory pages into RAM to prevent swapping but with different scopes. SHM_LOCK is a property of the shared memory segment — it persists regardless of which process holds the lock, and remains active even after the calling process detaches. mlock() is a property of the calling process’s address range — it applies to a specific virtual memory range in that process and is released when the process unmaps or exits.
Answer: No. SHM_LOCK only sets a lock flag on the segment. Individual pages are locked in RAM only as they are first accessed (page faulted in). To guarantee all pages are resident, touch every page after calling SHM_LOCK — e.g., write one byte per 4096-byte page. Once faulted in with the lock active, those pages will not be swapped out again.
Answer: Before kernel 2.6.10: only processes with CAP_IPC_LOCK capability (typically root). Since kernel 2.6.10: an unprivileged process can also call it if its effective UID matches the owner or creator UID of the segment, AND the locked bytes do not exceed RLIMIT_MEMLOCK. SHM_LOCK is Linux-specific and not in POSIX/SUSv3.
Answer: Yes — on Linux specifically, if shm_nattch > 0 (at least one process is still attached), another process can still call shmat() on the segment even after IPC_RMID. On most other UNIX systems this would fail. This Linux-specific behavior is non-portable and should not be relied upon in portable code.
Answer: Call shmctl(shmid, IPC_STAT, &ds) and read ds.shm_nattch. From the command line, ipcs -m shows the nattch column for every System V shared memory segment on the system. This field is incremented by the kernel on each shmat() and decremented on each shmdt().
Deep-dive into all eight fields, timestamps, mode flags, and a complete monitoring utility.
