一网打尽Linux IPC(三):System V IPC

示例代码

一、System V IPC概述

System V IPC(Inter-Process Communication)是Unix系统V版本中引入的一组进程间通信机制,包括:

  • 消息队列(Message Queue):进程间传递结构化数据
  • 信号量(Semaphore):进程同步机制
  • 共享内存(Shared Memory):进程间高效共享数据

System V IPC具有以下特点:

  1. 持久性:IPC对象在内核中持久存在,直到显式删除或系统重启
  2. 全局性:所有进程通过相同的key访问同一IPC对象
  3. 权限控制:通过IPC权限结构控制访问权限
  4. 丰富的控制操作:提供多种控制命令管理IPC对象

这些机制在早期的Unix系统中被广泛使用,虽然POSIX IPC提供了更现代的实现,但System V IPC因其悠久历史和广泛支持,仍在许多系统中使用。

System V IPC接口表如下:

接口 消息队列 信号量 共享内存
头文件 <sys/msg.h> <sys/sem.h> <sys/shm.h>
描述符 msqid_ds semid_ds shmid_ds
创建/打开 msgget() semget() shmget() + shmat()
关闭对象 shmdt()
控制操作 msgctl() semctl() shmctl()
执行IPC msgsnd() / msgrcv() msgop()
发送/接收 消息 测试/调整 信号量 操作共享内存

二、System V IPC通用概念

1. IPC键值(key_t)

每个System V IPC对象都通过一个唯一的键值标识。可以通过以下方式生成键值:

c 复制代码
/**
 * @brief   生成System V IPC键值
 * @param   pathname [IN] 现有文件的路径名
 * @param   proj [IN] 项目标识符(低8位有效)
 * @return  成功返回键值,失败返回-1
 * @note    基于文件的inode号、设备号和项目标识符生成键值。
 *          不同文件可能生成相同键值,删除重建文件会改变inode,从而改变键值。
 */
key_t ftok(const char *pathname, int proj);

2. IPC权限结构

所有System V IPC对象都包含一个ipc_perm结构,用于权限控制:

c 复制代码
struct ipc_perm {
    key_t           __key;      /* 提供给get调用的键值 */
    uid_t           uid;        /* 所有者的用户ID */
    gid_t           gid;        /* 所有者的组ID */
    uid_t           cuid;       /* 创建者的用户ID */
    gid_t           cgid;       /* 创建者的组ID */
    unsigned short  mode;       /* 权限位 */
    unsigned short  __seq;      /* 序列号 */
};

3. IPC创建标志

创建IPC对象时使用的标志:

  • IPC_CREAT:如果对象不存在则创建
  • IPC_EXCL:与IPC_CREAT一起使用,确保创建的是新对象

三、System V消息队列

消息队列允许进程以消息的形式交换数据,消息具有类型和内容,可以按类型接收消息。

1. 接口说明

c 复制代码
/**
 * @brief   创建或打开消息队列
 * @param   key [IN] IPC键值,可用ftok生成或IPC_PRIVATE
 * @param   msgflg [IN] 创建标志和权限,如IPC_CREAT|IPC_EXCL|0666
 * @return  成功返回消息队列标识符,失败返回-1
 * @note    使用IPC_PRIVATE会创建一个新的消息队列,只有知道标识符的进程可以访问。
 */
int msgget(key_t key, int msgflg);

/**
 * @brief   向消息队列发送消息
 * @param   msqid [IN] 消息队列标识符
 * @param   msgp [IN] 指向消息结构的指针
 * @param   msgsz [IN] 消息正文的长度(不含消息类型长度)
 * @param   msgflg [IN] 发送标志,如IPC_NOWAIT(非阻塞)
 * @return  成功返回0,失败返回-1
 * @note    消息结构必须包含long类型的mtype字段和字符数组mtext。
 */
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

/**
 * @brief   从消息队列接收消息
 * @param   msqid [IN] 消息队列标识符
 * @param   msgp [OUT] 指向消息结构的指针
 * @param   maxmsgsz [IN] 消息缓冲区的最大长度
 * @param   msgtyp [IN] 要接收的消息类型
 * @param   msgflg [IN] 接收标志,如IPC_NOWAIT(非阻塞)
 * @return  成功返回接收到的消息长度,失败返回-1
 * @note    msgtyp为0表示接收第一条消息,>0表示接收指定类型的消息,
 *          <0表示接收类型小于等于绝对值的消息中类型最小的消息。
 */
ssize_t msgrcv(int msqid, void *msgp, size_t maxmsgsz, long msgtyp, int msgflg);

/**
 * @brief   控制消息队列
 * @param   msqid [IN] 消息队列标识符
 * @param   cmd [IN] 控制命令
 * @param   buf [INOUT] 指向msqid_ds结构的指针
 * @return  成功返回0,失败返回-1
 * @note    常用命令:IPC_RMID(删除队列),IPC_STAT(获取状态),IPC_SET(设置参数)
 */
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

消息数据结构:

c 复制代码
struct msgbuf {
    long mtype;       /* 消息类型,必须大于0 */
    char mtext[1];    /* 消息正文,长度可变 */
};

2. 代码示例

以下是一个使用System V消息队列的例子,父进程从终端读取数据,发送到消息队列,子进程从消息队列接收数据,输出到终端。输入"q"退出。

代码链接:test_sysv_msg.c

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "jlog_core.h"

#define BUF_SIZE        128
#define TIP(str)        write(STDOUT_FILENO, str, strlen(str))

#define SYSV_MSG_TYPE   1
#define SYSV_MSG_PATH   "/tmp/test_sysv_msg"
#define SYSV_MSG_PROJ   123

struct mbuf {
    long mtype;
    char mtext[BUF_SIZE];
};

static int read_from_sysv_msg(void)
{
    struct mbuf msg;
    key_t key;
    int msgid;
    ssize_t num;
    int ret = 0;

    usleep(1000);

    key = ftok(SYSV_MSG_PATH, SYSV_MSG_PROJ);
    msgid = msgget(key, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
    if (msgid == -1) {
        LLOG_ERRNO("msgget(%s) failed!\n", SYSV_MSG_PATH);
        return -1;
    }

    for (;;) {
        memset(msg.mtext, 0, BUF_SIZE);
        num = msgrcv(msgid, &msg, BUF_SIZE, SYSV_MSG_TYPE, 0);
        if (num == -1) {
            LLOG_ERRNO("msgrcv() failed!\n");
            ret = -1;
            break;
        }
        if (num == 0)
            continue;
        if (num == 2 && msg.mtext[0] == 'q')
            break;

        TIP("msgrcv: ");
        if (write(STDOUT_FILENO, msg.mtext, num) != num) {
            LLOG_ERRNO("write STDOUT failed!\n");
            ret = -1;
            break;
        }
    }

    return ret;
}

static int write_to_sysv_msg(void)
{
    struct mbuf msg;
    key_t key;
    int msgid;
    ssize_t num;
    int ret = 0;

    key = ftok(SYSV_MSG_PATH, SYSV_MSG_PROJ);
    msgid = msgget(key, IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
    if (msgid == -1) {
        LLOG_ERRNO("msgget(%s) failed!\n", SYSV_MSG_PATH);
        return -1;
    }

    msg.mtype = SYSV_MSG_TYPE;
    for (;;) {
        usleep(1000);
        TIP("msgsnd: ");
        memset(msg.mtext, 0, BUF_SIZE);
        num = read(STDIN_FILENO, msg.mtext, BUF_SIZE);
        if (num == -1) {
            LLOG_ERRNO("read STDIN failed!\n");
            ret = -1;
            break;
        }
        if (num == 0)
            continue;

        if (msgsnd(msgid, &msg, num, 0) == -1) {
            LLOG_ERRNO("msgsnd() failed!\n");
            ret = -1;
            break;
        }
        if (num == 2 && msg.mtext[0] == 'q')
            break;
    }
    msgctl(msgid, IPC_RMID, 0);

    return ret;
}

int main(int argc, char *argv[])
{
    int ret = 0;

    SLOG_INFO("SYSV MSG: 父进程从终端写入获取数据发送消息,子进程接收消息将数据输出到终端。\n");
    SLOG_INFO("[input q to exit]\n");

    switch (fork()) {
        case -1:
            LLOG_ERRNO("fork() failed!\n");
            exit(EXIT_FAILURE);

        case 0: /* Child */
            ret = read_from_sysv_msg();
            SLOG_INFO("child process exit!\n");
            exit(ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE);

        default: /* Parent */
            ret = write_to_sysv_msg();
            wait(NULL); /* wait for child process exit */
            SLOG_INFO("parent process exit!\n");
            exit(ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
    }
}

3. 详细讲解

工作原理

  1. 使用ftok基于文件路径和项目标识符生成唯一的key。
  2. 父进程调用msgget创建消息队列(使用IPC_CREAT|IPC_EXCL确保创建新队列)。
  3. 父进程从标准输入读取数据,使用msgsnd发送消息到队列,消息类型为SYSV_MSG_TYPE
  4. 子进程调用msgget打开消息队列,使用msgrcv接收指定类型的消息。
  5. 通信完成后,父进程使用msgctl删除消息队列。

注意事项

  • 消息队列中的消息是有类型的,接收时可以按类型接收。
  • 消息队列在内核中持久存在,需要显式删除,否则会一直占用系统资源。
  • 默认情况下,msgsndmsgrcv是阻塞的,可以设置IPC_NOWAIT标志使其非阻塞。
  • 消息队列的最大消息数和每条消息的最大长度有限制,可以通过msgctlIPC_SET命令修改。

四、System V信号量

信号量是一种同步机制,用于协调多个进程对共享资源的访问。System V信号量以信号量集的形式存在,可以同时操作多个信号量。

1. 接口说明

c 复制代码
/**
 * @brief   创建或打开信号量集
 * @param   key [IN] IPC键值
 * @param   nsems [IN] 信号量集中信号量的数量
 * @param   semflg [IN] 创建标志和权限
 * @return  成功返回信号量集标识符,失败返回-1
 * @note    nsems必须大于0。Linux中信号量默认初始值为0,因此需要显式初始化。
 */
int semget(key_t key, int nsems, int semflg);

/**
 * @brief   信号量操作
 * @param   semid [IN] 信号量集标识符
 * @param   sops [IN] 操作结构数组
 * @param   nsops [IN] 操作数组的大小
 * @return  成功返回0,失败返回-1
 * @note    每个sembuf结构指定对哪个信号量进行何种操作。
 */
int semop(int semid, struct sembuf *sops, unsigned int nsops);

/**
 * @brief   控制信号量
 * @param   semid [IN] 信号量集标识符
 * @param   semnum [IN] 信号量编号(有些命令忽略此参数)
 * @param   cmd [IN] 控制命令
 * @param   arg [IN] 命令参数
 * @return  成功返回值依赖于cmd,失败返回-1
 * @note    常用命令:IPC_RMID(删除集合),GETVAL/SETVAL(获取/设置单个信号量值),
 *          GETALL/SETALL(获取/设置所有信号量值)。
 */
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */);

操作结构:

c 复制代码
struct sembuf {
    unsigned short  sem_num;    /* 信号量编号 */
    short           sem_op;     /* 操作值 */
    short           sem_flg;    /* 操作标志 */
};

其中sem_op

  • 正数:信号量值增加该数值
  • 0:等待信号量值变为0
  • 负数:信号量值减少该数值的绝对值,如果信号量值不足会阻塞

2. 详细讲解

工作原理

  1. 使用semget创建或打开信号量集。
  2. 使用semctlSETVALSETALL命令初始化信号量值。
  3. 进程使用semop对信号量进行操作:
    • P操作(获取资源):sem_op为负数,减少信号量值
    • V操作(释放资源):sem_op为正数,增加信号量值
    • 等待信号量变为0:sem_op为0
  4. 使用semctlIPC_RMID命令删除信号量集。

注意事项

  • System V信号量以集合形式存在,可以同时操作多个信号量,实现复杂同步逻辑。
  • semop操作是原子的,要同时修改的信号量要么都成功,要么都不成功。
  • 可以使用SEM_UNDO标志,使进程意外终止时自动撤销信号量操作。
  • 信号量值永远不会小于0,尝试减小到小于0的操作会阻塞,直到有进程增加信号量值。

五、System V共享内存

共享内存允许多个进程共享同一块物理内存,是最快的IPC机制。但需要额外的同步机制(如信号量)来保护共享数据。

1. 接口说明

c 复制代码
/**
 * @brief   创建或打开共享内存段
 * @param   key [IN] IPC键值
 * @param   size [IN] 共享内存段大小(字节)
 * @param   shmflg [IN] 创建标志和权限
 * @return  成功返回共享内存段标识符,失败返回-1
 * @note    大小会被上舍入到系统分页大小的整数倍。
 */
int shmget(key_t key, size_t size, int shmflg);

/**
 * @brief   将共享内存段映射到进程地址空间
 * @param   shmid [IN] 共享内存段标识符
 * @param   shmaddr [IN] 指定映射地址(通常为NULL,由系统选择)
 * @param   shmflg [IN] 映射标志,如SHM_RDONLY(只读映射)
 * @return  成功返回映射地址,失败返回(void*)-1
 * @note    调用成功后,进程可以通过返回的指针访问共享内存。
 */
void *shmat(int shmid, const void *shmaddr, int shmflg);

/**
 * @brief   解除共享内存映射
 * @param   shmaddr [IN] 共享内存映射地址
 * @return  成功返回0,失败返回-1
 * @note    进程退出时会自动解除映射。
 */
int shmdt(const void *shmaddr);

/**
 * @brief   控制共享内存段
 * @param   shmid [IN] 共享内存段标识符
 * @param   cmd [IN] 控制命令
 * @param   buf [INOUT] 指向shmid_ds结构的指针
 * @return  成功返回0,失败返回-1
 * @note    常用命令:IPC_RMID(标记删除),IPC_STAT(获取状态),IPC_SET(设置参数)
 */
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

2. 代码示例

以下是一个使用System V共享内存和信号量的例子,父进程从终端读取数据写入共享内存,子进程从共享内存读取数据输出到终端。使用两个信号量同步读写。

代码链接:test_sysv_shm.c

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include "jlog_core.h"

#define BUF_SIZE        128
#define TIP(str)        write(STDOUT_FILENO, str, strlen(str))

#define SYSV_SHM_SIZE   (8192 * 1)
#define SYSV_SEM_NUM    2

#define SYSV_SEMCTRL(idx, op)  do {     \
    sops.sem_num = idx;                 \
    sops.sem_op = op;                   \
    sops.sem_flg = 0;                   \
    semop(semid, &sops, 1);             \
} while (0)

#define SYSV_SEMWAIT(sel)  SYSV_SEMCTRL(sel, -1)
#define SYSV_SEMPOST(sel)  SYSV_SEMCTRL(sel, 1)

union semun {
    int                 val;
    struct semid_ds    *buf;
    unsigned short     *array;
#if defined(__linux__)
    struct seminfo     *__buf;
#endif
};

static int read_from_sysv_shm(int shmid, int semid)
{
    struct sembuf sops;
    char buf[BUF_SIZE];
    char *addr;
    ssize_t num;
    int bflag = 0;
    int ret = 0;

    addr = shmat(shmid, NULL, 0);
    if ((void*)addr == (void*)-1) {
        LLOG_ERRNO("shmat() failed!\n");
        return -1;
    }

    for (;;) {
        SYSV_SEMWAIT(0);
        memset(buf, 0, BUF_SIZE);
        memcpy(buf, addr, BUF_SIZE);
        num = strlen(buf);
        if (num == 0) {
            goto post;
        }
        if (num == 2 && buf[0] == 'q') {
            bflag = 1;
            goto post;
        }

        TIP("read SHM: ");
        if (write(STDOUT_FILENO, buf, num) != num) {
            LLOG_ERRNO("write STDOUT failed!\n");
            bflag = 1;
            ret = -1;
        }
post:
        SYSV_SEMPOST(1);
        if (bflag)
            break;
    }
    shmdt(addr);

    return ret;
}

static int write_to_sysv_shm(int shmid, int semid)
{
    struct sembuf sops;
    char buf[BUF_SIZE];
    char *addr;
    ssize_t num;
    int bflag = 0;
    int ret = 0;

    addr = shmat(shmid, NULL, 0);
    if ((void*)addr == (void*)-1) {
        LLOG_ERRNO("shmat() failed!\n");
        return -1;
    }

    for (;;) {
        SYSV_SEMWAIT(1);
        TIP("write SHM: ");
        memset(buf, 0, BUF_SIZE);
        num = read(STDIN_FILENO, buf, BUF_SIZE);
        if (num == -1) {
            LLOG_ERRNO("read STDIN failed!\n");
            bflag = 1;
            ret = -1;
            goto post;
        }
        if (num == 0) {
            goto post;
        }

        memcpy(addr, buf, BUF_SIZE);

        if (num == 2 && buf[0] == 'q') {
            bflag = 1;
        }
post:
        SYSV_SEMPOST(0);
        if (bflag)
            break;
    }
    shmdt(addr);

    return ret;
}

int main(int argc, char *argv[])
{
    union semun args[2];
    int semid;
    int shmid;
    int ret = 0;

    SLOG_INFO("SYSV SHM: 父进程从终端写入获取数据到共享内存,子进程读取共享内存将数据输出到终端。\n");
    SLOG_INFO("[input q to exit]\n");

    shmid = shmget(IPC_PRIVATE, SYSV_SHM_SIZE, IPC_CREAT | IPC_EXCL
                   | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
    if (shmid == -1) {
        LLOG_ERRNO("shmget() failed!\n");
        exit(EXIT_FAILURE);
    }

    semid = semget(IPC_PRIVATE, SYSV_SEM_NUM, IPC_CREAT | IPC_EXCL
                   | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);
    if (semid == -1) {
        shmctl(shmid, IPC_RMID, 0);
        LLOG_ERRNO("semget() failed!\n");
        exit(EXIT_FAILURE);
    }

    args[0].val = 0;
    args[1].val = 1;
    if (semctl(semid, 0, SETVAL, args[0]) == -1
            || semctl(semid, 1, SETVAL, args[1]) == -1) {
        semctl(semid, 0, IPC_RMID);
        semctl(semid, 1, IPC_RMID);
        shmctl(shmid, IPC_RMID, NULL);
        LLOG_ERRNO("semctl() failed!\n");
        exit(EXIT_FAILURE);
    }

    switch (fork()) {
        case -1:
            semctl(semid, 0, IPC_RMID);
            semctl(semid, 1, IPC_RMID);
            shmctl(shmid, IPC_RMID, NULL);
            LLOG_ERRNO("fork() failed!\n");
            exit(EXIT_FAILURE);

        case 0: /* Child */
            ret = read_from_sysv_shm(shmid, semid);
            SLOG_INFO("child process exit!\n");
            exit(ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE);

        default: /* Parent */
            ret = write_to_sysv_shm(shmid, semid);
            wait(NULL); /* wait for child process exit */
            semctl(semid, 0, IPC_RMID);
            semctl(semid, 1, IPC_RMID);
            shmctl(shmid, IPC_RMID, NULL);
            SLOG_INFO("parent process exit!\n");
            exit(ret == 0 ? EXIT_SUCCESS : EXIT_FAILURE);
    }
}

3. 详细讲解

工作原理

  1. 使用shmget创建共享内存段,使用IPC_PRIVATE确保创建新段。
  2. 使用semget创建包含两个信号量的信号量集,用于读写同步。
  3. 父进程和子进程分别使用shmat将共享内存映射到自己的地址空间。
  4. 使用两个信号量实现生产者-消费者同步:
    • 信号量0:读者信号量,初始为0,表示没有数据可读
    • 信号量1:写者信号量,初始为1,表示可以写入数据
  5. 父进程写入数据前等待写者信号量,写入后释放读者信号量。
  6. 子进程读取数据前等待读者信号量,读取后释放写者信号量。
  7. 通信完成后,父进程删除共享内存段和信号量集。

注意事项

  • 共享内存是最快的IPC机制,因为数据直接在进程间共享,无需内核复制。
  • 但共享内存没有内置同步机制,需要额外的同步机制(如信号量)保护共享数据。
  • 共享内存映射后,进程可以通过指针直接访问内存,就像访问普通内存一样。
  • 需要小心处理共享内存中的数据格式和字节序问题,特别是跨不同架构的系统。
  • 共享内存段在内核中持久存在,需要显式删除,否则会一直占用内存资源。

六、总结

System V IPC提供了一组强大但稍显古老的进程间通信机制:

  1. 消息队列:适合传递结构化消息,支持按类型接收消息,但性能不如共享内存。
  2. 信号量:强大的同步机制,支持信号量集和原子操作多个信号量,但API较复杂。
  3. 共享内存:最快的IPC机制,适合大量数据共享,但需要额外的同步机制。

优点

  • 功能强大,历史悠久,广泛支持
  • IPC对象在内核中持久存在
  • 提供丰富的控制操作

缺点

  • API较复杂,学习曲线陡峭
  • 需要显式删除IPC对象,否则会一直占用系统资源
  • 某些设计(如ftok)在现代系统中可能不够可靠

使用建议

  • 对于新项目,优先考虑POSIX IPC,其接口更简单、更符合现代编程习惯
  • 对于需要与旧系统兼容的项目,System V IPC仍然是必须的
  • 根据具体需求选择合适的机制:少量数据用消息队列,大量数据用共享内存,同步用信号量

在下一篇文章中,我们将介绍POSIX IPC,它是更现代、更符合标准化的IPC机制,接口更简洁,功能更强大。

相关推荐
大聪明-PLUS2 小时前
如何编写你的第一个 Linux 内核模块
linux·嵌入式·arm·smarc
知识分享小能手2 小时前
Ubuntu入门学习教程,从入门到精通,Ubuntu 22.04文件压缩与解压缩知识点详解(12)
linux·学习·ubuntu
用户6135411460162 小时前
Krb5-libs-1.18.2-5.ky10.x86_64.rpm 安装失败怎么办?附详细步骤
linux
范纹杉想快点毕业3 小时前
返璞归真还是拥抱现代?——嵌入式研发中的“裸机开发”与RTOS全景解析
c语言·数据库·mongodb·设计模式·nosql
SoveTingღ4 小时前
【问题解析】我的客户端与服务器交互无响应了?
服务器·c++·qt·tcp
zhougl9964 小时前
Vuex 模块命名冲突:问题解析与完整解决方案
linux·服务器·apache
一世琉璃白_Y4 小时前
Ubuntu(VMware)虚拟机网络异常排查与解决方案
linux·网络·ubuntu
爱丽_4 小时前
MyBatis动态SQL完全指南
服务器·sql·mybatis
AI+程序员在路上4 小时前
网桥及IP转发在嵌入式linux eth0与wlan0连接使用方法
linux·tcp/ip·php