文章目录
1. 认识共享内存
1.1 概念
共享内存区是最快的IPC(Inter-Process Communication,进程间通信)形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
1.2 共享内存示意图
基本实现原理:
操作系统通过某种调用方式,为两个进程分配同一块物理内存,这两个进程再在堆栈之间的共享区申请一块虚拟地址空间,然后将虚拟空间与物理内存通过页表的映射关系关联起来。这样进程就可以通过虚拟地址直接去使用物理内存,不用通过文件系统。A进程改变物理内存的值,B进程可以直接读取到。
下面我便来向大家介绍使用共享内存相关的系统调用函数。
2. 共享内存函数
2.1.ftok
2.1.1 功能
通过算法,用函数的两个参数获取一个唯一的key值(操作系统层面共享资源的标识信息)
2.1.2 声明
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
- pathname:文件路径
- roj_id:任意值,可以随便取
这两个参数可以随便取值,目的是为了用这两个参数生成一个唯一的key值。
- 返回值:创建成功返回一个key值,失败返回-1
2.1.3 实例
//common.h
#pragma once
#define PATHNAME "/tmp"
#define PROJ_ID 0x6688
#define SIZE 4096
//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include "common.h"
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID);
printf("key: %x\n", k);
return 0;
}
本段代码的作用就是利用ftok函数生成一个key值,这里我填入的两个参数分别是"/tmp"和0x6688,成功生成之后再将key值打印出来。
运行结果:
可以看到,这里成功获取到一个key值。
2.2. shmget
2.2.1 功能
用来创建共享内存
2.2.2 声明
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- key:这个共享内存段的标识
- size:共享内存大小
一般习惯设置为页(page,大小为4096个字节)的整数倍,因为操作系统在分配资源的时候是以页为单位的,假如你申请的大小是4097,但是实际分配的大小是4096*2,养成好的习惯!!
- shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
最常见的两个标志: IPC_CREAT和 IPC_EXCL
-
IPC_CREAT:创建标识符为key的共享内存,如果这个共享内存已经存在,那就直接打开它。
-
IPC_EXCL:该选项单独使用没有意义,一般和IPC_CREAT选项一块使用,确保打开的一定是一个全新的共享内存,如果要打开的共享内存已经存在,则报错返回。
-返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
该标识码的作用和key值一样,都是用来标识共享内存唯一性的。key值是在操作系统上标识IPC资源唯一性的,属于操作系统的概念。而这里的返回值是给用户去使用的
2.2.3 实例
//common.h
#pragma once
#define PATHNAME "/tmp"
#define PROJ_ID 0x6688
#define SIZE 4096
//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include "common.h"
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID);
printf("key: %p\n", k);
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid < 0)
{
perror("shmget");
return 1;
}
printf("shmid: %d\n", shmid);
return 0;
}
本段代码的作用是利用shmget函数创建一个共享内存,创建成功之后再将该共享内存的标识id返回回来。
运行结果:
我们看到,成功获取到了共享内存的key值以及shmid。但是该进程运行之后究竟有没有真的创建出来共享内存,仅仅通过运行结果我们还看不出来。
下面介绍一条查看系统中共享内存的指令:
ipcs -m
通过查看系统的共享内存资源,我们成功找到刚刚创建的共享内存信息。
最顶层的信息依次是:key值,shmid值,拥有者、权限、大小、挂接数。
我们再来看一种现象:
大家看到,当我再次运行该程序的时候,创建共享内存失败。
这不难解释,因为我在使用shmget函数时加了IPC_EXCL选项,第一次运行程序时已经把共享内存创建出来了,所以当第二次运行程序创建时,该共享内存已经存在,从而产生报错。
但是这里我就有一个问题了,当我./server结束后,这个进程有没有被终止掉?答案是该进程肯定已经被终止了。
但是我们发现创建共享内存的进程结束掉之后,IPC共享内存的资源并没有被释放掉!
回想一下管道通信,管道是随文件系统的,文件随进程,这就意味着管道的生命周期会随着进程的结束而结束。
但是IPC资源的声明周期随内核,如果不注定删除,这个资源将会一直被占用,除非机器重启。
因此我们必须在进程结束之前手动将这块资源给释放掉。
首先我先来向大家介绍一个释放共享内存的指令:
ipcrm -m shmid
下面我们来用这条指令手动删除掉刚刚创建的共享内存资源,
我们看到上一块内存资源被释放掉之后,再次运行程序就可以再次生成共享内存资源了。
当然手动释放不免显得有一些麻烦,我们更希望的是在进程结束的时候自动释放掉申请的共享内存资源。
下面来为大家介绍释放共享内存资源的函数,shmctl。
2.3 shmctl
2.3.1 功能
用于控制共享内存,实现增删查改功能
2.3.2 声明
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- shmid:由shmget返回的共享内存标识码
- cmd:将要采取的动作(有三个可取值)
- buf:指向一个保存着共享内存的模式状态和访问权限的数据结构,可用于修改操作系统描述共享内存的结构体。
- 返回值:成功返回0;失败返回-1
2.3.3 实例
//common.h
#pragma once
#define PATHNAME "/tmp"
#define PROJ_ID 0x6688
#define SIZE 4096
//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "common.h"
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID);
printf("key: %p\n", k);
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid < 0)
{
perror("shmget");
return 1;
}
printf("shmid: %d\n", shmid);
sleep(5);
shmctl(shmid, IPC_RMID, NULL);
sleep(5);
return 0;
}
本段代码的作用是先创建一个共享内存资源,5s后删掉这个共享内存资源,再过5秒进程结束。
这里我们需要来写一个监控脚本去每隔1s监视一次系统中的共享内存资源:
while :; do ipcs -m; sleep 1; echo "#########"; done
运行结果:
我们看到程序运行之后成功创建了一块共享内存资源,5s后该共享内存资源被释放,再过5s后进程结束。
学会共享内存的创建和删除之后,下面我们来看如何把共享内存和进程关联起来,以及取消关联。
2.4 shmat函数
2.4.1 功能
将共享内存段连接到进程地址空间
2.4.2 声明
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
- shmid: 共享内存标识
- shmaddr:指定连接的虚拟地址(如果不设置则由操作系统默认分配)
- shmflg:它的两个可能取值是SHM_RND和SHM_RDONLY (如果不设置则由操作系统默认分配)
- 返回值:成功返回一个指针,指向虚拟地址;失败返回-1
说明:
- shmaddr为NULL,核心自动选择一个地址
- shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
- shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr(shmaddr % SHMLBA)
- shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
2.5 shmdt函数
2.5.1 功能
将共享内存段与当前进程脱离
2.5.2 声明
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
- shmaddr: 由shmat所返回的指针
- 返回值:成功返回0;失败返回-1
注意:将共享内存段与当前进程脱离不等于删除共享内存段
2.5.3 实例
//common.h
#pragma once
#define PATHNAME "/tmp"
#define PROJ_ID 0x6688
#define SIZE 4096
//server.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "common.h"
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID);
printf("key: %p\n", k);
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid < 0)
{
perror("shmget");
return 1;
}
printf("shmid: %d\n", shmid);
sleep(5);
char* str = (char*)shmat(shmid, NULL, 0);
sleep(5);
shmdt(str);
sleep(5);
shmctl(shmid, IPC_RMID, NULL);
sleep(5);
return 0;
}
本代码的作用是先创建一个共享内存资源,5s后该进程挂接该资源,再5s后取消挂接,再5s后删除资源,再5s后结束进程。
同样的,这里我们用监视脚本来监视系统中的共享内存资源。
运行结果:
我们看到当进程与共享内存资源挂接时,共享内存的挂接数变为1,取消挂接后,挂接数变为0。
3. 进程间通信
以上就是使用共享内存实现通信的所有接口介绍,接下来我们就来使用共享内存实现进程之间的通信。
3.1 common.h(头文件)
//common.h
#pragma once
#define PATHNAME "/tmp"
#define PROJ_ID 0x6688
#define SIZE 4096
3.2 server.c(服务器)
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "common.h"
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID);
printf("key: %p\n", k);
int shmid = shmget(k, SIZE, IPC_CREAT | IPC_EXCL | 0666);
if(shmid < 0)
{
perror("shmget");
return 1;
}
printf("shmid: %d\n", shmid);
char* str = (char*)shmat(shmid, NULL, 0);
while(1)
{
sleep(1);
printf("%s\n", str);
}
shmdt(str);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
3.3 client.c(客户端)
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include "common.h"
int main()
{
key_t k = ftok(PATHNAME, PROJ_ID);
printf("key: %p\n", k);
int shmid = shmget(k, SIZE, 0);
if(shmid < 0)
{
perror("shmget");
return 1;
}
printf("shmid: %d\n", shmid);
char* str = (char*)shmat(shmid, NULL, 0);
char c = 'a';
for(; c < 'z'; c++)
{
str[c - 'a'] = c;
sleep(5);
}
shmdt(str);
return 0;
}
这里我们实现一个客户端和服务器之间的通信,客户端每隔5s向共享内存里写入一个小写字母,服务器每隔1s将共享内存里面的值读取到显示器上。
3.4 运行结果
我们看到,服务器每隔1s就会将客户端发送到共享内存的信息读取出来,中间没有任何的等待过程。
而回想管道实现通信时,如果客户端没有发送信息,服务器就必须以阻塞的方式去进行等待。
那么为什么共享内存没有等待的过程?这是因为共享内存的底层不提供任何同步与互斥的机制。两个进程通过共享内存一个在写,一个在读,它们相互之间没有任何联系,甚至不知道对方的存在。
本篇文章到这里就全部结束了,最后希望这篇文章能够为大家带来帮助。