标签:#Linux #进程间通信 #信号 #消息队列 #共享内存 #System‑V‑IPC
摘要:本篇梳理Linux下信号基础API、System‑V消息队列、System‑V共享内存核心接口,包含函数详解、原理剖析、可运行示例代码,
前言
进程间通信(IPC)是Linux后端开发的核心知识点。不同进程拥有独立虚拟地址空间,无法直接访问对方内存,必须借助操作系统提供的机制完成数据传递与事件通知。
本文涵盖三类经典IPC机制:
- 信号:异步事件通知,传递数字信号,不适合大批量数据传输
- System‑V消息队列:带消息类型的报文式通信
- 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消息类型,接收方可以按类型筛选消息。优点:支持按消息类型收发;缺点:内核空间拷贝,数据量大时效率不如共享内存。
整体流程
ftok():生成IPC唯一key值msgget():创建/获取消息队列IDmsgsnd():发送消息msgrcv():接收消息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 | 0664IPC_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:消息队列IDmsgp:指向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:消息队列IDmsgp:存放接收消息的缓冲区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常用选项:
IPC_STAT:获取消息队列状态,存入bufIPC_SET:设置消息队列属性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.txt,touch test.txt;分别编译运行发送、接收程序。
三、System‑V 共享内存
共享内存:Linux下效率最高的IPC方式 。原理:内核开辟一块物理内存,多个进程把这块物理内存映射到各自用户虚拟地址空间。进程读写自己虚拟地址,直接操作同一块物理内存,没有内核态、用户态之间数据拷贝 。
⚠️风险:共享内存本身不带同步互斥机制,多进程并发读写需要自己加信号量、互斥锁做保护。
流程
ftok()生成keyshmget()创建/获取共享内存IDshmat()将共享内存映射到进程用户空间- 直接读写返回的虚拟地址
shmdt()解除映射shmctl()删除共享内存
3.1 shmget() 创建共享内存
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
参数:
key:IPC keysize:共享内存字节大小,建议PAGE_SIZE(4096)整数倍;内核会向上对齐页大小shmflg:IPC_CREAT | 0664
返回:成功返回shmid;失败‑1。
3.2 shmat() 挂载映射共享内存
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
shmid:共享内存idshmaddr:指定映射到用户空间地址,填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;
}
四、易错点整理(修正原图笔记疏漏)
pause()返回值:原图没有写返回值,pause调用成功也返回‑1,只有被信号唤醒才返回。- msgsnd/msgrcv大小参数 :
msgsz只算消息正文,不包含long mtype,很多初学者会把整个struct msgbuf大小传入,造成bug。 - 共享内存size:建议4096倍数;内核会向上按内存页对齐。
- 共享内存本身无同步!多个进程同时读写会出现竞态,必须额外信号量/互斥锁保护。
- IPC对象生命周期 :System‑V IPC对象不会随进程退出自动销毁!进程结束,消息队列、共享内存还在内核残留,必须手动
msgctl/shmctl IPC_RMID或者ipcrm命令删除,否则一直占用系统资源。 ftok依赖文件inode:如果文件被删除重建,即使路径不变,ftok得到key会改变。
五、IPC机制简单对比
| 通信方式 | 数据载体 | 拷贝次数 | 同步机制 | 适用场景 |
|---|---|---|---|---|
| 信号 | 数字编号 | 无拷贝 | 信号处理回调 | 事件通知,传递简单信号,不适合大数据 |
| 消息队列 | 消息报文 | 用户态↔内核态拷贝2次 | 队列阻塞 | 不定长报文,带消息类型,中小数据量 |
| 共享内存 | 直接物理内存映射 | 0次拷贝 | 无自带同步 | 大数据高速传输,需要自己做互斥同步 |
管道、FIFO也属于IPC,管道同样存在两次拷贝,和消息队列类似。
小结
- 信号偏向事件通知,不适合传输业务数据;
- System‑V消息队列提供消息类型,编程简单,适合报文通信;
- 共享内存性能最高,但一定要自己处理多进程同步互斥;
- System‑V IPC对象不会进程退出自动清理,用完务必删除,避免内核资源泄漏。