这次我们介绍 shmget
, shmat
, shmdt
, 和 shmctl
这一组函数,它们构成了 System V 共享内存 (System V Shared Memory) IPC(进程间通信)机制的核心部分。
注意: 虽然 System V IPC 是历史悠久且广泛支持的标准,但在现代 Linux 编程中,POSIX 共享内存 (shm_open
, mmap
) 和 POSIX 消息队列 通常被认为是更现代、更可移植的选择。不过,理解 System V IPC 仍然很重要,因为它在许多遗留系统和特定场景中仍在使用。
1. 函数介绍
这四个函数共同工作,用于创建、访问、连接、分离和控制 System V 共享内存段。
shmget
(Shared Memory Get): 创建一个新的共享内存段,或者获取一个已存在的共享内存段的标识符 (ID)。这个 ID 是后续操作该共享内存段的关键。shmat
(Shared Memory Attach): 将一个由shmget
获取的共享内存段连接(或附加)到调用进程的虚拟地址空间中。连接成功后,进程就可以像访问普通内存一样访问这块共享内存。shmdt
(Shared Memory Detach): 将一个 previously attached 的共享内存段从调用进程的地址空间中分离(或去附加)。分离后,进程不能再通过之前返回的地址访问该共享内存段。shmctl
(Shared Memory Control): 对共享内存段执行控制操作,如获取其状态信息 (IPC_STAT
)、设置其权限 (IPC_SET
) 或销毁 (IPC_RMID
) 该共享内存段。
你可以把共享内存想象成一个公共的“白板”:
shmget
: 申请或找到一个特定的白板(通过 ID 标识)。shmat
: 把这个白板挂到你(进程)的墙上,这样你就能在上面写字或看别人写的字了。shmdt
: 把白板从你墙上取下来,你不能再访问它了(但白板本身还在,别人可能还在用)。shmctl
: 检查白板的状态(谁在用,什么时候创建的),修改谁能用它,或者直接把白板撕掉(销毁)。
2. 函数原型
#include <sys/types.h> // 通常需要
#include <sys/ipc.h> // 必需,包含 IPC_* 常量
#include <sys/shm.h> // 必需,包含 shm* 函数和 shmid_ds 结构
// 获取共享内存段标识符
int shmget(key_t key, size_t size, int shmflg);
// 连接共享内存段到进程地址空间
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 从进程地址空间分离共享内存段
int shmdt(const void *shmaddr);
// 控制共享内存段
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
3. 功能
shmget
: 根据一个键 (key) 创建或获取一个共享内存段,并返回其唯一标识符 (shmid)。shmat
: 将由shmid
标识的共享内存段映射到调用进程的虚拟内存中,并返回映射后的虚拟地址。shmdt
: 将由shmaddr
指定的共享内存段从调用进程的地址空间中断开连接。shmctl
: 根据cmd
命令对由shmid
标识的共享内存段执行各种控制操作。
4. 参数详解
shmget
key_t key
: 一个键值,用于标识一个全局唯一的共享内存段。- 特殊键:
IPC_PRIVATE
(通常定义为 0) 是一个特殊键,它总是创建一个新的、唯一的共享内存段。 - 生成键: 通常使用
ftok
函数根据一个路径名和一个项目 ID 来生成一个唯一的key_t
值。key_t ftok(const char *pathname, int proj_id);
- 特殊键:
size_t size
: 请求的共享内存段的大小(以字节为单位)。- 如果是创建新段(
IPC_CREAT
被设置且该键尚不存在),则size
指定新段的大小。 - 如果是获取已存在的段,则
size
可以为 0,或者必须小于或等于已存在段的大小。
- 如果是创建新段(
int shmflg
: 指定创建标志和权限。- 创建标志:
IPC_CREAT
: 如果指定的key
不存在,则创建一个新的共享内存段。IPC_EXCL
: 与IPC_CREAT
一起使用时,如果key
已经存在,则shmget
调用失败。这可以用来确保创建的是一个全新的段。
- 权限: 低 9 位用于指定访问权限,格式与文件权限相同(例如
0666
表示所有者、组、其他用户都可读写)。实际权限还会受到进程umask
的影响。
- 创建标志:
shmat
int shmid
: 由shmget
返回的共享内存段标识符。const void *shmaddr
: 指定共享内存段应连接到进程地址空间的期望地址。NULL
(推荐): 让内核选择一个合适的地址。这是最常用也是最安全的方式。- 非
NULL
: 指定一个具体地址。这需要非常小心,因为可能导致地址冲突或对齐问题。通常需要设置shmflg
中的SHM_RND
标志来指示地址可以被调整。
int shmflg
: 控制连接行为的标志。SHM_RND
: 如果shmaddr
非NULL
,则将连接地址向下舍入到SHMLBA
(共享内存低端边界)的整数倍。SHM_RDONLY
: 将共享内存段连接为只读。如果未设置,则连接为可读可写。
shmdt
const void *shmaddr
: 由之前成功的shmat
调用返回的连接地址。
shmctl
int shmid
: 由shmget
返回的共享内存段标识符。int cmd
: 指定要执行的控制命令。IPC_STAT
: 将共享内存段的当前状态信息复制到buf
指向的struct shmid_ds
结构中。IPC_SET
: 根据buf
指向的struct shmid_ds
结构中的shm_perm
成员来设置共享内存段的权限和所有者。IPC_RMID
: 立即销毁共享内存段。只有当所有进程都已将其分离(shmdt
)后,内存才会真正被释放。如果仍有进程 attached,销毁操作会被标记,待所有进程 detach 后才执行。
struct shmid_ds *buf
: 一个指向struct shmid_ds
结构的指针,用于传递或接收共享内存段的状态信息。struct shmid_ds
包含了许多关于共享内存段的元数据,例如:struct shmid_ds { struct ipc_perm shm_perm; // 操作权限 size_t shm_segsz; // 段大小 (字节) time_t shm_atime; // 最后 attach 时间 time_t shm_dtime; // 最后 detach 时间 time_t shm_ctime; // 最后 change 时间 pid_t shm_cpid; // 创建者 PID pid_t shm_lpid; // 最后操作者 PID shmatt_t shm_nattch; // 当前连接的进程数 // ... 可能还有其他字段 ... };
5. 返回值
shmget
:- 成功: 返回一个正整数,即共享内存段的标识符 (
shmid
)。 - 失败: 返回 -1,并设置
errno
。
- 成功: 返回一个正整数,即共享内存段的标识符 (
shmat
:- 成功: 返回共享内存段连接到进程地址空间的虚拟地址。
- 失败: 返回
(void *) -1
(即MAP_FAILED
,与mmap
相同),并设置errno
。
shmdt
:- 成功: 返回 0。
- 失败: 返回 -1,并设置
errno
。
shmctl
:- 成功: 对于
IPC_RMID
,IPC_SET
返回 0;对于IPC_STAT
返回 0 并填充buf
。 - 失败: 返回 -1,并设置
errno
。
- 成功: 对于
6. 相似函数,或关联函数
- POSIX 共享内存:
shm_open
,shm_unlink
,mmap
,munmap
。这是更现代、更推荐的共享内存方式。 - System V 消息队列:
msgget
,msgsnd
,msgrcv
,msgctl
。 - System V 信号量:
semget
,semop
,semctl
。 ftok
: 用于生成shmget
所需的key_t
键值。mmap
/munmap
: 另一种实现共享内存的方式(通过映射同一文件或使用MAP_SHARED
)。
7. 示例代码
示例 1:父子进程通过 System V 共享内存通信
这个经典的例子演示了如何使用 System V 共享内存在父子进程之间传递数据。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SHM_SIZE 1024 // 共享内存段大小
int main() {
key_t key;
int shmid;
char *data;
pid_t pid;
// 1. 生成一个唯一的 key (使用 ftok)
// 注意:确保 "/tmp" 存在且可访问
key = ftok("/tmp", 'R'); // 'R' 是项目 ID
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
printf("Generated key: %d\n", (int)key);
// 2. 创建共享内存段 (如果不存在则创建)
shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
printf("Shared memory segment created/retrieved with ID: %d\n", shmid);
// 3. fork 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
// 尝试清理已创建的共享内存
shmctl(shmid, IPC_RMID, NULL);
exit(EXIT_FAILURE);
}
if (pid == 0) {
// --- 子进程 ---
printf("Child process (PID: %d) started.\n", getpid());
// 4a. 子进程连接共享内存
data = (char *)shmat(shmid, (void *)0, 0);
if (data == (char *)(-1)) {
perror("shmat in child");
_exit(EXIT_FAILURE);
}
printf("Child: Shared memory attached at address: %p\n", (void *)data);
// 5a. 子进程读取数据
printf("Child: Reading from shared memory: %s\n", data);
// 6a. 子进程修改数据
strncpy(data, "Hello from CHILD process!", SHM_SIZE - 1);
data[SHM_SIZE - 1] = '\0'; // 确保字符串结束
printf("Child: Written to shared memory.\n");
// 7a. 子进程分离共享内存
if (shmdt(data) == -1) {
perror("shmdt in child");
_exit(EXIT_FAILURE);
}
printf("Child: Shared memory detached.\n");
_exit(EXIT_SUCCESS);
} else {
// --- 父进程 ---
printf("Parent process (PID: %d) started.\n", getpid());
// 4b. 父进程连接共享内存
data = (char *)shmat(shmid, (void *)0, 0);
if (data == (char *)(-1)) {
perror("shmat in parent");
// 清理
shmctl(shmid, IPC_RMID, NULL);
exit(EXIT_FAILURE);
}
printf("Parent: Shared memory attached at address: %p\n", (void *)data);
// 5b. 父进程写入初始数据
strncpy(data, "Hello from PARENT process!", SHM_SIZE - 1);
data[SHM_SIZE - 1] = '\0';
printf("Parent: Written initial data to shared memory.\n");
// 等待子进程完成
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Parent: Child exited with status %d.\n", WEXITSTATUS(status));
} else {
printf("Parent: Child did not exit normally.\n");
}
// 6b. 父进程读取子进程修改后的数据
printf("Parent: Reading modified data from shared memory: %s\n", data);
// 7b. 父进程分离共享内存
if (shmdt(data) == -1) {
perror("shmdt in parent");
// 仍然尝试清理
}
printf("Parent: Shared memory detached.\n");
// 8. 父进程销毁共享内存段
// 只有当所有进程都 detach 后,IPC_RMID 才会真正释放内存
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID");
exit(EXIT_FAILURE);
}
printf("Parent: Shared memory segment destroyed.\n");
}
return 0;
}
代码解释:
- 使用
ftok("/tmp", 'R')
生成一个唯一的key_t
键。/tmp
是一个通常存在的目录,'R'
是项目 ID(0-255)。 - 调用
shmget(key, SHM_SIZE, 0666 | IPC_CREAT)
创建或获取共享内存段。0666
设置了读写权限。 - 调用
fork()
创建子进程。 - 父子进程:
- 都调用
shmat(shmid, NULL, 0)
将共享内存段连接到自己的地址空间。NULL
让内核选择地址。 - 检查
shmat
的返回值是否为(char *)-1
。
- 都调用
- 父进程:
- 先向共享内存写入初始数据。
- 调用
waitpid
等待子进程结束。 - 子进程结束后,读取子进程写入的数据。
- 调用
shmdt
分离共享内存。 - 调用
shmctl(shmid, IPC_RMID, NULL)
销毁共享内存段。因为此时子进程已经 detach,所以内存会被立即释放。
- 子进程:
- 读取父进程写入的初始数据。
- 向共享内存写入自己的数据。
- 调用
shmdt
分离共享内存。 - 使用
_exit
退出。
示例 2:检查共享内存段状态
这个例子演示了如何使用 shmctl
的 IPC_STAT
命令来获取共享内存段的详细信息。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
int main() {
key_t key;
int shmid;
struct shmid_ds shmid_struct;
// 1. 生成 key
key = ftok(".", 'S'); // 使用当前目录
if (key == -1) {
perror("ftok");
exit(EXIT_FAILURE);
}
// 2. 创建共享内存段
shmid = shmget(key, 2048, 0666 | IPC_CREAT);
if (shmid == -1) {
perror("shmget");
exit(EXIT_FAILURE);
}
printf("Shared memory segment created with ID: %d\n", shmid);
// 3. 获取并打印共享内存段状态
if (shmctl(shmid, IPC_STAT, &shmid_struct) == -1) {
perror("shmctl IPC_STAT");
shmctl(shmid, IPC_RMID, NULL); // 清理
exit(EXIT_FAILURE);
}
printf("\n--- Shared Memory Segment Status ---\n");
printf("Key: %d\n", (int)shmid_struct.shm_perm.__key); // 注意:成员名可能因系统而异
printf("ID: %d\n", shmid);
printf("Size: %zu bytes\n", shmid_struct.shm_segsz);
printf("Creator UID: %d\n", shmid_struct.shm_perm.uid);
printf("Creator GID: %d\n", shmid_struct.shm_perm.gid);
printf("Permissions: %o\n", shmid_struct.shm_perm.mode & 0777);
printf("Current number of attached processes: %lu\n", (unsigned long)shmid_struct.shm_nattch);
// 注意:时间字段可能需要 #define _GNU_SOURCE 和正确的包含
// printf("Last attach time: %s", ctime(&shmid_struct.shm_atime));
// printf("Last detach time: %s", ctime(&shmid_struct.shm_dtime));
// printf("Last change time: %s", ctime(&shmid_struct.shm_ctime));
printf("Creator PID: %d\n", shmid_struct.shm_cpid);
printf("Last operator PID: %d\n", shmid_struct.shm_lpid);
printf("------------------------------------\n");
// 4. 简单使用共享内存 (连接、写入、分离)
char *data = (char *)shmat(shmid, NULL, 0);
if (data != (char *)-1) {
snprintf(data, 100, "Data written by process %d", getpid());
printf("Written to shared memory: %s\n", data);
shmdt(data);
} else {
perror("shmat for usage");
}
// 5. 再次检查状态 (连接数应该变为 1 然后又变回 0)
// 这里简化处理,实际连接和分离是瞬间的
if (shmctl(shmid, IPC_STAT, &shmid_struct) == -1) {
perror("shmctl IPC_STAT 2");
} else {
printf("Current number of attached processes (after usage): %lu\n", (unsigned long)shmid_struct.shm_nattch);
}
// 6. 销毁共享内存段
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl IPC_RMID");
exit(EXIT_FAILURE);
}
printf("Shared memory segment destroyed.\n");
return 0;
}
代码解释:
- 使用
ftok
生成键,并用shmget
创建一个共享内存段。 - 定义一个
struct shmid_ds
类型的变量shmid_struct
。 - 调用
shmctl(shmid, IPC_STAT, &shmid_struct)
获取共享内存段的状态信息,并填充到shmid_struct
中。 - 打印
shmid_struct
中的各种字段,如大小、权限、创建者 UID/GID、连接进程数等。 - 简单地连接、使用(写入数据)、分离共享内存段。
- 再次调用
shmctl IPC_STAT
查看状态变化(主要是shm_nattch
)。 - 最后调用
shmctl(shmid, IPC_RMID, NULL)
销毁共享内存段。
重要提示与注意事项:
- 清理: 使用 System V IPC 资源(共享内存、消息队列、信号量)后,务必调用相应的
ctl
函数(如shmctl
)并使用IPC_RMID
命令进行销毁。否则,这些资源会一直存在于系统中,直到系统重启或手动使用ipcrm
命令删除。 ftok
的可靠性:ftok
生成的键依赖于文件的inode
和mtime
。如果文件被删除后重新创建,即使路径名相同,生成的键也可能不同。确保用作ftok
参数的文件是稳定存在的。- 错误处理: 始终检查这些函数的返回值,并进行适当的错误处理。
- 权限: 共享内存段的权限模型与文件系统类似,但检查是在
shmget
,shmat
等调用时进行的。 - 与
mmap
的比较: System V 共享内存是内核管理的 IPC 对象,而通过mmap
和MAP_SHARED
实现的共享内存更像是一种内存映射文件的方式。POSIX 共享内存 (shm_open
) 则结合了两者的优点,提供了命名的、基于文件描述符的共享内存机制。
总结:
shmget
, shmat
, shmdt
, shmctl
这一组函数提供了 System V 共享内存 IPC 机制。虽然在现代编程中可能不如 POSIX 共享内存流行,但理解它们对于维护遗留代码和在特定系统环境中工作仍然至关重要。掌握它们的用法和生命周期管理是进行 Linux 进程间通信编程的基础之一。