目录
前言:
1. System V共享内存
1. 共享内存的理解
2. 共享内存的使用步骤
3. 共享内存的使用
1. 共享内存的创建
查看共享内存
2. 共享内存的释放
3. 共享内存的挂接
4. 共享内存的去挂接
4. 共享内存的使用示例
1. 两进程挂接与去挂接演示:
2. 两进程通信演示;
Linux🌷
前言:
在之前写的进程间通信的两篇博客中,不管是匿名管道通信还是命名管道通信都是基于文件的一种通信方式;
而今天我们要学习另一种通信方式:System V标准下的 “共享内存”;
在这里简单介绍下System V标准,System V标准是计算机学术界的一些领头人物站在OS层面,专门为进程间通信设计的方案;
System V提供了三个主流方案:
1. 共享内存——也就是今天博客的主要内容;
2. 消息队列;
3. 信号量;
其中共享内存和消息队列是为了传输数据设计的,信号量是为了保证进程间的同步和互斥设计的;
下面开始我们的正文!🔮
1. System V共享内存
1. 共享内存的理解
共享内存是一种允许两个或多个进程访问同一块物理内存的通信机制;
用一张图更好的理解下:
其本质就是先在物理内存中开辟一块空间,然后让相互通信的进程通过页表映射将这块共享空间映射到进程的虚拟地址空间中,因此两个进程便看到了同一份资源,便可以进行进程间通信了,物理内存中开辟的空间就是我们的共享内存;
OS中可能存在多对使用共享内存进行通信的进程,共享内存也自然不止一个,OS也当然要将共享内存进行管理,管理方法——“先描述,在组织”;
下面看一下共享内存的数据结构:
struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};
我们申请一块共享内存之后 为了保证这块共享内存的唯一性 我们必须要对它做个标识
我们在 linux 系统中我们使用 key 值来唯一标识一块共享内存
在我们的 shmid_ds
结构体的第一行有一个叫做 shm_perm
的结构体 它的类型是 ipc_perm
它的结构体定义如下
struct ipc_perm{
__kernel_key_t key;
__kernel_uid_t uid;
__kernel_gid_t gid;
__kernel_uid_t cuid;
__kernel_gid_t cgid;
__kernel_mode_t mode;
unsigned short seq;
};
我们看它的第一行有这么一段描述 __kernel_key_t key;
而这个key就是唯一标识共享内存的key值
2. 共享内存的使用步骤
1. 共享内存的创建(开辟共享内存);
2. 共享内存的挂接(在页表中建立共享内存的映射关系);
3. 共享内存的去挂接(在页表中去掉共享内存的映射关系);
4. 共享内存的释放;
3. 共享内存的使用
1. 共享内存的创建
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
key:共享内存段的唯一标识,用户可使用如下函数进行key的自动生成
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
pathname:自定义的路径名;
proj_id:自定义的项目ID;
返回值:成功返回一个非负整数,失败返回-1;
只要pathname和proj_id是一样的,那么生成的key便是一样的,因此两个进程便可以识别同一共享内存 ;
@size:共享内存的大小,建议是4KB的整数倍。
因为OS在分配内存资源时,是以页面为单位进行分配的,页面的大小是4KB,虽然是这样分配的,但是申请多少还是给你多少,为了避免给用户造成一些错误;
@shmflg:由九个权限标志组成,它们的用法和创建文件时使用的mode模式标志是一样的。
我们一般有如下两种用法:
1. IPC_CREAT:共享内存不存在时,创建一个共享内存并返回,如果创建的共享内存已经存在的话,则直接返回当前已经存在的共享内存;
2. IPC_CREAT | IPC_EXCL:如果共享内存不存在则创建并返回,如果共享内存已经存在则返回出错,也就是说如果调用成功,得到的一定是一个最新的,没有被别人使用的共享内存!IPC_EXCL单独使用是没有意义的,
3. 共享内存也是有权限的,我们还可以在上述基础上(|或权限),达到共享内存权限的设定;
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATH_NAME "./test.c"
#define PROJ_ID 0x666
#define SIZE 4097
int main()
{
//生成key
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key<0)
{
perror("ftok fail\n");
return 1;
}
//创建共享内存
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
if(shmid<0)
{
perror("shmget fail\n");
return 2;
}
printf("key:%u ,shmid:%d\n",key,shmid);
return 0;
}
查看共享内存
我们可以使用 ipcs 命令查看系统中的进程间通信资源的信息;
如果我们只想知道三种通信方式其中一种的通信信息的话,我们可以在 ipcs 后面携带选项:
-q:列出消息队列相关信息;
-m:列出共享内存相关信息;
-s:列出信号量相关信息;
其中它的每列信息含义如下:
命名 | 含义 |
---|---|
key | 系统区别各个共享内存的唯一标识 |
shmid | 共享内存的用户层id |
owner | 共享内存的拥有者 |
perms | 共享内存的权限 |
bytes | 共享内存的大小 |
nattch | 挂接共享内存的进程数 |
status | 共享内存的状态 |
key和shmid的区别:
我们的key是系统中是被共享内存的唯一标识 而shmid是我们用户层识别共享内存的标识
这就好像是inode和文件名的区别
系统中使用inode来标识一个文件 而我们使用文件名来标识一个文件
2. 共享内存的释放
共享内存的生命周期是随内核的 就算进程结束了共享内存也不会被释放
如果再次运行test.c程序 我们就会发现这样子的错误
想要释放共享内存只有两种方式
1. 关机重启
2. 主动进行释放
使用命令方式释放共享内存:
我们可以使用 ipcrm -m shmid命令
释放指定id的共享内存资源
使用函数方式释放共享内存:
我们一般使用shmctl函数来控制共享内存 它的函数原型如下
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid:用户层面标识唯一一个共享内存;
cmd:具体的控制命令;
buf:获取或设置所控制共享内存的数据结构(一般设为NULL);
返回值:成功返回0,失败返回-1;
其中关于 cmd 参数 常用的命令有以下几个
选项 | 作用 |
---|---|
IPC_STAT | 获取共享内存的当前关联值 此时参数buf作为输出型参数 |
IPC_SET | 在进程有足够权限的前提下 将共享内存的当前关联值设置为buf所指的数据结构中的值 |
IPC_RMID | 删除共享内存段 |
将上述代码修改下,继续运行:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#define PATH_NAME "./test.c"
#define PROJ_ID 0x666
#define SIZE 4097
int main()
{
//生成key
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key<0)
{
perror("ftok fail\n");
return 1;
}
//创建共享内存
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL);
if(shmid<0)
{
perror("shmget fail\n");
return 2;
}
printf("key:%x ,shmid:%d\n",key,shmid);
//释放共享内存
shmctl(shmid,IPC_RMID,NULL);
printf("delete success!\n");
return 0;
}
我们可以看到 共享内存 被释放了
3. 共享内存的挂接
我们一般使用 shmat
函数来进行共享内存的挂接
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid: 待关联共享内存的用户级标识符;
shmaddr:指定共享内存映射到进程地址空间的某一地址(通常设置为NULL,让OS来指定 );
shmflg:挂接共享内存时设置的某些属性(一般设置为0);
返回值:成功则返回共享内存映射到进程地址空间中的起始地址,失败则返回-1;
4. 共享内存的去挂接
我们一般使用shmat
函数来进行共享内存和虚拟地址空间的去挂接
int shmdt(const void *shmaddr);
shmaddr:shmat函数时得到的起始地址
返回值:成功返回0,失败返回-1;
4. 共享内存的使用示例
1. 两进程挂接与去挂接演示:
我们可以看到nattch是由0->1->2->1->0的;
注意:我们在shmget的时候必须设置权限,否则会挂不上去;
- comm.h:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <unistd.h>
#define PATH_NAME "./"
#define PROJ_ID 0x333
#define SIZE 4097
- makefile:
.PHONY:all
all:server client
server:server.c
gcc -o $@ $^
client:client.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f server client
- server.c:
#include "comm.h"
int main()
{
//生成共享内存key
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key<0)
{
perror("ftok fail");
return 1;
}
//创建共享内存
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
if(shmid<0)
{
perror("shmget fail");
return 2;
}
printf("shmget success! key:0x%x shmid:%d\n",key,shmid);
sleep(3);
//挂接共享内存
char* mem = (char*)shmat(shmid,NULL,0);
printf("shmat success!\n");
sleep(3);
//去挂接共享内存
shmdt(mem);
printf("shmdt success!\n");
sleep(3);
//释放共享内存
shmctl(shmid,IPC_RMID,NULL);
printf("shmrm success!\n");
sleep(3);
return 0;
}
- client.c:
#include "comm.h"
int main()
{
//生成key
key_t key = ftok(PATH_NAME,PROJ_ID);
if(key<0)
{
perror("ftok fail");
return 1;
}
//获取shmid
int shmid = shmget(key,SIZE,IPC_CREAT|0666);
if(shmid<0)
{
perror("shmget fail");
return 2;
}
//挂接共享内存
char* mem = (char*)shmat(shmid,NULL,0);
sleep(1);
printf("client shmat success!\n");
//去挂接
shmdt(mem);
sleep(1);
printf("client shmdt success!\n");
return 0;
}
2. 两进程通信演示;
我们在上述代码中添加了通信的内容,结果如上所示;
- server.c:
//通信
while(1)
{
printf("%s\n",mem);
sleep(1);
}
- client.c:
//通信
char c='A';
while(c<'Z')
{
mem[c-'A']=c;
c++;
mem[c-'A']='\0';
sleep(2);
}
客户端每隔2秒往共享内存中写入数据,服务端每隔1秒读一次;
我们发现在客户端还没有写入数据时,服务端就开始读了;
并且,服务端再读数据之后,并没有把数据拿走;
这两点是区别与管道通信的,管道是具有同步机制(读写两端会相互等待的),读了之后数据就带走了;
坚持打卡!😀