Linux编程——进程间通信(信号灯集、消息队列)

目录

  • 一、信号灯集
    • 1.1 概念
    • 1.2 信号灯集创建步骤⭐⭐⭐
    • 1.3 信号灯集对应函数
    • 1.4 练习
  • 二、消息队列
    • 2.1 特点
    • 2.2 消息队列的创建步骤
    • 2.3 函数
    • 2.4 练习

一、信号灯集

1.1 概念

信号灯(semaphore),也叫信号量。它是不同进程间或一个给定进程内部不同线程间同步的机制;System V的信号灯是一个或者多个信号灯的一个集合。其中的每一个都是单独的计数信号灯。而Posix信号灯指的是单个计数信号灯。
通过信号灯集实现共享内存的同步操作。

1.2 信号灯集创建步骤⭐⭐⭐

  1. 创建key值 ftok
  2. 创建或打开信号灯集 semget
  3. 初始化信号灯:semctl
  4. PV操作:semop
  5. 删除信号灯集 :semctl

1.3 信号灯集对应函数

  1. semget
    int semget(key_t key, int nsems, int semflg);

    • 功能:创建/打开信号灯
    • 参数:
      • key:ftok产生的key值
      • nsems:信号灯集中包含的信号灯数目
      • semflg:信号灯集的访问权限,通常为
        IPC_CREAT | IPC_EXCL |0666
    • 返回值:
      • 成功:信号灯集ID
      • 失败:-1

    例:创建或打开信号灯集 ↓
    Linux编程——进程间通信(信号灯集、消息队列)_第1张图片
    运行:
    Linux编程——进程间通信(信号灯集、消息队列)_第2张图片

    注意:由于系统原因,第一次创建信号灯集,semid为0,不能用,要排除。故第一次创建完之后要删除semid为0的信号灯集,重新创建。
    →查看系统当前信号灯集:ipcs -s
    →删除系统信号灯集:ipcrm -s semid

  2. semctl
    int semctl ( int semid, int semnum, int cmd…/*union semun arg*/);

    • 功能:信号灯集合的控制(初始化/删除)
    • 参数:
      • semid:信号灯集ID
      • semnum: 要操作的集合中的信号灯编号
      • cmd:信号灯集的控制方式
        • GETVAL:获取信号灯的值,返回值是获得值
        • SETVAL:设置信号灯的值(初始化信号灯),需要用到第四个参数:共用体
        • IPC_RMID:从系统中删除信号灯集合(如有多个信号灯,最后只删除任意一个信号灯即可将所有的信号灯删除)
    • 返回值:成功 0;失败 -1

    用法:第四个参数是一个共用体,需自己创建,共用体第一个参数就是信号灯的初值
    Linux编程——进程间通信(信号灯集、消息队列)_第3张图片
    一般,该共用体只用到第一个成员变量,信号灯集的初始化代码可如下:

union semun{
    int val;
}mysemun;
mysemun.val = 10;
semctl(semid, 0, SETVAL, mysemun);
  1. semop
    int semop (int semid, struct sembuf *opsptr, size_t nops);

    • 功能:对信号灯集合中的信号量进行PV操作
    • 参数:
      • semid:信号灯集ID
      • opsptr:操作方式,注意是一个结构体指针
      • nops: 要操作的信号灯的个数1个
    • 返回值:成功 :0;失败:-1

    struct sembuf结构体成员:

struct sembuf {
       short  sem_num; // 要操作的信号灯的编号
       short  sem_op;  
            // 0 :  等待,直到信号灯的值变成0
            // 1 :  释放资源,V操作
            //-1 :  申请资源,P操作                    
       short  sem_flg; 
            // 0(阻塞),IPC_NOWAIT, SEM_UNDO
};

例:信号灯创建整个流程

#include 
#include 
#include 
#include 
#include 

union semun //创建共用体
{
    int val;
};
int main(int argc, char const *argv[])
{
#if 1
    //1.创建key值
    key_t key = ftok(".", 'a');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key:%d\n", key);
    //2.创建或打开信号灯集
    int semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666); //创建信号灯集,包含2个信号灯
    if (semid <= 0)
    {
        if (errno == EEXIST) //存在则只打开
            semid = semget(key, 2, 0666);
        else
        {
            perror("semget err");
            return -1;
        }
    }
#endif
#if 1
    else                 //3.初始化。若信号灯集已经创建好,则不需要初始化
    {                    // 所以要放到else里面
        union semun sem; //共用体变量

        sem.val = 0;
        semctl(semid, 0, SETVAL, sem);

        sem.val = 5;
        semctl(semid, 1, SETVAL, sem);
    }
    printf("semid:%d\n", semid);
    printf("0:%d\n", semctl(semid, 0, GETVAL));
    printf("1:%d\n", semctl(semid, 1, GETVAL));
    //4.pv操作
    struct sembuf buf;
    //v
    buf.sem_num = 0; //信号灯编号
    buf.sem_op = 1;  //sem.val+1
    buf.sem_flg = 0; //阻塞:当信号的值减为0时,申请不到资源,阻塞
    semop(semid, &buf, 1); //1:1个灯
    //p
    buf.sem_num = 1; //信号灯编号
    buf.sem_op = -1; //sem.val-1
    buf.sem_flg = 0; //阻塞:当信号的值减为0时,申请不到资源,阻塞
    semop(semid, &buf, 1);

    printf("0:%d\n", semctl(semid, 0, GETVAL));
    printf("1:%d\n", semctl(semid, 1, GETVAL));
    //5.删除信号灯集
    semctl(semid, 0, IPC_RMID);
#endif
    return 0;
}
  1. 对部分信号灯集创建步骤进程简单 函数封装
    将信号灯集的初始化和PV操作进行函数的封装,目的是为了代码结构的模块化,使得代码不显得过于啰嗦,对于部分操作的代码实现只需要一次编写便可以对此调用,减少代码编写的重复工作。
#include 
#include 
#include 
#include 
#include 

union semun {
    int val;
};
void myinit(int semid,int num,int val) //3.信号灯集初始化函数
{
    union semun sem;
    sem.val = val;
    semctl(semid, num, SETVAL, sem);
}
void semop(int semid,int num,int op) //4.pv操作函数
{
    struct sembuf buf;
    buf.sem_num = num;
    buf.sem_op = op;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}
int main(int argc, char const *argv[])
{
    //创建key值
    key_t key = ftok(".", 'a');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }
    printf("key:%d\n", key);
    //2.创建或打开信号灯集
    int semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
    if (semid <= 0)
    {
        if (errno == EEXIST)
            semid = semget(key, 2, 0666);
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else
    {//3.初始化
        myinit(semid,0,0);
        myinit(semid,1,5);
    }
    printf("semid:%d\n", semid);
    //4.pv操作
    semop(semid,0,1);
    semop(semid,1,-1);

    printf("0:%d\n", semctl(semid, 0, GETVAL));
    printf("1:%d\n", semctl(semid, 1, GETVAL));
    //5.删除信号灯集
    semctl(semid, 0, IPC_RMID);
    return 0;
}

1.4 练习

两个进程实现通信(分文件编写),一个进程循环从终端输入,另一个进程循环打印,输入一次打印一次,当输入quit时结束。封装函数,使用用共享内存加信号灯集实现。

/***input.c***/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

union semun {
    int val;
};
void myinit(int semid,int num,int val)
{
    //3.初始化
    union semun sem;

    sem.val = val;
    semctl(semid, num, SETVAL, sem);
}
void sem_op(int semid,int num,int op)
{//4.pv操作
    struct sembuf buf;
    buf.sem_num = num;
    buf.sem_op = op;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}
int main(int argc, char const *argv[])
{
    //创建key值
    key_t key;
    key = ftok(".", 'a');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }

    //创建或打开共享内存
    int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid <= 0) //=0不使用
    {
        //若已经创建,给shmid重新赋值
        if (errno == 17)
            shmid = shmget(key, 128, 0666);
        else
        {
            perror("shmget err");
            return -1;
        }
    }
    //映射
    char *p = shmat(shmid, NULL, 0);
    if (p == (void *)-1)
    {
        perror("shmat err");
        return -1;
    }

    //2.创建或打开信号灯集
    int semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
    if (semid <= 0)
    {
        if (errno == EEXIST)
            semid = semget(key, 2, 0666);
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else
    {//3.初始化
        myinit(semid,0,0);
    }
    printf("semid:%d\n", semid);  

    //写操作
    while (1)
    {
        fgets(p, 32, stdin); //将终端输入放到共享内存
        sem_op(semid,0,1); //v
        printf("0:%d\n", semctl(semid, 0, GETVAL));
        if (strcmp(p, "quit\n") == 0)
        {
            break;
        }
    }
    //5.删除信号灯集
    semctl(semid, 0, IPC_RMID);
    return 0;
}
/***output.c***/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

union semun {
    int val;
};
void myinit(int semid,int num,int val)
{
    //3.初始化
    union semun sem;

    sem.val = val;
    semctl(semid, num, SETVAL, sem);
}
void sem_op(int semid,int num,int op)
{//4.pv操作
    struct sembuf buf;
    buf.sem_num = num;
    buf.sem_op = op;
    buf.sem_flg = 0;
    semop(semid, &buf, 1);
}
int main(int argc, char const *argv[])
{
    //创建key值
    key_t key;
    key = ftok(".", 'a');
    if (key < 0)
    {
        perror("ftok err");
        return -1;
    }

    //创建或打开共享内存
    int shmid = shmget(key, 128, IPC_CREAT | IPC_EXCL | 0666);
    if (shmid <= 0) //=0不使用
    {
        //若已经创建,给shmid重新赋值
        if (errno == 17)
            shmid = shmget(key, 128, 0666);
        else
        {
            perror("shmget err");
            return -1;
        }
    }
    //映射
    char *p = shmat(shmid, NULL, 0);
    if (p == (void *)-1)
    {
        perror("shmat err");
        return -1;
    }
    //2.创建或打开信号灯集
    int semid = semget(key, 2, IPC_CREAT | IPC_EXCL | 0666);
    if (semid <= 0)
    {
        if (errno == EEXIST)
            semid = semget(key, 2, 0666);
        else
        {
            perror("semget err");
            return -1;
        }
    }
    else
    // {//3.初始化
    //     myinit(semid,0,0);
    // }
    printf("semid:%d\n", semid);  


    while (1)
    {
        printf("0:%d\n", semctl(semid, 0, GETVAL));
        sem_op(semid,0,-1);//p
        if (strcmp(p, "quit\n") == 0)
        {
            break;
        }
        fputs(p,stdout); //共享内存内容输出到终端
        //printf("0:%d\n", semctl(semid, 0, GETVAL));
    }
    //5.删除信号灯集
    semctl(semid, 0, IPC_RMID);
    return 0;
}

二、消息队列

2.1 特点

  • 消息队列是IPC对象的一种
  • 消息队列由消息队列ID来唯一标识
  • 消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。
  • 消息队列可以按照类型来发送(添加)/接收(读取)消息

2.2 消息队列的创建步骤

  1. 创建key值 ftok
  2. 创建或打开消息队列 msgget
  3. 添加消息:按照类型将消息添加到已打开的消息队列的末尾 msgsnd
  4. 读取消息:可以按照类型将消息从消息队列中读走 msgrcv
  5. 删除消息队列 msgctl

2.3 函数

  1. msgget
    int msgget(key_t key, int flag);

    • 功能:创建或打开一个消息队列
    • 参数:
      • key值
      • flag:创建消息队列的权限IPC_CREAT|IPC_EXCL|0666
    • 返回值:成功:msgid;失败:-1
  2. msgsnd
    int msgsnd(int msqid, const void *msgp, size_t size, int flag);

    • 功能:往消息队列中添加消息
    • 参数:
      • msqid:消息队列的ID
      • msgp:指向消息的指针,指向一个消息结构体(需自己创建)
        Linux编程——进程间通信(信号灯集、消息队列)_第4张图片
        →常用消息结构msgbuf如下:
        struct msgbuf{
        long mtype; //消息类型
        char mtext[N] //消息正文
        };
      • size:发送的消息正文的字节数(即除了结构体消息类型的剩余大小)。
      • flag:
        • IPC_NOWAIT消息没有发送完成函数也会立即返回
        • 0:直到发送完成函数才返回
    • 返回值:成功:0;失败:-1
      使用:msgsnd(msgid, &msg,sizeof(msg)-sizeof(long), 0)
      注意:消息结构除了第一个成员必须为long类型外,其他成员可以根据应用的需求自行定义。
  3. msgrcv
    int msgrcv(int msgid, void* msgp, size_t size, long msgtype, int flag);

    • 功能:读取消息
    • 参数:
      • msgid:消息队列的ID
      • msgp:存放读取消息的空间
      • size:接受的消息正文的字节数
      • msgtype:
        • 0:接收消息队列中第一个消息
        • >0:接收消息队列中第一个类型为msgtyp的消息.
        • <0:接收消息队列中类型值不小于msgtyp的绝对值且类型值又最小的消息。
      • flag:
        • 0:若无消息函数会一直阻塞
        • IPC_NOWAIT:若没有消息,进程会立即返回ENOMSG
    • 返回值:成功:接收到的消息的长度;失败:-1
  4. msgctl
    int msgctl ( int msgqid, int cmd, struct msqid_ds *buf );

    • 功能:对消息队列的操作,删除消息队列
    • 参数:
      • msqid:消息队列的队列ID
      • cmd:
        • IPC_STAT:读取消息队列的属性,并将其保存在buf指向的缓冲区中。
        • IPC_SET:设置消息队列的属性。这个值取自 buf参数。
        • IPC_RMID:从系统中删除消息队列。
        • buf:消息队列缓冲区
    • 返回值:成功:0;失败:-1

    用法:要删除消息队列就用→ msgctl(msgid, IPC_RMID, NULL)

2.4 练习

  1. 单个消息读取
    Linux编程——进程间通信(信号灯集、消息队列)_第5张图片
    Linux编程——进程间通信(信号灯集、消息队列)_第6张图片
    若将msgrcv的倒数第二个参数msgtype改为0,也可以打印出类型1的消息,原因是msgrcv中msgtype为0时,可以匹配任意类型消息,且默认读取消息队列中第一个消息(更改后的代码如下图所示)。
    Linux编程——进程间通信(信号灯集、消息队列)_第7张图片

  2. 多个消息读取

    即,添加多个消息类型,进程按类别发送,各取所需。

    需要修改的代码如下:
    Linux编程——进程间通信(信号灯集、消息队列)_第8张图片
    Linux编程——进程间通信(信号灯集、消息队列)_第9张图片
    运行结果:
    Linux编程——进程间通信(信号灯集、消息队列)_第10张图片
    从运行结果可以看出,发出的类型1和类型2消息已被接收,由于程序中并没有发送类型3消息,所以在接收3类型消息时程序会发生阻塞。

当然,上面代码示例是在一个进程中实现的消息接收发送的功能,只是为了函数的简单使用,最重要的韩还是进程间的通信。

消息队列不同于管道,通信的两个进程可以是完全无关的进程,它们之间不需要约定同步的方法。只要消息队列存在并且有存放消息的空间,发送进程就可以向队列中存放消息,并且可以在接收进程开始之前终止其执行。但使用管道通信的进程,不论是匿名管道还是有名管道,通信的两个进程都必须是正在运行的进程。这一点正是消息队列的优点。

你可能感兴趣的:(#,c语言,linux,笔记,嵌入式开发,进程间通信)