Linux进程间通信:信号、消息队列、共享内存完整笔记

标签:#Linux #进程间通信 #信号 #消息队列 #共享内存 #System‑V‑IPC

摘要:本篇梳理Linux下信号基础API、System‑V消息队列、System‑V共享内存核心接口,包含函数详解、原理剖析、可运行示例代码,

前言

进程间通信(IPC)是Linux后端开发的核心知识点。不同进程拥有独立虚拟地址空间,无法直接访问对方内存,必须借助操作系统提供的机制完成数据传递与事件通知。

本文涵盖三类经典IPC机制:

  1. 信号:异步事件通知,传递数字信号,不适合大批量数据传输
  2. System‑V消息队列:带消息类型的报文式通信
  3. System‑V共享内存:效率最高IPC,直接内存映射,无数据拷贝

一、信号相关系统调用

信号本质是软件中断,用于通知进程发生某个事件,信号只能传递编号,不能携带大量数据

1.1 kill() --- 向进程发送信号

c 复制代码
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

功能 :给指定pid的进程发送一个编号为sig的信号。

参数说明

参数 说明
pid 接收信号的进程PID
sig 信号编号,如SIGKILL(9)SIGALRM(14)

返回值

  • 成功返回 0
  • 失败返回 -1,设置errno

⚠️注意:kill( )不是专门用来杀死进程,只是发送信号;发送SIGKILL才会强制终止进程。

1.2 pause() --- 进程休眠等待信号

复制代码
#include <unistd.h>
int pause(void);

功能:将进程挂起进入休眠,直到捕捉到一个信号、执行完信号处理函数后,pause才返回。

关键点:唤醒pause的信号必须被捕获(注册信号处理函数) ;如果信号是默认终止行为,进程直接退出,pause不会返回。

返回值:永远返回‑1

1.3 alarm() --- 定时产生SIGALRM信号

复制代码
#include <unistd.h>
unsigned int alarm(unsigned int seconds);

功能 :seconds秒之后,给当前进程发送SIGALRM(14)闹钟信号。

  • 调用alarm(0)会取消已设置的闹钟。
  • 返回值:上一次闹钟剩余的秒数。

简单示例:alarm + pause 定时demo

复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void sig_alarm(int sig)
{
    printf("收到闹钟信号 SIGALRM\n");
}

int main(void)
{
    signal(SIGALRM, sig_alarm);
    alarm(3);   //3秒后发送SIGALRM
    printf("进程休眠等待信号...\n");
    pause();
    printf("pause被唤醒,程序结束\n");
    return 0;
}

编译运行:gcc alarm_demo.c -o alarm_demo && ./alarm_demo


二、System‑V IPC 消息队列

消息队列:内核维护的消息链表。进程把数据封装成消息体,带有mtype消息类型,接收方可以按类型筛选消息。

优点:支持按消息类型收发;缺点:内核空间拷贝,数据量大时效率不如共享内存。

整体流程

  1. ftok():生成IPC唯一key值
  2. msgget():创建/获取消息队列ID
  3. msgsnd():发送消息
  4. msgrcv():接收消息
  5. msgctl():控制、删除消息队列

2.1 ftok() 生成IPC key

复制代码
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);

功能:根据一个存在的文件路径 + 项目id,生成一个IPC的key值,用于标识IPC对象。

原理:将文件inode号与proj_id做运算生成key;pathname对应的文件必须真实存在,否则返回‑1。

参数 说明
pathname 磁盘上真实存在文件路径
proj_id 项目ID,仅低8位有效,0‑255

返回:成功返回key_t键值;失败返回‑1。

注意:不同文件,哪怕相同proj_id,得到key不一样。

2.2 msgget() 创建消息队列

复制代码
#include <sys/msg.h>
int msgget(key_t key, int msgflg);

功能:根据key创建或者打开消息队列,返回消息队列ID。

参数:

  • key:ftok生成的IPC键值
  • msgflg:标志位,常用 IPC_CREAT | 0664
    • IPC_CREAT:不存在则创建;存在直接打开
    • 0664:权限,类似文件权限

返回:成功返回msgid;失败‑1。

2.3 消息体结构体 struct msgbuf

复制代码
struct msgbuf {
    long mtype;      /* 消息类型,必须 > 0 */
    char mtext[1];   /* 消息正文,柔性数组,可自定义大小 */
};

mtype必须大于0,msgrcv可以指定只接收某一类消息,实现简单优先级。

2.4 msgsnd() 发送消息

复制代码
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数:

  • msqid:消息队列ID
  • msgp:指向msgbuf消息结构体
  • msgsz:消息正文mtext字节大小,不包含mtype(long类型)
  • msgflg:标志位,填0代表阻塞模式;消息队列满时进程阻塞等待。

返回:成功0;失败‑1。

2.5 msgrcv() 接收消息

复制代码
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

参数说明

  • msqid:消息队列ID
  • msgp:存放接收消息的缓冲区
  • msgsz:能接收正文最大字节
  • msgtyp:指定接收消息类型
    • msgtyp >0:只接收mtype等于msgtyp的消息
    • msgtyp =0:接收队列第一条消息,忽略类型
  • msgflg:0阻塞模式;没有对应消息就阻塞等待。

返回:成功返回实际收到正文字节数;失败返回‑1。

2.6 msgctl() 消息队列控制(删除/获取状态)

复制代码
int msgctl(int msqid, int cmd, struct msqid_ds *buf);

cmd常用选项:

  1. IPC_STAT:获取消息队列状态,存入buf
  2. IPC_SET:设置消息队列属性
  3. IPC_RMID删除消息队列 ,此时第三个参数buf传NULL

返回:成功0,失败‑1。

2.7 Shell操作消息队列命令

复制代码
ipcs -a                 #查看系统所有IPC对象(消息队列、共享内存、信号量)
ipcrm -q  msgid         #通过msgid删除消息队列
ipcrm -Q  msgkey        #通过key值删除消息队列

消息队列完整示例

msg_send.c 发送端

复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>

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

int main(void)
{
    key_t key = ftok("./test.txt", 0x11);
    if(key == -1){ perror("ftok"); return -1; }

    int msqid = msgget(key, IPC_CREAT | 0664);
    if(msqid == -1){ perror("msgget"); return -1; }

    struct msgbuf buf;
    buf.mtype = 100;  //消息类型
    strcpy(buf.mtext, "hello message queue");

    int ret = msgsnd(msqid, &buf, strlen(buf.mtext), 0);
    if(ret == -1){ perror("msgsnd"); return -1; }
    printf("消息发送完成\n");
    return 0;
}

msg_recv.c 接收端

复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>

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

int main(void)
{
    key_t key = ftok("./test.txt",0x11);
    int msqid = msgget(key, IPC_CREAT|0664);

    struct msgbuf buf;
    ssize_t n = msgrcv(msqid, &buf, sizeof(buf.mtext),100,0);
    if(n == -1){ perror("msgrcv"); return -1; }
    printf("收到消息:%s\n", buf.mtext);

    //msgctl(msqid, IPC_RMID, NULL); //使用完删除队列
    return 0;
}

注意:当前目录需要存在test.txttouch test.txt;分别编译运行发送、接收程序。


三、System‑V 共享内存

共享内存:Linux下效率最高的IPC方式 。原理:内核开辟一块物理内存,多个进程把这块物理内存映射到各自用户虚拟地址空间。进程读写自己虚拟地址,直接操作同一块物理内存,没有内核态、用户态之间数据拷贝

⚠️风险:共享内存本身不带同步互斥机制,多进程并发读写需要自己加信号量、互斥锁做保护。

流程

  1. ftok()生成key
  2. shmget() 创建/获取共享内存ID
  3. shmat() 将共享内存映射到进程用户空间
  4. 直接读写返回的虚拟地址
  5. shmdt() 解除映射
  6. shmctl() 删除共享内存

3.1 shmget() 创建共享内存

复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);

参数:

  • key:IPC key
  • size:共享内存字节大小,建议PAGE_SIZE(4096)整数倍;内核会向上对齐页大小
  • shmflgIPC_CREAT | 0664

返回:成功返回shmid;失败‑1。

3.2 shmat() 挂载映射共享内存

复制代码
void *shmat(int shmid, const void *shmaddr, int shmflg);

参数:

  • shmid:共享内存id
  • shmaddr:指定映射到用户空间地址,填NULL,操作系统自动选地址(推荐)
  • shmflg
    • SHM_RDONLY:只读挂载
    • 0:读写权限

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

3.3 shmdt() 解除映射

复制代码
int shmdt(const void *shmaddr);

将共享内存从当前进程地址空间剥离,不会删除内核中的共享内存对象

3.4 shmctl() 共享内存控制

复制代码
int shmctl(int shmid, int cmd, struct shmid_ds *buf);

cmd:IPC_RMID表示删除共享内存对象,第三个参数传NULL

注意:执行IPC_RMID不会立刻销毁;只有所有进程全部shmdt解除映射之后,才真正释放内存。

shell操作共享内存

复制代码
ipcs -a
ipcrm -m shmid      #按id删除共享内存
ipcrm -M shmkey     #按key删除共享内存

共享内存简单示例

shm_write.c 写端

复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>

#define SHM_SIZE 4096

int main(void)
{
    key_t key = ftok("./test.txt",0x22);
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT|0664);
    if(shmid == -1){ perror("shmget"); return -1; }

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

    strcpy(p, "share memory test data");
    printf("写入共享内存完成\n");

    shmdt(p);
    return 0;
}

shm_read.c 读端

复制代码
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define SHM_SIZE 4096
int main(void)
{
    key_t key = ftok("./test.txt",0x22);
    int shmid = shmget(key, SHM_SIZE, IPC_CREAT|0664);
    char *p = shmat(shmid, NULL,0);

    printf("读取共享内存:%s\n", p);

    shmdt(p);
    shmctl(shmid, IPC_RMID, NULL); //删除共享内存
    return 0;
}

四、易错点整理(修正原图笔记疏漏)

  1. pause()返回值:原图没有写返回值,pause调用成功也返回‑1,只有被信号唤醒才返回。
  2. msgsnd/msgrcv大小参数msgsz只算消息正文,不包含long mtype,很多初学者会把整个struct msgbuf大小传入,造成bug。
  3. 共享内存size:建议4096倍数;内核会向上按内存页对齐。
  4. 共享内存本身无同步!多个进程同时读写会出现竞态,必须额外信号量/互斥锁保护。
  5. IPC对象生命周期 :System‑V IPC对象不会随进程退出自动销毁!进程结束,消息队列、共享内存还在内核残留,必须手动msgctl/shmctl IPC_RMID或者ipcrm命令删除,否则一直占用系统资源。
  6. ftok依赖文件inode:如果文件被删除重建,即使路径不变,ftok得到key会改变。

五、IPC机制简单对比

通信方式 数据载体 拷贝次数 同步机制 适用场景
信号 数字编号 无拷贝 信号处理回调 事件通知,传递简单信号,不适合大数据
消息队列 消息报文 用户态↔内核态拷贝2次 队列阻塞 不定长报文,带消息类型,中小数据量
共享内存 直接物理内存映射 0次拷贝 无自带同步 大数据高速传输,需要自己做互斥同步

管道、FIFO也属于IPC,管道同样存在两次拷贝,和消息队列类似。

小结

  1. 信号偏向事件通知,不适合传输业务数据;
  2. System‑V消息队列提供消息类型,编程简单,适合报文通信;
  3. 共享内存性能最高,但一定要自己处理多进程同步互斥;
  4. System‑V IPC对象不会进程退出自动清理,用完务必删除,避免内核资源泄漏。
复制代码
相关推荐
拂拉氏12 分钟前
【知识讲解】 Linux进程的认识与创建
linux·进程
疯狂打码的少年13 分钟前
【数据库技术】关系代数连接运算(等值连接/自然连接/外连接)
jvm·数据库·笔记·oracle
金立基包装胶水23 分钟前
水性纸袋热封胶,常见故障有多少种?
大数据·笔记·其他
维克兜率天27 分钟前
4.1.3 策略类型全景图:六大策略,你适合哪个
笔记·python·算法·量化
路弥行至30 分钟前
ROS2 通信避坑实录:从 topic/msg 底层原理到一次内存错位复盘
网络·经验分享·笔记·其他·内存·topic·ros2
Alan CGH31 分钟前
一次非寻常的压测优化经历
java·网络·压力测试
常驻客栈38 分钟前
B-深入理解计算机系统第3版之第10章:系统级 I/O
开发语言·笔记·学习笔记·常驻客栈·深入理解计算机系统
Yang961138 分钟前
一人一机一手机:鼎讯G-4000A让光缆查找告别双人配合
网络
myy-learn43 分钟前
27-进程通信
linux·服务器·网络