解码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 消息队列IDipcrm -m 共享内存IDipcrm -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)

概念

  • 按 “消息类型” 分类的队列式通信机制,不同类型消息存于不同队列,接收时需指定类型。

    image

  • 特点:自带数据分类、默认阻塞模式(队列满时写阻塞,队列空时读阻塞),默认容量 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 机制:内核分配一块物理内存,多个进程将其映射到自身虚拟地址空间,直接通过虚拟地址读写(无需内核转发数据)。

    image

  • 特点:无数据拷贝开销,效率极高,但需配合信号量等机制实现进程互斥(避免同时读写导致数据错乱)。

  • 页大小:默认 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

适用场景

  • 需按类型传输数据 → 消息队列;
  • 需高效共享大量数据 → 共享内存 + 信号量(互斥);
  • 需协调多个进程访问临界资源 → 信号量集。
posted @ 2025-11-15 18:48  YouEmbedded  阅读(4)  评论(0)    收藏  举报