目录
一、共享内存
1.1 申请共享内存块
1.2 释放共享内存块
1.3 挂接共享内存
二、共享内存的使用
2.1 Server端与Client端
2.2 挂接与运行
三、共享内存总结
3.1 共享内存的特点
3.2 共享内存实现访问控制
共享内存是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,即进程不再通过执行进入内核的系统调用来传递彼此的数据,减少了拷贝的次数。
共享内存是操作系统提供的。
共享内存=共享内存块+对应的共享内存的内核数据结构。
接下来我们来看看其系统调用接口——shmget。
其作用是返回一个申请的System V的共享内存块。
返回值:
- 成功则返回有效共享内存的用户层标识符(类似于文件的fd),失败则返回-1并设置错误码。
参数:
1.key:
- server端根据key值创建共享内存,client端可以通过该唯一的key值找到对应的共享内存块。
2. size:
- 想创建多大的共享内存
3.shmflg:
- 有两个参数(IPC_CREAT、IPC_EXCL)
- IPC_CREAT 表示创建共享内存,如果该共享内存已经存在,则获取该共享内存;如果该共享内存不存在,则创建共享内存并返回。
- IPC_EXCL单独使用是无意义的,当配合IPC_CREAT使用时,如果该共享内存存在,则出错返回;如果不存在,则创建并返回。所以IPC_CREAT和IPC_EXCL一起使用一定能创建出一个共享内存。
- 并可以传入内存的访问权限。
关于形成唯一的key值,我们还可以使用一个系统接口——ftok。
该接口的功能是将 pathname 和 proj_id 结合起来,通过算法计算出一个 key 值
返回值:
通过pathname和proj_id计算出一个System的 key 值。失败返回-1。
参数:
1.pathname:
传入一个路径,传入路径的的本质就是拿到该路径下的这个文件的inode值。
2.proj_id
项目id,可以传入0-255之间的整数即可,但是超过了,也会自动进行截断。
所以,接下来就是我们的第一步:创建公共的key值
接下来我们来使用一个ftok生成key值。
#define PATH_NAME "." //.表示当前文件所在路径,该路径下一定要有权限
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(4096)大小的整数倍
//Server端代码:
int main()
{
key_t k = ftok(PATH_NAME, PROJ_ID); //key_t 其实就是 int 类型
Log("create key done", Debug) << "Server key:" << k << endl;
return 0;
}
//Client端代码:
int main()
{
key_t k = ftok(PATH_NAME, PROJ_ID);
Log("create key done", Debug) << "Client key:" << k << endl;
return 0;
}
结果如下:两个文件生成的key值相同,这样我们就可以让两个进程访问一个共享内存。
有了key值的准备,接下来我们使用一下 shmget 。
// 2.创建共享内存
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL);
if (shmid == -1)
{
perror("shmget error");
exit(1);
}
然后我们来观察结果,发现,在第一次创建时,共享内存确实开辟成功了,可是当我们再次运行时,因为上一次申请的共享内存没有释放,所以导致开辟空间出错。
所以,接下来我们学习一下查看ipc的命令,使用命令 ipcs-m 可以看到开辟出的共享内存块。
所以我们可以使用 ipcrm -m +shmid 来删除开辟的共享内存。因此我们可以知道,Ststem V IPC资源的生命周期是随内核的!
在程序中,我们便可以使用 shmctl 来释放共享内存
实现一个将十进制转化为十六进制的写法,可以让key值以十六进制进行显示。
接下来就是将我们的Client端挂接到Server端
shmat:
返回值:
返回共享内存的起始地址(类似于malloc),失败的话返回-1,并设置错误码。
参数:
1.shmid:
表示要挂接的共享内的标识符id。
2.shmaddr:
指定共享内存的起始地址,传入该参数可以对共享内存进行更多的操作;如果只是使用挂接,传入nullptr即可。
3.shmflg:
表示挂接方式,直接传入0,就表示以读写方式进行挂接。
shmdt:
返回值:
成功返回0,失败返回-1,并设置错误码。
参数:
传入共享内存的起始地址即可释放。
首先我们将创建共享内存时的权限设置一下,这样进程才能挂接至共享内存中。
接下来我们编写一个脚本 while :;do ipcs -m; sleep 1;done 来实时监测共享内存的挂接情况(从0至1),这样程序就挂接上共享内存了。
我们接下来的目的是让Server端创建一块共享内存,然后Server端和Client同时挂接上共享内存,然后Client不断发送数据,Server端接收数据。
Server端代码(创建、释放、挂接、解挂接)
#define PATH_NAME "/home/wzh" /// 该路径下一定要有权限
#define PROJ_ID 0x66
#define SHM_SIZE 4096 // 共享内存的大小,最好是页(4096)大小的整数倍
#include "comm.hpp"
string TransToHex(key_t k)
{
char buffer[32];
snprintf(buffer, sizeof(buffer), "0x%x", k);
return buffer;
}
int main()
{
key_t k = ftok(PATH_NAME, PROJ_ID);
Log("create key done", Debug) << "Server key:" << TransToHex(k) << endl;
// 2.创建共享内存
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget error");
exit(1);
}
// 3.将指定的共享内存,链接到自己的地址空间
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
Log("attcah shm done ", Debug) << "shmid : " << shmid << endl;
printf("shmaddr:%p\n", shmaddr);
//进行通信:
//服务端进行读取
sleep(3);
for (;;)
{
if (*(shmaddr + 55) == 'd')
break;
printf("%s\n", shmaddr);
sleep(1);
}
// 4.将指定的共享内存,从自己的地址空间中去关联
int n = shmdt(shmaddr);
Log("detach shm done", Debug) << "shmid : " << shmid << endl;
cout << "n:" << n << endl;
// 删除共享内存
shmctl(shmid, IPC_RMID, nullptr); // 选项IPC_RMID 功能是删除共享内存
Log("rm shm done", Debug) << "shmid : " << shmid << endl;
return 0;
}
然后我们编写Client端的代码
#include "comm.hpp"
int main()
{
// 1.创建公共的key值
key_t k = ftok(PATH_NAME, PROJ_ID);
Log("create key done", Debug) << "Client key:" << k << endl;
//2.寻找共享内存(IPC_CREAT)
int shmid = shmget(k, SHM_SIZE, IPC_CREAT);
if (shmid < 0)
{
Log("create shm done", Error) << "Client key:" << k << endl;
exit(2);
}
//3.挂接
char *shmaddr = (char *)shmat(shmid, nullptr, 0);
printf("shmaddr:%p\n", shmaddr);
// 4.使用
// 将共享内存看作一个char形的buffer
for (char a = 'a'; a <= 'z';a++)
{
// 每一次都向shmaddr[共享内存的起始地址进行写入]
snprintf(shmaddr, SHM_SIZE - 1,
"hello serve , 我是Client进程 我的pid:%d,inc:%c\n", getpid(), a);
sleep(1);
}
// 5.去关联
int n = shmdt(shmaddr);
assert(n != -1);
Log("detach shm success", Debug) << "Client key:" << k << endl;
return 0;
}
先运行Server端创建共享内存,然后让client去关联申请的共享内存,然后我们来观察现象:
现象如下:Server端和Client端成功挂接上共享内存:
接下来将代码运行起来,测试两个进程间能否通信成功:
堆栈相对而生,其中这块区域就是可以存放共享内存、内存映射和共享库,如果双方进程想进行通信,可以直接进行访问,也就是内存级的读和写。
之前在文件操作中,我们使用系统调用read、write这些操作,这些属于内核级操作。而共享内存的使用是直接在进程内部进行操作的,所以效率比管道高,其不用经过内核处理。
共享内存被创建,默认申请为全零。
结论1:只要是通信双方使用shm,一方直接向共享内存中写入数据,另一方面就可以立马看到数据。
共享内存是所有进程间通信(IPC)中速度最快的,不需要过多的拷贝!
共享内存是速度最快的拷贝方式,因为直接访问内存本质是减少了拷贝的次数。
结论2:共享内存缺乏访问控制!会带来并发问题。比如写方还没有将数据写完,读端就将数据读取了。
鉴于共享内存无法实现访问控制,我们可以额外创建管道来实现共享内存的访问控制。
首先我们使用 Init类,当创建对象时,就创建了fifo管道,当进程结束时fifo自动删除。
然后我们定义以下4个接口,OpenFIFO、Wait、Signal、Closefifo本质对应的是open打开fifo文件;Wait对应read等待数据的写入,无数据则阻塞;Signal对应Client端进行write写入数据;Closefifo调用close关闭文件。
class Init
{
public:
Init()
{
umask(0);
int n = mkfifo("./fifo", 0666);
assert(n == 0);
(void)n;
Log("create fifo success", Notice) << endl;
}
~Init()
{
unlink("./fifo");
Log("create fifo success", Notice) << endl;
}
};
//创建管道
Init init;
#define READ O_RDONLY
#define WRITE O_WRONLY
int OpenFIFO(string pathname, int flags)
{
Log("等待中……", Notice) << endl;
int fd = open(pathname.c_str(), flags);
assert(fd >= 0);
return fd;
}
void Wait(int fd)
{
uint32_t temp = 0;
ssize_t s = read(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
Log("唤醒中……", Notice) << endl;
}
void Signal(int fd)
{
uint32_t temp = 1;
ssize_t s = write(fd, &temp, sizeof(uint32_t));
assert(s == sizeof(uint32_t));
(void)s;
}
void Closefifo(int fd)
{
close(fd);
}
//Server端改动:
int fd = OpenFIFO("./fifo", READ);
for (;;)
{
Wait(fd);
printf("%s\n", shmaddr);
sleep(1);
if (strcmp(shmaddr, "quit") == 0)
{
printf("quit\n");
break;
}
}
Closefifo(fd);
//Client端改动:
int fd = OpenFIFO("./fifo", WRITE);
while (true)
{
ssize_t s = read(0, shmaddr, SHM_SIZE - 1);
Signal(fd);
if (strcmp(shmaddr, "quit") == 0)
break;
}
Closefifo(fd);
这样,server端就具有了访问控制,如果没有数据,则阻塞等待数据。