OS52.【Linux】System V 共享内存(1)

目录

1.回顾进程间通信的本质

2.共享内存的原理

申请共享内存的方法

释放共享内存的方法

注意事项

3.相关的系统调用

shmget

★逻辑链

shmflg

shmflg=IPC_CREAT

[shmflg=IPC_CREAT | IPC_EXCL](#shmflg=IPC_CREAT | IPC_EXCL)

IPC_EXCL

key

key的生成算法

[反思: 为什么要让用户使用ftok生成key?](#反思: 为什么要让用户使用ftok生成key?)

size

返回值

★shmid和key的区别

查看操作系统共享内存的方法

4.代码设计

get_key函数

get_new_shared_memory函数

命令行关闭共享内存的方法

共享内存的权限

共享内存的大小

让进程和共享内存联系起来

让进程去除共享内存的挂接

释放共享内存

shmid_ds结构体

让两个进程挂接共享内存

让两个进程通信


1.回顾进程间通信的本质

先让不同的进程,看到同一份资源,而且这个资源不能是某个进程独有的,因为进程之间具有独立性,各个进程的地址空间是隔离的

2.共享内存的原理

假设两个毫不相关的进程A和进程B需要使用共享内存通信,根据进程间通信的本质,需要让它们看到同一份共享内存资源

下图中的"某一块物理空间"就是共享内存,其通过页表映射到A和B进程地址空间的共享区,这样它们就能看到同一份共享内存资源

将共享内存全部映射到进程A和进程B地址空间的共享区,这样进程A和进程B使用一个起始地址就可以操作这块共享内存了

申请共享内存的方法

由上得知申请共享内存的方法:

1.申请内存

2.映射到两个进程的地址空间,即挂接 到进程地址空间

3.返回首地址

释放共享内存的方法

为了确保不出问题,需要先取消挂接共享内存到进程地址空间,再释放共享内存

注意事项

申请共享内存和释放共享内存进程无法直接参与(因为进程具有独立性),共享内存是由操作系统分配的,进程需要使用系统调用

3.相关的系统调用

shmget

shmget用于创建共享内存

下面解释它的3个参数

★逻辑链

如何申请共享内存? 或者说申请共享内存的方法是什么? 答: 依靠shmflg

共享内存的大小? 答: 依靠size

操作系统中有大量的共享内存,申请共享内存后,如何保证尚未通信的进程能拿到同一个共享内存区域呢? 答: 依靠key

shmflg

说明共享内存如何创建和如何获取

shmflg=IPC_CREAT

如果申请的共享内存不存在,那么就创建

如果申请的共享内存存在,就获取返回

shmflg=IPC_CREAT | IPC_EXCL

在IPC_CREAT的基础上添加IPC_EXCL

如果申请的共享内存不存在,那么就创建

如果申请的共享内存存在,就出错返回

这样做的目的: 保证申请的共享内存是新的!

注: IPC_EXCL不单独使用

IPC_EXCL

根据https://stackoverflow.com/questions/62876903/what-is-ipc-excl-short-for的回答:

IPC_EXCL是I nterP rocess C ommunication EXCL usive的缩写,可以从opengroup网https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/fcntl.h.html对O_EXCL的解释Exclusive use flag类比过来:

注: exclusive adj.独有的

key

共享内存的标识符,这保证了不同进程看到了同一个共享内存

在进程之间尚未通信时,需要让它们都找到同一个共享内存,那么它们就要拿到同一个key

换句话说,这里的key类似命名管道,让进程们都打开同一个路径下的同一个命名管道,就能通信了

key不需要手动生成,可以使用ftok系统调用:

注意事项: pathname必须是真实存在的路径,proj_id不能为0

key的生成算法

在glibc-2.42的/sysvipc/ftok.c中定义了ftok函数:

cpp 复制代码
key_t
ftok (const char *pathname, int proj_id)
{
  struct __stat64_t64 st;
  key_t key;

  if (__stat64_time64 (pathname, &st) < 0)
    return (key_t) -1;

  key = ((st.st_ino & 0xffff) | ((st.st_dev & 0xff) << 16)
	 | ((proj_id & 0xff) << 24));

  return key;
}

借助了3个变量st.st_ino、st.st_dev、proj_id来尽量确保key的唯一性

struct __stat64_t64定义在/sysdeps/unix/sysv/linux/struct_stat_time64.h中:

cpp 复制代码
#if __TIMESIZE == 64
# define __stat64_t64 stat64
#else
# include <struct___timespec64.h>

struct __stat64_t64
  {
# define __struct_timespec struct __timespec64
# include <bits/struct_stat_time64_helper.h>
  };
#endif /* __TIMESIZE == 64  */

struct __stat64_t64成员变量定义在/sysdeps/unix/sysv/linux/bits/中:

cpp 复制代码
  /* Content of internal __stat64_t64 struct.  */
  __dev_t st_dev;		/* Device.  */
  __ino64_t st_ino;		/* file serial number.	*/
  __mode_t st_mode;		/* File mode.  */
  __nlink_t st_nlink;		/* Link count.  */
  __uid_t st_uid;		/* User ID of the file's owner.  */
  __gid_t st_gid;		/* Group ID of the file's group.  */
  __dev_t st_rdev;		/* Device number, if device.  */
  __off64_t st_size;		/* Size of file, in bytes.  */
  __blksize_t st_blksize;	/* Optimal block size for I/O.  */
  __blkcnt64_t st_blocks;	/* Number 512-byte blocks allocated. */
#ifdef __USE_XOPEN2K8
# ifndef __struct_timespec
#  define __struct_timespec struct timespec
# endif
  /* Nanosecond resolution timestamps are stored in a format
     equivalent to 'struct timespec'.  This is the type used
     whenever possible but the Unix namespace rules do not allow the
     identifier 'timespec' to appear in the <sys/stat.h> header.
     Therefore we have to handle the use of this header in strictly
     standard-compliant sources special.  */
  __struct_timespec st_atim;
  __struct_timespec st_mtim;
  __struct_timespec st_ctim;
# define st_atime st_atim.tv_sec
# define st_mtime st_mtim.tv_sec
# define st_ctime st_ctim.tv_sec
# undef __struct_timespec
#else
/* The definition should be equal to the 'struct __timespec64' internal
   layout.  */
# if __BYTE_ORDER == __BIG_ENDIAN
#  define __fieldts64(name)					\
   __time64_t name; __int32_t :32; __int32_t name ## nsec
# else
#  define __fieldts64(name)					\
   __time64_t name; __int32_t name ## nsec; __int32_t :32
# endif

  __fieldts64 (st_atime);
  __fieldts64 (st_mtime);
  __fieldts64 (st_ctime);

  unsigned long int __glibc_reserved4;
  unsigned long int __glibc_reserved5;

# undef __fieldts64
#endif

1.st_ino的注释: file serial number,就是文件的inode编号,具有唯一性

2.st_dev的注释: st_dev,即设备号,具有唯一性

key的构成:

cpp 复制代码
  key = ((st.st_ino & 0xffff) | ((st.st_dev & 0xff) << 16)
	 | ((proj_id & 0xff) << 24));

根据与运算和左移运算的规则:

  1. st.st_ino构成key的低16位

  2. st.st_dev构成key的中间8位

3.proj_id构成key的高8位

从图中可以得知: key是32位的

参考资料:

https://stackoverflow.com/questions/3155291/which-file-should-i-pass-as-pathname-argument-of-ftok

https://stackoverflow.com/questions/54492824/what-is-the-formula-used-to-produce-a-ftok-key

建议: 自己造的key可能和系统冲突,最好用ftok生成

结论: ftok系统调用生成key的算法和内核无关

反思: 为什么要让用户使用ftok生成key?

从上面的生成算法来看,key完全可以由操作系统生成,那为什么要让用户使用ftok生成key?

答: 进程通信是由程序员决定的,毕竟操作系统不知道哪几个进程要通信

如果进程A和进程B执行ftok传的pathname和proj_id参数都一样,那么它们生成的key也就一样,这样就能使用同一块共享内存进行通信了

size

共享内存的大小,单位字节

返回值

shmget如果执行成功,那么返回"a valid shared memory identifier",即一个有效的共享内存标识符,简称为shmid,如果执行失败,返回-1

★shmid和key的区别

一个是进程级别,一个是操作系统级别

++多个进程能访问同一块共享内存,是通过key,这个操作系统标识共享内存唯一导致的++

++而一个进程能创建多个共享内存,决定访问哪一块共享内存,是通过shmid决定的++

参考资料: https://stackoverflow.com/questions/19518607/shared-memory-whats-the-difference-between-the-key-and-the-id

查看操作系统共享内存的方法

bash 复制代码
ipcs -m

4.代码设计

目标: 设计进程A和进程B使用共享内存通信的代码

创建以下文件:

bash 复制代码
shared_memory/
├── makefile
├── header.hpp
├── shm1.cpp
└── shm2.cpp

makefile写入:

bash 复制代码
all: shm1.out shm2.out
shm1.out:shm1.cpp
	g++ -o $@ $^ -g -std=c++11
shm2.out:shm2.cpp
	g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
	rm -f shm1.out shm2.out

get_key函数

封装一个取得key的函数,为了确保两个进程生成的key一样,ftok的pathname可以传入当前进程所处的目录,如果它们没有手动改变自己所处的目录的话

写入header.hpp中:

cpp 复制代码
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <stdlib.h>
#define SIZE 1024
#define PROJ_ID 0x1234
key_t get_key()
{
    char buffer[SIZE];
    char* pathname=getcwd(buffer,sizeof(buffer));
    if (pathname==nullptr)
    {
        perror("getcwd failed");
        exit(1);
    }
    return ftok(pathname,PROJ_ID);
}

shm1.cpp和shm2.cpp都写入:

cpp 复制代码
#include <stdio.h>
#include "header.hpp"
int main()
{
    printf("key=0x%X\n",get_key());
    return 0;
}

运行结果: 两个进程生成的key都一样

(开了两个终端,以此来说明两个进程毫不相关)

get_new_shared_memory函数

get_new_shared_memory内部调用get_key来生成

cpp 复制代码
//使用IPC_EXCL来确保获得新的共享内存
int get_new_shared_memory()
{
    key_t key=get_key();
    //申请新的共享内存
    int shmid=shmget(key,4096,IPC_CREAT|IPC_EXCL);
    if (shmid==-1)
    {
        perror("shmget failed");
        exit(2);
    }
    return shmid;
}

测试代码:

shm1.cpp:

cpp 复制代码
#include <stdio.h>
#include <iostream>
#include "header.hpp"
int main()
{
    int shmid=get_new_shared_memory();
    std::cout<<"shmid="<<shmid<<std::endl;
    for(;;);
    return 0;
}

运行结果:

执行前:

执行中(shm1.out死循环未退出):

执行后(shm1.out结束运行):

发现进程没有关闭共享内存,结束后共享内存没有消失

结论: 操作系统不会主动关闭共享内存,即共享内存的生命周期是随内核的,用户不主动关闭,共享内存会一直存在,除非内核重启或者用户手动释放

如果shm1.out调用两次get_new_shared_memory(),第二次会报错:

cpp 复制代码
#include <stdio.h>
#include <iostream>
#include "header.hpp"
int main()
{
    int shmid1=get_new_shared_memory();
    std::cout<<"shmid="<<shmid1<<std::endl;
    int shmid2=get_new_shared_memory();
    std::cout<<"shmid="<<shmid2<<std::endl;
    for(;;);
    return 0;
}

运行结果:

使用IPC_CREAT | IPC_EXCL确保申请的共享内存是新的

命令行关闭共享内存的方法

bash 复制代码
ipcrm -M key
#或
ipcrm -m shmid

共享内存的权限

在shmget通过或运算添加权限标志,例如:

cpp 复制代码
int shmid=shmget(key,4096,IPC_CREAT|IPC_EXCL|0666);

运行结果:

共享内存的大小

修改大小:

cpp 复制代码
int shmid=shmget(key,4097,IPC_CREAT|IPC_EXCL|0666);

运行结果:

Shared Memory Segments表的bytes一列元素的大小和shmget的第二个参数size一样

尽管申请的大小不是4096的整数倍,但操作系统在底层实现时,会向上取整到4096的整数倍,一般情况下,x86的一个页的大小是4096字节,申请了4097个字节,操作系统会申请2个页的空间,这叫页对齐

证明: 可以看Linux 6.18.6的/ipc/shm.c的源码:

shmget系统调用,会调用内核的ksys_shmget函数:

cpp 复制代码
long ksys_shmget(key_t key, size_t size, int shmflg)
{
	struct ipc_namespace *ns;
	static const struct ipc_ops shm_ops = {
		.getnew = newseg,
		.associate = security_shm_associate,
		.more_checks = shm_more_checks,
	};
	struct ipc_params shm_params;

	ns = current->nsproxy->ipc_ns;

	shm_params.key = key;
	shm_params.flg = shmflg;
	shm_params.u.size = size;

	return ipcget(ns, &shm_ids(ns), &shm_ops, &shm_params);
}

经过层层调用会调到newseg,这个函数负责++创建一个新的共享内存段,肯定和页对齐有关++

cpp 复制代码
/**
 * newseg - Create a new shared memory segment
 * @ns: namespace
 * @params: ptr to the structure that contains key, size and shmflg
 *
 * Called with shm_ids.rwsem held as a writer.
 */
static int newseg(struct ipc_namespace *ns, struct ipc_params *params)
{
	key_t key = params->key;
	int shmflg = params->flg;
	size_t size = params->u.size;
	int error;
	struct shmid_kernel *shp;
	size_t numpages = (size + PAGE_SIZE - 1) >> PAGE_SHIFT;
	struct file *file;
	char name[13];
    //后面代码省略
}

重点看numpages

cpp 复制代码
size_t numpages = (size + PAGE_SIZE - 1) >> PAGE_SHIFT;

x86下,PAGE_SIZE为4096

PAGE_SHIFT定义在/tools/include/linux/mm.h

cpp 复制代码
#define PAGE_SHIFT		12

那么当size为4097时,(size + PAGE_SIZE - 1)为8192,即0b10000000000000,右移12位后,numpages为0b10,即十进制的2

结论: 向shmget传入的第2个参数size,操作系统在实际分配共享内存时,会向上对齐到一个页的整数倍后然后分配,即(LaTex公式: \lceil \frac{size}{pagesize}\rceil*pagesize )这种分配方式可以提高内存管理的效率,减少内存碎片,但进程只能用size那么多

让进程和共享内存联系起来

共享内存创建好了之后,需要让进程和共享内存联系起来

使用shmat系统调用:

shamddr: 共享内存挂接 到共享区的什么位置,程序员不好指定,设为空指针,让操作系统决定

shmflg: 可以控制挂接的共享内存的权限,这里填0,和传入shmget的权限位保持一致

shmat返回值: 接受共享内存的起始地址,由于返回void*类型,这样可以方便程序员进行强制类型转换

测试单个进程挂接共享内存:

cpp 复制代码
#include <stdio.h>
#include <iostream>
#include "header.hpp"
int main() 
{ 
    int shmid=get_new_shared_memory(); 
    std::cout<<"shm1.out已经创建共享内存"<<std::endl;
    sleep(4);
    void* start_addr=shmat(shmid,nullptr,0);
    if (start_addr==(void *) -1)
    {
        perror("shmat failed");
        exit(3);
    }
    std::cout<<"shm1.out已经挂接共享内存"<<std::endl;
    sleep(4);
    std::cout<<"shm1.out即将退出"<<std::endl;
    return 0;
}

运行结果:

可以看到,只有nattch一列在变化:

结论: nattch简单理解为: 标识共享内存的挂接数,即当前一共多少个进程挂接了这个共享内存,类似文件系统的引用计数

让进程去除共享内存的挂接

使用shmdt系统调用:

shmflg: 可以控制挂接的共享内存的权限,这里填0,和传入shmget的权限位保持一致

参数shmaddr为共享内存的起始地址,不需要交代空间大小,内核已经记录了大小

测试单个进程取消挂接共享内存:

shm1.cpp写入:

cpp 复制代码
#include <stdio.h>
#include <iostream>
#include "header.hpp"
int main() 
{ 
    int shmid=get_new_shared_memory(); 
    std::cout<<"shm1.out已经创建共享内存"<<std::endl;
    sleep(4);
    void* start_addr=shmat(shmid,nullptr,0);
    if (start_addr==(void *) -1)
    {
        perror("shmat failed");
        exit(3);
    }
    std::cout<<"shm1.out已经挂接共享内存"<<std::endl;
    sleep(4);
    if (-1==shmdt(start_addr))
    {
        perror("shmdt failed");
        exit(4);
    }
    sleep(4);
    std::cout<<"shm1.out即将退出"<<std::endl;
    return 0;
}

运行结果:

注意nattch变化:

释放共享内存

使用shmctl(ctl是contrl的缩写)系统调用:

cmd是对共享内存进行的操作,删除共享内存可以用IPC_RMID,手册中的提示: 必须没有任何进程挂接到这个共享内存,才能删除这个共享内存

shmid_ds结构体

如何理解shmctl的第3个参数struct shmid_ds *buf?

操作系统中运行着大量的进程,这些进程或多或少都有使用共享内存通信,那么操作系统就要为这些进程申请共享内存,一旦共享内存变多,操作系统就需要管理这些共享内存,即++先描述再组织++

使用内核结构体描述共享内存:

cpp 复制代码
struct shmid_ds {
    struct ipc_perm shm_perm;    /* Ownership and permissions */
    size_t          shm_segsz;   /* Size of segment (bytes) */
    time_t          shm_atime;   /* Last attach time */
    time_t          shm_dtime;   /* Last detach time */
    time_t          shm_ctime;   /* Creation time/time of last
                                    modification via shmctl() */
    pid_t           shm_cpid;    /* PID of creator */
    pid_t           shm_lpid;    /* PID of last shmat(2)/shmdt(2) */
    shmatt_t        shm_nattch;  /* No. of current attaches */
    ...
};

struct ipc_perm {
        key_t          __key;    /* Key supplied to shmget(2) */
        uid_t          uid;      /* Effective UID of owner */
        gid_t          gid;      /* Effective GID of owner */
        uid_t          cuid;     /* Effective UID of creator */
        gid_t          cgid;     /* Effective GID of creator */
        unsigned short mode;     /* Permissions + SHM_DEST and
                                    SHM_LOCKED flags */
        unsigned short __seq;    /* Sequence number */
    };

struct shmid_ds是用户态描述共享内存的属性的结构体,ds是data structure的缩写

删除的时候不管shmid_ds,传NULL即可,即:

cpp 复制代码
shmctl(shmid,IPC_RMID,nullptr);

让两个进程挂接共享内存

目标: shm1.out负责创建,之后shm1.out和shm2.out都挂接到这个共享内存上

shm1.cpp写入:

cpp 复制代码
#include <stdio.h>
#include <iostream>
#include "header.hpp"
int main() 
{ 
    int shmid=get_new_shared_memory(); 
    std::cout<<"shm1.out已经创建共享内存"<<std::endl;
    sleep(4);
    void* start_addr=shmat(shmid,nullptr,0);
    if (start_addr==(void *) -1)
    {
        perror("shmat failed");
        exit(3);
    }
    std::cout<<"shm1.out已经挂接共享内存"<<std::endl;
    sleep(4);
    if (-1==shmdt(start_addr))
    {
        perror("shmdt failed");
        exit(4);
    }
    std::cout<<"shm1.out取消挂接"<<std::endl;
    std::cout<<"等待shm2.out取消挂接"<<std::endl;
    sleep(4);
    if (-1==shmctl(shmid,IPC_RMID,nullptr))
    {
        perror("stmctl failed");
        exit(5);       
    }
    std::cout<<"shm1.out删除了共享内存"<<std::endl;
    std::cout<<"shm1.out即将退出"<<std::endl;
    sleep(2);
    return 0;
}

shm2.cpp写入:

cpp 复制代码
#include <stdio.h>
#include <iostream>
#include "header.hpp"
int main() 
{ 
    sleep(1);
    int shmid=get_old_shared_memory(); 
    void* start_addr=shmat(shmid,nullptr,0);
    if (start_addr==(void *) -1)
    {
        perror("shmat failed");
        exit(3);
    }
    std::cout<<"shm2.out已经挂接共享内存"<<std::endl;
    sleep(6);
    if (-1==shmdt(start_addr))
    {
        perror("shmdt failed");
        exit(4);
    }
    std::cout<<"shm2.out取消挂接"<<std::endl;
    sleep(4);
    std::cout<<"shm2.out即将退出"<<std::endl;
    sleep(2);
    return 0;
}

注: get_old_shared_memory是获取已经创建过的共享内存

cpp 复制代码
int get_old_shared_memory()
{
    key_t key=get_key();
    //获取旧的共享内存
    int shmid=shmget(key,4097,IPC_CREAT|0666);
    if (shmid==-1)
    {
        perror("shmget failed");
        exit(2);
    }
    return shmid;
}

监控脚本monitor.sh:

bash 复制代码
#!/usr/bin/bash
for ((;;))
do
    ipcs -m
    sleep 0.3s
    clear
done

运行结果:

关键部分:

nattch==0:

nattch==1:

nattch==2:

nattch==1:

nattch==0:

让两个进程通信

注意: 向共享内存中读写数据不需要系统调用,因为共享内存被操作系统映射到进程地址空间的共享区了,可以直接使用地址读写共享内存

shm2.cpp向共享内存写入用户输入的数据,shm1.cpp读取共享内存的数据

shm1.cpp写入:

cpp 复制代码
#include <stdio.h>
#include <iostream>
#include "header.hpp"
int main() 
{
    //为了减小代码行数,系统调用的错误执行结果都没有判断
    int shmid=get_new_shared_memory(); 
    char* start_addr=(char*)shmat(shmid,nullptr,0);
    for (;;) 
    {
        sleep(1);
        std::cout<<start_addr;
        fflush(stdout);
    }
    shmdt(start_addr);
    shmctl(shmid,IPC_RMID,nullptr);
    return 0;
}

shm2.cpp写入:

cpp 复制代码
#include <stdio.h>
#include <iostream>
#include "header.hpp"
int main() 
{
    //为了减小代码行数,系统调用的错误执行结果都没有判断  
    int shmid=get_old_shared_memory(); 
    char* start_addr=(char*)shmat(shmid,nullptr,0);
    for (;;)
    {
        std::cin>>start_addr;
    }
    shmdt(start_addr);
    return 0;
}

运行结果:

本篇文章过长,到此结束,下篇继续讲

相关推荐
智能零售小白白2 小时前
零售会员营销自动化:标签体系与精准触达的技术实现
运维·自动化·零售
wbs_scy2 小时前
Linux 实战:从零实现动态进度条(含缓冲区原理与多版本优化)
linux·运维·服务器
wbs_scy2 小时前
Makefile 完全指南:从入门到工程化,自动化构建不再难
运维·自动化
之歆2 小时前
Linux 网络配置与 TCP/IP 协议
linux·网络·tcp/ip
DeeplyMind3 小时前
第11章 容器运行参数详解
运维·docker·容器
REDcker3 小时前
curl开发者快速入门
linux·服务器·c++·c·curl·后端开发
成震19713 小时前
UBUNTU 安装虚拟机
linux·运维·ubuntu
海盗猫鸥3 小时前
Linux基础指令1
linux·服务器
最贪吃的虎3 小时前
windows上如何可视化访问并远程操作linux系统上运行的浏览器或者linux可视化桌面
java·linux·运维·windows·分布式·后端·架构