Linux高级系统编程- 消息队列 与 内存共享

消息队列

消息队列是消息的链表,存放在内存中,由内核维护
特点:
        1、消息队列中的消息是有类型的。
        2、消息队列中的消息是有格式的。
        3、消息队列可以实现消息的随机查询。消息不一定要以先进先出的次序读取,编程时 可以按消息的类型读取。
        4、消息队列允许一个或多个进程向它写入或者读取消息。
        5、与无名管道、命名管道一样,从消息队列中读出消息,消息队列中对应的数据都会被删除。
        6、每个消息队列都有消息队列标识符,消息队列的标识符在整个系统中是唯一的。
        7、只有内核重启或人工删除消息队列时,该消息队列才会被删除。若不人工删除消息队列,消息队列会一直存在于系统中
注意 :
        在 ubuntu 某些版本中消息队列限制值如下 :
        每个消息内容最多为8K 字节
        每个消息队列容量最多为16K 字节
        系统中消息队列个数最多为1609
        系统中消息个数最多为16384

消息队列操作命令

ipcs -q 查看消息队列
ipcrm -q msgid 删除消息队列 id msgid 的消息队列

获取Key

System V 提供的进程间通讯机制 (IPC 通信机制 ) 需要一个 key
key 值可以是人为指定的,也可以通过 ftok 函数获得。

ftok函数

所需头文件 :
        #include
        #include
函数 :
        key_t ftok(const char *pathname, int proj_id);
参数:
        pathname:路径名 , 这个路径必须是存在的而且可以访问的
        //~
        ///
        //./
        proj_id:项目 ID ,非 0 整数 ( 只有低 8 位有效 )
返回值:
        成功返回 key
        失败返回 -1
注意 :
        当调用该函数时传入的参数一致获取到的key 值也将相同
        多进程通讯时要保证路径名相同,id 也相同
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    key_t key01 = ftok("./",2023);
    key_t key02 = ftok("./",2023);
    key_t key03 = ftok("./",2022);
    printf("key1=%u\n",key01);
    printf("key2=%u\n",key02);
    printf("key3=%u\n",key03);
    return 0;
}

创建或获取消息队列--msgget函数

        创建一个新的或打开一个已经存在的消息队列。不同的进程调用此函数,只要用相同的 key 值就能得到同一个消息队列的标识符。
所需头文件
        #include
函数
        int msgget(key_t key, int msgflg);
参数:
        key: IPC 键值。
        msgflg:标识函数的行为及消息队列的权限。
        msgflg 的取值:
                IPC_CREAT:创建消息队列。
                IPC_EXCL:检测消息队列是否存在。
        位或权限位:消息队列位或权限位后可以设置消息队列的访问权限,格式和 open 函数的 mode_t 一样,但可执行权限未使用。
返回值:
        成功:消息队列的标识符(msgid)
        失败:返回-1
#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    //获取key值
    //./表示当前路径
    key_t key = ftok("./",2023);
    printf("key=%u\n",key);
    //创建消息队列,权限为可读可写
    //返回值为消息队列id
    int msgid = msgget(key,IPC_CREAT | 0666);
    printf("msgid=%d\n",msgid);
    //判断消息队列是否存在
    key_t key02 = ftok("/",2022);
    int msgid02 = msgget(key02,IPC_EXCL);
    printf("msgid02=%d\n",msgid02);
    return 0;
}

消息的格式

typedef struct _msg
{
        long mtype ; /* 消息类型 , 必须是第一个成员 , 必须是 long , 就是该消息的 id*/
        char mtext [ 100 ]; /* 消息正文 , 用户自定义 */
        ... /* 消息的正文可以有多个成员 */
} MSG ;

发送消息--msgsnd函数

作用 : 将新消息添加到消息队列。
函数 :
所需头文件 :
        #include
函数 :
        int msgsnd(int msqid, const void *msgp,size_t msgsz, int msgflg);
参数 :
        msqid:消息队列的标识符。
        msgp:待发送消息结构体的地址。
        msgsz:消息正文的字节数。
        msgflg:函数的控制属性
        0: msgsnd 调用阻塞直到条件满足为止。 ( 推荐 )
        IPC_NOWAIT: 若消息没有立即发送则调用该函数的进程会立即返回。
返回值 :
        成功:0
        失败:-1

接收消息--msgrcv函数

作用 : 从标识符为 msqid 的消息队列中接收一个消息。一旦接收消息成功,则消息 在消息 队列中被删除。
函数
所需头文件
        #include
函数 :
        ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
参数 :
        msqid:消息队列的标识符,代表要从哪个消息列中获取消息。
        msgp:存放消息结构体的地址。
        msgsz:消息正文的字节数。
        msgtyp:消息的类型、可以有以下几种类型
        msgtyp=0:返回队列中的第一个消息
        msgtyp>0:返回队列中消息类型为 msgtyp 的消息
        msgtyp<0:返回队列中消息类型值小于或等于 msgtyp 绝对值的消息 , 如果这种 消息有若干个, 则取类型值最小的消息。
注意:
        若消息队列中有多种类型的消息,msgrcv 获取消息的时候按消息类型获取, 不是先进先出的。
在获取某类型消息的时候 , 若队列中有多条此类型的消息 , 则获取最先添加的消息, 即先进先出原则
        msgflg:函数的控制属性
        0:msgrcv调用阻塞直到接收消息成功为止。
        MSG_NOERROR:若返回的消息字节数比 nbytes 字节数多 , 则消息就会截短到 nbytes字节 , 且不通知消息发送进程
        IPC_NOWAIT:调用进程会立即返回。若没有收到消息则立即返回 -1
返回值 :
        成功返回读取消息的长度
        失败返回-1

案例1:简单的消息收发

张三发出消息类型为 10 20 的消息
李四接收消息类型为 10 的消息
王五接收消息类型为 20 的消息
代码:zs.c
#include 
#include 
#include 
#include 
#include 
typedef struct struct_msg{
    long msgType;
    char name[20];
    char text[128];
}Msg;
int main(int argc, char const *argv[])
{
    //获取key值
        key_t key = ftok("./",258);
    //创建或获取消息对象
    int msgid = msgget(key,IPC_CREAT| 0666);
    //准备要发送的消息
    Msg msg01 = {10,"张三","你好 李四"};
    Msg msg02;
    msg02.msgType=20;
    strcpy(msg02.name,"张三");
    strcpy(msg02.text,"你好 王五");
    //发送消息给李四
    msgsnd(msgid,&msg01,sizeof(msg01) - sizeof(long),0);
    //发现消息给王五
    msgsnd(msgid,&msg02,sizeof(msg01) - sizeof(long),0);
    return 0;
}

ls.c

#include 
#include 
#include 
#include 
#include 
typedef struct struct_msg{
    long msgType;
    char name[20];
    char text[128];
}Msg;
int main(int argc, char const *argv[])
{
    key_t key = ftok("./",258);
    int msqid = msgget(key,IPC_CREAT|0666);
    Msg msg;
    msgrcv(msqid,&msg,sizeof(msg)-sizeof(long),10,0);
    printf("李四接收到的消息为\n");
    printf("姓名:%s说%s\n",msg.name,msg.text);
    return 0;
}

ww.c

#include 
#include 
#include 
#include 
#include 
typedef struct struct_msg{
    long msgType;
    char name[20];
    char text[128];
}Msg;
int main(int argc, char const *argv[])
{
    key_t key = ftok("./",258);
    int msqid = msgget(key,IPC_CREAT|0666);
    Msg msg;
    msgrcv(msqid,&msg,sizeof(msg)-sizeof(long),20,0);
    printf("王五接收到的消息为\n");
    printf("姓名:%s说%s\n",msg.name,msg.text);
    return 0;
}

案例2:多人聊天程序

#include 
#include 
#include 
#include 
#include 
#include 
#include 
typedef struct info{
    long type;
    char name[30];
    char text[128];
}INF;
int main(int argc, char const *argv[])
{
    key_t key = ftok("./",1);
    printf("请输入姓名\n");
    int name[30]={0};
    scanf("%s",&name);
    printf("请输入接收的类型\n");
    int rtype;
    scanf("%d",&rtype);
    printf("请输入发送的类型\n");
    int stype;
    scanf("%d",&stype);
    int msgid = msgget(key,IPC_CREAT|0666);
    int i = 0;
    for(i = 0; i<2; i++){
        pid_t pid = fork();
        if (pid == 0)
        {
            break;
        }
        
    }
    if(i==0){
        //接收
        INF inf;
        while (1)
        {
            msgrcv(msgid,&inf,sizeof(INF)-sizeof(long),rtype,0);
            printf("%s:%s\n",inf.name,inf.text);
            if(strcmp("886",inf.text)==0){
                break;
            }
        }
        _exit(0);
    }else if(i==1){
        //发送
        INF inf;
        while (1)
        {
            strcpy(inf.name,name);
            scanf("%s",inf.text);
            inf.type = stype;
            msgsnd(msgid,&inf,sizeof(INF)-sizeof(long),0);
            // printf("%s:%s\n",info.name,info.text);
            if(strcmp("886",inf.text)==0){
                break;
            }
        }
        _exit(0);
    }else if(i==2){
        while (1)
        {
           int id = waitpid(-1,NULL,WNOHANG);
            if (id == -1)
            {
                break;
                /* code */
            }
        }
        
        
        
    }
    return 0;
}

消息队列控制(了解)

作用 : 对消息队列删除 , 获取 , 修改等操作
语法
所需头文件 :
        #include
函数 :
                int msgctl(int msqid, int cmd, struct msqid_ds *buf);
参数:
        msqid:消息队列的标识符。
        cmd:函数功能的控制。
        IPC_RMID:删除由 msqid 指示的消息队列,将它从系统中删除并破坏相关 数据结构。
        IPC_STAT:将 msqid 相关的数据结构中各个元素的当前值存入到由 buf 指 向的结构中。
        IPC_SET:将 msqid 相关的数据结构中的元素设置为由 buf 指向的结构中 的对应值。
        buf:msqid_ds 数据类型的地址,用来存放或更改消息队列的属性。
返回值:
        成功: 返回 0
        失败: 返回 -1
struct msqid_ds 类型:
struct msqid_ds {
        struct ipc_perm msg_perm; /* 所有者与权限 */
        time_t msg_stime; /* 最后一条消息发送的时间 */
        time_t msg_rtime; /* 最后一条消息接收的时间 */
        time_t msg_ctime; /* 最后一次更改的时间 */
        unsigned long __msg_cbytes; /* 队列中的当前字节数(非标准) */
        msgqnum_t msg_qnum; /* 队列中的当前消息数 */
        msglen_t msg_qbytes; /* 队列中允许的最大字节数 */
        pid_t msg_lspid; /* 最后一次发送的进程 id */
        pid_t msg_lrpid; /* 最后一次接收的进程 id */
};
struct ipc_perm 类型 :
struct ipc_perm {
        key_t __key; /* 提供给 msgget 的密钥 (2) */
        uid_t uid; /* 所有者的有效 UID */
        gid_t gid; /* 所有者的有效 GID */
        uid_t cuid; /* 创建者的有效 UID */
        gid_t cgid; /* 创建者的有效 GID */
        unsigned short mode; /* 权限 */
        unsigned short __seq; /* 序列号 */
};

磁盘映射

磁盘映射MMAP

        存储映射 I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相 映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存 入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用 read write 函数 的情况下,使用地址(指针)完成 I/O 操作。 使用存储映射这种方法,首先应通知内 核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap 函数来实现。
Linux高级系统编程- 消息队列 与 内存共享_第1张图片

与文件读取的区别(了解)

        首先简单的回顾一下常规文件系统操作(调用read/fread 等类函数)中,函数的调用
过程:
        1、进程发起读文件请求。
        2、内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此 文件的inode( 存储文件元信息的区域 , 中文名索引节点 )
        3、 inode address_space( 地址空间 ) 上查找要请求的文件页是否已经缓存在页缓存 中。如果存在,则直接返回这片文件页的内容。
        4、如果不存在,则通过 inode 定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后 再次发起读页面过程,进而将页缓存中的数据发给用户进程。
        总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成 读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用 户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这 样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一 样,待写入的buffer 在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存, 再写回磁盘中(延迟写回),也是需要两次数据拷贝。
        而使用mmap 操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映 射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺 页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数 据传入内存的用户空间中,供进程使用。
        总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap 操 控文件,只需要从磁盘到用户主存的一次数据拷贝过程。
mmap 效率更高

mmap函数

作用:建立映射区
所需头文件
        #include
函数
        void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
参数 :
        addr 内核创建映射的地址,填 NULL, 由内核自己选取地址
        length 长度 要申请的映射区的长度
        prot 权限
        PROT_READ 可读
        PROT_WRITE 可写
        flags 标志位
        MAP_SHARED 共享的 -- 对映射区的修改会影响源文件
        MAP_PRIVATE 私有的
        fd 文件描述符 需要打开一个文件
        offset 指定一个偏移位置 ,从该位置开始映射
返回值
        成功 返回映射区的首地址
        失败 返回 MAP_FAILED ((void *) -1)

munmap函数

作用:

释放映射区
所需头文件
        #include
函数
        int munmap(void *addr, size_t length)
参数 :
        addr 映射区的首地址
        length 映射区的长度
返回值
        成功 返回 0
        失败 返回 -1

truncate函数

作用:

        会将参数path 指定的文件大小改为参数 length 指定的大小 . 如果原来的文件大小比 参数length , 则超过的部分会被删去 .
所需头文件
        #include
        #include
函数
        int truncate(const char *path, off_t length);
参数 :
        path 要拓展的文件
        length 要拓展的长度

使用步骤

1, 打开文件
2, 指定文件大小
3, 建立映射
4, 释放映射区
#include 
#include 
#include 
#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    // 1、通过open打开文件
    int fd = open("tmp", O_RDWR | O_CREAT, 0666);
    if (fd < 0)
    {
        perror("open");
        return 0;
    }
    // 2、拓展文件大小
    truncate("tmp", 16);
    // 3、mmap建立映射
    char *buf = (char *)mmap(NULL, 16, PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);
    // 4、使用内存区域,读
    printf("buf=%s\n", buf);
    printf("buf=%s\n", buf);
    // 4、使用内存区域,写
    //strcpy(buf, "hello mmap");
    // 5、断开映射
    munmap(buf, 16);
    close(fd);
    return 0;
}

共享内存

Linux高级系统编程- 消息队列 与 内存共享_第2张图片

        共享内存允许两个或者多个进程共享给定的存储区域。
        物理内存: 电脑物理内存就是指的内存条
        虚拟内存: 是系统默认在 C 盘划分的一部分磁盘空间临时存储数据用的
特点:
        1、共享内存是进程间共享数据的一种最快的方法。 一个进程向共享的内存区域写入了 数据,共享这个内存区域的所有进程就可以立刻看到其中的内容.
        2、使用共享内存要注意的是多个进程之间对一个给定存储区访问的互斥。 若一个进程 正在向共享内存区写数据,则在它做完这一步操作前,别的进程不应当去读、写这些数据

ubuntu 部分版本中共享内存限制值如下

共享存储区的最小字节数: 1
共享存储区的最大字节数: 32M
共享存储区的最大个数: 4096
每个进程最多能映射的共享存储区的个数: 4096

共享内存命令

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

shmget函数

作用:

创建或打开一块共享内存区 , 获得一个共享存储标识符
所需头文件
        #include
        #include
函数
        int shmget(key_t key, size_t size, int shmflg);
参数
        key:IPC 键值 ( 进程间通讯键值 )
        size:该共享存储段的长度 ( 字节 )
        shmflg:标识函数的行为及共享内存的权限。
        IPC_CREAT:创建 , 如果不存在就创建
        IPC_EXCL:如果已经存在则返回失败
        位或权限位: 共享内存位或权限位后可以设置共享内存的访问权限,格式和 open函数的 mode_t 一样 , 但可执行权限未使用。
返回值 :
        成功:返回共享内存标识符。
        失败:返回-1

shmat函数

作用:

建立进程和物理内存的映射
头文件:
        #include
        #include
        void *shmat(int shmid, const void *shmaddr,int shmflg);
参数 :
        shmid:共享内存标识符。
        shmaddr:共享内存映射地址 ( 若为 NULL 则由系 统自动指定 ) ,推荐使用 NULL。
        shmflg:共享内存段的访问权限和映射条件
        0:共享内存具有可读可写权限。
        SHM_RDONLY:只读。
        SHM_RND:( shmaddr 非空时才有效)没有指定 SHM_RND 则此段连接到
        shmaddr 所指定的地址上 (shmaddr 必需页对齐 )
        指定了 SHM_RND 则此段连接到 shmaddr- shmaddr%SHMLBA 所表示的地址 上。
返回值:
        成功:返回共享内存段映射地址
        失败:返回 -1
注意 :
        shmat函数使用的时候第二个和第三个参数一般设为 NULL 0, 即系统自动指定共享 内存地址, 并且共享内存可读可写

shmdt函数

作用:

        将共享内存和当前进程分离( 仅仅是断开本进程与共享内存的联系 , 并不删除共享内存)
所需头文件
        #include
        #include
函数
        int shmdt(const void *shmaddr);
参数:
        shmaddr:共享内存映射地址。
返回值:
        成功返回 0
        失败返回 -1

shmctl函数

作用:

共享内存空间的控制。
        #include
        #include
        int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
        shmid:共享内存标识符。
        cmd:函数功能的控制。
        IPC_RMID:删除。
        IPC_SET:设置 shmid_ds 参数。
        IPC_STAT:保存 shmid_ds 参数。
        SHM_LOCK:锁定共享内存段 ( 超级用户 )
        SHM_UNLOCK:解锁共享内存段。
        buf: shmid_ds 数据类型的地址,用来存放或修改共享内存的属性。
返回值:
        成功返回 0
        失败返回 -1
注意:
        SHM_LOCK 用于锁定内存,禁止内存交换。并不代表共享内存被锁定后禁止其它进程访问。其真正的意义是:被锁定的内存不允许被交换到虚拟内存中。这样做的优势在 于让共享内存一直处于内存中,从而提高程序性能

使用步骤

1, 获取唯一 key
2, 获取共享内存标识符
3, 建立共享内存映射
4, 操作映射
5, 关闭映射
#include 
#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    // 1、获取唯一的key值
    key_t key = ftok("/", 2023);
    // 2、根据唯一的key的共享内存标识(分配物理内存)
    int shm_id = shmget(key, 32, IPC_CREAT | 0666);
    printf("shm_id=%d\n", shm_id);
    // 3、建立进程和物理内存的映射
    char *p = (char *)shmat(shm_id, NULL, 0);
    //4,操作映射,写
    strcpy(p, "hello shm");
    //4,操作映射,读
    // printf("%s\n",p);
    // 5、断开进程和物理内存的映射
    shmdt(p);
    return 0;
}

你可能感兴趣的:(Linux高级系统编程,linux,c语言)