解码IPC-消息队列、共享内存与信号量集
System-V IPC(消息队列、共享内存、信号量集)
进程间通信(IPC)是进程间的信息交换,用于实现数据传输、共享资源、控制进程等目的。Linux 继承的 System-V IPC 包含三种核心机制:消息队列、共享内存、信号量集,它们均通过唯一键值(key)标识,需手动创建 / 删除,是进程间协同的核心工具。
基础共性知识
核心概念
- System-V IPC 是 Unix 系统遗留的进程间通信标准,包含消息队列、共享内存、信号量集三类持久性资源(创建后不手动删除则常驻内存)。
- 每个 IPC 对象通过 key_t 类型的键值(key) 唯一标识,进程通过键值关联同一 IPC 对象。
- 查看系统中 IPC 对象:
ipcs -a(查看所有)、ipcs -q(仅消息队列)、ipcs -m(仅共享内存)、ipcs -s(仅信号量集)。 - 删除系统中 IPC 对象:
ipcrm -q 消息队列ID、ipcrm -m 共享内存ID、ipcrm -s 信号量集ID。
System-V IPC 核心命令汇总表
| 命令 | 核心功能 | 常用参数 | 参数说明 | 示例命令 | 适用场景 |
|---|---|---|---|---|---|
| ipcs | 查看系统中 System-V IPC 对象 | -a |
查看所有 IPC 对象(消息队列、共享内存、信号量集) | ipcs -a |
快速排查系统中所有 IPC 资源 |
-q |
仅查看 消息队列 | ipcs -q |
检查进程 A/B 通信的消息队列是否存在 | ||
-m |
仅查看 共享内存 | ipcs -m |
查看共享内存的大小、权限、使用状态 | ||
-s |
仅查看 信号量集 | ipcs -s |
检查信号量集的个数、占用进程 | ||
-i <id> |
查看指定 ID 的 IPC 对象详情(需配合 -q/-m/-s) | ipcs -q -i 456 |
查看 ID=456 的消息队列详细信息(权限、消息数) | ||
-t |
显示 IPC 对象最后操作时间 | ipcs -m -t |
排查共享内存是否长期未使用 | ||
-p |
显示 IPC 对象最后操作的进程 PID | ipcs -s -p |
定位操作信号量集的进程 | ||
| ipcmk | 手动创建 System-V IPC 对象 | -Q |
创建 消息队列 | ipcmk -Q |
临时测试消息队列通信,无需写代码 |
-M <size> |
创建 共享内存,指定大小(支持 K/M/G 后缀,如 4K=4096) | ipcmk -M 8K -p 0664 |
创建 8KB 共享内存(权限 0664,同进程 A/B) | ||
-S <num> |
创建 信号量集,指定信号量个数 | ipcmk -S 2 |
创建包含 2 个信号量的信号量集 | ||
-p <perms> |
指定 IPC 对象权限(默认 0644,如 0664 允许同组读写) | ipcmk -Q -p 0664 |
创建权限适配进程 A/B 的消息队列 | ||
| ipcrm | 删除系统中 System-V IPC 对象 | -q <msqid> |
删除指定 ID 的 消息队列 | ipcrm -q 456 |
清理进程 A/B 残留的消息队列(避免占用资源) |
-m <shmid> |
删除指定 ID 的 共享内存 | ipcrm -m 789 |
删除无用的共享内存 | ||
-s <semid> |
删除指定 ID 的 信号量集 | ipcrm -s 101 |
删除不再使用的信号量集 | ||
-a |
删除当前用户拥有的所有 IPC 对象(谨慎使用) | ipcrm -a |
批量清理自己创建的所有 IPC 残留资源 |
键值生成函数 ftok ()
用于生成唯一的 IPC 键值,避免手动指定键值冲突。
#include <sys/types.h>
#include <sys/ipc.h>
/**
* @brief 生成 System-V IPC 唯一键值
* @param pathname 系统中已存在且可访问的文件路径(用于获取文件唯一属性)
* @param proj_id 项目ID(仅使用低8位,必须非0,取值范围1-255)
* @return 成功返回生成的 key_t 类型键值;失败返回 -1(errno 标识错误)
* @note 键值组成:proj_id低8位 + 文件设备编号低8位 + 文件inode编号低16位
* 不保证绝对唯一(不同文件可能组合出相同键值),但实际使用中足够可靠
*/
key_t ftok(const char *pathname, int proj_id);
消息队列(Message Queue)
概念
-
按 “消息类型” 分类的队列式通信机制,不同类型消息存于不同队列,接收时需指定类型。

-
特点:自带数据分类、默认阻塞模式(队列满时写阻塞,队列空时读阻塞),默认容量 16384 字节(宏定义 MSGMNB),单条消息最大 8192 字节(宏定义 MSGMAX)。
创建 / 打开消息队列(msgget ())
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
/**
* @brief 创建或打开一个 System-V 消息队列
* @param key 键值(由 ftok() 生成或指定 IPC_PRIVATE)
* @param msgflg 标志位 + 权限组合
* 标志位:IPC_CREAT(不存在则创建)、IPC_EXCL(与IPC_CREAT同用,存在则失败)
* 权限:八进制表示(如0644,无需执行权限,格式同 open())
* @return 成功返回消息队列标识符(非负整数);失败返回 -1(errno 标识错误)
* @note IPC_PRIVATE 表示创建私有消息队列(仅当前进程及子进程可访问)
* 若仅打开已有队列,msgflg 填 0 + 权限(如0644)
*/
int msgget(key_t key, int msgflg);
发送消息(msgsnd ())
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
// 消息结构体(必须包含 mtype 字段,且 mtype > 0)
struct msgbuf {
long mtype; // 消息类型(严格大于0,用于接收时筛选)
char mtext[1024]; // 消息正文(可自定义长度或结构体)
};
/**
* @brief 向指定消息队列发送消息
* @param msqid 消息队列标识符(msgget() 返回值)
* @param msgp 指向 msgbuf 结构体的指针(存储消息类型和正文)
* @param msgsz 消息正文(mtext)的字节数(非负整数,可设为0)
* @param msgflg 标志位:0(默认阻塞)、IPC_NOWAIT(非阻塞,队列满则失败)
* @return 成功返回 0;失败返回 -1(errno 标识错误)
* @note 队列满时默认阻塞,直到队列有空闲空间
* 消息总长度不能超过 MSGMAX(8192字节),队列总容量不超过 MSGMNB(16384字节)
*/
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 示例:发送类型为1的消息
int send_msg(int msg_id, const char *content) {
struct msgbuf msg;
msg.mtype = 1; // 消息类型必须>0
strncpy(msg.mtext, content, sizeof(msg.mtext)-1);
int ret = msgsnd(msg_id, &msg, strlen(msg.mtext), 0);
if (ret == -1) {
fprintf(stderr, "msgsnd failed: errno=%d, %s\n", errno, strerror(errno));
}
return ret;
}
接收消息(msgrcv ())
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
/**
* @brief 从指定消息队列接收消息
* @param msqid 消息队列标识符(msgget() 返回值)
* @param msgp 存储接收消息的缓冲区(msgbuf 结构体指针)
* @param msgsz 缓冲区(mtext)的最大字节数
* @param msgtyp 接收的消息类型筛选:
* =0:接收队列中第一条消息(不区分类型)
* >0:接收类型等于 msgtyp 的第一条消息(MSG_EXCEPT 标志则接收不等于的第一条)
* <0:接收类型≤|msgtyp|的最小类型第一条消息
* @param msgflg 标志位:0(默认阻塞)、IPC_NOWAIT(非阻塞)、MSG_NOERROR(消息超长时截断)
* @return 成功返回接收的消息正文字节数;失败返回 -1(errno 标识错误)
* @note 队列中无匹配类型消息时,默认阻塞直到有对应消息
* 消息超长且未设 MSG_NOERROR 时,返回失败(errno=E2BIG)
*/
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
// 示例:接收类型为1的消息
ssize_t recv_msg(int msg_id, char *buf, size_t buf_len) {
struct msgbuf msg;
ssize_t ret = msgrcv(msg_id, &msg, sizeof(msg.mtext), 1, 0);
if (ret == -1) {
fprintf(stderr, "msgrcv failed: errno=%d, %s\n", errno, strerror(errno));
return -1;
}
// 拷贝消息正文到用户缓冲区
strncpy(buf, msg.mtext, buf_len-1);
buf[buf_len-1] = '\0';
return ret;
}
控制消息队列(msgctl ())
用于获取属性、设置属性、删除消息队列(核心用途)。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
#include <stdio.h>
/**
* @brief 控制消息队列(获取属性、设置属性、删除)
* @param msqid 消息队列标识符
* @param cmd 控制命令:
* IPC_STAT:获取队列属性,存入 buf 指向的 msqid_ds 结构体
* IPC_SET:设置队列属性(从 buf 写入内核,仅 uid、gid、mode、msg_qbytes 可改)
* IPC_RMID:删除消息队列(立即生效,唤醒所有阻塞的读写进程)
* @param buf 存储属性的 msqid_ds 结构体指针(IPC_RMID 时可设为 NULL)
* @return 成功返回 0;失败返回 -1(errno 标识错误)
* @note 删除队列是必须操作,否则 IPC 对象会一直占用内存
* 只有队列创建者、所有者或root用户可执行 IPC_RMID 命令
*/
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 示例:删除消息队列
int delete_msg_queue(int msg_id) {
int ret = msgctl(msg_id, IPC_RMID, NULL);
if (ret == -1) {
fprintf(stderr, "msgctl delete failed: errno=%d, %s\n", errno, strerror(errno));
}
return ret;
}
进程 A 与 B 通过消息队列通信
需求:A 创建队列→发 SIGUSR1 给 B→B 写 PID 到队列→发 SIGUSR2 给 A→A 读 PID 并输出
进程 A 代码(a.c):创建队列、收 SIGUSR2、读消息
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
struct msgbuf {
long mtype; // Message type (must be > 0)
char mtext[1024]; // Message content
};
int msg_id; // Message queue ID (global for signal handler)
// Delete message queue
int delete_msg_queue(int msg_id) {
int ret = msgctl(msg_id, IPC_RMID, NULL);
if (ret == -1) {
fprintf(stderr, "msgctl delete failed: %s\n", strerror(errno));
}
return ret;
}
// 核心修复:仅拷贝实际接收的有效字节,避免垃圾值
ssize_t recv_msg(int msg_id, char *buf, size_t buf_len) {
struct msgbuf msg;
// Receive message type 1, block mode, truncate if too long
ssize_t ret = msgrcv(msg_id, &msg, sizeof(msg.mtext), 1, MSG_NOERROR);
if (ret == -1) {
fprintf(stderr, "msgrcv failed: %s\n", strerror(errno));
return -1;
}
// 只拷贝 ret 个有效字节(避免拷贝 msg.mtext 中多余的垃圾值)
if (ret > 0) {
// 确保不越界:如果有效字节数 >= 缓冲区长度,只拷贝 buf_len-1 字节
size_t copy_len = (ret < buf_len - 1) ? ret : (buf_len - 1);
memcpy(buf, msg.mtext, copy_len); // 用 memcpy 精准拷贝有效字节
buf[copy_len] = '\0'; // 严格在有效字节后加终止符
} else {
buf[0] = '\0'; // 空消息直接设终止符
}
return ret;
}
// SIGUSR2 handler: read message
void sigusr2_handler(int sig) {
char buf[1024];
ssize_t ret = recv_msg(msg_id, buf, sizeof(buf));
if (ret > 0) {
printf("Process A received: %s\n", buf);
}
delete_msg_queue(msg_id);
exit(0);
}
int main() {
pid_t pid_a = getpid();
printf("=====================================\n");
printf("Process A started! rocess A's PID: %d\n", pid_a);
printf("Tip: Copy this PID to Process B!\n");
printf("=====================================\n\n");
key_t key = ftok(".", 0x01);
if (key == -1) {
perror("ftok failed");
exit(1);
}
// Create queue with 0664 permission (read/write for group)
msg_id = msgget(key, IPC_CREAT | 0664);
if (msg_id == -1) {
perror("msgget failed");
exit(1);
}
printf("Process A: Message queue created (id=%d)\n", msg_id);
if (signal(SIGUSR2, sigusr2_handler) == SIG_ERR) {
perror("signal SIGUSR2 failed");
exit(1);
}
pid_t pid_b;
printf("Process A: Enter Process B's PID: ");
if (scanf("%d", &pid_b) != 1) {
fprintf(stderr, "scanf failed: invalid PID\n");
exit(1);
}
if (kill(pid_b, SIGUSR1) == -1) {
perror("kill SIGUSR1 failed");
exit(1);
}
printf("Process A: Sent SIGUSR1 to B (PID=%d)\n", pid_b);
while (1) {
pause();
}
return 0;
}
进程 B 代码(b.c):收 SIGUSR1、写 PID、发 SIGUSR2
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <string.h> // For memset
struct msgbuf {
long mtype; // Message type (must match Process A)
char mtext[1024]; // Message content
};
pid_t pid_a; // Process A's PID (global for signal handler)
int msg_id; // Message queue ID
// 发送前清空缓冲区,避免残留垃圾值
int send_msg(int msg_id, const char *content) {
struct msgbuf msg;
msg.mtype = 1; // Must be > 0
memset(&msg.mtext, 0, sizeof(msg.mtext)); // 关键:清空缓冲区,所有字节设为0
// Copy content (max 1023 bytes to leave space for '\0')
strncpy(msg.mtext, content, sizeof(msg.mtext) - 1);
msg.mtext[sizeof(msg.mtext) - 1] = '\0'; // Force terminator
// Send message: length = actual content length (no '\0')
int ret = msgsnd(msg_id, &msg, strlen(msg.mtext), 0);
if (ret == -1) {
fprintf(stderr, "msgsnd failed: %s\n", strerror(errno));
}
return ret;
}
// SIGUSR1 handler: send PID to queue
void sigusr1_handler(int sig) {
char buf[1024];
sprintf(buf, "Process B's PID is %d", getpid());
int ret = send_msg(msg_id, buf);
if (ret == 0) {
printf("Process B: Sent message to queue: %s\n", buf);
if (kill(pid_a, SIGUSR2) == -1) {
perror("kill SIGUSR2 failed");
exit(1);
}
printf("Process B: Sent SIGUSR2 to A (PID=%d)\n", pid_a);
}
exit(0);
}
int main() {
pid_t pid_b = getpid();
printf("=====================================\n");
printf("Process B started! Process B's PID: %d\n", pid_b);
printf("Tip: Copy this PID to Process A!\n");
printf("=====================================\n\n");
key_t key = ftok(".", 0x01);
if (key == -1) {
perror("ftok failed");
exit(1);
}
// Open queue with 0664 permission (match Process A)
msg_id = msgget(key, 0664);
if (msg_id == -1) {
perror("msgget failed (check if A created queue first)");
exit(1);
}
printf("Process B: Opened message queue (id=%d)\n", msg_id);
if (signal(SIGUSR1, sigusr1_handler) == SIG_ERR) {
perror("signal SIGUSR1 failed");
exit(1);
}
printf("Process B: Enter Process A's PID: ");
if (scanf("%d", &pid_a) != 1) {
fprintf(stderr, "scanf failed: invalid PID\n");
exit(1);
}
printf("Process B: Waiting for SIGUSR1 from A (PID=%d)...\n", pid_a);
while (1) {
pause();
}
return 0;
}
共享内存(Shared Memory)
概念
-
最高效的 IPC 机制:内核分配一块物理内存,多个进程将其映射到自身虚拟地址空间,直接通过虚拟地址读写(无需内核转发数据)。

-
特点:无数据拷贝开销,效率极高,但需配合信号量等机制实现进程互斥(避免同时读写导致数据错乱)。
-
页大小:默认 PAGE_SIZE=4096 字节,申请的共享内存大小会自动向上对齐为页大小的倍数。
申请 / 打开共享内存(shmget ())
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <stdio.h>
/**
* @brief 申请或打开 System-V 共享内存段
* @param key 键值(ftok() 生成或 IPC_PRIVATE)
* @param size 申请的共享内存大小(字节数,自动对齐为 PAGE_SIZE 倍数)
* @param shmflg 标志位 + 权限组合:
* 标志位:IPC_CREAT(不存在则创建)、IPC_EXCL(存在则失败)、SHM_RDONLY(只读)
* 权限:八进制表示(如0666,无需执行权限)
* @return 成功返回共享内存标识符;失败返回 -1(errno 标识错误)
* @note 新创建的共享内存内容初始化为 0
* 最大可申请大小受系统限制(可通过 /proc/sys/kernel/shmmax 查看)
*/
int shmget(key_t key, size_t size, int shmflg);
// 示例:申请4KB共享内存
int create_shm() {
key_t key = ftok(".", 0x02);
if (key == -1) {
perror("ftok failed");
return -1;
}
// 申请4KB内存,创建权限0666
int shm_id = shmget(key, 4096, IPC_CREAT | 0666);
if (shm_id == -1) {
perror("shmget failed");
return -1;
}
printf("Shared memory created, id=%d\n", shm_id);
return shm_id;
}
映射共享内存(shmat ())
将物理内存映射到进程虚拟地址空间,获得访问入口。
#include <sys/types.h>
#include <sys/shm.h>
#include <errno.h>
#include <stdio.h>
/**
* @brief 将共享内存映射到当前进程虚拟地址空间
* @param shmid 共享内存标识符(shmget() 返回值)
* @param shmaddr 映射后的虚拟地址:NULL(推荐,由系统自动分配)、非NULL(需页对齐)
* @param shmflg 标志位:0(默认读写)、SHM_RDONLY(只读)、SHM_EXEC(可执行)
* @return 成功返回映射后的虚拟地址(void*);失败返回 (void*)-1(errno 标识错误)
* @note 映射成功后,进程可通过返回的地址直接读写共享内存
* 进程退出时会自动解除映射,但建议手动调用 shmdt()
*/
void *shmat(int shmid, const void *shmaddr, int shmflg);
// 示例:映射共享内存(读写模式)
void *map_shm(int shm_id) {
void *addr = shmat(shm_id, NULL, 0); // 系统分配地址,读写模式
if (addr == (void*)-1) {
perror("shmat failed");
return NULL;
}
printf("Shared memory mapped to address: %p\n", addr);
return addr;
}
解除映射(shmdt ())
断开进程与共享内存的虚拟地址关联(不删除物理内存)。
#include <sys/shm.h>
#include <errno.h>
#include <stdio.h>
/**
* @brief 解除共享内存与当前进程的映射
* @param shmaddr 映射时返回的虚拟地址
* @return 成功返回 0;失败返回 -1(errno 标识错误)
* @note 解除映射后,进程无法再通过该地址访问共享内存
* 仅减少共享内存的关联进程数(shm_nattch),不释放物理内存
*/
int shmdt(const void *shmaddr);
// 示例:解除映射
int unmap_shm(void *shm_addr) {
int ret = shmdt(shm_addr);
if (ret == -1) {
perror("shmdt failed");
} else {
printf("Shared memory unmapped\n");
}
return ret;
}
控制共享内存(shmctl ())
用于获取属性、设置属性、删除共享内存。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <errno.h>
#include <stdio.h>
/**
* @brief 控制共享内存(获取属性、设置属性、删除)
* @param shmid 共享内存标识符
* @param cmd 控制命令:
* IPC_STAT:获取属性(存入 buf 指向的 shmid_ds 结构体)
* IPC_SET:设置属性(uid、gid、mode、shm_qbytes 可改)
* IPC_RMID:标记删除(所有进程解除映射后释放物理内存)
* SHM_LOCK:锁定内存(禁止交换到磁盘)、SHM_UNLOCK:解锁
* @param buf 存储属性的 shmid_ds 结构体指针(IPC_RMID 时可设为 NULL)
* @return 成功返回 0(IPC_RMID/SHM_LOCK 等)或索引值(IPC_INFO 等);失败返回 -1
* @note IPC_RMID 仅标记删除,需所有进程解除映射(shm_nattch=0)后才真正释放
* 必须手动删除,否则物理内存会一直被占用
*/
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// 示例:删除共享内存
int delete_shm(int shm_id) {
int ret = shmctl(shm_id, IPC_RMID, NULL);
if (ret == -1) {
perror("shmctl delete failed");
} else {
printf("Shared memory marked for deletion\n");
}
return ret;
}
信号量集(Semaphore Set)
概念
- 用于实现进程间互斥与同步的 IPC 资源,本质是 “非负整数计数器”,代表临界资源的可用数量。
- 核心操作:P 操作(申请资源,计数器 - 1,为 0 则阻塞)、V 操作(释放资源,计数器 + 1,唤醒阻塞进程),均为原子操作(不可打断)。
- 临界资源:多个进程可能同时访问的资源(如共享内存、硬件设备);临界区:访问临界资源的代码段(需用信号量保护)。
- 信号量集:包含多个信号量,可同时管理多种临界资源(如 1 个信号量控制共享内存,1 个控制打印机)。
创建 / 打开信号量集(semget ())
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdio.h>
/**
* @brief 创建或打开 System-V 信号量集
* @param key 键值(ftok() 生成或 IPC_PRIVATE)
* @param nsems 信号量集中的信号量个数(创建时必须>0,且≤系统限制 SEMMSL)
* @param semflg 标志位 + 权限组合:
* 标志位:IPC_CREAT(不存在则创建)、IPC_EXCL(存在则失败)
* 权限:八进制表示(如0666,写权限=修改信号量值,读权限=查看)
* @return 成功返回信号量集标识符;失败返回 -1(errno 标识错误)
* @note 新创建的信号量值默认未初始化(需用 semctl() 的 SETVAL/SETALL 设置)
* 单个信号量集最大信号量数可通过 /proc/sys/kernel/sem 查看(第一个值为 SEMMSL)
*/
int semget(key_t key, int nsems, int semflg);
// 示例:创建包含1个信号量的信号量集
int create_sem_set() {
key_t key = ftok(".", 0x03);
if (key == -1) {
perror("ftok failed");
return -1;
}
// 创建1个信号量,权限0666
int sem_id = semget(key, 1, IPC_CREAT | IPC_EXCL | 0666);
if (sem_id == -1) {
// 若已存在,直接打开
sem_id = semget(key, 1, 0666);
if (sem_id == -1) {
perror("semget failed");
return -1;
}
}
printf("Semaphore set created/opened, id=%d\n", sem_id);
return sem_id;
}
操作信号量(semop ())
实现 P/V 操作,是信号量的核心函数。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdio.h>
// 信号量操作结构体(指定操作的信号量、操作类型、标志)
struct sembuf {
unsigned short sem_num; // 信号量集中的下标(从0开始)
short sem_op; // 操作类型:>0(V操作)、<0(P操作)、=0(等零操作)
short sem_flg; // 标志:0(默认阻塞)、IPC_NOWAIT(非阻塞)、SEM_UNDO(进程退出自动撤销操作)
};
/**
* @brief 对信号量集执行P/V等操作(原子操作)
* @param semid 信号量集标识符
* @param sops 指向 sembuf 结构体数组的指针(可批量操作多个信号量)
* @param nsops 结构体数组的元素个数(操作的信号量个数)
* @return 成功返回 0;失败返回 -1(errno 标识错误)
* @note P操作(sem_op=-1):信号量值≥1时减1,否则阻塞(无IPC_NOWAIT时)
* V操作(sem_op=1):信号量值加1,永远不阻塞
* 等零操作(sem_op=0):信号量值=0时继续,否则阻塞
* SEM_UNDO 标志:进程异常退出时,系统自动恢复信号量值(避免死锁)
*/
int semop(int semid, struct sembuf *sops, size_t nsops);
// 封装P操作(申请资源)
int sem_p(int sem_id, int sem_num) {
struct sembuf sops = {sem_num, -1, SEM_UNDO}; // 第sem_num个信号量,P操作,自动撤销
int ret = semop(sem_id, &sops, 1);
if (ret == -1) {
perror("sem P operation failed");
}
return ret;
}
// 封装V操作(释放资源)
int sem_v(int sem_id, int sem_num) {
struct sembuf sops = {sem_num, 1, SEM_UNDO}; // V操作
int ret = semop(sem_id, &sops, 1);
if (ret == -1) {
perror("sem V operation failed");
}
return ret;
}
控制信号量集(semctl ())
用于设置信号量值、获取值、删除信号量集等。
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdio.h>
// 必须手动定义该联合体(系统未默认提供)
union semun {
int val; // SETVAL 时使用(设置单个信号量值)
struct semid_ds *buf; // IPC_STAT/IPC_SET 时使用(获取/设置属性)
unsigned short *array; // GETALL/SETALL 时使用(批量获取/设置信号量值)
struct seminfo *__buf; // IPC_INFO 时使用(系统级信息)
};
/**
* @brief 控制信号量集(设置值、获取值、删除等)
* @param semid 信号量集标识符
* @param semnum 信号量集中的下标(操作单个信号量时);批量操作时忽略
* @param cmd 控制命令:
* SETVAL:设置单个信号量值(arg.val 为新值)
* GETVAL:获取单个信号量值(返回值为信号量值)
* SETALL:批量设置信号量值(arg.array 为值数组)
* GETALL:批量获取信号量值(arg.array 存储结果)
* IPC_RMID:删除信号量集(立即生效,唤醒所有阻塞进程)
* @param arg 联合结构体(存储命令所需参数)
* @return 成功返回 0(SETVAL/IPC_RMID 等)或信号量值(GETVAL);失败返回 -1
* @note 删除信号量集是必须操作,否则会一直占用系统资源
* 只有创建者、所有者或root用户可执行 IPC_RMID
*/
int semctl(int semid, int semnum, int cmd, ...);
// 示例:设置信号量初值
int set_sem_value(int sem_id, int sem_num, int value) {
union semun arg;
arg.val = value;
int ret = semctl(sem_id, sem_num, SETVAL, arg);
if (ret == -1) {
perror("semctl SETVAL failed");
} else {
printf("Semaphore %d set to %d\n", sem_num, value);
}
return ret;
}
// 示例:删除信号量集
int delete_sem_set(int sem_id) {
int ret = semctl(sem_id, 0, IPC_RMID); // semnum 忽略
if (ret == -1) {
perror("semctl IPC_RMID failed");
} else {
printf("Semaphore set deleted\n");
}
return ret;
}
示例:多个信号量的初值设置
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
struct seminfo *__buf;
};
int main() {
key_t key = ftok(".", 'm'); // 生成 key
int semid = semget(key, 3, IPC_CREAT | 0666); // nsems=3(3个信号量)
if (semid == -1) {
perror("semget failed");
exit(EXIT_FAILURE);
}
printf("信号量集(3个信号量)创建成功,semid:%d\n", semid);
// 设置所有信号量的初值:[2, 0, 1]
union semun sem_un;
unsigned short init_vals[3] = {2, 0, 1}; // 3个信号量的初值数组
sem_un.array = init_vals; // SETALL 对应 array 成员
int ret = semctl(semid, 0, SETALL, sem_un); // semnum=0 无效,SETALL 忽略该参数
if (ret == -1) {
perror("semctl SETALL failed");
semctl(semid, 0, IPC_RMID);
exit(EXIT_FAILURE);
}
printf("3个信号量初值设置成功:[2, 0, 1]\n");
// 验证:获取每个信号量的当前值
for (int i = 0; i < 3; i++) {
int val = semctl(semid, i, GETVAL, 0);
if (val == -1) {
perror("semctl GETVAL failed");
semctl(semid, 0, IPC_RMID);
exit(EXIT_FAILURE);
}
printf("信号量%d 的当前值:%d\n", i, val);
}
// 清理资源
semctl(semid, 0, IPC_RMID);
printf("信号量集删除成功\n");
return 0;
}
三进程共享内存互斥访问
需求:A、B 修改共享内存,C 实时输出,用信号量实现互斥
共享头文件 shm_sem.h(核心:ftok 生成键值)
#ifndef SHM_SEM_H
#define SHM_SEM_H
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
// -------------- 用ftok生成唯一键值(核心优化)--------------
// 共享内存:文件路径为当前目录(.),proj_id=0x01(非0值)
#define SHM_FTOK_PATH "."
#define SHM_FTOK_PROJ 0x01// 信号量:文件路径相同,proj_id=0x02(不同ID避免键值冲突)
#define SEM_FTOK_PATH "."
#define SEM_FTOK_PROJ 0x02// 共享配置
#define SHM_SIZE 4096 // 共享内存大小(4KB)
#define SEM_NUMS 1 // 信号量个数(1个互斥锁)
// 封装ftok生成键值(避免重复代码)
key_t get_ftok_key(const char *path, int proj_id) {
key_t key = ftok(path, proj_id);
if (key == -1) {
perror("ftok failed");
fprintf(stderr, "Check if path '%s' exists and is accessible\n", path);
exit(1); // 键值生成失败,直接退出(后续操作无意义)
}
printf("ftok generated key: 0x%x (path: %s, proj_id: 0x%x)\n", key, path, proj_id);
return key;
}
// P操作:申请资源(信号量-1,阻塞等待)
int sem_p(int sem_id) {
struct sembuf sops = {0, -1, SEM_UNDO}; // SEM_UNDO:进程退出自动释放锁
int ret = semop(sem_id, &sops, 1);
if (ret == -1) {
perror("sem_p failed");
}
return ret;
}
// V操作:释放资源(信号量+1)
int sem_v(int sem_id) {
struct sembuf sops = {0, 1, SEM_UNDO};
int ret = semop(sem_id, &sops, 1);
if (ret == -1) {
perror("sem_v failed");
}
return ret;
}
#endif // SHM_SEM_H
信号量创建程序 create_sem.c(用 ftok 键值)
#include "shm_sem.h"
int main() {
// 用ftok生成信号量键值(调用封装函数,自动处理错误)
key_t sem_key = get_ftok_key(SEM_FTOK_PATH, SEM_FTOK_PROJ);
// 创建信号量集(不存在则创建,存在则报错,避免重复创建)
int sem_id = semget(sem_key, SEM_NUMS, IPC_CREAT | IPC_EXCL | 0666);
if (sem_id == -1) {
if (errno == EEXIST) {
fprintf(stderr, "Semaphore set already exists! Use 'ipcs -s' to check, 'ipcrm -s <id>' to delete\n");
return 1;
}
perror("semget failed");
return 1;
}
// 设置信号量初值为1(互斥锁)
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
} arg;
arg.val = 1;
if (semctl(sem_id, 0, SETVAL, arg) == -1) {
perror("semctl SETVAL failed");
semctl(sem_id, 0, IPC_RMID); // 创建失败,清理信号量
return 1;
}
printf("Semaphore set created successfully! id=%d, initial value=1\n", sem_id);
return 0;
}
进程 A process_a.c(用 ftok 键值创建共享内存)
#include "shm_sem.h"
int main() {
int sem_id, shm_id;
char *shm_addr;
// 生成键值(共享内存+信号量)
key_t shm_key = get_ftok_key(SHM_FTOK_PATH, SHM_FTOK_PROJ);
key_t sem_key = get_ftok_key(SEM_FTOK_PATH, SEM_FTOK_PROJ);
// 打开信号量集(必须先运行 create_sem)
sem_id = semget(sem_key, SEM_NUMS, 0666);
if (sem_id == -1) {
fprintf(stderr, "Please run ./create_sem first!\n");
return 1;
}
// 创建共享内存(不存在则创建,存在则打开)
shm_id = shmget(shm_key, SHM_SIZE, IPC_CREAT | 0666);
if (shm_id == -1) {
perror("shmget failed");
return 1;
}
// 映射共享内存
shm_addr = (char*)shmat(shm_id, NULL, 0);
if (shm_addr == (char*)-1) {
perror("shmat failed");
return 1;
}
printf("Process A: Shared memory mapped at %p\n", shm_addr);
// 互斥修改共享内存(5次)
for (int i = 0; i < 5; i++) {
if (sem_p(sem_id) == -1) { // 申请锁失败则退出
break;
}
// 临界区:修改共享内存
sprintf(shm_addr, "Process A: Count = %d", i);
printf("Process A modified: %s\n", shm_addr);
if (sem_v(sem_id) == -1) { // 释放锁失败则退出
break;
}
sleep(2); // 每隔2秒修改一次
}
// 清理资源
shmdt(shm_addr);
printf("Process A: Exit successfully\n");
return 0;
}
进程 B process_b.c(用 ftok 键值打开共享内存)
#include "shm_sem.h"
int main() {
int sem_id, shm_id;
char *shm_addr;
// 生成键值(与进程A一致)
key_t shm_key = get_ftok_key(SHM_FTOK_PATH, SHM_FTOK_PROJ);
key_t sem_key = get_ftok_key(SEM_FTOK_PATH, SEM_FTOK_PROJ);
// 打开信号量集
sem_id = semget(sem_key, SEM_NUMS, 0666);
if (sem_id == -1) {
fprintf(stderr, "Please run ./create_sem first!\n");
return 1;
}
// 打开共享内存(进程A已创建)
shm_id = shmget(shm_key, SHM_SIZE, 0666);
if (shm_id == -1) {
perror("shmget failed (start Process A first!)");
return 1;
}
// 映射共享内存
shm_addr = (char*)shmat(shm_id, NULL, 0);
if (shm_addr == (char*)-1) {
perror("shmat failed");
return 1;
}
printf("Process B: Shared memory mapped at %p\n", shm_addr);
// 互斥修改共享内存(5次)
for (int i = 0; i < 5; i++) {
if (sem_p(sem_id) == -1) {
break;
}
sprintf(shm_addr, "Process B: Count = %d", i);
printf("Process B modified: %s\n", shm_addr);
if (sem_v(sem_id) == -1) {
break;
}
sleep(2);
}
// 清理资源
shmdt(shm_addr);
printf("Process B: Exit successfully\n");
return 0;
}
进程 C process_c.c(用 ftok 键值,最后清理资源)
#include "shm_sem.h"
int main() {
int sem_id, shm_id;
char *shm_addr;
// 生成键值(与A/B一致)
key_t shm_key = get_ftok_key(SHM_FTOK_PATH, SHM_FTOK_PROJ);
key_t sem_key = get_ftok_key(SEM_FTOK_PATH, SEM_FTOK_PROJ);
// 打开信号量集
sem_id = semget(sem_key, SEM_NUMS, 0666);
if (sem_id == -1) {
fprintf(stderr, "Please run ./create_sem first!\n");
return 1;
}
// 打开共享内存
shm_id = shmget(shm_key, SHM_SIZE, 0666);
if (shm_id == -1) {
perror("shmget failed (start Process A first!)");
return 1;
}
// 映射共享内存
shm_addr = (char*)shmat(shm_id, NULL, 0);
if (shm_addr == (char*)-1) {
perror("shmat failed");
return 1;
}
printf("Process C: Shared memory mapped at %p\n", shm_addr);
printf("Process C monitoring shared memory:\n");
// 互斥读取共享内存(10次,每次0.5秒)
for (int i = 0; i < 10; i++) {
if (sem_p(sem_id) == -1) {
break;
}
printf("Current content: %s\n", shm_addr);
if (sem_v(sem_id) == -1) {
break;
}
sleep(1); // 1秒
}
// 清理资源(等待A/B退出后再删除)
sleep(2); // 等待A/B完成最后一次修改
shmdt(shm_addr);
shmctl(shm_id, IPC_RMID, NULL);
semctl(sem_id, 0, IPC_RMID);
printf("Process C: Deleted shared memory and semaphore. Exit successfully\n");
return 0;
}
三种 System-V IPC 对比总结
| 特性 | 消息队列 | 共享内存 | 信号量集 |
|---|---|---|---|
| 核心用途 | 数据传输(按类型分类) | 高效共享数据(无拷贝) | 进程互斥与同步 |
| 效率 | 中等(内核转发数据) | 最高(直接访问内存) | 中等(原子操作) |
| 数据安全性 | 自带阻塞、类型筛选 | 无(需配合信号量保护) | 原子操作,无数据传输 |
| 资源释放 | 需手动删除(ipcrm/msgctl) | 需手动删除(ipcrm/shmctl) | 需手动删除(ipcrm/semctl) |
| 关键函数 | msgget/msgsnd/msgrcv/msgctl | shmget/shmat/shmdt/shmctl | semget/semop/semctl |
适用场景
- 需按类型传输数据 → 消息队列;
- 需高效共享大量数据 → 共享内存 + 信号量(互斥);
- 需协调多个进程访问临界资源 → 信号量集。

浙公网安备 33010602011771号