Linux 物联网网关主控系统-Linux主控部分(三)
- 一、进程间通信
-
- 1.历史和分类
- [2.IPC 对象核心通用特性(System V IPC 专属)](#2.IPC 对象核心通用特性(System V IPC 专属))
- [3.IPC 对象的系统操作工具](#3.IPC 对象的系统操作工具)
- 二、共享内存
- 三、消息队列
- 四、信号灯(信号量)
- 五、信号
- 六、总结(进程间通讯方式比较)
一、进程间通信
1.历史和分类
Linux 的进程间通信机制源自 UNIX 两大经典派系的改进与扩充,是对原有机制的全面继承:
早期 UNIX:仅支持管道、有名管道、信号三种基础通信方式,功能简单,适用场景有限;
AT&T 贝尔实验室:基于早期方式改进,形成System V IPC,特点是通信进程仅限单个计算机内,核心包含共享内存、消息队列、信号灯;
BSD(加州大学伯克利分校):创新实现基于套接字(socket)的通信机制,支持计算机之间的跨主机进程通信,突破了单主机限制。
Linux 整合了上述所有 UNIX 通信方式,按特性和溯源分为三大类,也是实际开发中最常用的方式,其中System V IPC是本项目的核心使用类型:

2.IPC 对象核心通用特性(System V IPC 专属)
共享内存、消息队列、信号灯均为IPC(Inter-Process Communication)对象,拥有统一的核心操作逻辑和特性,是区别于传统 IPC 的关键:
1.标识体系:通过key_t类型的key 值唯一关联 IPC 对象,key 值可由ftok()函数生成(基于路径 + 字符),也可使用IPC_PRIVATE(私有键);
2.操作流程:所有 IPC 对象遵循统一三步操作------ 通过 key 值打开 / 创建 IPC 通道→通过 IPC 对象 ID访问 / 操作通道→通过控制函数管理 / 删除 IPC 对象;
3.生命周期:由内核维护,创建后若不手动删除,会一直保留在系统中,直至系统重启,因此使用后必须手动释放,避免资源泄漏。
✅ 只有共享内存、消息队列、信号灯 是 System V IPC 对象,有这 3 个专属共性;
❌ 管道、有名管道、信号、socket 都不是,没有这些共性,各玩各的。
其他 IPC 方式的核心区别:
没统一的 key/ID,用文件描述符、路径等方式访问;
进程退出 / 关闭句柄,内核会自动回收资源,不用手动删;
各自操作逻辑完全不同,没有统一规范。
3.IPC 对象的系统操作工具
Linux 提供专用命令,用于查看和删除系统中的 IPC 对象,是开发调试、资源释放的必备工具,核心为ipcs和ipcrm,二者配合使用:
- ipcs:查看系统中已存在的 IPC 对象
通用使用:直接输入ipcs,查看所有类型 IPC 对象(共享内存、消息队列、信号灯);
精准查看:ipcs -m(共享内存)、ipcs -s(信号灯)、ipcs -q(消息队列)。 - ipcrm:删除系统中指定的 IPC 对象
核心用法:ipcrm + 类型参数 + IPC对象ID,如ipcrm -m 共享内存ID、ipcrm -q 消息队列ID、ipcrm -s 信号灯ID;
核心作用:释放 IPC 对象占用的系统资源,解决 "IPC 对象永久驻留内核" 的问题。
二、共享内存
1.核心特点(最核心的两个点)
1.效率最高:内核预留一块公共内存区,进程直接将其映射到自己的地址空间,读写无需数据拷贝,直接操作内存,速度远快于其他 IPC;
2.需同步配合:多进程可同时访问这块内存,无天然的互斥 / 同步机制,必须搭配信号灯(信号量) 使用,防止数据读写冲突。
2.项目实际应用场景
用于zigbee 网络数据的跨进程共享:
1.串口采集线程将 zigbee 的网络环境数据写入共享内存;
2.web/APP 对应的 boa 进程等其他进程,定时从共享内存读取数据;
3.读写规则:读数据不清除原有内容,写数据直接覆盖更新,保证数据实时性。
3.共享内存四步使用流程(固定步骤,所有场景通用)
1.创建 / 打开:通过 key 值创建或打开一块共享内存,得到唯一的内存 ID;
2.映射:将内核的共享内存,映射到当前进程的私有地址空间,进程才能直接读写;
3.撤销映射:进程使用完后,解除自身地址空间与共享内存的关联;
4.删除:手动删除内核中的共享内存对象,释放系统资源(核心,不删则永久驻留)。

对应函数
核心函数 1:shmget(创建 / 打开共享内存)

核心函数 2:shmat(映射共享内存到进程地址空间)

核心函数 3:shmdt(撤销共享内存映射)

核心函数 4:shmctl(控制 / 删除共享内存)

4.示例代码
✅ 正确顺序(共享内存 / 消息队列 完全一样)
先运行 server(创建共享内存 / 消息队列)
再运行 client(访问已创建的对象)
shm-client.c 完整代码
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <time.h>
#define SEG_SIZE 1024 // 共享内存段大小,示例中未显式定义,补充完整
int main()
{
key_t key_info;
char *mem_ptr;
int seg_id;
// 生成唯一键值
if ((key_info = ftok("/app", 'i')) < 0)
{
perror("ftok info");
exit(-1);
}
// 打开已存在的共享内存段
seg_id = shmget(key_info, SEG_SIZE, 0777);
if (seg_id == -1) {
perror("shmget client");
exit(-1);
}
// 映射共享内存到进程地址空间
mem_ptr = shmat(seg_id, NULL, 0);
if (mem_ptr == (void *)-1) {
perror("shmat client");
exit(-1);
}
// 读取共享内存中的数据并打印
printf("The time, direct from memory: ..%s", mem_ptr);
// 撤销共享内存映射
shmdt(mem_ptr);
return 0;
}
shm-server.c 完整代码
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <time.h>
#define SEG_SIZE 1024 // 共享内存段大小,示例中未显式定义,补充完整
int main()
{
long now;
int n;
key_t key_info;
char *mem_ptr;
int seg_id;
// 生成唯一键值
if ((key_info = ftok("/app", 'i')) < 0)
{
perror("ftok info");
exit(-1);
}
// 创建共享内存段(不存在则创建,权限0777)
seg_id = shmget(key_info, SEG_SIZE, IPC_CREAT | 0777);
if (seg_id == -1) {
perror("shmget server");
exit(-1);
}
// 映射共享内存到进程地址空间
mem_ptr = shmat(seg_id, NULL, 0);
if (mem_ptr == (void *)-1) {
perror("shmat server");
exit(-1);
}
// 循环63次,每秒向共享内存写入当前时间
for (n = 0; n < 63; n++)
{
time(&now); // 获取当前系统时间
strcpy(mem_ptr, ctime(&now));// 将时间字符串写入共享内存
sleep(1); // 等待1秒
}
// 删除共享内存段
shmctl(seg_id, IPC_RMID, NULL);
return 0;
}
三、消息队列
1、核心定位
消息队列是 Linux 内核维护的链表式消息队列,属于 System V IPC 的一种,和共享内存、信号量并列。
- 核心特点:异步通信,发送方把消息放进队列就可以走,接收方按需从队列取消息,不需要双方同时在线。
- 核心逻辑:和你刚才的共享内存示例一样,也是服务端 + 客户端的配对模式,通过ftok生成的唯一 key 访问同一个队列。
消息队列在该项目中使用
从web页面或者APP下发的命令,最终要通过串口发送给zigbee网络的终端节点,以控制各种外设,需要考虑以下问题:
所有的命令不能丢失(连续点击页面按钮)
命令需要按照一定顺序排列
zigbee 网络可能延时相比较有点大,不能发送太快
2.消息队列函数调用流程

发送端(msgsnd 侧)
ftok:生成唯一键值
msgget:创建 / 打开消息队列
msgsnd:按自定义消息结构体(含type类型),将消息写入内核队列
msgctl(IPC_RMID):删除消息队列(收尾)
接收端(msgrcv 侧)
ftok:用相同参数生成同一键值
msgget:打开已存在的消息队列
msgrcv:按指定type(如type=1),从队列中筛选对应类型的消息读取
msgctl(IPC_RMID):删除消息队列(收尾)
3.核心函数
| 函数名 | 函数原型 | 核心功能 | 重点参数 | 返回值 |
|---|---|---|---|---|
| msgget | int msgget(key_t key, int msgflg); | 创建/打开消息队列 | key:键值;msgflg:创建标志+权限 | 成功:msqid,失败:-1 |
| msgsnd | int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg); | 发送消息 | msqid、消息结构体、数据长度 | 成功:0,失败:-1 |
| msgrcv | ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg); | 接收消息 | msgtyp:消息类型 | 成功:长度,失败:-1 |
| msgctl | int msgctl(int msqid, int cmd, struct msqid_ds *buf); | 控制/删除 | cmd=IPC_RMID 删除队列 | 成功:0,失败:-1 |
4.示例代码
msg-server.c
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#include <time.h>
#define MSG_SIZE 1024 // 消息内容最大长度
// 自定义消息结构体,必须以long mtype开头
struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
};
int main()
{
key_t key_info;
int msq_id;
struct msgbuf msg;
// 1. 生成唯一键值(和共享内存完全一致)
if ((key_info = ftok("/app", 'i')) < 0)
{
perror("ftok info");
exit(-1);
}
// 2. 创建消息队列(对应shmget,加IPC_CREAT)
msq_id = msgget(key_info, IPC_CREAT | 0777);
if (msq_id == -1) {
perror("msgget server");
exit(-1);
}
// 3. 循环发送63条时间消息(对应共享内存server的写操作)
for (int n = 0; n < 63; n++)
{
time_t now = time(NULL);
msg.mtype = 1; // 消息类型设为1,客户端按类型1读取
strcpy(msg.mtext, ctime(&now)); // 把时间写入消息内容
// 发送消息到队列
if (msgsnd(msq_id, &msg, strlen(msg.mtext), 0) == -1) {
perror("msgsnd");
exit(-1);
}
sleep(1); // 每秒发一条
}
// 4. 删除消息队列(对应shmctl IPC_RMID)
msgctl(msq_id, IPC_RMID, NULL);
return 0;
}
msg-client.c
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#define MSG_SIZE 1024
// 必须和服务端完全一致的消息结构体
struct msgbuf {
long mtype;
char mtext[MSG_SIZE];
};
int main()
{
key_t key_info;
int msq_id;
struct msgbuf msg;
// 1. 生成相同的键值,找到同一个队列
if ((key_info = ftok("/app", 'i')) < 0)
{
perror("ftok info");
exit(-1);
}
// 2. 打开已存在的消息队列(不加IPC_CREAT,对应共享内存client的shmget)
msq_id = msgget(key_info, 0777);
if (msq_id == -1) {
perror("msgget client");
exit(-1);
}
// 3. 读取消息(对应共享内存的printf读操作)
// msgtyp=1:只读取类型为1的消息,和服务端对应
if (msgrcv(msq_id, &msg, MSG_SIZE, 1, 0) == -1) {
perror("msgrcv");
exit(-1);
}
printf("The time, from message queue: %s", msg.mtext);
// 消息队列不需要"解绑"操作,读取后直接退出即可
return 0;
}
四、信号灯(信号量)
1.核心概念

本项目中的作用
共享内存需要互斥访问
有进程在访问时,其他进程不能访问
信号量初始值为 1
2.核心函数
| 函数名 | 函数原型 | 核心功能 | 重点参数说明 | 返回值 |
|---|---|---|---|---|
| semget | int semget(key_t key, int nsems, int semflg); | 创建 / 打开信号灯集 | key:ftok 生成键值;nsems:信号灯集内信号灯数目(PPT 中设 1);semflg:IPC_CREAT|0666(权限) | 成功:信号灯集 ID(semid)失败:-1 |
| semctl | int semctl(int semid, int semnum, int cmd, .../union semun arg/); | 控制信号灯(初始化 / 获取值 / 删除) | semid:信号灯集 ID;semnum:信号灯编号(从 0 开始);cmd:SETVAL(设值)/GETVAL(取值)/IPC_RMID(删除集);arg:union semun 类型(初始化值用) | 成功:0(SETVAL/IPC_RMID)/ 信号灯值(GETVAL)失败:-1 |
| semop | int semop(int semid, struct sembuf *opsptr, size_t nops); | 执行 PV 操作(核心) | semid:信号灯集 ID;opsptr:struct sembuf 结构体指针(定义 P/V 操作);nops:操作的信号灯个数(PPT 中设 1) | 成功:0失败:-1 |
3.示例代码
头文件 sem_comm.h(PV 操作封装)
c
#ifndef SEM_COMM_H
#define SEM_COMM_H
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <unistd.h>
#define SEM_INIT_VAL 1 // PPT:信号量初始值=1
// 信号量必须的联合体
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
// 初始化信号量
int init_sem(int semid, int semnum, int val) {
union semun un;
un.val = val;
semctl(semid, semnum, SETVAL, un);
return 0;
}
// P操作:-1,加锁
int sem_p(int semid) {
struct sembuf buf = {0, -1, SEM_UNDO};
semop(semid, &buf, 1);
return 0;
}
// V操作:+1,解锁
int sem_v(int semid) {
struct sembuf buf = {0, 1, SEM_UNDO};
semop(semid, &buf, 1);
return 0;
}
// 删除信号量
int del_sem(int semid) {
semctl(semid, 0, IPC_RMID);
return 0;
}
#endif
服务端 sem_server.c(创建 + 写共享内存)
c
#include "sem_comm.h"
#define SHM_SIZE 1024
int main() {
key_t key = ftok(".", 's');
int semid = semget(key, 1, IPC_CREAT | 0666);
int shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666);
char *shmaddr = shmat(shmid, NULL, 0);
init_sem(semid, 0, SEM_INIT_VAL);
// 互斥访问共享内存
sem_p(semid);
printf("服务端:正在写入共享内存...\n");
sprintf(shmaddr, "IPC TEST BY SERVER");
sleep(3);
printf("服务端:写入完成\n");
sem_v(semid);
sleep(5);
shmdt(shmaddr);
shmctl(shmid, IPC_RMID, NULL);
del_sem(semid);
return 0;
}
客户端 sem_client.c(读取共享内存)
c
#include "sem_comm.h"
#define SHM_SIZE 1024
int main() {
key_t key = ftok(".", 's');
int semid = semget(key, 1, 0666);
int shmid = shmget(key, SHM_SIZE, 0666);
char *shmaddr = shmat(shmid, NULL, 0);
// 互斥读取
sem_p(semid);
printf("客户端:读取数据:%s\n", shmaddr);
sem_v(semid);
shmdt(shmaddr);
return 0;
}
五、信号
1.核心概念
信号是软件中断,用于进程间通知、异常处理
简单、携带信息量少,不能传输大量数据
每个信号有 默认处理方式:忽略、终止、暂停、继续
进程可以 自定义信号处理函数(捕捉信号)
当我们按下 ctrl+c 终止主控程序时,希望能够实现如下操作:
进程退出
释放所有申请的资源
互斥锁、条件变量、消息队列、共享内存、信号量、线程、设备文件描述符(摄像头、串口)
2.信号核心函数总表
| 函数名 | 函数原型 | 核心功能 | 重点参数说明 | 返回值 |
|---|---|---|---|---|
| signal | sighandler_t signal(int signum, sighandler_t handler); | 注册信号处理方式 | signum:信号编号;handler:SIG_IGN(忽略)/SIG_DFL(默认)/ 自定义函数 | 成功:旧处理函数地址;失败:SIG_ERR |
| kill | int kill(pid_t pid, int sig); | 向指定进程发送信号 | pid:目标进程 ID;sig:要发送的信号 | 成功:0;失败:-1 |
| raise | int raise(int sig); | 向自己发送信号 | sig:信号编号 | 成功:0;失败:-1 |
| pause | int pause(void); | 进程阻塞,等待信号 | 无参数 | 永远返回 -1 |
| sleep | unsigned int sleep(unsigned int seconds); | 休眠指定秒数,被信号唤醒会提前返回 | seconds:秒数 | 剩余未休眠秒数 |
3.常用信号
SIGINT 2 终端中断(Ctrl+C)
SIGQUIT 3 退出(Ctrl+\)
SIGKILL 9 强制杀死进程(不能捕捉、不能忽略)
SIGUSR1 10 用户自定义信号 1
SIGUSR2 12 用户自定义信号 2
SIGALRM 14 闹钟信号
SIGCHLD 17 子进程退出时发给父进程
4.示例代码
signal 程序负责等着收信号(被 Ctrl+C 或 kill 通知)
kill 程序负责主动发信号给别人
signal 标准示例代码
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
// 自定义信号处理函数
void handle(int sig)
{
printf("收到信号:%d,我不会退出!\n", sig);
}
int main()
{
// 注册 SIGINT(Ctrl+C) 的处理方式
signal(SIGINT, handle);
printf("等待信号...按 Ctrl+C 测试\n");
while(1)
{
pause(); // 等待信号
}
return 0;
}
kill 发送信号示例代码
c
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if(argc != 3)
{
printf("用法:%s <PID> <信号编号>\n", argv[0]);
return -1;
}
int pid = atoi(argv[1]);
int sig = atoi(argv[2]);
kill(pid, sig);
printf("已发送信号 %d 给进程 %d\n", sig, pid);
return 0;
}
编译运行
bash
gcc signal.c -o signal
./signal
# 另开终端
ps -ef | grep signal
kill -2 进程号
六、总结(进程间通讯方式比较)
signal: 唯一的异步通信方式
msg: 常用于 cs 模式中,按消息类型访问,可有优先级
shm: 效率最高 (直接访问内存),需要同步、互斥机制
sem: 配合共享内存使用,用以实现同步和互斥
pipe: 具有亲缘关系的进程间,单工,数据在内存中
fifo: 可用于任意进程间,双工,有文件名,数据在内存