Linux软件编程:进程间通信(消息队列、共享内存与信号灯)

在Linux系统中,进程间通信(IPC,Inter-Process Communication)是多任务编程中不可或缺的一环。上一篇我们介绍了管道和信号,本篇将聚焦于三种更加高级和灵活的IPC方式:消息队列共享内存信号灯(也称信号量集)。它们是System V IPC的典型代表,共享相同的设计理念——通过内核维护的IPC对象进行通信。

1. IPC对象与键值

消息队列、共享内存和信号灯都属于IPC对象(即内核中用于通信的内存文件)。每个IPC对象由一个唯一的键值(key)标识,通过ftok函数生成。键值类似于文件名,进程通过它来获取或创建同一个IPC对象。

ftok 函数

c

#include <sys/types.h>
#include <sys/ipc.h>

key_t ftok(const char *pathname, int proj_id);
  • 功能:根据一个存在的文件路径pathname和一个整数proj_id生成一个键值。

  • 参数

    • pathname:一个存在的可访问文件的路径。

    • proj_id:项目ID,通常取一个ASCII字符(如'A')或1~255之间的整数。

  • 返回值:成功返回键值(key_t类型),失败返回-1。

查看和删除IPC对象

  • ipcs:查看当前系统中的IPC对象(消息队列、共享内存、信号灯)。

    bash

    ipcs -q   # 仅查看消息队列
    ipcs -m   # 仅查看共享内存
    ipcs -s   # 仅查看信号灯
  • ipcrm:删除IPC对象。

    bash

    ipcrm -Q key   # 根据键值删除消息队列
    ipcrm -q msqid # 根据ID删除消息队列
    ipcrm -M key   # 根据键值删除共享内存
    ipcrm -m shmid # 根据ID删除共享内存
    ipcrm -S key   # 根据键值删除信号灯
    ipcrm -s semid # 根据ID删除信号灯

2. 消息队列

消息队列是一个存放在内核中的链表,每个消息具有独立的类型,进程可以按照类型读取消息,从而实现有序通信。

2.1 创建/获取消息队列 —— msgget

c

#include <sys/msg.h>

int msgget(key_t key, int msgflg);
  • 功能:创建或获取一个消息队列的ID。

  • 参数

    • key:IPC键值,可由ftok生成,或使用IPC_PRIVATE创建私有队列。

    • msgflg:标志位,常用组合:

      • IPC_CREAT:若队列不存在则创建。

      • IPC_EXCL:与IPC_CREAT同时使用,若队列已存在则报错。

      • 权限位,如0664

  • 返回值:成功返回消息队列ID(非负整数),失败返回-1。

2.2 发送消息 —— msgsnd

c

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
  • 功能:向消息队列发送一条消息。

  • 参数

    • msqid:消息队列ID。

    • msgp:指向消息缓冲区的指针,缓冲区必须包含一个长整型成员(消息类型)和紧随其后的消息数据。

    • msgsz:消息数据的大小(不包括消息类型)。

    • msgflg:通常为0,或IPC_NOWAIT(非阻塞)。

  • 返回值:成功返回0,失败返回-1。

消息结构示例

c

struct msgbuf {
    long mtype;       /* 消息类型,必须 > 0 */
    char mtext[1];    /* 消息数据,可定义为任意长度 */
};

2.3 接收消息 —— msgrcv

c

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
  • 功能:从消息队列接收消息。

  • 参数

    • msqid:消息队列ID。

    • msgp:存放消息的缓冲区。

    • msgsz:缓冲区大小(用于接收消息数据)。

    • msgtyp:指定接收的消息类型:

      • 0:接收队列中的第一条消息。

      • >0:接收类型等于msgtyp的第一条消息。

      • <0:接收类型小于或等于msgtyp绝对值的最小类型的第一条消息。

    • msgflg:通常为0,或IPC_NOWAITMSG_NOERROR等。

  • 返回值:成功返回实际接收到的数据字节数,失败返回-1。

2.4 控制消息队列 —— msgctl

c

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
  • 功能:对消息队列执行控制操作。

  • 参数

    • msqid:消息队列ID。

    • cmd:命令,常用:

      • IPC_RMID:删除消息队列。

      • IPC_STAT:获取队列状态,存入buf

      • IPC_SET:设置队列属性。

    • buf:与命令相关的结构体指针,删除时传NULL

  • 返回值:成功返回0,失败返回-1。

2.5 示例:两个程序通过消息队列通信

send.c(发送端)

c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_SIZE 128

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

int main() {
    key_t key = ftok(".", 'a');
    int msqid = msgget(key, IPC_CREAT | 0664);
    if (msqid == -1) {
        perror("msgget");
        exit(1);
    }

    struct msgbuf msg;
    msg.mtype = 1;  // 使用类型1

    while (1) {
        printf("Send: ");
        fgets(msg.mtext, MSG_SIZE, stdin);
        if (strncmp(msg.mtext, "quit", 4) == 0) break;
        if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
            perror("msgsnd");
            break;
        }
    }

    msgctl(msqid, IPC_RMID, NULL);  // 最后删除队列(仅由一方删除)
    return 0;
}

recv.c(接收端)

c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/msg.h>

#define MSG_SIZE 128

struct msgbuf {
    long mtype;
    char mtext[MSG_SIZE];
};

int main() {
    key_t key = ftok(".", 'a');
    int msqid = msgget(key, IPC_CREAT | 0664);
    if (msqid == -1) {
        perror("msgget");
        exit(1);
    }

    struct msgbuf msg;

    while (1) {
        if (msgrcv(msqid, &msg, MSG_SIZE, 1, 0) == -1) {
            perror("msgrcv");
            break;
        }
        printf("Received: %s", msg.mtext);
        if (strncmp(msg.mtext, "quit", 4) == 0) break;
    }
    return 0;
}

3. 共享内存

共享内存是最高效的IPC方式,它允许多个进程直接共享同一块物理内存,避免了数据拷贝。但由于多个进程同时访问,通常需要配合信号灯等同步机制。

3.1 创建/获取共享内存 —— shmget

c

#include <sys/shm.h>

int shmget(key_t key, size_t size, int shmflg);
  • 功能:创建或获取一个共享内存段。

  • 参数

    • key:键值。

    • size:共享内存大小(字节),创建时需指定,获取时可填0。

    • shmflg:标志位,与msgget类似,如IPC_CREAT | 0664

  • 返回值:成功返回共享内存ID,失败返回-1。

3.2 映射共享内存 —— shmat

c

void *shmat(int shmid, const void *shmaddr, int shmflg);
  • 功能:将共享内存段附加到进程的地址空间。

  • 参数

    • shmid:共享内存ID。

    • shmaddr:指定映射地址,通常传NULL让系统自动选择。

    • shmflg:标志,如SHM_RDONLY(只读),默认为0(读写)。

  • 返回值:成功返回映射后的虚拟地址,失败返回(void *)-1

3.3 解除映射 —— shmdt

c

int shmdt(const void *shmaddr);
  • 功能:将共享内存从当前进程分离。

  • 参数shmaddrshmat返回的地址。

  • 返回值:成功返回0,失败返回-1。

3.4 控制共享内存 —— shmctl

c

int shmctl(int shmid, int cmd, struct shmid_ds *buf);
  • 功能:对共享内存执行控制操作。

  • 参数

    • cmd:常用IPC_RMID(删除共享内存)。

    • buf:通常传NULL

  • 返回值:成功返回0,失败返回-1。

3.5 示例:两个进程通过共享内存交换数据

write.c(写入数据)

c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok(".", 'b');
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0664);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    char *addr = shmat(shmid, NULL, 0);
    if (addr == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    while (1) {
        printf("Input: ");
        fgets(addr, SHM_SIZE, stdin);
        if (strncmp(addr, "quit", 4) == 0) break;
    }

    shmdt(addr);
    // 通常由最后使用的进程删除共享内存
    shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

read.c(读取数据)

c

#include <stdio.h>
#include <stdlib.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok(".", 'b');
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0664);
    if (shmid == -1) {
        perror("shmget");
        exit(1);
    }

    char *addr = shmat(shmid, NULL, 0);
    if (addr == (void *)-1) {
        perror("shmat");
        exit(1);
    }

    while (1) {
        if (addr[0] != '\0') {  // 简单轮询,实际应用中需同步
            printf("Read: %s", addr);
            if (strncmp(addr, "quit", 4) == 0) break;
            addr[0] = '\0';  // 清空标记
        }
    }

    shmdt(addr);
    return 0;
}

4. 信号灯(信号量集)

信号灯是一个或多个信号量的集合(数组),用于进程/线程间的同步与互斥。与单个信号量不同,System V信号灯允许对多个信号量同时操作。

4.1 创建/获取信号灯 —— semget

c

#include <sys/sem.h>

int semget(key_t key, int nsems, int semflg);
  • 功能:创建或获取一个信号灯集。

  • 参数

    • key:键值。

    • nsems:信号量的个数。

    • semflg:标志位,如IPC_CREAT | 0664

  • 返回值:成功返回信号灯ID,失败返回-1。

4.2 控制信号灯 —— semctl

c

int semctl(int semid, int semnum, int cmd, ...);
  • 功能:对信号灯集中的某个信号量执行控制命令。

  • 参数

    • semid:信号灯ID。

    • semnum:信号量下标(从0开始)。

    • cmd:常用命令:

      • IPC_RMID:删除信号灯集。

      • SETVAL:设置信号量的值(需要第四个参数)。

      • GETVAL:获取信号量的值。

    • 第四个参数(可选):类型为union semun,需用户自定义。

  • 返回值:成功取决于命令,失败返回-1。

union semun(通常需在程序中定义):

c

union semun {
    int val;                    /* 用于 SETVAL */
    struct semid_ds *buf;        /* 用于 IPC_STAT/IPC_SET */
    unsigned short *array;       /* 用于 GETALL/SETALL */
    struct seminfo *__buf;       /* 用于 IPC_INFO */
};

4.3 操作信号量 —— semop

c

int semop(int semid, struct sembuf *sops, size_t nsops);
  • 功能:对信号灯集中的信号量进行P/V操作(申请/释放)。

  • 参数

    • semid:信号灯ID。

    • sops:指向struct sembuf数组的指针,每个元素描述一个操作。

    • nsops:数组元素个数。

  • 返回值:成功返回0,失败返回-1。

struct sembuf 定义:

c

struct sembuf {
    unsigned short sem_num;  /* 信号量下标 */
    short          sem_op;   /* 操作数:正数为释放(V),负数为申请(P),0为等待到0 */
    short          sem_flg;  /* 标志:IPC_NOWAIT 或 SEM_UNDO */
};

4.4 示例:共享内存 + 信号灯实现同步

下面的示例利用两个信号量实现“写一次、读一次”的同步。信号量0用于表示数据是否可读(初值0),信号量1用于表示缓冲区是否可写(初值1)。

可以加一句:

semctl(semid, 0, IPC_RMID);


5. 总结

IPC方式特点适用场景
消息队列消息有类型,可选择性接收;内核维护队列;数据有边界。需要按类型分类处理的通信,如客户端-服务器。
共享内存最快,无需数据拷贝;但需要同步机制。大量数据交换,配合信号灯使用。
信号灯用于同步与互斥,可操作多个信号量。控制对共享资源的访问,实现进程同步。

这三种IPC方式灵活强大,但也需要谨慎管理资源(及时删除IPC对象),避免内存泄漏和死锁。熟练掌握它们,将为你的Linux系统编程打下坚实基础。


作业练习

  1. 利用消息队列实现两个进程的聊天功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值