【Linux 物联网网关主控系统-Linux主控部分(三)】

Linux 物联网网关主控系统-Linux主控部分(三)

一、进程间通信

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,二者配合使用:

  1. ipcs:查看系统中已存在的 IPC 对象
    通用使用:直接输入ipcs,查看所有类型 IPC 对象(共享内存、消息队列、信号灯);
    精准查看:ipcs -m(共享内存)、ipcs -s(信号灯)、ipcs -q(消息队列)。
  2. 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: 可用于任意进程间,双工,有文件名,数据在内存

相关推荐
萝卜白菜。4 小时前
TongWeb7.0 集中管理heimdall配置文件说明
linux·运维·服务器
北京阿尔泰科技厂家4 小时前
CAN通讯+8路高速模拟量采集:阿尔泰科技DAM-C3054P工业数据采集可靠之选
物联网·can通讯·模拟量采集·工业自动化·仪器仪表·采集模块·传感器信号采集
IMPYLH4 小时前
Linux 的 install 命令
linux·运维·服务器·bash
Zero_Era5 小时前
物联网加密芯片LKT4305GM
物联网·5g
浦信仿真大讲堂5 小时前
CST FAQ 006:Linux系统CST安装指导
linux·运维·服务器·仿真软件·达索软件
zhaoshuzhaoshu5 小时前
蓝牙 ACL 与 SCO 链路联系与详细区别对比
网络·物联网·蓝牙·无线
北京耐用通信5 小时前
耐达讯自动化CC-Link IE转EtherCAT网关:让工业自动化更简单
人工智能·科技·物联网·自动化·信息与通信
AI+程序员在路上5 小时前
Linux C 条件变量阻塞线程用法:等待时CPU占用率为0
linux·运维·c语言
北京耐用通信5 小时前
架桥记:耐达讯自动化CC-Link IE转EtherCAT的工业协议融合实战
人工智能·科技·物联网·网络协议·自动化