我们无论使用命名管道还是匿名管道,都是在文件层面上实现的通信,实际上还有基于系统层面的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:
#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:
#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
哈哈,励志轩的车又被推上去了