Bootstrap

Linux系统编程——System V 共享内存

目录

一、前言

二、共享内存概念

三、共享内存创建与使用 

1、共享内存的创建

系统调用—shmget()

2、共享内存的删除

1、使用命令行删除

2、使用系统接口来删除共享内存

四、进程间通信 

五、以管道控制共享内存的访问


一、前言

System是一种操作系统进程间通信的标准,标准主要有三种:

  1. System V 消息duilie
  2. System V 共享内存
  3. System V 信号量

我们下面主要研究的是 物理内存

二、共享内存概念

        在上一篇进程间的管道通信中我们提到过,在进程间进行通信的时候,由于程序地址空间的存在,进程间的独立性使得他们之间的通信很麻烦,如果想要通信则需要两个进程看到同一份资源(管道文件),而本节进程间通信时他们看到的同一份资源是  共享内存。

     那么共享内存是什么呢?

             实际上,我们在学习程序地址空间的时候,如上图所示,我们已经看到了有一个区域的名字是共享区,在之前我们学习动静态库的时候,就说过动态库是在进程运行的时候加载到程序地址空间中的共享区的。当程序需要的时候,就会来到这部分读取数据。这一块内存就可以看作是一块只读共享区,共享内存进程通信实际上就是这个原理。

             共享内存进程通信就是在物理内存中开辟一块可以让所有进程都看到的内存空间,然后多个进程只需要向这块空间中读取或者写入数据,这样就达到了多个进程间一起通信的目的。

也就是说,共享内存进程间的通信就是在物理内存中开辟一块空间当作共享内存,然后通信的进程们通过各自的页表将这块物理内存(共享内存)映射到各自的程序地址空间中。 

三、共享内存创建与使用 

1、共享内存的创建

系统调用—shmget()

共享内存:share memory(shm)

创建共享内存的系统调用 shmget()

可以看到这个接口的参数一共有三个

1、size_t size:该参数传入的是想要开辟的共享内存的大小,单位是 byte字节。值得注意的是系统是按照4KB大小为单位开辟空间的,因为我们在磁盘一篇中学到系统I/O的单位大小就是4KB,也就是说无论这个参数传的是1、1024还是4096时,系统都会开辟4KB,当传入4097时,系统就会开辟8KB。但是虽然系统是按照4KB为单位开辟的空间,但是实际上用户能使用的空间的大小还是传入的size字节大小。对于现有的共享内存段,这个参数会被忽略掉。

2、int shmflg:这里传入的是一组标识位,可以控制shemget的行为,它包括权限标志(类似0666)和命令标志,就像我们使用文件接口open时的O_WRONLY、O_RDONLY一样。共享内存接口标识符的两个最重要的宏是:IPC_CREAT、IPC_EXCL

IPC_CREAT:传入该宏,表示创建一个新的共享内存段,若共享内存段已经存在,则获取此内存段;若不存在就创建一个新的内存段。

IPC_EXCL:该宏需要和IPC_CREAT一起使用。表示如果创建的内存段不存在,则正常创建,否则返回错误。使用该宏保证的是此次使用shmget()接口创建成功时,创建出来的共享内存是全新的。

3、key_t key,这是一个键值,key_t是一个整型,此参数其实是传入的是一个整数。通常这个键是通过 ftok 函数从一个文件路径一个项目ID生成的。如果key 参数设置成 IPC_PRIVATE,则会创建一个新的唯一的共享内存段。

     这个key值其实就是共享内存段在操作系统层面的唯一标识符。共享内存是Linux系统的一种进程通信的手段, 而操作系统中共享内存段一定是有许多的, 为了管理这些共享内存段, 操作系统一定会描述共享内存段的各种属性。类似其他管理方式,共享内存也会为操作系统维护一个结构体,在这个结构体内会维护一个key值,表示此共享内存在系统层面的唯一标识符,其一般由用户传入,但是为了区别每一块的共享内存,key的获取也是有一定的方法的。

ftok()函数的作用是将 一个文件 和 项目id 转换为一个System V IPC key值。用户就是使用这个函数来生成key值。

他有两个参数,第一个参数显而易见是文件的路径,第二个参数则是随意的8bite位的数值。ftok()函数执行成功则会返回一个key值,这个key值是该函数通过传入文件的inode值和传入的proi_id值通过一定的算法计算出来的。由于每一个文件的inode值是唯一的,所以我们不用担心key值得重复。

shmget()接口的返回值:

如果创建共享内存成功或者找到共享内存则返回共享内存的id,该id的作用是可以让通信的进程找到同一份块的资源。

简单使用

运行结果:

当我们退出进程之后,再次运行结果,发现会报错

这是什么原因呢?事实上,共享内存并不会随着进程的退出而退出,在创建共享内存的进程退出之后,共享内存是依旧存在于操作系统中的。

我们可以通过命令查看:

因为我们以当前的key值创建的共享内存已经存在了,所以不能再以相同的key值创建了,之后再需要创建新的共享内存的时候,就需要先删除嗲之前已经创建的存在的共享内存。

2、共享内存的删除

共享内存的删除我们有着两种方法:系统的调用接口:shmctl() 和 操作行命令 ipcrm -m

1、使用命令行删除

ipcrm,是一个用于删除进程通信相关内容的命令

后面的跟的 -m 的选项表示删除共享内存。ipcrm -m

我们删除上面创建的共享内存

2、使用系统接口来删除共享内存

shmctl()接口是用来控制共享内存的,通过传入不同的选项来控制共享内存。

该接口一共有三个参数。

  1. int shmid:表示是需要删除的共享内存的id,就是创建共享内存的shmget()接口的返回值。
  2. int cmd:这个参数就是传入的控制共享内存的参数。其中有一个是专门用来删除共享内存的:IPC_RMID
  3. struct shmid_ds *buf,这里需要传入一个指针,指针应该指向一个 shmid_ds 结构体,此结构体的内容是:

我们在删除共享内存的时候,一般用不上这个,所以在删除共享内存的时候,只需要传入 nullptr 

运行结果:

所以我们让通信的进程看到创建出来的共享资源,实际上就是将物理内存中的共享内存通过页表映射到程序地址空间的共享区中,此时共享内存已经创建好了,但是我们还需要让通信的进程看到这一部分内存才行。让通信的进程看到共享内存的方式被称为 attach(链接、挂载),我们可以使用系统调用 shmat 来链接。

共享内存链接接口shmat()

shmat即share memory attach

可以看到该接口有着三个参数

1、int shmid:显而易见了,就是想要链接的共享内存id,即创建共享内存shmget()接口的返回值。

2、const void *shmaddr:这里传入一个地址,此参数是用来指定链接地址的 ,通常可以选择传入 nullptr ,即自动选择链接地址。如果传入的不是nullptr,那么就要根据第三个参数中是否传入了SHM_RND来决定链接地址。

  • 如果第三个参数传入了SHM_RND,则就以传入的 shamaddr 作为链接地址。否则链接的地址会自动向下调整为SHMLBA的整数倍 shmaddr  - (shmaddr % SHMLBA)

3、int shmflg:这里需要传入宏,一般会使用两个宏 SHM_RNDSHM_RDONLY;前者是和后者配合使用的,后者表示链接只读共享内存。

返回值:

shmat()链接共享内存成功之后会返回一个地址,该地址与我们C++中的开辟空间的malloc和new相同,需要根据接受地址的数据类型来进行类型强转,进而控制数据的读取或者写入格式。

四、进程间通信 

下面我们就利用共享内存来实现进程间的通信

//common.hpp
#include<iostream>
#include<unistd.h>
#include<sys/ipc.h>
#include<sys/shm.h>

#define SHM_SIZE 4096
#define PATH_NAME ".shm_test"
#define PROJ_ID 0X14
//shmserver.cpp
#include"common.hpp"
using std::cout;
using std::cerr;
using std::endl;

int main()
{
    key_t key=ftok("PATH_NAME",PROJ_ID);
    cout<<"Start creat share memory!"<<endl;

    int shmid=shmget(key,SHM_SIZE,IPC_CREAT|IPC_EXCL|0666);
    if(shmid==-1)
    {
        cerr<<"shmget error!"<<endl;
        exit(1);
    }
    cout<<"Share memory had creat success!shmid::"<<shmid<<"  key::"<<key<<endl;
    
    sleep(2);
    char* str=(char*)shmat(shmid,nullptr,0);
    if(str==(void*)-1)
    {
        cerr<<"shmat error!"<<endl;
        exit(2);
    }
    cout<<"Shmat success!"<<endl;

    while(true)
    {
        cout<<str<<endl;
        sleep(1);
    }

    int ret=shmctl(shmid,IPC_RMID,nullptr);
    if(ret!=-1)
    {
        cerr<<"shmat error!"<<endl;
        exit(2);
    }
    return 0;
}

 

//shmclient.cpp
#include"common.hpp"
using std::cout;
using std::endl;
using std::cerr;


int main()
{
    key_t key=ftok(PATH_NAME,PROJ_ID);
    cout<<"Start share memory!"<<endl;
    sleep(1);
    int shmid=shmget(key,SHM_SIZE,IPC_CREAT);
    if(shmid==-1)
    {
        cerr<<"shmget error!"<<endl;
        exit(2);
    }
    cout<<"shmget success!shmid::"<<shmid<<"  key::"<<key<<endl;

    sleep(2);
    char *str=(char*)shmat(shmid,nullptr,0);
    if(str==(void*)-1)
    {
        cerr<<"shmat error!"<<endl;
        exit(2);
    }
    cout<<"Shmat success!"<<endl;

    int cnt=0;
    while(true)
    {
        str[cnt]='A'+cnt;
        cnt++;
        str[cnt]='\0';
        sleep(1);
    }
    return 0;
}
//makefile
.PHONY:all
all:myserver myclient
myserver:shmserver.cpp
	g++ -std=c++11 -o $@ $^
myclient:shmclient.cpp
	g++ -std=c++11 -o $@ $^
.PHONY:clean
clean:
	rm -f myserver myclient

运行结果:

 对于这个运行结果,我们最直接的能看出来的是读取端会不断打印共享内存的內容,连带着之前的內容一起打印,这说明与管道文件的那种读取完就清除缓存的方式不同,在共享内存中,读取完数据之后,数据并不会被自动清除,共享内存段更像是一块普通的内存区域,读取操作并不会影响原先的数据。


下面我们分别在进程通信时、终止一个进程时、终止两个进程时利用ipcs -m命令观察共享内存段信息: 

 可以看到nattach这个值在变化,显而易见nattach是共享内存的连接数,其是通过shmat()接口链接上的,同样也会有取消链接共享内存块的接口:shmdt()

该接口的作用是取消进程与共享内存块之间的链接,它的参数只有一个,即shmat()接口调用成功之后的返回值,也就是进程和共享内存块成功链接的链接地址。当成功分离后,返回0,否则返回-1.

所以说shmat()接口实际上是为了让进程能看到共享内存块,而shmdt()则是将共享内存块的地址隐藏起来不让进程找到。所以我们在进程通信之后,在删除共享内存块之前还需要分离进程和共享内存块。

修改后的源文件:

//shmserver.cpp
#include"common.hpp"
using std::cout;
using std::cerr;
using std::endl;

int main()
{
    key_t key=ftok("PATH_NAME",PROJ_ID);
    cout<<"Start creat share memory!"<<endl;

    int shmid=shmget(key,SHM_SIZE,IPC_CREAT);
    if(shmid==-1)
    {
        cerr<<"shmget error!"<<endl;
        exit(1);
    }
    cout<<"Share memory had creat success!shmid::"<<shmid<<"  key::"<<key<<endl;
    
    sleep(2);
    char* str=(char*)shmat(shmid,nullptr,0);
    if(str==(void*)-1)
    {
        cerr<<"shmat error!"<<endl;
        exit(2);
    }
    cout<<"Shmat success!"<<endl;

    while(true)
    {
        cout<<str<<endl;
        sleep(1);
    }

    int dtret=shmdt(str);
    if(dtret==-1)
    {
        cerr<<"Dettach fail!"<<endl;
        exit(2);
    }
    cout<<"Dettach success!"<<endl;

    int ret=shmctl(shmid,IPC_RMID,nullptr);
    if(ret!=-1)
    {
        cerr<<"shmctl error!"<<endl;
        exit(2);
    }
    cout<<"Shmctl success!"<<endl;
    return 0;
}
//shmclient.cpp
#include"common.hpp"
using std::cout;
using std::endl;
using std::cerr;


int main()
{
    key_t key=ftok(PATH_NAME,PROJ_ID);
    cout<<"Start share memory!"<<endl;
    sleep(1);
    int shmid=shmget(key,SHM_SIZE,IPC_CREAT);
    if(shmid==-1)
    {
        cerr<<"shmget error!"<<endl;
        exit(2);
    }
    cout<<"shmget success!shmid::"<<shmid<<"  key::"<<key<<endl;

    sleep(2);
    char *str=(char*)shmat(shmid,nullptr,0);
    if(str==(void*)-1)
    {
        cerr<<"shmat error!"<<endl;
        exit(2);
    }
    cout<<"Shmat success!"<<endl;

    int cnt=0;
    while(true)
    {
        str[cnt]='A'+cnt;
        cnt++;
        str[cnt]='\0';
        sleep(1);
    }

    int dtret=shmdt(str);
    if(dtret==-1)
    {
        cerr<<"Dettach fail!"<<endl;
        exit(2);
    }
    cout<<"Dettach success!"<<endl;
    return 0;
}

五、以管道控制共享内存的访问

 上面提到过,共享内存区别于管道通信,管道通信是是有一定的访问控制的,比如在管道中有数据的时候才会去读,当没有数据的时候则开始等待。但是共享内存没有类似的访问控制机制,使用共享内存块进行进程间的通信是最快的。所以下来我们要利用管道的自带访问控制的特点来控制共享内存的读取。

  • 我们在向共享内存中写入数据的同时也向管道中写入数据,向管道中写入的数据不需要有意义,只是作为触发控制的条件。
  • 在需要从共享内存中读取数据的时候,我们先从管道中读取数据,若管道中没有数据,此时等待。
  • 所以我们在对管道写入和读取数据的时候,实际上是将管道中的数据作为信号,客户端写入数据,说明此时共享内存中已经存在数据,可以进行读取。
//common.hpp
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <cerrno>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string>
#include <unistd.h>
#include <cassert>

#define SHM_SIZE 4096
#define PATH_NAME ".fifo"
#define PROJ_ID 0x14

#define FIFO_FILE ".fifo"

// 创建命名管道文件
void CreatFifo() {
    umask(0);
    if(mkfifo(FIFO_FILE, 0666) < 0)
    {
        std::cerr << strerror(errno) << std::endl;
        exit(-1);
    }
}

#define READER O_RDONLY
#define WRITER O_WRONLY

// 以一定的方式打开管道文件
int Open(const std::string &filename, int flags)
{
    return open(filename.c_str(), flags);
}

// 用于服务端, 等待读取管道文件数据, 即读取信号
int Wait(int fd) {
    uint32_t value = 0;
    ssize_t res = read(fd, &value, sizeof(value));
    
    return res;
}

// 用于客户端, 向管道中写入数据, 即写入信号
void Signal(int fd) {
    uint32_t cmd = 1;
    write(fd, &cmd, sizeof(cmd));
}

// 关闭管道文件, 删除管道文件
void Close(int fd, const std::string& filename) {
    close(fd);
    unlink(filename.c_str());
}
//server.cpp
// ipcShmServer 服务端代码, 即 接收端
// 需要创建、删除共享内存块
// 需要创建、删除命名管道
#include "common.hpp"
using std::cout;
using std::endl;
using std::cerr;

int main() {
    // 0. 创建命名管道
    CreatFifo();
    int fd = Open(FIFO_FILE, READER);       // 只读打开命名管道
    assert(fd >= 0);

    // 1. 创建共享内存块
    int key = ftok(PATH_NAME, PROJ_ID);
    if(key == -1) {
        cerr << "ftok error. " << strerror(errno) << endl;
        exit(1);
    }

    cout << "Create share memory begin. " << endl;
    int shmId = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if(shmId == -1) {
        cerr << "shmget error" << endl;
        exit(2);
    }
    cout << "Creat share memory success, key: " << key << " , shmId: " << shmId << endl;

    // 2. 连接共享内存块
    sleep(2);
    char* str = (char*)shmat(shmId, nullptr, 0);
    if(str == (void*)-1) {
        cerr << "shmat error" << endl;
        exit(3);
    }
    cout << "Attach share memory success. \n" << endl;

    // 3. 使用共享内存块
    while(true) {
        if (Wait(fd) <= 0)
            break;              // 如果从管道读取数据失败, 或管道文件关闭, 则退出循环
        
        cout << str;
        sleep(1);
    }
    cout << "\nThe server has finished using shared memory. " << endl;

    sleep(1);
    // 3. 分离共享内存块
    int resDt = shmdt(str);
    if(resDt == -1) {
        cerr << "shmdt error" << endl;
        exit(4);
    }
    cout << "Detach share memory success. \n" << endl;

    // 4. 删除共享内存块
    int res = shmctl(shmId, IPC_RMID, nullptr);
    if(res == -1) {
        cerr << "shmget error" << endl;
        exit(5);
    }
    cout << "Delete share memory success. " << endl;

    // 5. 删除管道文件
    Close(fd, FIFO_FILE);
    cout << "Delete FIFO success. " << endl;

    return 0;
}
//client.cpp
// ipcShmServer 服务端代码, 即 接收端
// 需要创建、删除共享内存块
// 需要创建、删除命名管道
#include "common.hpp"
using std::cout;
using std::endl;
using std::cerr;

int main() {
    // 0. 创建命名管道
    CreatFifo();
    int fd = Open(FIFO_FILE, READER);       // 只读打开命名管道
    assert(fd >= 0);

    // 1. 创建共享内存块
    int key = ftok(PATH_NAME, PROJ_ID);
    if(key == -1) {
        cerr << "ftok error. " << strerror(errno) << endl;
        exit(1);
    }

    cout << "Create share memory begin. " << endl;
    int shmId = shmget(key, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
    if(shmId == -1) {
        cerr << "shmget error" << endl;
        exit(2);
    }
    cout << "Creat share memory success, key: " << key << " , shmId: " << shmId << endl;

    // 2. 连接共享内存块
    sleep(2);
    char* str = (char*)shmat(shmId, nullptr, 0);
    if(str == (void*)-1) {
        cerr << "shmat error" << endl;
        exit(3);
    }
    cout << "Attach share memory success. \n" << endl;

    // 3. 使用共享内存块
    while(true) {
        if (Wait(fd) <= 0)
            break;              // 如果从管道读取数据失败, 或管道文件关闭, 则退出循环
        
        cout << str;
        sleep(1);
    }
    cout << "\nThe server has finished using shared memory. " << endl;

    sleep(1);
    // 3. 分离共享内存块
    int resDt = shmdt(str);
    if(resDt == -1) {
        cerr << "shmdt error" << endl;
        exit(4);
    }
    cout << "Detach share memory success. \n" << endl;

    // 4. 删除共享内存块
    int res = shmctl(shmId, IPC_RMID, nullptr);
    if(res == -1) {
        cerr << "shmget error" << endl;
        exit(5);
    }
    cout << "Delete share memory success. " << endl;

    // 5. 删除管道文件
    Close(fd, FIFO_FILE);
    cout << "Delete FIFO success. " << endl;

    return 0;
}
//makefile
.PHONY:all
all:myserver myclient
myserver:server.cpp
	g++ -std=c++11 -o $@ $^
myclient:client.cpp
	g++ -std=c++11 -o $@ $^
.PHONY:clean
clean:
	rm -f myserver myclient .fifo

 运行结果:

;