两个进程,操作系统在内存空间中创建一个共享内存。在之前学习库的时候,有一个共享库的概念。我们可以按照其概念来了解共享内存的概念:将共享内存映射到页表中,和进程的地址空间建立联系。我们可以将共享内存的虚拟地址交给用户。两个进程之间就可以通过虚拟地址找到同一个共享内存进行通信。
- 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 */
- };
函数的原型:
函数的参数:
- key:这个共享内存端的名字
- size:共享内存的大小
- shmfig:由九个权限表标志构成,用法和创建文件时使用的mode模式标志是一样的,利用位图进行传参。
在这里的标识码有两个:IPC_CREAT和IPC_EXCL。
- IPC_CREAT:如果内核中不存在键值和key相同的共享内存,则新建一个共享内存;如果存在这样的共享内存,则返回该共享内存的标识码(可以获得一个存在的共享内存)
- IPC_EXCL:单独使用没有意义,只有和IPC_CREAT一起使用才有意义
- IPC_CREAT | IPC_EXCL:如果内核中不存在键值和key相同的共享内存,则新建一个共享内存;如果存在这样的共享内存,则报错(如果我成功返回,这个shm则是一个新的共享内存)
函数的返回值:
成功则返回一个非负整数,即给共享内存段的标识码;失败则返回-1
ftok函数的原型:
函数的功能:
系统建立IPC通讯(如消息队列、共享内存等) 必须指定一个ID值。通常情况下,该id值通过ftok函数得到。
函数的参数:
- pathname:必须是一个已经存在且程序可范围的文件。
- proj_id:虽然定义为一个整数,其实实际只有8个bit位有效,即如果该参数大于255,则只有后8bit有效。
函数的返回值:
当函数执行成功,则会返回key_t键值,否则返回-1。在一般的UNIX中,通常是将文件的索引节点取出,然后在前面加上子序号就得到key_t的值。
shmctl函数的原型:
函数的功能:
用来控制共享内存
函数的参数:
- shmid:有shmget函数返回的共享内存的标识码
- cmd:将要采取的动作(有三个可取值)
命令 说明 IPC_STAT 把shmid_ds结构中的数据设置为共享内存的当前关联值 IPC_SET 在进程有足够权限的前提下,把共享内粗的当前关联值设为shmid_ds数据结构中给出的值 IPC_RMID 删除共享内存段
- buf:指向一个保存着恭喜昂内存的模式状态和访问权限的数据结构
函数的返回值:
成功返回0,失败返回-1
函数的原型:
函数的功能:
将创建好的共享内存连接到某个进程,并指定内存空间
函数的参数:
- shmid:shmget返回的共享内存标识
- shmaddr:把共享内存连接到当前进程去的时候准备放置它的那个地址
shmaddr:把共享内存连接到当前进程去的时候准备放置它的那个地址,如果设为NULL为让系统自动选择。 shmaddr为NULL,核心自动选择一个地址 shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。 shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式: shmaddr - (shmaddr % SHMLBA)
- shmflg:其是一组按位OR(或)在一起的标志。它的两个可能取值是SHM_RND和SHM_RDONLY,shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
函数的返回值:
如果成功返回一个指针,指向共享内存的第一个字节;如果失败,则返回-1
函数的原型:
函数的功能:
将共享内存段与当前进程脱离
函数的参数
- shmaddr:由shmat返回的指针
函数的返回值:
如果成功返回0;失败则返回-1
只要拥有相同的文件路径和一个数字,我们可以使用这个函数来使服务端和客户端形成的key是一样的,代码如下:
- key_t GetCommonKey(const std::string& pathname, const int proj_id)
- {
- key_t k = ftok(pathname.c_str(), proj_id);
-
- if(k < 0)
- {
- perror("GetCommonKey");
- }
- return k;
- }
共享内存不会随着进程的结束而自动释放,如果不进行释放,该共享内存会一直存在,需要手动进行释放, 共享内存的生命周期随内核 | 文件的生命周期是随进程的。
- ipcs -m; // 进行共享内存的查看
- ipcrm -m key // 进行共享内存的删除
key属于用户形成,内核使用的字段,用户不能使用key来进行shm的管理,内核进行区分shm的唯一性的
shmid属于内核给用户返回的一个标识符,用来进行用户及对共享内存进行管理的id值。
- // 获取一个相同的key值,需要访问一个相同的文件路径和一个随便的数字
- const std::string pathname = "/home/hrx/code/111/lesson3/shm";
- const int proj_id = 0x66;
-
- key_t GetKeyCommon()
- {
- key_t k = ftok(_pathname.c_str(), _proj_id);
-
- if (k < 0)
- {
- perror("GetCommonKey");
- }
- return k;
- }
- int Shmget(int key, int size, int flag) // 将key值的共享内存创建出来
- {
- int shmid = shmget(key, size, flag);
- if (shmid < 0)
- {
- perror("shmid");
- }
- return shmid;
- }
-
- std::string ToHex(int u) // 将一个数字转换为十六进制的格式
- {
- char buff[128];
- snprintf(buff, sizeof buff, "0x%x", u);
- return buff;
- }
-
- Shm(const std::string &pathname, int proj_id, int who) // 构造函数
- : _pathname(pathname), _proj_id(proj_id), _who(who)
- {
- _key = GetKeyCommon();
- if (who == Creater)
- GetShmCeaterUse();
- else
- GetShmCeaterUse();
- }
-
- bool GetShmCeaterUse()
- {
- if (_who == Creater)
- {
- _shmid = Shmget(_key, 1024, IPC_CREAT | IPC_EXCL);
- sleep(10);
- if (_shmid > 0)
- return true;
- }
- return false;
- }
-
- bool GetShmForUse()
- {
- if (_who == User)
- {
- _shmid = Shmget(_key, 1024, IPC_CREAT);
- if (_shmid > 0)
- return true;
- }
- return false;
- }
- ~Shm()
- {
- if (_who == Creater)
- {
- // 进行共享库的删除
- int res = shmctl(_shmid, IPC_RMID, nullptr);
- }
- std::cout << "Shm remove done..." << std::endl;
- }
- void* Attach()
- {
- void* attaddr = shmat(_shmid, nullptr, 0);
- if(attaddr == nullptr)
- {
- perror("attaddr");
- }
- std::cout << "name: " << GetName() << " attach shm..." << std::endl;
- return attaddr;
- }
- void Dattach(const char* addr)
- {
- if(addr == nullptr) return;
- shmdt(addr);
- std::cout << "name: " << GetName() << "dis attach shm..." << std::endl;
- }
刚开始的时候,我们会发现在共享内存的页面没有任何数字的变化,因为共享内存的全西安没有关闭,我们需要在创建共享内存的时候,为共享内存的权限设为可以进行操作。
- // 修改共享内存的代码:
- _shmid = Shmget(_key, 1024, IPC_CREAT | IPC_EXCL | 0x666);
当我们开始实行代码后,会发现这个共享内存的挂接数量从0到1到2,最后,又逐渐减少。注意: 将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。
在服务器端和客户端一起进行挂接,下面是现象:
- #pragma once
-
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
- #include
-
- #define Creater 1
- #define User 2
- const std::string pathname = "/home/hrx/code/111/lesson3/shm";
- const int proj_id = 0x66;
-
- class Shm
- {
- public:
- key_t GetKeyCommon()
- {
- key_t k = ftok(_pathname.c_str(), _proj_id);
-
- if (k < 0)
- {
- perror("GetCommonKey");
- }
- return k;
- }
-
- int Shmget(int key, int size, int flag)
- {
- int shmid = shmget(key, size, flag);
- if (shmid < 0)
- {
- perror("shmid");
- }
- return shmid;
- }
-
- public:
-
- int getkey()
- {
- return _key;
- }
-
- int getshmid()
- {
- return _shmid;
- }
-
- std::string GetName()
- {
- if(_who == Creater) return "Creater";
- else if(_who == User) return "User";
- else return "None";
- }
-
- std::string ToHex(int u)
- {
- char buff[128];
- snprintf(buff, sizeof buff, "0x%x", u);
- return buff;
- }
-
- Shm(const std::string &pathname, int proj_id, int who)
- : _pathname(pathname), _proj_id(proj_id), _who(who)
- {
- _key = GetKeyCommon();
- if (who == Creater)
- GetShmCeaterUse();
- else
- GetShmForUse();
- _addr = Attach(); // 直接进行挂接
- }
-
- bool GetShmCeaterUse()
- {
- if (_who == Creater)
- {
- _shmid = Shmget(_key, 1024, IPC_CREAT | IPC_EXCL | 0666);
- std::cout << "key:" << _key << ' ' << "shmid:" << _shmid << std::endl;
- if (_shmid > 0)
- return true;
- }
- return false;
- }
-
- bool GetShmForUse()
- {
- if (_who == User)
- {
- _shmid = Shmget(_key, 1024, IPC_CREAT);
- std::cout << "key:" << _key << ' ' << "shmid:" << _shmid << std::endl;
- if (_shmid > 0)
- return true;
- }
- return false;
- }
-
- ~Shm()
- {
- if (_who == Creater)
- {
- // 进行共享库的删除
- int res = shmctl(_shmid, IPC_RMID, nullptr);
- }
- std::cout << "Shm remove done..." << std::endl;
- }
-
- void* Addr()
- {
- return _addr;
- }
-
- void Zero()
- {
- if(_addr)
- memset(_addr, 0, sizeof _addr);
- }
-
- void* Attach()
- {
- if(_addr != nullptr) Dattach((char*)_addr);
- void* attaddr = shmat(_shmid, nullptr, 0);
- if(attaddr == nullptr)
- {
- perror("attaddr");
- }
- std::cout << "name: " << GetName() << " attach shm..." << std::endl;
- return attaddr;
- }
-
- void Dattach(const char* addr)
- {
- if(addr == nullptr) return;
- shmdt(addr);
- std::cout << "name: " << GetName() << "dis attach shm..." << std::endl;
- }
-
- private:
- key_t _key;
- int _who;
- int _shmid;
- void* _addr;
- const std::string _pathname;
- const int _proj_id;
- };
在将服务端与客户端进行挂接成功后,我们要来进行通信,将共享内存看成数组来进行通信:
服务端:
Shm shm(pathname, proj_id, Creater); char* shmaddr = (char*)shm.Addr(); while (true) { std::cout << shmaddr << std::endl; sleep(1); }
客户端
Shm shm(pathname, proj_id, User); shm.Zero(); char* shmaddr = (char*)shm.Addr(); sleep(3); char ch = 'a'; while (ch <= 'z') { shmaddr[ch - 'a'] = ch; ch++; sleep(2); }
在进行共享内存的使用时,共享内存不提供任何保护机制,数据不一致问题。我在写一个字符串,但是还没有写完就被读取了一半,造成了数据不一致问题。我们在写共享内存没有使用任何系统调用,共享内存是进程间通信速度最快的,因为共享内存大大地减少了拷贝次数。
管道提供了保护措施,所以,我们可以建立一个管道,将数据放在管道中,当写完后,直接将数据发送给另一个进程,共享内存的容量大小:如果容量大小为4097,共享内存的大小可以i自己随意定制,但是建议共享内存的大小为4096的整数倍。,如果不是整数倍,操作系统会给你整数倍的容量。
- // 服务器端
- Shm shm(pathname, proj_id, Creater);
- char* shmaddr = (char*)shm.Addr();
-
- // 2.创建管道
- NamePipe fifo(comepath, Creater);
-
- fifo.openforread();
-
- while (true)
- {
- std::string tmp;
- fifo.ReadNamePipe(&tmp);
- std::cout << shmaddr << std::endl;
- }
- // 客户端
- NamePipe fifo(comepath, User);
- fifo.operforwrite();
- char ch = 'a';
- while (ch <= 'z')
- {
- std::string tmp = "once";
- fifo.WriteNamePipe(tmp);
- shmaddr[ch - 'a'] = ch;
- ch++;
- sleep(2);
- }
当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。
我们先来看看管道通信:
从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:
我们再来看看共享内存通信:
从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:
所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。
但是共享内存也是有缺点的,我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。