Linux:进程间通信之共享内存

我们无论使用命名管道还是匿名管道,都是在文件层面上实现的通信,实际上还有基于系统层面的system v标准的进程间通信方式。

因为操作系统不相信用户,所以用户使用的时候只能通过调用的方式

进程间通信的本质:先让不同的进程看到同一份资源。

system v提供的主流方式有三个:

1.共享内存 2.消息队列(有些落伍)3.信号量

前两个以传送数据为目的,第三个以实现进程间同步后者互斥为目的。

共享内存

共享内存其实和我们之前在文件层面上进行通信的原理差不多,只不过是在操作系统层面上操作

通过系统调用,创造出一片新的内存空间,然后让两个进程都挂接到这个新的内存空间上,也就是把我们新开辟的内存空间,通过页表的映射在两个进程上,两个进程就可以拿到同一份地址空间

此时我们就让不同的进程看到了同一份资源,这种通信方案称之为共享内存

怎么创建一个共享内存:

使用shmget创建

key是自己自定义的

size通常是4kb的整数倍

shmflg有两种类型:

  • 如果单独使用IPC_CREATE或者shmflg为0:如果不存在共享内存,则创建一个共享内存,如果创建的共享内存已经存在,直接返回当前已经存在的共享内存。(基本不会空手而归)

  • IPC_CREATE | IPC_EXCL:如果不存在共享内存,则创建;如果已经有了共享内存,则返回出错。意义在于如果我调用成功,得到的一定是一个最新的,没有被别人使用过的共享内存!

  • 但是IPC_EXCL单独使用是没有意义的

使用ftok来共享

两个参数:自己设定的内存的路径,自己设定的id

增加两个概念:

key:是共享内存对于系统外部的标识符,通常是整数或字符串,也就是我们使用shemget创建共享内存时候的那个key

什么是ID:内核分配给文件的特有标识符,通常是整数

什么叫key的算法:生成id的算法

所以只要我们在生成共享内存的时候,只要我们内部的数据是一样的,并且生成id的key算法是一样的,那么这两个进程就可以共享同一块共享内存

key_t是一个特殊的类型,用于表示系统中的共享内存、消息队列和信号量等 IPC(进程间通信)机制中的键值,就像ssize_t一样

来写一个:

comm.h:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>        
#include <sys/stat.h>
#include <fcntl.h>
#define MY_FIFO "./fifo"
#include<sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0X6666
#define SIZE 4097

shared.c:

cpp 复制代码
#include"comm.h"
int main(){
    key_t key=ftok(PATH_NAME,PROJ_ID);
    if(key<0){
        perror("key");
        return 1;
    }
    int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
    if(shmid<0){
        perror("shmget");
        return 1;
    }
    printf("key:%u,shmid:%d\n",key,shmid);

    return 0;
}

结果:

因为共享内存文件已经创建成功了,所以第二次会返回File exists

ipcs -m是一个命令,用于显示系统中所有的共享内存段的信息

不加-m选项就是显示所有共享内存,消息队列,信号量

我们会发现:我们的进程结束后共享内存文件并没有随着进程的销毁而销毁,还能查到;如果是文件的话,当和这个文件的所有进程都被关闭后,那么文件也被关闭了。**system V 的IPC资源,生命周期是随内核的!**这个IPC只能通过程序员显示的释放(利用系统调用命令)或者是OS重启 !(内核不死或者程序员不让它死,他就死不了)

你看我退出重连就没有了:

于是就产生了一个问题:我们拿C语言创建一个新指针或者申请一块新内存的时候,就需要free()释放,那么用shmget创建共享内存的时候,进程退出就不会造成内存泄漏吗?你也没管你的共享内存啊!

指针为什么需要free()?因为指针的内存是用malloc、colloc、realloc等动态内存函数分配的,函数分配的内存得由程序员自己释放;而共享内存是由操作系统管理的。操作系统会在进程退出时清理相关资源。但是,如果你的进程是异常退出(例如通过调用exit() 函数),那么仍然需要手动来确保资源的释放和清理。

删除共享内存的命令:

ipcrm -m shmid

我这里的shmid是1,key是0x66014933

shmid和key有什么关系?

我们来试试用key删除共享内存:

删不掉,为什么?

key是用户创建,内核使用的标识符,是OS层面进行标识唯一性的,不能用来管理共享内存的。

shmid是生成共享内存返回给用户使用的id,用来在用户层进行共享内存的管理。比如:删除它,关联/去关联。

因为命令行就是用户级的,所以使用的是shmid

使用shmctl来操作共享内存

三个参数:shmid就是shmid,cmd是一个选项,将要采取的动作(有三个可取值),shmid_ds:就是描述共享内存的数据结构,只不过它是用户层的数据结构,是内核上描述共享内存的子集。

shmid_ds的本质上就是一个结构体,用户使用共享内存的时候,通常就通过shmctl来和这个结构体交互

struct shmid_ds的内容:

返回值:成功返回0;失败返回-1

我们现在如果要删除这个共享内存,cmd置为IPC_RMID,shmid_ds置为NULL

我们在程序里试一下:

shared.c

#include"comm.h"
int main(){
    key_t key=ftok(PATH_NAME,PROJ_ID);
    if(key<0){
        perror("key");
        return 1;
    }
    int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
    if(shmid<0){
        perror("shmget");
        return 1;
    }
    printf("key:%u,shmid:%d\n",key,shmid);
    sleep(5);
    shmctl(shmid,IPC_RMID,NULL);
    printf("key:0x%X,shmid->%d->shm delete succes!\n",key,shmid);
    sleep(5);//此时已经删掉了

    return 0;
}

你会发现欸?我们创建着创建着,这个shmid居然还是递增的欸!这让我们想到什么?数组啊!

内核再组织IPC资源的时候是通过数组组织的。

目前我们创建的共享内存的perms是0,代表权限为0,意思就是谁都不能读谁都不能写 ;如果我们想创建一个有权限被读取的共享文件,就可以在创建的时候加一个0666,这样perms就变成了666。说明我们可以给共享内存设置权限,并且共享内存的权限管理依赖于文件系统,也就是一切皆文件。

#include"comm.h"
int main(){
    umask(0);
    key_t key=ftok(PATH_NAME,PROJ_ID);
    if(key<0){
        perror("key");
       return 1;
    }
    int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);//增加权限
    if(shmid<0){
        perror("shmget");
        return 1;
    }
    printf("key:%u,shmid:%d\n",key,shmid);
    sleep(5);
    //shmctl(shmid,IPC_RMID,NULL);//不删除,删除的话查询权限可能为0
    printf("key:0x%X,shmid->%d->shm delete succes!\n",key,shmid);
    sleep(5);//此时已经删掉了

    return 0;
}

shmat让进程和共享内存产生关系

功能:讲内核级的共享内存链接到地址空间

参数shmid: 共享内存标识,shmaddr:指定连接的地址,shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY

返回值:成功返回一个指针,指向共享内存的起始地址(这个地址是虚拟地址。ps:只要用户用的地址都是虚拟地址);失败返回-1。这个返回值如同函数malloc的返回值。

如果shmaddr为NULL的时候,os会自动选择一个地址

shmaddr不为NULL时且shmflg没有SHM_RND标记,则以shmaddr为链接地址

shmaddr不为NULL且shmflg设置了SHM_RND标记,则则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr - (shmaddr % SHMLBA)

shmflg=SHM_RDONLY,表示连接操作用来只读共享内存。

shmdt去除关联关系

功能:将共享内存段和当前进程脱离

参数:shmaddr:shmat返回的指针

返回值:成功返回0,失败返回-1

使共享内存和进程脱离关系不等于删除共享内存段,起不到释放共享内存的作用

#include"comm.h"
int main(){
    umask(0);
    key_t key=ftok(PATH_NAME,PROJ_ID);
    if(key<0){
        perror("key");
       return 1;
    }
    int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
    if(shmid<0){
        perror("shmget");
        return 1;
    }
    printf("key:%u,shmid:%d\n",key,shmid);
    sleep(5);
    char *mat=shmat(shmid,NULL,0);
    if(mat==(char*)-1){
        perror("shmat");
    }
    printf("attaches shm success\n");
    sleep(5);
    shmdt(mat);
    printf("detaches shm success\n");
    shmctl(shmid,IPC_RMID,NULL);
    printf("key:0x%X,shmid->%d->shm delete succes!\n",key,shmid);
    sleep(5);//此时已经删掉了

    return 0;
}

注意对mat的判断:因为shmat的返回值可能是指针,也可能是-1,所以不能随便解引用,应该转换类型

这样就实现了创建-挂接-去挂-删除了

来写一个利用共享内存通信的实例

写一个客户端和服务端的通信:

comm.h:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>        
#include <sys/stat.h>
#include <fcntl.h>
#define MY_FIFO "./fifo"
#include<sys/shm.h>
#define PATH_NAME "./"
#define PROJ_ID 0X6666
#define SIZE 4097

server.c

#include"comm.h"
int main(){
    key_t key=ftok(PATH_NAME,PROJ_ID);
    if(key<0){
        perror("ftok");
        return 1;
    }
    int shmid=shmget(key,SIZE,IPC_CREAT|0666|IPC_EXCL);
    if(shmid<0){
        perror("shmget");
        return 1;
    }
    printf("key:%u,shmid:%d",key,shmid);
    printf("attaches shm success \n");  
    char *shm=(char *)shmat(shmid,NULL,0);
    if(shm==(char *)-1){
        perror("shmat");
        return 1;
    }
    //开始通信
    while(1){
        sleep(1);
        printf("%s\n",shm);
    }
    shmctl(shmid,IPC_RMID,NULL);
    printf("delete");

}

client.c:

#include"comm.h"
int main(){
    key_t key=ftok(PATH_NAME,PROJ_ID);
    if(key<0){
        perror("ftok");
        return 1;
    }
    printf("%u\n",key);
    int shmid=shmget(key,SIZE,IPC_CREAT);
    //和server的形成规则相同,代表是同一块共享内存
    if(shmid<0){
        perror("shmget");
        return 1;
    }
    char *shm =shmat(shmid, NULL, 0);   
    if (shm == (char *) -1) {
        perror("shmat");
        return 1;
    }


    printf("client process attaches success!\n");   
    char c='A';
    while(c<='Z'){
        shm[c-'A']=c;
        c++;
        shm[c-'A']=0;//向shm[0]写入A,再把shm[1]写为0,再写为B
        sleep(2);
    }    
    shmdt(shm);
    printf("client process detaches success\n");
    //client去挂接,server负责删除


}

先运行server.c,在运行client.c,此时我们看到client所写的消息就都被server读到了

一开始一直出现:

一开始一直出现这样的情况,因为我做了错饭:IPC_EXCL这个标志与IPC_CREAT 一起使用,表示如果共享内存段已存在,则shmget失败,并返回 -1,同时设置 errno 为 EEXIST,我的客户端不允许在已经存在的共享内存段上执行操作。

服务端已经创建了,客户端再这么写就会出现上述情况

共享内存的使用有没有类似管道的read这样的接口呢?

没有,共享内存一旦建立并映射到进程的地址空间后,就可以直接实现共享了;像malloc的空间一样,不需要系统调用接口

使用系统接口的本质是因为管道是把数据从进程拷贝到内核文件里,然后再由内核文件拷贝到另外一个进程的空间里;read或write本质是将数据从内核拷贝到用户,或者从用户拷贝到内核

我们可以看见server.exe启动时,client.exe还没启动,server.exe一直在刷新:

说明在client还没写入时,server已经在读取了!

因为**共享内存是所有进程间通信最快的,**省略了若干次数据拷贝的问题,管道就比他慢

但是共享内存不提供任何同步或互斥机制,就需要我们自己保证他的安全:

例如我向管道里写入写满的时候,我就写不进去了;没有数据的时候,就读不了了;

但是共享内存是同时进行的,你向共享内存写入了一个"我讨厌 上学",而读取端读到了"我讨厌"就获取数据了,谁知道你是讨厌什么呢?造成数据不一致,所以共享内存在多进程通信的时候是不太安全的。

共享内存的三个特点:

生命周期随内核。

共享内存不提供任何同步或者互斥机制。

共享内存是所有进程间通信中速度最快的。

共享内存的size

size大小建议为4kb的整数被,也就是4096的整数倍

共享内存在内核中申请的基本单位是页,这个页叫做内存页,这个内存页是4KB。

如果我申请了4097就会向上取整,获得两页的大小

但是我们看见的又是4097

如果我向操作系统要了4096个字节,它给了我250个字节,那么就是操作系统的问题;有时候我要的少,操作系统给多了也会有问题:比如你设置了不能超过10bits,但是操作系统给了你20bits就又出错了;所以操作系统会按两页的大小给我申请,但是我用4097,就是4097

哈哈,励志轩的车又被推上去了

相关推荐
w_outlier1 小时前
匿名管道在进程池中的应用案例
linux·c++·管道通信
wyw00001 小时前
Ubuntu22.04安装VMware Tools
linux
哦*&1 小时前
ubuntu18.04 Anconda安装及使用
linux
master cat1 小时前
linux启用 IPv4 转发
linux·运维·服务器
人类群星闪耀时1 小时前
自动化运维的利器:Ansible、Puppet和Chef详解
运维·自动化·ansible
wzdxsa1 小时前
TCP的三次握手四次挥手
服务器·网络·tcp/ip
Persus2 小时前
Python 在自动化运维时常用到的方法
运维·python·自动化
小小小汐-2 小时前
【linux】基础IO(下)
linux
麻将上头了2 小时前
Centos怎么执行脚本
linux·运维·笔记·centos
时差freebright2 小时前
【Linux 报错】“make: ‘xxxx‘ is up to date.” 解决办法
linux·开发语言·gcc