0807|IO进程线程day9 IPC对象概念及示例(消息队列、共享内存、信号灯集)

 0 什么是IPC机制

概念:        

        IPC机制:Inter Process Communication,即进程间通信机制。

        进程与进程间的用户空间相互独立,内核空间共享。所以如果要实现进程间的通信,需要使用进程间通信机制。

分类(3类):

  1. 传统的进程间通信机制
        无名管道  pipe
        有名管道  fifo
        信号      signal
  2. system v操作系统的IPC对象
        消息队列  message queue
        共享内存  shared memory
        信号灯集  semaphore
  3. 可用于跨主机传输的通信
        套接字  socket

一、消息队列(message queue)

1.1 消息队列的概念

1) 消息队列的原理

   消息队列是在内核中创建一个容器(队列),进程需要将数据打包成结点,添加到队尾。或者从队列中读取结点,实现进程间通信。

0807|IO进程线程day9 IPC对象概念及示例(消息队列、共享内存、信号灯集)_第1张图片

2) 消息队列的特点

  1. 消息队列是面向记录的,其中消息具有特定的格式以及优先级。
  2. 消息队列总体上是根据先进先出的原则来实现读取的,也可以选择消息的类型后,按照先进先出的原则读取。
  3. 消息队列独立于进程。等进程结束后,消息队列以及其中的内容不会消失,依然存在,除非手动删除,或者重启操作系统。

3) 查看消息队列

查看消息队列: ipcs
                          ipcs -q
              
删除消息队列:ipcrm -q  msqid

1.2 消息队列的函数

1) ftok【计算键值】

功能:

        ① 该函数通过pathname提供的id,以及proj_id提供的8bit的值,计算key值(键值),给msgget shmget semget函数使用.

        ② 只要pathname和proj_id一致,则计算的key值就一致。那么通过相同key值找到的IPC对象就是同一个。

原型:

       #include 
       #include 

       key_t ftok(const char *pathname, int proj_id);

参数:

    char *pathname:文件的路径以及名字; 该文件必须存在且可访问
    int proj_id:传入一个非0参数;

返回值:

        成功,返回计算得到的key值;

        失败,返回-1,更新errno;

2) msgget【通过key在内核内存中找到对应的消息队列,并返回队列id】

功能:通过key值到内核内存中找对应的消息队列,并返回消息队列的id--->msqid;

原型:

       #include 
       #include 
       #include 

       int msgget(key_t key, int msgflg);

参数:

    key_t keyftok函数返回出来的key值;
    int msgflg

  • IPC_CREAT:若消息队列不存在,则创建消息队列。若消息队列存在,则忽略该选项;
  • IPC_CREAT|0664:创建的同时指定消息队列的权限。
  • IPC_CREAT|IPC_EXCL:若消息队列不存在,则创建消息队列。若消息队列存在,则报错;

返回值:

        >=0, 成功返回消息队列的id号 msqid;

        =-1, 函数运行失败,更新errno;

3) msgsnd【打包数据发送到队列】

功能:将数据打包后发送到消息队列中;

原型:

       #include 
       #include 
       #include 

       int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);

参数:

    int msqid   :指定要发送到哪个消息队列中;
    void *msgp:指定要发送的消息包的首地址;

    通用格式如下:
    struct msgbuf 
    {
        long mtype;       /* message type, must be > 0 */    消息类型,必须大于0;
        char mtext[1];    /* message data */  消息内容,类型根据需求修改,想要发什么类型就填什么类型。
                                              大小与下一个参数msgsz指定的一致
    };   

    size_t msgsz:消息内容的大小,以字节为单位。   
    int msgflg

  • 0:阻塞方式发送,当消息队列满了,则当前函数阻塞;
  • IPC_NOWAIT:非阻塞方式,当消息队列满了,该函数不阻塞,且函数运行失败,errno == EAGAIN.

返回值:

        =0, 函数运行成功;

        =-1, 函数运行失败,更新errno;

练习:将数据发送至消息队列中,当类型位0时停止输入

#include 
#include 
#include 
#include 
#include 
#include 

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

int main(int argc, const char *argv[])
{
    //创建消息队列
    //创建key值
    key_t key =ftok("/home/ubuntu/IO/05_IPC/04_msg/",1);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    printf("key = %#x\n",key);

    //创建消息队列
    int msqid = msgget(key,IPC_CREAT|0664);
    if(msqid < 0)
    {
        perror("msgget");
        return -1;
    }
    printf("msqid = %d\n",msqid);

    struct msgbuf sndbuf;
    while(1)
    {
        printf("请输入消息类型 >>> ");                                        
        scanf("%ld",&sndbuf.mtype);
        getchar();
        if(0 == sndbuf.mtype)
            break;

        printf("请输入消息内容 >>> ");
        fgets(sndbuf.mtext,sizeof(sndbuf.mtext),stdin);
        sndbuf.mtext[strlen(sndbuf.mtext)-1]=0;

        //向消息队列中发送消息
        if(msgsnd(msqid,&sndbuf,sizeof(sndbuf.mtext),0) < 0)
        {
            perror("msgsnd");
            return -1;
        }
        printf("发送成功\n");
        system("ipcs -q");//让c代码执行shell命令
    }


    return 0;
}
                                                                              
                                                                              

4) msgrcv【从消息队列中读取数据】

功能:从消息队列中读取数据;

原型:

       #include 
       #include 
       #include 
       
       ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,
 int msgflg);

参数:

    int msqid   :指定要发送到哪个消息队列中;
    void *msgp:指定要发送的消息包的首地址;

    通用格式如下:
    struct msgbuf 
    {
        long mtype;       /* message type, must be > 0 */    消息类型,必须大于0;
        char mtext[1];    /* message data */  消息内容,类型根据需求修改,想要发什么类型就填什么类型。
                                              大小与下一个参数msgsz指定的一致
    };   


    size_t msgsz:消息内容的大小,以字节为单位。   
    long msgtyp :指定要读取的消息类型;

    msgtyp == 0, 读取消息队列中的第一条消息; 先进先出;
    msgtyp > 0,  指定消息类型读取,读取消息队列中第一条消息类型为 msgtyp参数指定的消息;
                 msgflg指定了MSG_EXCEPT,读取消息队列中第一条消息类型 不等于 msgtyp参数指定的消息;
                 vi -t MSG_EXCEPT;   #define MSG_EXCEPT      020000 
    msgtyp < 0,  读取消息队列中第一条最小的,且类型小于等于 msgtyp参数绝对值的消息。         

   int msgflg

        0:阻塞方式,当消息队列中没有消息了,该函数阻塞; 

        IPC_NOWAIT:非阻塞方式运行,当消息队列中没有消息了,该函数不阻塞,运行失败,errno == ENOMSG;

返回值:

        >0, 成功读取到的字节数;

        =-1, 函数运行失败,更新errno;

代码示例:

若消息队列中有消息:
    mtype 100   101  99   100  101
    mtext aaa   bbb  ccc  ddd  eee
i. msgtyp == 0
    while(1)                                                                   
    {
        //阻塞方式读取消息队列中第一条消息,先进先出的原则
        //res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 0, 0);
        
        //非阻塞方式读取消息队列中第一条消息,先进先出的原则
        res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 0, IPC_NOWAIT);
        if(res < 0)
        {
            perror("msgrcv");
            return -1;
        }
        printf("res=%ld : %ld %s\n", res, rcv.mtype, rcv.mtext);
    }
​
输出顺序:
    100 aaa   101 bbb   99 ccc   100 ddd    101 eee
ii. msgtyp > 0
    while(1)
    {
        //1.阻塞方式读取消息队列中第一条消息类型 == 101 的消息
        //res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 101, 0);
​
        //2.非阻塞方式读取消息队列中第一条消息 == 101 的消息
        //res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 101, IPC_NOWAIT);
                                                                                  
        //3.非阻塞方式读取消息队列中第一条消息类型 != 101的消息 
        res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), 101, IPC_NOWAIT|020000);
        if(res < 0)
        {
            perror("msgrcv");
            return -1;
        }
        printf("res=%ld : %ld %s\n", res, rcv.mtype, rcv.mtext);
    }
注释1,2的现象:
    101 bbb     101 eee
第3个的现象:
    100 aaa    99 ccc   100 ddd  
iii. msgtyp
    while(1)
    {
        //阻塞方式读取消息队列中<=msgtyp参数指定的消息类型中最小的那条消息;
        res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), -100, 0);
       
        //非阻塞方式读取消息队列中<=msgtyp参数指定的消息类型中最小的那条消息;
        //res = msgrcv(msqid, &rcv, sizeof(rcv.mtext), -100, IPC_NOWAIT);
​
        if(res < 0)
        {
            perror("msgrcv");
            return -1;
        }
        printf("res=%ld : %ld %s\n", res, rcv.mtype, rcv.mtext);
    }
​现象:
res=128 : 99 ccc
res=128 : 100 aaa
res=128 : 100 ddd
​

5) msgctl【控制消息队列,用于删除消息队列】

功能:控制消息队列,常用于删除消息队列;

原型:

       #include 
       #include 
       #include 

       int msgctl(int msqid, int cmd, struct msqid_ds *buf);

参数:

        ​​​​​​int msqid   :指定要控制的消息队列的id号;
        int cmd

    IPC_STAT:获取消息队列的属性,属性存储在第三个参数中;
    IPC_SET:设置消息队列属性,属性存储在第三个参数中;
    IPC_RMID:删除消息队列,第三个参数无效,填NULL即可;

返回值:

        成功,返回0;

        失败,返回-1,更新errno

常用示例:

    //删除消息队列
    if(msgctl(msqid, IPC_RMID, NULL) < 0)
    {
        perror("msgctl");
        return -1;
    }
    printf("删除消息队列成功\n");

6) 作业

1. 要求用消息队列实现AB进程对话

  • A进程先发送一句话给B进程,B进程接收后打印
  • B进程再回复一句话给A进程,A进程接收后打印
  • 重复1.2步骤,当收到quit后,要结束AB进程

2. 实现随时收发:用多进程 多线程。

二、共享内存(shared memory)

2.1 共享内存的概念

1) 共享内存的原理

        在内核内存中创建一个共享内存,共享内存可以被分别映射到不同的进程的用户空间中,每个进程在各自的用户空间中就可以操作同一个共享内存,从而可以操作同一个物理地址空间。

0807|IO进程线程day9 IPC对象概念及示例(消息队列、共享内存、信号灯集)_第2张图片2) 共享内存的特点

① 共享内存是 最高效的 进程间通信方式。

  • 进程可以在用户空间,通过指针直接访问共享内存,对共享内存进行读写,不需要任何的数据拷贝。

② 共享内存是在内核空间中被创建,可以被映射到不同的进程中。

③ 多个进程可以同事访问共享内存,因此对于共享内存的操作,需要引入进程的同步互斥机制:信号灯集

④ 共享内存独立于进程,即使进程结束,共享内存及其中的数据依然存在,除非手动删除或者重启操作系统。

⑤ 共享内存中的数据即使被读取后,依然存在,不会被删除。

3) 查看共享内存

查看共享内存:ipcs
              ipcs -m   
删除共享内存:ipcrm -m  shmid

2.2 共享内存的函数

1) ftok【计算键值】

功能:① 该函数通过pathname提供的id,以及proj_id提供的8bit的值,计算key值(键值),给msgget shmget semget函数使用.

          ② 只要pathname和proj_id一致,则计算的key值就一致。那么通过相同key值找到的IPC对象就是同一个。

原型:

       #include 
       #include 

       key_t ftok(const char *pathname, int proj_id);

参数:

    char *pathname:文件的路径以及名字; 该文件必须存在且可访问
    int proj_id:传入一个非0参数;

返回值:

        成功,返回计算得到的key值;

        失败,返回-1,更新errno;

2) shmget【通过key在内核内存中找到对应的消息队列,并返回队列id】

功能:通过key值到内核内存中找对应的共享内存,并返回共享内存的id--->msqid;

原型:

       #include 
       #include 

       int shmget(key_t key, size_t size, int shmflg);

参数:

    key_t key ftok函数返回出来的key值;

    ssize_t 指定要申请多少个字节的共享内存;
    int shmflg

  • IPC_CREAT:若共享内存不存在,则创建共享内存。若共享内存存在,则忽略该选项;
  • IPC_CREAT|0664:创建的同时指定共享内存的权限。
  • IPC_CREAT|IPC_EXCL:若共享内存不存在,则创建共享内存。若共享内存存在,则报错;

返回值:

        >=0, 成功返回共享内存的id号 shmid;

        =-1, 函数运行失败,更新errno;

3) shmat【将共享内存映射到用户空间中】

功能:将共享内存映射到用户空间中;

原型:

       #include 
       #include 

       void *shmat(int shmid, const void *shmaddr, int shmflg);

参数:

    int shmid   :指定要映射的共享内存id号;
    const void *shmaddr 

        指定共享内存要映射到用户空间的位置,填对应空间的首地址; 例如:(void*)0x10

        填NULL,代表让操作系统自动映射;
    int shmgflg

  • 0:默认方式映射,进程对共享内存可读可写;
  • SHM_RDONLY:只读,进程对共享内存只读;

返回值:

        成功,返回共享内存映射到用户空间的首地址;

        失败,返回 (void *) -1,更新errno;

注意:

        获取到的映射空间的首地址的指向不允许修改,若修改后会导致首地址找不到,导致内存泄漏,与堆空间首地址不能改变的概念一致

0807|IO进程线程day9 IPC对象概念及示例(消息队列、共享内存、信号灯集)_第3张图片

4) shmdt【断开映射】

功能:将共享内存与进程的用户空间断开映射;

           当进程不想操作共享内存的时候,就可以断开映射

原型:

       #include 
       #include 

 
       int shmdt(const void *shmaddr);

参数:

    void *shmaddr 指定要断开映射的用户空间的首地址;

返回值:

        成功,返回0;

        失败,返回-1,更新errno;

5) shmctl【控制共享内存,常用于删除共享内存】

功能:通过key值到内核内存中找对应的共享内存,并返回共享内存的id--->msqid;

原型:

       #include 
       #include 

       int shmctl(int shmid, int cmd, struct shmid_ds *buf);

参数:

   ​​​​​​int msqid   :指定要控制的消息队列的id号;
   int cmd

    IPC_STAT:获取消息队列的属性,属性存储在第三个参数中;
    IPC_SET: 设置消息队列属性,属性存储在第三个参数中;
    IPC_RMID:删除消息队列,第三个参数无效,填NULL即可;

返回值:

        成功,返回0;

        失败,返回-1,更新errno;

6) 常用示例

    //删除共享内存
    if(shmctl(shmid, IPC_RMID, NULL) < 0)
    {
        perror("shmctl");
        return -1;
    }
    printf("删除共享内存成功\n");

7) 函数使用示例

写入

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, const char *argv[])
{
    //创建key值                                             
    key_t key = ftok("./",10);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    printf("key = %#x\n",key);

    //创建共享内存,获得shmid号
    int shmid = shmget(key, 32, IPC_CREAT|0664);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }
    printf("shmid = %d\n",shmid);

    //映射共享内存到用户空间
    void* addr= shmat(shmid, NULL, 0);
    if((void*)-1 == addr)
    {
        perror("shmat");
        return -1;
    }
    printf("addr = %p\n",addr);

    //先往共享内存中存储一个int类型数据
    //再在int类型数据后面存储一个字符串
    *(int*)addr=10;
    strcat((char*)addr+4,"hello world");
    //*(char*)addr+4="hello world";


    system("ipcs -m");
    return 0;
}

读取

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main(int argc, const char *argv[])
{
    //创建key值
    key_t key = ftok("./",10);
    if(key < 0)
    {
        perror("ftok");
        return -1;
    }
    printf("key = %#x\n",key);

    //创建共享内存,获得shmid号
    int shmid = shmget(key, 32, IPC_CREAT|0664);
    if(shmid < 0)
    {
        perror("shmget");
        return -1;
    }
    printf("shmid = %d\n",shmid);

    //映射共享内存到用户空间
    void* addr= shmat(shmid, NULL, 0);
    if((void*)-1 == addr)
    {                                             
        perror("shmat");
        return -1;
    }
    printf("addr = %p\n",addr);

    //先往共享内存中存储一个int类型数据
    //再在int类型数据后面存储一个字符串
    printf("%d",*(int*)addr);
    printf("%s\n",(char*)addr+4);

    system("ipcs -m");
    return 0;
}

结果:

0807|IO进程线程day9 IPC对象概念及示例(消息队列、共享内存、信号灯集)_第4张图片

 

8) 作业

1.要求在共享内存中存入字符串 “1234567”。A进程循环打印字符串,B进程循环倒置字符串,要求结果不允许出现乱序:

        提示:共享内存中存储 flag + string.

三、信号灯集(semaphore)

3.1 信号灯集的概念

1)信号灯集的原理

2)信号灯集的核心操作

3.2 信号灯集的函数

你可能感兴趣的:(#,IPC,IO进程线程,linux,c语言,c#)