进程间通信之共享内存

一、共享内存实现进程间通信的原理

共享内存实际是操作系统在实际物理内存中开辟的一段内存。

共享内存实现进程间通信,是操作系统在实际物理内存开辟一块空间,一个进程在自己的页表中,将该空间和进程地址空间上的共享区的一块地址空间形成映射关系。另外一进程在页表上,将同一块物理空间和该进程地址空间上的共享区的一块地址空间形成映射关系。 ​ 当一个进程往该空间写入内容时,另外一进程访问该空间,会得到写入的值,即实现了进程间的通信。

要实现进程间通信需要两个进程看到同一块空间,系统开辟的共享内存就是两个进程看到的同一资源。

注意:共享内存实现进程间通信是进程间通信最快的。

二、管理共享内存的数据结构

共享内存实现进程间通信不只仅限于两个进程之间,可以用于多个进程之间。并且系统中可能会有多个进程在进行多个通信。所以系统需要将这些通信的进程管理起来。如果不管理,操作系统怎么知道这块共享内存挂接了哪个进程等信息。

如何管理?就是先描述再组织。

描述共享内存的数据结构里保存了一个ipc_perm结构体,这个结构体保存了IPC(进程将通信)的关键信息。

/* Obsolete, used only for backwards compatibility and libc5 compiles */
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;
};

其中key是共享内存的唯一标识。

三、共享内存函数
1、ftok函数

作用:是算出一个唯一的key值返回。

参数:第一个是地址,第二个是至少8为的项目id,不能为0,两参数可以是任意值,但是要符合格式。

返回值:ftok如果成功返回一个key值,如果失败返回-1。如果失败了再重新填写参数即可。

ftok中的参数可以随便填写,但是要符合格式,ftok只是利用参数,再运用一套算法,算出一个唯一的key值返回。这个key值可以传给共享内存参数,作为struct ipc_perm中唯一标识共享内存的key。

#define PATHNAME "."
#define PROJID 0x3456
​
const int gsize=4096;//暂时
​
key_t getKey()
{
    key_t k=ftok(PATHNAME,PROJID);
    if(k == -1)
    {
        cerr << "error: " << errno << " : " << strerror(errno) << endl;
        exit(1);
    }
    return k;
}
2、shmget函数

参数:

key:为共享内存的名字,一般是ftok的返回值。

size:共享内存的大小,以page为单位,大小为4096的整数倍。 shmflg:权限标志,常用两个IPC_CREATIPC_EXCL,一般后面还加一个权限,相当于文件的权限。

IPC_CREAT:创建一个共享内存返回,已存在打开返回

单独使用IPC_CREAT: 创建一个共享内存,如果共享内存不存在,就创建之,如果已经存在,获取已经存在的共享内存并返回

IPC_EXCL不能单独使用,一般都要配合IPC_CREAT

IPC_CREAT | IPC_EXCL: 创建一个共享内存,如果共享内存不存在,就创建之, 如果已经存在,则立马出错返回 -- 如果创建成功,对应的shm,一定是最新的!

返回值:

成功返回一个非负整数,即共享内存的标识码,失败返回-1。

为什么已经有一个key来标识共享内存,还需要一个返回值来标识共享内存?因为key是内核级别的,供内核标识,shmget返回值是用户级别的,供用户使用的。

代码:

int createShmHelper(key_t k, int size, int flag)
{
    int shmid=shmget(k,gsize,flag);
    if(shmid == -1)
    {
        cerr << "error: " << errno << " : " << strerror(errno) << endl;
        exit(2);
    }
    return shmid;
}
int createShm(key_t k,int size)//用于server
{
   umask(0);
   int s_value=createShmHelper(k,size, IPC_CREAT|IPC_EXCL|0666);
   return s_value;
}
​
int getShm(key_t k,int size) //用于client
{
   int c_value=createShmHelper(k,size,IPC_CREAT);
   return c_value;
}

当我们执行两次程序,第一次没有报错,即是成功创建了一个共享内存,而第二次执行就报错了,报错信息是文件已存在。

这是因为我们在进程中创建了一个共享内存,但是我们执行程序的时候共享内存还在,但是我们没有释放共享内存,所以就会报错。

这里得出一个结论:IPC(进程将通信)资源生命周期不随进程,而是随内核的,不释放会一直占用,除非重启。所以,shmget创建的共享内存要释放掉,不然会内存泄漏。

可以用ipcs -m来查看共享内存。

可以用命令行来释放共享内存:

ipcrm -m shmid(shmget返回值)
3、shmctl函数

用命令的方法删除太麻烦了,我们也可以用shmctl函数直接进行删除。

函数功能:管理对共享内存的控制

参数:

shmid:共享内存标识符

cmd:

IPC_STAT:得到共享内存的状态,把共享内存的shmid_ds结构复制到buf中

IPC_SET:改变共享内存的状态,把buf所指的shmid_ds结构中的uid、gid、mode复制到共享内存的shmid_ds结构内

IPC_RMID:删除这片共享内存

buf: 共享内存管理结构体。具体说明参见共享内存内核结构定义部分

代码:

void delShm(int shmid)
{
    int n = shmctl(shmid, IPC_RMID, nullptr);
    if(n==-1){
        cerr << "error: " << errno << " : " << strerror(errno) << endl;
        exit(3);
    }
    (void)n;
}
4、shmat函数, shmdt函数

void *shmat(int shmid, const void *shmaddr, int shmflg)

作用:使创建的共享内存与调用该函数进程的进程地址空间参数相关联

shmid共享内存标识符
shmaddr指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
shmflgSHM_RDONLY:为只读模式,其他为读写模式

注意:shmaddr为空的话,shmflg设为0。

int shmdt(const void *shmaddr)

作用:与shmat函数相反,是用来断开与共享内存附加点的地址,禁止本进程访问此片共享内存

shmaddr:连接的共享内存的起始地址

char* attachShm(int shmid)
{
    char *start = (char*)shmat(shmid, nullptr, 0);
    return start;
}
​
void detachShm(char *start)
{
    int n = shmdt(start);
    assert(n != -1);
    (void)n;
}

每隔一秒循环打印共享内存的写法:

while :;do ipcs -m ; sleep 1;done