目录
System V共享内存
共享内存数据结构
共享内存函数
共享内存的关联
共享内存的去关联
用共享内存实现serve&client通信
共享内存与管道进行对比
System V共享内存
管道通信本质是基于文件的,也就是说操作系统并没有为此做过多的设计工作,而system V IPC是操作系统特地设计的一种通信方式。但是不管怎么样,它们的本质都是一样的,都是在想尽办法让不同的进程看到同一份由操作系统提供的资源。
system V IPC提供的通信方式有以下三种:
system V共享内存
system V消息队列
system V信号量
其中,system V共享内存和system V消息队列是以传送数据为目的的,而system V信号量是为了保证进程间的同步与互斥而设计的,虽然system V信号量和通信好像没有直接关系,但属于通信范畴。
共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。
这样两个进程就可以看到一块相同的资源。当一个进程往该空间写入内容时,另外一进程访问该空间,会得到写入的值,即实现了进程间的通信。
共享内存数据结构
共享内存实现进程间通信不只仅限于两个进程之间,可以用于多个进程之间。并且系统中可能会有多个进程在进行多个通信。所以系统需要将这些通信的进程管理起来。如果不管理,操作系统怎么知道这块共享内存挂接了哪个进程等信息。这里就用到了先描述,再组织。
这里可以简单看一下管理共享内存数据结构的代码:
/* Obsolete, used only for backwards compatibility and libc5 compiles */
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值,这个key值用于标识系统中共享内存的唯一性。
可以看到上面共享内存数据结构的一个成员是shm_perm,shm_perm是一个ipc_perm类型的结构体变量,每个共享内存的key值存储在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;
};
共享内存的建立与释放
共享内存的建立大致包括以下两个过程:
在物理内存当中申请共享内存空间。
将申请到的共享内存挂接到地址空间,即建立映射关系。
共享内存的释放大致包括以下两个过程:
将共享内存与地址空间去关联,即取消映射关系。
释放共享内存空间,即将物理内存归还给系统。
共享内存函数
- ftok函数
其中:
pathname
:是一个指向文件名的指针,该文件必须是存在且可以访问的。这个文件名可以是相对路径或绝对路径。proj_id
:是一个整数,通常用作项目的唯一标识符。在UNIX系统中,这个参数虽然是int
类型,但实际上只使用8位(即0~255)。不能是0。
ftok中的参数可以随便填写,但是要符合格式,ftok只是利用参数,再运用一套算法,算出一个唯一的key值返回。这个key值可以传给共享内存参数,作为struct ipc_perm中唯一标识共享内存的key。
ftok函数并没有涉及内核层面。
使用示例:
int n = ftok(_pathname.c_str(), _proc_id);
if (n ==-1 )
{perror("ftok");
}
共享内存的创建
创建共享内存我们需要用shmget函数,shmget函数的函数原型如下:
参数:
key:为共享内存的名字,一般是ftok的返回值。
size:共享内存的大小,以page为单位,大小为4096的整数倍。
shmflg:权限标志,常用两个IPC_CREAT和IPC_EXCL,一般后面还加一个权限,相当于文件的权限。
IPC_CREAT:创建一个共享内存返回,已存在打开返回
IPC_EXCL:配合着IPC_CREAT使用,共享内存已存在出错返回
使用:IPC_CREAT | IPC_EXCL | 0666
注意这里我们创建的时候,要加权限,权限或在shmflg中,如果没有权限的话,后面可能会关联失败。
返回值:
成功返回一个有效的共享内存标识符(用户层标识符),失败返回-1。
使用示例:
int main() { key_t key = ftok("/tmp", 0x66); // 使用ftok生成一个唯一的key int shmid = shmget(key, 1024, 0666 | IPC_CREAT|IPC_EXCL); // 创建一个大小为1024字节的共享内存段且是全新的 if (shmid == -1) { perror("shmget failed"); } printf("Shared memory segment ID: %d\n", shmid); // ... 后续可以使用shmat等函数操作共享内存 ... // 最后,应使用shmctl(shmid, IPC_RMID, NULL)来删除共享内存段 return 0;
}
当我执行两次程序,会发现,第一次程序没有报错,第二次程序报错了,错误信息为:文件存在。
这是因为我们在进程运行过程中创建了一个共享内存空间,但是在程序退出时并没有释放共享内存,再次执行程序时,共享内存依然存在,而我们用的参数组合是:IPC_CREAT | IPC_EXCL 。表示文件存在就出错返回。
当我们的进程运行完毕后,申请的共享内存依旧存在,并没有被操作系统释放。实际上,管道是生命周期是随进程的,而共享内存的生命周期是随内核的,也就是说进程虽然已经退出,但是曾经创建的共享内存不会随着进程的退出而释放。
这说明,如果进程不主动删除创建的共享内存,那么共享内存就会一直存在,直到关机重启(system V IPC都是如此),同时也说明了IPC资源是由内核提供并维护的。
此时我们若是要将创建的共享内存释放,有两个方法,一就是使用命令释放共享内存,二就是在进程通信完毕后调用释放共享内存的函数进行释放。
这里得出一个结论:IPC(进程将通信)资源生命周期不随进程,而是随内核的,不释放会一直占用,除非重启。所以,shmget创建的共享内存要释放掉,不然会内存泄漏。
这里我们可以执行命令行来释放共享内存:ipcrm -m shmid(shmget返回值)
这里介绍 ipcs命令:
单独使用ipcs命令时,会默认列出消息队列、共享内存以及信号量相关的信息,若只想查看它们之间某一个的相关信息,可以选择携带以下选项:
-q:列出消息队列相关信息。
-m:列出共享内存相关信息。
-s:列出信号量相关信息。
上面标题含义:
key 系统区别各个共享内存的唯一标识
shmid 共享内存的用户层id
owner 共享内存的拥有者
perms 共享内存的权限
bytes 共享内存的大小
nattch 关联共享内存的进程数
status 共享内存的状态
注意: key是在内核层面上保证共享内存唯一性的方式,而shmid是在用户层面上保证共享内存的唯一性,key和shmid之间的关系类似于fd和FILE*之间的的关系。
共享内存的释放
- shmctl函数
参数
- shmid:共享内存标识符,即要控制的共享内存段的标识符。这个值通常是通过调用shmget()函数获得的。
- cmd:控制命令,用于指定要执行的操作。常用的命令包括IPC_STAT(获取共享内存段的状态)、IPC_SET(修改共享内存段的属性)和IPC_RMID(删除共享内存段)等。
- buf:指向struct shmid_ds结构的指针,用于存储共享内存段的信息。当cmd为IPC_STAT时,该结构用于接收共享内存段的状态信息;当cmd为IPC_SET时,该结构中的信息将被用于更新共享内存段的属性。如果不需要获取或设置共享内存段的信息,可以将此参数设置为NULL。
返回值
- 成功时,shmctl函数返回0。
- 失败时,返回-1,并设置errno以指示具体的错误原因。
使用示例
int main() { key_t key = ftok("somefile", 0x65); // Generate a unique key based on file path int shmid = shmget(key, 1024, 0666 | IPC_CREAT); // Create a new shared memory segment if (shmid == -1) { perror("shmget"); // Print error message if shmget fails return 1; } // Remove the shared memory segment if (shmctl(shmid, IPC_RMID, NULL) == -1) { perror("shmctl"); // Print error message if shmctl fails return 1; } return 0;
}
用shmctl命令之后就可以删除释放共享内存,执行多次也不会发生内存泄漏。
我们可以在程序运行时,使用以下监控脚本时刻关注共享内存的资源分配情况:
while :; do ipcs -m;echo "###################################";sleep 1;done
共享内存的关联
shmat函数
作用:将共享内存与进程地址空间的映射起来,构建页表映射关系,物理内存加载到进程地址空间。
函数参数:
- shmid:共享内存标识符,由shmget函数返回。
- shmaddr:指定共享内存出现在进程内存地址的什么位置。如果设置为NULL,则由内核自动选择一个合适的地址位置。
- shmflg:控制共享内存的附加方式和行为。常用的标志位包括:
- SHM_RDONLY:以只读方式附加共享内存段。如果省略此标志,则默认以读写方式附加。
- SHM_RND:将附加地址舍入到最接近的SHMLBA(共享内存锁定字节对齐)的倍数,以提高性能。
- SHM_REMAP:如果该区域已经附加到调用进程的地址空间,则重新附加它。
- SHM_EXEC:将共享内存段标记为可执行(这在某些系统上可能不被支持)。
函数返回值:
- 成功:返回指向共享内存第一个字节的指针。这里与malloc相似
- 失败:返回-1,并将错误代码存储在errno中。
使用示例:
// 附加共享内存到进程地址空间 char *shmaddr; shmaddr = shmat(shmid, NULL, 0); if (shmaddr == (char *)-1) { perror("shmat"); }
注意这里是void*类型的-1 。
另外这里要注意权限问题,如果没有共享内存没有权限的话,可能会关联失败。
如果没有加上权限0666,则会出现下面的报错
当加入权限时,此时再运行程序,即可发现关联该共享内存的进程数由0变成了1,而共享内存的权限显示也不再是0,而是我们设置的666权限。
我们应该在使用shmget函数创建共享内存时,在其第三个参数处设置共享内存创建后的权限,权限的设置规则与设置文件权限的规则相同。
共享内存的去关联
取消共享内存与进程地址空间之间的关联我们需要用shmdt函数,shmdt函数的函数原型如下:
- shmdt函数
作用:删除共享内存与进程地址空间的映射关系,将页表映射关系删除,释放进程地址空间。
参数:
- shmaddr:这是需要分离的共享内存段的起始地址,该地址应该是之前通过shmat函数成功附加到当前进程地址空间的共享内存段的地址。
返回值:
- 成功时,shmdt函数返回0。
- 失败时,返回-1,并设置相应的错误码(errno)以指示错误原因
注意事项:
- shmdt函数只是将共享内存从当前进程的地址空间中分离出来,它并不会删除该共享内存段。 将共享内存段与当前进程脱离不等于删除共享内存,只是取消了当前进程与该共享内存之间的联系。如果系统中没有其他进程再使用该共享内存段,并且已经通过shmctl函数设置了IPC_RMID命令来标记其删除,那么系统会在所有进程都分离该共享内存后将其删除。
- 在使用shmdt函数之前,必须确保已经通过shmat函数成功附加了共享内存段,并且传入的shmaddr参数是有效的。shmat和shmdt要一起使用才起作用。
- 在多进程环境中,需要特别注意同步和互斥问题,以避免数据竞争和不一致性。
用共享内存实现serve&client通信
实现进程间通信步骤:
- 创建共享内存
- 共享内存关联进程
- 删除共享内存与进程的关联
- 释放共享内存
这里我们同样的将这些步骤封装在一个类当中。创建共享内存只需要实例化一个对象就行了。
需要client和server都和共享内存关联。client端不创建共享内存,不释放共享内存,server端创建共享内存,并且释放共享内存。
类中(shm.hpp) 内容如下:
#ifndef __SHM_HPP__
#define __SHM_HPP__#include <iostream>
#include <string>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>#define gCreater 1
#define gUser 2
const std::string pathname = "/home/HCC/linux/code11";
const int gproj_id = 0x66;
const int gShmSize = 4097; // 这里系统是按照4096*n的样式开辟空间,因为4096是访问的基本单位,开4098的话会开辟2*4096的空间,但是会浪费4094的空间class Shm
{
private:key_t GetCommKey(){key_t k = ftok(_pathname.c_str(), _proj_id);if (k < 0){perror("ftok");}return k;}int GetShmHelper(key_t key, int size, int flag){int shmid = shmget(key, size, flag);if (shmid < 0){perror("shmget");}return shmid;}std::string RoleToString(int who){if (who == gCreater)return "Creater";else if (who == gUser)return "gUser";elsereturn "None";}void *AttachShm()//挂接函数{if (_addrshm != nullptr)DetachShm(_addrshm);void *shmaddr = shmat(_shmid, nullptr, 0);if (shmaddr == nullptr){perror("shmat");}std::cout << "who: " << RoleToString(_who) << " attach shm..." << std::endl;return shmaddr;}void DetachShm(void *shmaddr){if (shmaddr == nullptr)return;shmdt(shmaddr);std::cout << "who: " << RoleToString(_who) << " detach shm..." << std::endl;}public:Shm(const std::string &pathname, int proj_id, int who): _pathname(pathname), _proj_id(proj_id), _who(who), _addrshm(nullptr){_key = GetCommKey();if (_who == gCreater)GetShmUseCreate();else if (_who == gUser)GetShmForUse();_addrshm = AttachShm();std::cout << "shmid: " << _shmid << std::endl;std::cout << "_key: " << ToHex(_key) << std::endl;}~Shm(){DetachShm(_addrshm);//析构的时候先分离if (_who == gCreater){int res = shmctl(_shmid, IPC_RMID, nullptr);//移除空间——只能创建者移除}std::cout << "shm remove done..." << std::endl;}std::string ToHex(key_t key){char buffer[128];snprintf(buffer, sizeof(buffer), "0x%x", key);return buffer;}bool GetShmUseCreate(){if (_who == gCreater){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | IPC_EXCL | 0666);if (_shmid >= 0)return true;std::cout << "shm create done..." << std::endl;}return false;}bool GetShmForUse(){if (_who == gUser){_shmid = GetShmHelper(_key, gShmSize, IPC_CREAT | 0666);if (_shmid >= 0)return true;std::cout << "shm get done..." << std::endl;}return false;}void Zero(){if(_addrshm){memset(_addrshm, 0, gShmSize);}}void *Addr(){return _addrshm;}private:key_t _key;int _shmid;std::string _pathname;int _proj_id;int _who;void *_addrshm;
};#endif
这样写客户端和服务端的话,会有一个问题。就是这段内存加载进进程地址空间,双方随时都可以访问,这样就造成了数据不一致问题,可能写端还没写完,读端就开始读了。我们为了解决这个问题,利用了管道使用write/read会阻塞的特性。使每次写端写完时读端才去读取,这样就初步解决了数据不一致问题。同时也能实现服务端随客户端同步关闭。
这里得出一个结论:共享内存实现的进程间通信底层不提供任何同步与互斥机制。如果想让两进程很好的合作起来,在IPC里要有信号量来支撑。
客户端代码:client.cc
#include"Shm.hpp"
#include"NamedPipe.hpp"int main()
{Shm shm(pathname,gUser,gProc_id);shm.Zero();char *shmaddr=(char*)shm.AddrShm();NamedPipe fifo(path,gCreater);fifo.OpenForWrite();int i=0;for(char ch='a';ch<='f';ch++){sleep(1);shmaddr[i++]=ch;std::string tmp="wake";fifo.WriteNamedPipe(tmp);//当写完才给读端发信号,唤醒读端。}return 0;
}
服务端代码:server.cc
#include "NamedPipe.hpp"
#include "Shm.hpp"
int main()
{Shm shm(pathname, gCreater, gProc_id);char *shmaddr = (char *)shm.AddrShm();// 打开管道NamedPipe fifo(path, gCreater);fifo.OpenForRead();while (true){std::string tmp;int n=fifo.ReadNamePipe(&tmp);if(n==0){std::cout<<"read over "<<std::endl;break;}else if(n<0){std::cout<<" read error"<<std::endl;break;}std::cout << shmaddr << std::endl;}return 0;
}
共享内存与管道进行对比
当共享内存创建好后就不再需要调用系统接口进行通信了,而管道创建好后仍需要read、write等系统接口进行通信。实际上,共享内存是所有进程间通信方式中最快的一种通信方式。
先看看管道通信:
从这张图可以看出,使用管道通信的方式,将一个文件从一个进程传输到另一个进程需要进行四次拷贝操作:
服务端将信息从输入文件复制到服务端的临时缓冲区中。
将服务端临时缓冲区的信息复制到管道中。
客户端将信息从管道复制到客户端的缓冲区中。
将客户端临时缓冲区的信息复制到输出文件中。
再来看看共享内存通信:
从这张图可以看出,使用共享内存进行通信,将一个文件从一个进程传输到另一个进程只需要进行两次拷贝操作:
从输入文件到共享内存。
从共享内存到输出文件。
所以共享内存是所有进程间通信方式中最快的一种通信方式,因为该通信方式需要进行的拷贝次数最少。
当然这里还有一个问题:因为写端比读端慢,相比较于管道,当写端比读端慢时,没有写入时,读端要进入阻塞状态,但是上面发现使用共享内存的读端一直在读,并没有阻塞。
我们知道管道是自带同步与互斥机制的,但是共享内存并没有提供任何的保护机制,包括同步与互斥。
这里得出一个结论:共享内存实现的进程间通信底层不提供任何同步与互斥机制。如果想让两进程很好的合作起来,在IPC里要有信号量来支撑。
注意:两进程之间用通过同样的规则即ftok参数要一样,来获取同样的key值,然后在创建共享内存时,用这个key值唯一标识共享内存,所以两进程时通过同样的key值来实现看到同一份资源。
消息队列
消息队列提供了一个从一个进程向另外一个进程发送一块数据的方法每个数据块都被认为是有一个类型,接收者进程接收的数据块可以有不同的类型值特性方面IPC 资源必须删除,否则不会自动清除,除非重启,所以 system V IPC 资源的生命周期随内核
消息队列大概是这样的:
进程A如果需要将数据交给进程B,就把数据先描述再组织起来,然后放入msg_queue当中。这个msg_queue与共享内存一样是操作系统维护的资源,多个进程可以同时看到。打包的一个数据有一个标识符表明数据是A的。进程B 需要进程A的数据时,只需要将队列中标识符是A的一个元素提取出来,从而拿到A的数据。进程A要想拿到进程B的数据也是类似。
消息队列与共享内存有类似的接口,比如说:
信号量
信号量主要用于同步和互斥的,下面先来看看什么是同步和互斥。
由于各进程要求共享资源,而且有些资源需要互斥使用,因此各进程间竞争使用这些资源,进程的这种关系为进程的互斥系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源。在进程中涉及到互斥资源的程序段叫临界区特性方面IPC 资源必须删除,否则不会自动清除,除非重启,所以 system V IPC 资源的生命周期随内核
进程同步
进程同步机制的主要任务,是对多个相关进程在执行次序上进行协调,使并发执行的诸进程之间能按照一定的规则共享系统资源,并能很好地相互合作,从而使程序的执行具有可再现性。
用户进程可以通过使用操作系统提供的一对原语(原子性)来对信号量进行操作,从而很方便的实现了进程互斥、进程同步。
信号量其实就是一个变量 ,可以用一个信号量来表示系统中某种资源的数量,比如:系统中只有一台打印机,就可以设置一个初值为 1 的信号量。
共享内存没有同步互斥机制,信号量就可以解决这个问题。他给共享内存增加了PV操作。PV操作是原子性的,使得每次操作都是原子性的。(后置++/--不是原子性的,不能替代信号量)
信号量也与共享内存和消息队列有类似的接口:
OS统一管理共享内存,消息队列,信号量
1.System V
2.xxxget,xxxctl
3.xxxid_ds, ipc_perm
我们发现,共享内存,消息队列和信号量都有各自的 ipc_perm数据结构。
ipc_perm 是一个结构体,里面存储着ipc的一些基本信息。管理好他们也就管理了ipc。
OS把他们先描述再组织起来放在一个柔性数组(struct kern_ipc_perm*xxx[n])(可变数组)当中。
我们每创建一个ipc通信资源,这个数组就把资源加入数组中,当删除时,遍历数组然后从数组中去掉。
这个操作类似于文件描述符表与fd一样,每一个ipc资源在数组里面就是一个下标。从而OS就实现了对ipc资源的管理。
当然这里出现一个问题,就是3种方式共用同一个下标,OS是如何区分呢。在这个下标中有一个字段是指向目标类型的,就是xxx_per->mode。寻找时我们只需要通过这个标识符知道是哪种资源,然后通过指针强转去访问就可以得到对应的数据。这也类似于多态。