一、system v共享内存
共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到
内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据
堆栈之间的共享区是属于内核空间还是在用户空间?
答案是用户空间。
二、原理
进程-》进程地址空间-》页表->映射到物理内存
另一个进程也是一样
操作系统申请了一块空间,这块空间可以被多个页表映射到不同的进程的进程地址空间中,这种工作方式就叫共享内存
释放共享内存,就是把页表里关于进程和该块内存的映射关系去掉,然后再释放申请的空间
动态库是从磁盘加载到内存,动态库的地址采用的是一种与位置无关的地址(就是相对地址),当动态库被加载到内存,然后被各个页表映射到各个进程地址空间不同的区域,
三、共享内存的建立
操作系统内有多对进程,并且有多个共享内存,os需要将这些共享内存管理起来,因此,共享内存也是个内核数据结构
3.1linux下共享内存的源码
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 */
}
3.2shmget函数
功能:用来创建共享内存
原型
int shmget(key_t key, size_t size, int shmflg);
参数
key:这个共享内存段名字
size:共享内存大小
shmflg:由九个权限标志构成,它们的用法和创建文件时使用的mode模式标志是一样的
返回值:成功返回一个非负整数,即该共享内存段的标识码;失败返回-1
返回值可以理解为共享内存的(用户层面)标识符。
第三个参数一般填写IPC_CREAT | IPC_EXCL,单独使用IPC_CREAT表示如果底层不存在共享内存,就创建它,并返回;如果底层已存在共享内存,出错返回。
单独使用IPC_EXCL没有意义,所以一般是两个宏结合使用,表示创建共享内存,如果底层已经存在,获取之,并返回,如果不存在,创建之,并返回
用户访问共享内存,都是通过shmid(shgmet返回值)访问的
3.2.1第一个参数key_t key的理解
要让对方进程也能看到我创建的共享内存,通过key即可,数据具体是几不重要,重要的是在系统内唯一,所以要让两个进程shmget函数使用的key值相同。
怎么让key值唯一?
通过ftok函数,本质是使用同样的算法规则形成唯一值。
3.2.2ftok函数的作用
ftok
函数用于将一个文件路径和一个项目 ID(通常是一个小于 255 的整数)转换为一个 key_t
类型的值,这个值可以用作 IPC(进程间通信)中的键。ftok
函数的目的是生成一个尽可能唯一的键,以便在多个进程或计算机之间进行通信时,能够准确地定位和访问共享资源。
ftok
函数的工作原理是通过对文件路径名进行散列(hash),然后与项目 ID 进行位运算(通常是按位与操作)来生成一个唯一的键。这个键的设计目的是在给定的文件系统和项目 ID 下是唯一的,以便不同的进程可以使用它来访问相关的 IPC 结构。
当在两个不同的源文件中使用相同的路径名和相同的项目 ID 如0x66
(自定义) 调用 ftok
函数时,由于这两个参数完全相同,ftok
函数会为每个源文件生成相同的 key_t
值。这是因为:
- 路径名
"."
表示当前目录,这是一个已知的、固定的路径。 - 项目 ID
0x66
是一个固定的整数值。 ftok
函数的散列算法是确定的,所以对于相同的输入,它会生成相同的散列值。- 最终的
key_t
值是通过对散列值和项目 ID 进行位运算得到的,由于输入相同,所以结果也相同。
因此,即使两个源文件不是同一个文件,只要它们使用相同的路径名和项目 ID 调用 ftok
函数,生成的 key_t
值就会相同。
ftok函数内部是一套算法,内部会将传入的第一个参数和第二个参数形成一个唯一值的key值(是多少不重要),需要传入一个路径(第一个参数),以及一个项目id(第二个参数,一般是0~255)
shmget创建共享内存后,即便进程结束,它依然会存在,因为system V IPC资源,声明周期随内核,所以需要手动删除或者代码删除。
3.2.3查看共享内存命令
ipcs -m
Linux中的ipcs命令用于提供和显示进程间通信(IPC)设施的信息。
-m就是memory的意思,就是共享内存
3.3shmat函数
用来将共享内存关联到自己的进程。
申请空间后,需要与进程建立页表的映射关系,这个行为叫attch(动词,把……附上)
第一个参数是要挂接的共享内存(用户层面的)id,第二个参数是你要挂接这个共享内存时,要指定的虚拟地址(一般我们这里设置为null,系统自动帮我们选择合适的地址,因为我们并不清楚虚拟地址的使用情况),第三个参数是你要挂接的方式(可以设置为只读的挂接),shmflg它的两个可能取值是SHM_RND和SHM_RDONLY
这个函数的返回值是,创建成功就返回这个共享内存的虚拟地址,失败是-1
3.3.1shmat函数的作用
shmat函数会将已经申请好的共享内存空间通过页表和进程建立页表映射关系
- 内存映射:当一个进程调用
shmat
函数时,它会将由shmget
函数创建的共享内存区附加到自己的地址空间。这个过程涉及到虚拟地址到物理地址的映射,即操作系统会将共享内存的物理地址映射到进程的虚拟地址空间中的一个地址上。 - 页表操作:为了实现这种映射,操作系统会使用页表来维护虚拟地址和物理地址之间的关系。当进程访问映射后的虚拟地址时,硬件会根据页表自动转换到对应的物理地址,从而访问共享内存中的数据。
- 数据交换:由于多个进程可以将同一块共享内存映射到各自的地址空间,它们可以通过这种方式直接读写共享内存中的数据,实现数据的交换和同步。
- 权限控制:共享内存的使用受到权限控制,就像文件权限一样。例如,共享内存的创建者可以设置权限,以允许其他进程对共享内存进行读和写操作。
- 性能优势:共享内存是IPC中效率最高的方式,因为它避免了数据复制的开销,直接在内存中进行数据交换,因此读写速度非常快。
3.3.2第二个参数设置为Null的理由
其中第二个参数shmaddr
通常用来指定共享内存应该附加到进程地址空间中的哪个位置。如果这个参数被设置为nullptr
或为0,则表示让内核自动选择一个合适的地址来附加共享内存。
shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存
3.3.3将共享内存加载到进程的地址空间的理解
- 进程地址空间:每个进程在Linux系统中都有其独立的虚拟地址空间。这个地址空间由操作系统的内存管理单元(MMU)通过页表映射到物理内存。进程地址空间包含了代码、数据、堆和栈等区域。
- “加载”共享内存区:这里的“加载”不是将数据结构属性写入进程地址空间,而是将共享内存区域的物理内存映射到进程的虚拟地址空间中。这意味着进程可以通过一个虚拟地址来访问这块共享内存,而这个虚拟地址会通过页表映射到共享内存的物理地址上。
- 附加共享内存:当
shmat
函数被调用时,操作系统会将共享内存区(由shmget
创建或获取的)映射到调用进程的虚拟地址空间中的某个地址上。这个映射过程涉及到更新进程的页表,以便将共享内存区的物理地址与进程虚拟地址空间中的某个虚拟地址关联起来。这样,进程就可以通过这个虚拟地址来访问共享内存区中的数据。 - 共享内存的访问:一旦共享内存区被附加到进程的地址空间,进程就可以像访问自己的局部变量一样访问共享内存中的数据。但是,需要注意的是,由于多个进程可能同时访问同一块共享内存,因此需要适当的同步机制来避免竞态条件。
3.3.4关联的进程的理解
shmat
函数被调用时,将会根据shmget
函数返回的标识符shmid
,把共享内存区附加到该进程的地址空间上。这样,进程就能够直接访问这块共享内存区域,实现与其他同样映射了该共享内存区域的进程之间的数据共享和通信。
“关联的进程”可以理解为任何使用相同shmid
的进程,它们可以通过shmat
将相同的共享内存区映射到各自的虚拟地址空间中,从而实现相互之间的数据交换和同步。
3.4shmdt函数去掉页表映射关系
这个行为叫detach(分离)
shmdt
函数用于分离(detach)当前进程与共享内存区的链接。
shmdt
函数的作用是断开当前进程与之前通过shmat
附加上的共享内存区之间的关联。它的参数是一个指针shmaddr
,这个指针指向的是通过shmat
函数附加的共享内存区的地址。
返回值:成功返回0;失败返回-1.
当一个进程完成了对共享内存区的使用,或者在进程终止前,它应该调用shmdt
来解除与共享内存区的关联。这样做的目的是减少共享内存的引用计数,当引用计数降到0时,内核才会实际上从物理内存中删除该共享内存段。
需要注意的是,调用shmdt
并不会删除共享内存,也不会影响其他进程对该共享内存区的访问。它只是减少了共享内存的引用计数,并且使得当前进程不再能够访问这块共享内存。
3.4.1shmdt函数的参数的理解
shmaddr
参数是指向共享内存区的起始地址。当调用 shmdt
函数时,操作系统会将进程地址空间上对应的 shmaddr
地址解除映射,即分离该地址与共享内存区之间的关联。
shmdt函数清楚本进程的共享内存,这里的“清除”并不是指将内存中这块区域free掉,而是指取消地址映射关系,使得进程不再能够通过这个地址访问共享内存区的内容。实际上,共享内存区的数据仍然存在于物理内存中,只是当前进程不再能够通过原来的虚拟地址来访问这些数据。
当 shmdt
被调用时,操作系统会更新进程的页表,移除与 shmaddr
相关的映射条目。这样,进程就无法通过这个地址来访问共享内存区了。如果其他进程仍然保持对这块共享内存的映射,它们不受影响,可以继续访问共享内存区中的数据。只有当所有进程都调用了 shmdt
并且没有其他进程再使用这块共享内存时,操作系统才会真正地从物理内存中移除共享内存区。
3.5删除共享内存
3.5.1命令行中删除共享内存
ipcrm -m 2
这里的2是shmid
3.5.2代码中删除共享内存
代码删除共享内存,通过shmctl的不同参数实现
int shmctl(int shmid ,int cmd,struct shmid_ds*buf);
功能:用于控制共享内存
原型
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数
shmid:由shmget返回的共享内存标识码
cmd:将要采取的动作(有三个可取值)
buf:指向一个保存着共享内存的模式状态和访问权限的数据结构
返回值:成功返回0;失败返回-1
shmctl函数用于控制共享内存,其参数包括共享内存标识符、控制命令以及一个可选的结构指针。
- shmid:这是共享内存标识符,即要控制的共享内存段的ID。每个共享内存段在创建时都会被分配一个唯一的ID,用于后续的操作和引用。
- cmd:这是控制命令,用于指定要执行的操作。常用的命令包括IPC_RMID(删除共享内存段)和IPC_SET(修改共享内存段的属性)等。这些命令允许你管理共享内存的状态,例如删除一个不再需要的共享内存段或者修改其访问权限。
- buf:这是一个指向
struct shmid_ds
结构的指针,用于存储共享内存段的信息。如果不需要获取共享内存段的信息,这个参数可以设置为NULL。
其中第二个参数可以取的值有
shmctl函数通过这些参数来执行特定的控制操作,而不是直接操作共享内存中的数据。例如,如果想要删除一个共享内存段,你可以调用shmctl(shmid, IPC_RMID, NULL);
。这会告诉操作系统移除对应的共享内存段,但并不会影响到共享内存中的数据,因为这些数据可能仍然被其他进程所使用或引用。只有当所有进程都不再使用这块共享内存并且调用了shmdt
之后,共享内存才会被真正地从物理内存中移除。
代码逻辑,想验证创建共享内存后怎么通信,把下面的.c文件再实现同样的一份.c文件,然后在通信逻辑部分,把访问共享内存当成访问普通内存一样使用这个共享内存段
int main()
{
// 1.使用ftok函数(算法)创建公共的key值,是多少不重要
key_t k = ftok(PATH_NAME, PROJ_ID);
assert(k != -1);
Log("create key done", Debug) << "server key:" << k << endl;
// 2.shmget函数创建共享内存
int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666);
if (shmid == -1)
{
perror("shmget");
exit(1);
}
Log("create shm done", Debug) << "shmid:" << shmid << endl;
sleep(10);
// 3.使用shmat函数将指定的共享内存关联到自己的地址空间(和自己的进程关联)
char *shmaddr = (char *)shmat(shmid, nullptr, 0); // 返回的是在该进程地址空间中,指向该共享内存的起始地址
Log("attach shm done", Debug) << "shmid:" << shmid << endl;
sleep(10);
// 通信逻辑
// 4.使用shmdt函数,在页表内去除该共享内存和本进程的关联
int n = shmdt(shmaddr);
assert(n != -1);
(void)n;
Log("detach shm done", Debug) << "shmid:" << shmid << endl;
sleep(10);
// 最后一步,删除共享内存
n = shmctl(shmid, IPC_RMID, nullptr);
assert(n != -1);
(void)n;
Log("delete shm done", Debug) << "shmid:" << shmid << endl;
return 0;
}
管道文件的通信方式与共享内存通信方式的比较
管道通信的一次IO
共享内存的通信方式,进程A从键盘输入的数据,不经过缓冲区直接写入shm,而一写入shm(共享内存),另一个进程B能立马从shm看到数据(然后直接从内存中读取到数据),而不用再创建自己的缓冲区,这样的效率就会很快
注意:共享内存没有进行同步与互斥