我与Linux的爱恋:共享内存
🔥个人主页:guoguoqiang. 🔥专栏:Linux的学习
文章目录
- 共享内存的引入及其原理
- 匿名管道方式
- 命名管道通信方式
- 共享内存
- 共享内存的创建与管理
- 共享内存相关函数
- **创建共享内存--shmget**
- ipcs 介绍
- ipcs -m [options]
- 删除共享内存 ipcrm -m [shmid]
- 挂接共享内存--shmat
- 解除挂接共享内存--shmdt
- 共享内存管理 --shmctl
共享内存的引入及其原理
匿名管道方式
匿名管道(也称为管道或标准管道)是一种单向的进程间通信(IPC)机制,用于在父进程和子进程之间传输数据。匿名管道的创建和操作由操作系统内核自动管理,无需用户显式创建文件。
**创建和操作的过程:**当父进程调用 fork 系统调用来创建子进程时,操作系统会自动创建一个匿名管道。这个管道是两个文件描述符:一个用于写入(写端),另一个用于读取(读端)。父进程将数据写入管道时,数据被写入内核中的一个缓冲区。子进程从管道中读取数据时,数据从内核缓冲区读取到用户空间。每次通信时,数据在父进程和子进程之间通过内核缓冲区进行一次拷贝。由于内核缓冲区与用户空间的数据拷贝,匿名管道通信涉及两次数据拷贝。当进程尝试写入或读取管道时,需要进行用户态到内核态的转换。这种转换可能涉及上下文切换,增加了通信的开销。
-
引入背景(与匿名管道对比)
- 匿名管道是一种进程间通信方式,主要用于具有父子关系的进程之间的通信。它的优点是简单易用,实现了单向的数据传输。然而,匿名管道存在一些局限性。
- 数据传输方式:匿名管道是基于字节流的通信方式,数据在管道中流动,当一个进程向管道写入数据,另一个进程从管道读取数据时,数据需要在管道缓冲区和进程的用户空间之间进行多次拷贝。例如,在父进程写入数据到管道后,数据先被拷贝到内核的管道缓冲区,子进程读取时,又从缓冲区拷贝到子进程的用户空间。这种频繁的数据拷贝在处理大量数据时效率较低。
- 通信范围:匿名管道主要用于父子进程之间通信,对于无亲缘关系的进程间通信不太方便。
- 共享内存作为一种改进的进程间通信方式被引入。它允许多个进程直接共享一块物理内存区域,数据在进程间传递时,不需要像匿名管道那样频繁地进行数据拷贝,从而提高了通信效率,并且可以用于多个无亲缘关系的进程之间的通信。
-
共享内存原理(在匿名管道背景下对比理解)
- 内存分配与共享:
- 与匿名管道不同,共享内存是由操作系统分配一块物理内存区域供多个进程共享。在Linux系统中,例如通过
shmget
系统调用可以申请一块共享内存段。假设两个无亲缘关系的进程A和B要进行通信,系统会为它们分配一块共享内存区域。 - 匿名管道则是在内核中开辟缓冲区作为通信通道,没有直接共享物理内存的概念。它的缓冲区大小有限,且数据在管道中的流动是单向的,从写端流向读端。
- 与匿名管道不同,共享内存是由操作系统分配一块物理内存区域供多个进程共享。在Linux系统中,例如通过
- 地址映射:
- 共享内存通过
shmat
系统调用将分配的共享内存段映射到进程的虚拟地址空间。对于进程A和B,它们可以将共享内存映射到各自虚拟地址空间的不同位置,但这些虚拟地址最终都指向同一块物理共享内存。这就好像两个进程在自己的“地盘”(虚拟地址空间)上开了一个“门”(映射后的虚拟地址),通过这个“门”可以访问同一块“公共区域”(物理共享内存)。 - 匿名管道的读写操作是通过文件描述符来进行的。进程通过继承(父子进程情况下)或者打开(命名管道类似情况)管道对应的文件描述符,按照管道的规则(半双工,字节流)进行读写操作,不涉及内存地址的映射。
- 共享内存通过
- 数据访问与同步:
- 在共享内存中,因为多个进程可以同时访问共享内存区域,所以需要同步机制来防止数据冲突。例如,使用信号量或互斥锁。如果进程A和B都要对共享内存中的一个变量进行写操作,没有同步机制就会导致数据混乱。比如,进程A先读取变量的值为10,在准备写入更新后的值20之前,进程B读取了变量的值还是10,然后进程B写入了更新后的值30,最后进程A写入20,这样就导致数据不一致。
- 匿名管道在数据访问上相对简单,因为它是半双工的字节流通信。写进程向管道写入数据,读进程从管道读取数据,数据的流动是有序的,只要按照正确的读写顺序,一般不会出现数据冲突问题。但是它的读写操作可能会因为管道缓冲区满(写操作阻塞)或者管道为空(读操作阻塞)而被阻塞。
- 生命周期与管理:
- 共享内存的生命周期相对灵活。它可以在多个进程需要通信的时间段内一直存在,直到进程通过
shmctl
等系统调用显式地释放共享内存段。例如,进程A和B完成通信后,可以将共享内存段标记为释放,由操作系统回收内存。 - 匿名管道的生命周期与使用它的进程紧密相关。对于父子进程使用的匿名管道,当父子进程都关闭了管道的文件描述符后,管道就不复存在了。而且匿名管道是临时的通信通道,没有像共享内存那样在文件系统(或其他类似存储管理机制)中有明确的创建、标记和释放过程。
- 共享内存的生命周期相对灵活。它可以在多个进程需要通信的时间段内一直存在,直到进程通过
- 内存分配与共享:
命名管道通信方式
**命名管道(Named Pipe)**是一种在操作系统中用于实现进程间通信(IPC)的方法。它允许不同进程通过一个特殊的文件进行数据交换。命名管道与普通文件类似,但有一些独特的特点,使其适合于进程间的单向或双向通信;命名管道通常被实现为FIFO(First In, First Out)文件。FIFO文件是特殊的文件类型,它确保数据按照写入的顺序被读取,与匿名管道不同,命名管道有一个明确的名字,这个名字在文件系统中可以被识别和访问。这允许不相关的进程通过知道管道名称来进行通信。
创建和使用:在Unix/Linux系统中,可以使用mkfifo命令或mknod系统调用来创建命名管道;进程可以打开命名管道进行写入操作。数据将被写入到内存中的FIFO缓冲区。另一个进程可以打开同一个命名管道进行读取操作,从FIFO缓冲区中取出数据。
通信方式:
单向通信:命名管道支持单向通信,即一个进程写入数据,另一个进程读取数据。为了实现双向通信,可以创建两个命名管道,分别用于不同方向的数据流动。
阻塞和非阻塞模式:在默认情况下,读取操作会阻塞,直到管道中有数据可读;写入操作也可能阻塞,直到有进程打开管道进行读取。可以设置管道为非阻塞模式,进程将立即返回,而不是等待数据准备好。
优点
无需磁盘I/O:命名管道的数据传输不涉及磁盘I/O操作,数据仅在内存中流动,从而提高了通信效率。
简洁:使用命名管道可以在进程间传输数据,避免了复杂的共享内存或其他IPC机制。
缺点
缓冲区管理:由于数据在内存中处理,写入进程和读取进程需要分别管理自己的缓冲区。写入进程需要将数据从应用程序缓冲区写入管道,而读取进程则需要将数据从管道读取到自己的缓冲区。
有限的功能:命名管道仅支持基本的单向或双向数据流,不支持更复杂的IPC需求,如同步或互斥机制,这些需要其他IPC方法来实现。
-
引入背景(与命名管道对比)
- 命名管道是一种进程间通信方式,它在文件系统中有一个名称,多个不相关的进程可以通过这个名称来访问它。虽然命名管道能够实现进程间的通信,并且可以用于无亲缘关系的进程之间,但它也有一些局限性。
- 通信效率方面:命名管道的数据传输方式和匿名管道类似,也是基于字节流的。当进程通过命名管道进行通信时,数据需要在管道缓冲区和进程的用户空间之间多次拷贝。例如,一个进程向命名管道写入数据时,数据先被拷贝到管道的内核缓冲区,另一个进程读取时,数据又从缓冲区拷贝到该进程的用户空间。对于大量数据的传输,这种方式会导致效率降低。
- 共享内存的引入就是为了克服这种数据拷贝带来的效率问题,它允许多个进程直接共享一块物理内存区域,从而在进程间进行高效的数据交换。
-
共享内存原理(在命名管道背景下对比理解)
内存分配与共享
- 命名管道:命名管道是通过
mkfifo
命令或mkfifo()
系统调用在文件系统中创建一个特殊的文件类型(FIFO)来实现通信。它本质上是基于文件系统的一种通信通道,没有直接共享物理内存的操作。多个进程通过打开这个命名管道文件进行读写操作来传递数据,数据存储在管道的内核缓冲区中。 - 共享内存:操作系统为共享内存分配一块物理内存区域。在Linux系统中,可以通过
shmget
系统调用请求分配共享内存段。例如,两个进程A和B要进行通信,系统会为它们分配一块共享内存区域。这个区域可以被多个进程直接访问,就像它们共同拥有一块内存一样,不需要像命名管道那样通过文件系统的缓冲机制来传递数据。
地址映射
- 命名管道:进程通过普通的文件打开操作(如
open()
函数)打开命名管道文件,获取文件描述符。然后使用read()
和write()
函数基于文件描述符进行读写操作,不涉及将内存区域映射到进程虚拟地址空间的操作。进程只是将数据写入管道文件的内核缓冲区或者从缓冲区读取数据,通过文件系统的I/O机制来实现通信。 - 共享内存:通过
shmat
系统调用将分配的共享内存段映射到进程的虚拟地址空间。进程A和B可以将共享内存映射到各自虚拟地址空间的不同位置,但这些虚拟地址最终都指向同一块物理共享内存。这使得进程可以像访问自己的本地内存一样访问共享内存区域,大大提高了数据访问的直接性。
数据访问与同步
- 命名管道:数据在命名管道中的流动是按照先进先出(FIFO)的原则进行的。由于命名管道通常是半双工通信,一个进程在某一时刻只能进行读或写操作。如果多个进程同时对一个命名管道进行读写操作,数据的读写顺序由管道的缓冲机制和操作系统的调度决定。虽然数据的读写相对有序,但如果没有额外的同步机制,也可能会出现一些复杂的情况,如多个写进程同时写入数据可能会导致数据混乱。
- 共享内存:因为多个进程可以同时访问共享内存区域,所以必须有同步机制来防止数据冲突。常用的同步机制包括信号量和互斥锁。例如,当进程A和B都要对共享内存中的一个数据结构进行修改时,如果没有同步措施,可能会出现数据不一致的情况。通过信号量可以控制对共享内存的访问权限,如限制同时访问的进程数量;互斥锁则可以确保在同一时刻只有一个进程能够访问共享内存中的关键区域。
生命周期与管理
- 命名管道:命名管道在文件系统中有一个实际的文件名,一旦创建,它会一直存在于文件系统中,直到被显式地删除(如使用
unlink
或rm
等操作)。它的生命周期相对独立于使用它的进程,即使没有进程正在使用它,它依然存在。 - 共享内存:共享内存的生命周期由进程通过系统调用进行控制。在使用前通过
shmget
等系统调用创建,使用过程中通过shmat
进行映射,当进程不再需要共享内存时,通过shmdt
撤销映射,最后通过shmctl
释放共享内存段。它的存在是为了满足进程间通信的需求,当通信结束后可以被释放。
共享内存
共享内存的创建与管理
**共享内存的创建:**通常,由一个进程(比如服务器进程)首先创建一个共享内存区,并获取一个唯一标识符(ID)。这一过程通常通过系统调用(如 Unix/Linux 中的 shmget())完成。
**关联共享内存:**其他需要访问该共享内存的进程通过这个标识符附加(attach)到同一个共享内存区,这个操作可以通过 shmat() 等系统调用实现。关联成功后,这些进程可以像操作自己的内存一样直接读写共享内存区。
**取消关联与销毁:**当进程不再需要共享内存时,可以调用 shmdt() 取消对共享内存的关联。而当所有进程都不再使用这块共享内存后,可以通过 shmctl() 进行销毁操作,以释放系统资源。
这样当A进程与B进程通信时,只需要通过共享虚拟地址映射到物理地址,对该物理地址直接进行写入;而B进程是通过共享虚拟地址映射到物理地址,从该物理地址中直接读取。这样就避免了2次拷贝,而且无需在用户态和内核态之间频繁切换,减少了进程上下文切换和内核态内存管理的负担。
刚刚所有的操作,都是操作系统做的
操作系统提供1,2步骤的系统调用,供进程A,B来进程调用-----系统调用
共享内存在系统中可以同时存在多份,供不同个数,不同对进程同时进行通信
操作系统注定了要对共享内存进行管理---->先描述,在组织------共享内存不是简单的一段内存空间,也要有描述并管理共享内存的数据结构和匹配的算法
共享内存 = 内存空间(数据)+ 共享内存的属性!
//共享内存对应的内核数据结构
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; /* Last change time */
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 */
...
};
共享内存相关函数
获取唯一的key值 ftok
ftok 函数在 C 语言中用于生成一个 System V IPC(进程间通信)的 key,该 key 用于标识共享内存段、消息队列或信号量等 IPC 资源。
ftok
是在Unix/Linux系统编程中用于生成一个唯一的键值(key)的函数,这个键值通常被用于创建诸如共享内存段、消息队列、信号量等进程间通信(IPC)机制的标识符。以下是关于它的详细介绍:
类似于文件路径的概念,key 具有唯一性。通过 ftok 函数,pathname 和 proj_id 结合起来生成一个唯一的 key,这保证了即使多个进程都调用 ftok,只要使用相同的文件路径和项目标识符,生成的 key 就会一致,从而指向相同的 IPC 资源。否则,如果路径或项目 ID 不同,生成的 key 也会不同,无法访问相同的共享资源。
- 函数原型
在C语言中,ftok
函数的原型如下:
#include <sys/types.h>
#include <sys/ipc.h>
key_t ftok(const char *pathname, int proj_id);
其中:
pathname
:是一个指向有效文件路径名的字符串指针。这个文件通常是存在于文件系统中的普通文件,通过指定这个文件,ftok
函数会基于该文件的一些属性(如文件的inode编号等)来生成键值的一部分。一般建议选择一个在系统运行期间不会经常变动的文件,比如系统启动脚本文件等,以确保每次调用ftok
生成的键值相对稳定且唯一。proj_id
:是一个整数类型的项目标识符。它可以是任意的整数值,但通常建议选择一个在项目范围内有意义且唯一的值。这个值会与基于文件属性生成的部分共同组成最终的键值。例如,在一个包含多个不同功能模块且都需要使用进程间通信机制的项目中,不同模块可以根据自身的功能特点设置不同的proj_id
值,以便区分不同模块所创建的共享内存段、消息队列等。
ftok
函数的返回值类型是key_t
,它是一个整数类型的键值。如果函数调用成功,会返回一个有效的、可用于后续创建进程间通信机制的键值(非负数);如果调用失败,会返回-1
,并且会设置全局变量errno
来指示具体的错误原因,常见的错误原因可能包括指定的文件不存在、没有访问该文件的权限(EACCES 表示权限错误,ENOENT 表示文件不存在)等。
生成的 key 可以用于像 msgget()、shmget() 或 semget() 等 IPC 机制函数来创建或访问 IPC 资源。
- 工作原理
- 当调用
ftok
函数时,它首先会获取指定文件(由pathname
指定)的一些关键属性信息,比如文件的inode编号(在Unix/Linux文件系统中,每个文件都有一个唯一的inode编号,用于标识文件的存储位置、权限等信息)。然后,它会将这个文件属性相关的信息与给定的整数proj_id
进行某种数学运算(具体运算方式是由系统内部实现决定的,通常是基于位运算等方式将两者组合起来),从而生成一个唯一的键值。 - 这个键值的唯一性是相对的,它依赖于所选择的文件以及
proj_id
的值。只要保证在同一系统中,对于要创建的同一种类型的进程间通信机制(比如都是创建共享内存段),所选择的文件和proj_id
的值组合是唯一的,那么生成的键值就是可用的。
- 应用场景
- 创建共享内存段:在多个进程需要共享一块内存区域进行通信的场景下,首先需要通过
ftok
函数生成一个键值,然后利用这个键值作为参数调用shmget
系统调用(用于获取共享内存段的标识符)来创建共享内存段。例如:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#define PATH_NAME "/etc/passwd"
#define PROJ_ID 1
int main() {
key_t key = ftok(PATH_NAME, PROJ_ID);
if (key == -1) {
perror("ftok");
return 1;
}
int shmid = shmget(key, sizeof(int), IPC_CREAT | 0666);
if (shmid == -1) {
perror("shmget");
return 1;
}
// 后续步骤:映射共享内存到进程地址空间、进行数据读写等
return 0;
}
在这个例子中,选择了/etc/passwd
文件作为ftok
函数的pathname
参数(这只是一个示例,实际应用中可根据情况选择合适的文件),proj_id
设置为1
。生成键值后,再用它来创建共享内存段。
-
创建消息队列:类似地,在创建消息队列用于进程间通信时,也需要先通过
ftok
函数生成键值,然后用这个键值作为参数调用msgget
系统调用(用于获取消息队列的标识符)来创建消息队列。 -
创建信号量:对于创建信号量的情况,同样先通过
ftok
函数生成键值,再以该键值作为参数调用semget
系统调用(用于获取信号量的标识符)来创建信号量。
- 注意事项
- 文件选择的稳定性:如前所述,选择作为
ftok
函数pathname
参数的文件应该在系统运行期间尽量保持稳定,避免频繁变动。因为如果文件发生变化(比如被删除、重命名等),可能会导致后续基于该文件生成的键值也发生变化,从而影响已经创建的进程间通信机制的正常使用。 proj_id
的唯一性:虽然proj_id
可以是任意整数,但在一个项目中,应该确保不同的进程间通信需求(比如创建不同的共享内存段、消息队列等)所设置的proj_id
值是唯一的,否则可能会导致生成的键值冲突,进而出现通信故障等问题。
以下是关于在进程间通信等场景下“key”(键值)的详细讨论:
- 键值的作用与重要性
在多种进程间通信(IPC)机制中,如共享内存、消息队列、信号量等,键值(key)起着至关重要的作用。它充当了一种标识符,使得不同进程能够在系统环境中准确地定位和访问到特定的IPC资源。
例如,当多个进程需要共享一块内存区域(共享内存的情况),每个进程都需要通过相同的键值来获取对该共享内存段的访问权限。同样,对于消息队列和信号量,键值也是各个进程能够找到并与之交互的关键标识。如果没有一个统一且唯一的键值作为指引,进程将无法准确地找到并使用对应的IPC资源,从而导致通信或同步等操作无法正常进行。
- 生成方式与ftok函数
如前面提到的,在Unix/Linux系统中,常用ftok
函数来生成键值。ftok
函数基于一个指定的文件路径名(pathname
)和一个项目标识符(proj_id
)来生成键值。
- 基于文件属性:
ftok
函数会利用指定文件的一些内在属性,比如文件的inode编号等。文件的inode编号在文件系统中是唯一标识该文件的一个重要属性,通过结合这个属性以及proj_id
,ftok
函数能够生成一个相对独特的键值。这使得在同一系统环境下,只要选择的文件和proj_id
组合得当,就可以得到一个可用于特定IPC目的的键值。 - 稳定性与唯一性考虑:
- 在选择
pathname
指定的文件时,要确保其在系统运行期间具有相对的稳定性。因为如果文件发生变动,如被删除、重命名等,可能会影响后续基于该文件生成键值的一致性。例如,如果一个共享内存段是基于某个文件生成的键值来创建的,当该文件消失后,其他进程可能无法再通过相同的键值准确找到并使用该共享内存段。 - 对于
proj_id
,虽然它可以是任意整数,但在一个项目范围内,应该保证其唯一性。不同的IPC需求(如创建不同的共享内存段、不同的消息队列等)如果使用相同的proj_id
且搭配相同的文件(不太可能,但理论上存在这种情况),可能会导致生成相同的键值,进而引发资源访问冲突等问题。
- 在选择
- 键值的范围与类型
- 类型:通常,由
ftok
函数生成的键值类型是key_t
,这是一个在系统中定义的整数类型。它在不同的系统实现中可能有不同的取值范围,但一般来说是一个能够满足标识IPC资源需求的整数范围。 - 范围影响:键值的取值范围会影响到其在系统中的唯一性和可识别性。如果取值范围过小,可能会导致在一个复杂的系统环境中,尤其是当有大量不同的IPC资源需要创建时,出现键值重复的情况,从而使得不同的IPC资源无法准确区分。相反,如果取值范围过大,虽然能保证唯一性的概率更高,但可能会在存储和处理键值时带来一些额外的开销,比如占用更多的内存空间来存储键值等。
- 键值在不同IPC机制中的应用
- 共享内存:在共享内存的创建和使用过程中,首先通过
ftok
生成键值,然后将该键值作为参数传递给shmget
系统调用。shmget
根据键值来查找或创建特定的共享内存段,并返回一个共享内存段标识符(shmid
)。后续进程通过这个shmid
来对共享内存进行映射、读写等操作。 - 消息队列:对于消息队列,同样先利用
ftok
生成键值,再将键值作为参数传给msgget
系统调用。msgget
依据键值找到或创建相应的消息队列,并返回一个消息队列标识符(msgqid
)。进程通过这个msgqid
来发送和接收消息。 - 信号量:在创建信号量时,也是先通过
ftok
生成键值,接着把键值作为参数给semget
系统调用。semget
基于键值确定或创建特定的信号量,并返回一个信号量标识符(semid
)。进程利用这个semid
来执行信号量的相关操作,如等待、释放等。
- 键值的管理与维护
- 一致性维护:在一个项目或系统的运行过程中,要确保生成键值的方式保持一致。这包括对
pathname
指定的文件的维护,使其不发生不必要的变动;以及对proj_id
的正确使用,保证不同IPC资源创建时使用的proj_id
值符合唯一性要求。 - 冲突处理:如果在系统运行过程中发现键值冲突的情况(如不同IPC资源使用了相同的键值,导致资源访问混乱),需要及时排查原因。可能是因为文件变动影响了
ftok
生成键值的方式,或者是proj_id
使用不当造成的。根据具体原因采取相应的措施,如重新选择合适的文件和proj_id
来生成新的键值,以恢复正常的IPC操作。
创建共享内存–shmget
key: 这是一个整数,通常使用 ftok 函数生成,用于唯一标识一个共享内存段。不同的进程可以通过这个 key 值来访问同一个共享内存 段。
size: 这是请求的共享内存段的大小,以字节为单位。这个值必须大于 0。
shmflg: 这是一组标志,通常包括访问权限位(与文件的权限位类似)以及一些其他可选标志。访问权限位可以使用八进制数表示,例如 0644
标志包括
返回值:
成功时,返回共享内存段的标识符(一个非负整数)。
失败时,返回 -1,并设置 errno 以指示错误原因。
测试
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <cerrno>
int main()
{
key_t key = ftok("/home/gwq/code/lesson12/ftok/test.cc", 5);
int shmid = shmget(key, 128, IPC_CREAT);
std::cout << shmid << std::endl;
key_t key1 = ftok("/home/gwq/code/lesson12/ftok", 66);
int shmid1 = shmget(key1, 128, IPC_CREAT|IPC_EXCL);
std::cout << shmid1 << std::endl;
if(shmid1==-1){
perror("shmid fail");
}
key_t key2 = ftok("/home/gwq/code/lesson12/shmid/test.cc", 66);
int shmid2 = shmget(key, 128, IPC_CREAT|IPC_EXCL);
std::cout << shmid2 << std::endl;
if(shmid2 ==-1){
perror("shmid fail");
}
return 0;
}
第一个创建成功,第二个创建成功,第三个创建失败,原因第三次传入的key是第一个使用过的
ipcs 介绍
ipcs 是一个用于查看 POSIX 系统中进程间通信 (IPC) 资源信息的命令行工具。使用 -m 选项可以专门查看共享内存 (shared memory) 的相关信息。
ipcs -m [options]
如何创建共享内存时添加权限,只需要在shmget的第三个参数或运算上对应的权限即可
key_t key = ftok("/home/gwq/code/lesson12/ftok/test.cc", 5);
int shmid = shmget(key, 128, IPC_CREAT,0666);
std::cout << shmid << std::endl;
删除共享内存 ipcrm -m [shmid]
挂接共享内存–shmat
shmat(Shared Memory Attach)函数的作用是将一个共享内存段映射到调用进程的地址空间中,这样进程就可以像访问本地内存一样访问共享内存
参数:
shmid: 共享内存段的标识符,通常由 shmget 函数返回。
shmaddr: 指定共享内存段附加到进程地址空间的地址。如果这个参数是 NULL,则系统会自动选择一个合适的地址。如果它不是 NULL,并且 shmflg 中包含了 SHM_RND 标志,则共享内存段会被附加到 (shmaddr - (shmaddr % SHMLBA)) 地址处,其中 SHMLBA 是共享内存的低边界地址对齐。
shmflg: 这是一组标志,用于控制共享内存段的附加行为。它可以设置为 0,或者以下标志的组合:
返回值:
成功时,返回共享内存段在进程地址空间中的地址。
失败时,返回 (void *) -1,并设置 errno 以指示错误原因。
解除挂接共享内存–shmdt
shmdt 函数用于从调用进程的地址空间中分离(detach)一个共享内存段
参数:
shmaddr: 共享内存段在进程地址空间中的地址,这个地址是由 shmat 函数返回的。
返回值:
成功时,返回 0。
失败时,返回 -1,并设置 errno 以指示错误。
错误代码(errno):
EINVAL: shmaddr 指定的地址无效,或者它不是通过 shmat 返回的地址。
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(){
key_t key=ftok("/home/gwq/code/lesson12/shm",1);
//创建共享内存
int shmid=shmget(key,256,IPC_CREAT|0666);
sleep(2);
//挂接共享内存池
void* shmaddr =shmat(shmid,NULL,0);
sleep(2);
//删除共享内存
shmdt(shmaddr);
std::cout << "dettach : " << shmid << std::endl;
return 0;
}
测试结果:输入while :; do ipcs -m; sleep 1; done,每1s查看一次共享内存,发现nattch的变化与代码一致
共享内存管理 --shmctl
参数:
shmid: 这是由 shmget 函数返回的共享内存标识符,用于唯一标识要进行控制操作的共享内存段。
cmd: 这是一个整数,表示要执行的控制命令。
buf: 这是一个指向 shmid_ds 结构体的指针,用于存储共享内存段的属性信息。当使用 IPC_STAT 命令时,buf 指向的结构体用于接收共享内存段的属性信息;当使用 IPC_SET 命令时,buf 指向的结构体用于指定新的属性值;当使用 IPC_RMID 命令时,buf 可以设置为 NULL。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int main(){
key_t key=ftok("/home/gwq/code/lesson12/shm",800);
int shmid=shmget(key,256,IPC_CREAT|0666);
sleep(2);
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
共享内存的特性
共享没有没有同步和互斥机制。即读写双方可以同时访问共享内存,这会导致数据不一致问题,这个问题的解决方案将在后序文章中介绍;
共享内存是所有的进程通信中,速度最快的;
共享内存内部的数据由用户自己维护(读完要自己清空);
共享内存的生命周期是随内核的,用户不主动删除,共享内存会一直存在(除非内核重启或用户释放);
共享内存的大小一般建议是4096的整数倍,内存管理的一页大小为4096字节(4KB)。若申请4097,则系统会分配4096 * 2,但用户还是只能使用4097的空间,会存在4095字节空间的浪费。
- 高效的数据共享
- 减少数据拷贝:共享内存允许多个进程直接访问同一块物理内存区域。与其他进程间通信方式(如管道)相比,它避免了数据在不同进程空间之间频繁的拷贝。例如,在使用管道通信时,数据从一个进程写入管道,再从管道读取到另一个进程,这个过程涉及数据在内核缓冲区和进程用户空间之间的多次拷贝。而在共享内存中,进程可以像访问自己的本地内存一样直接读写共享内存区域,大大提高了数据传输的效率,尤其适用于需要频繁交换大量数据的进程间通信场景。
- 快速的数据访问:由于共享内存直接映射到进程的虚拟地址空间,进程对共享内存的访问速度基本等同于对自身内存的访问速度。进程可以通过简单的内存读写指令来操作共享内存中的数据,不需要像使用消息队列等方式那样进行复杂的系统调用和数据传递操作,进一步提高了数据访问的效率。
- 灵活性
- 适用范围广:共享内存可以用于各种类型的进程间通信,无论是有亲缘关系(如父子进程)还是无亲缘关系的进程。对于父子进程,共享内存可以作为一种高效的通信方式,在它们之间传递复杂的数据结构或大量的数据。对于无亲缘关系的进程,通过合适的同步机制,也可以利用共享内存进行通信和数据共享,例如在多进程的服务器 - 客户端模型中,服务器和客户端进程可以通过共享内存交换数据。
- 数据结构和大小灵活:共享内存区域可以存储各种类型的数据结构,如数组、结构体等。并且,共享内存的大小可以根据实际需求进行分配,从很小的数据区域到很大的内存块都可以。例如,在一个图像处理程序中,多个进程可以共享一块足够大的内存区域来存储图像数据,进行并行的图像滤波、色彩调整等操作。
- 生命周期可控性
- 创建与销毁过程明确:在操作系统中,共享内存的生命周期是由进程通过系统调用进行控制的。一般通过
shmget
系统调用创建共享内存段,在使用过程中可以通过shmat
将其映射到进程的虚拟地址空间,当进程不再需要共享内存时,通过shmdt
撤销映射,最后通过shmctl
释放共享内存段。这种明确的创建、使用和释放过程使得进程可以根据实际通信需求灵活地管理共享内存的生命周期。 - 独立于进程生命周期(在一定程度上):共享内存段一旦被创建,它可以在多个进程的整个通信周期内持续存在,只要没有被显式地释放。这意味着即使创建共享内存的部分进程已经结束,只要还有其他进程在使用该共享内存段并且没有释放它,共享内存依然可以正常工作。例如,在一个长期运行的服务器程序中,服务器进程可以创建共享内存段与多个客户端进程进行通信,即使某个客户端进程意外退出,服务器进程仍然可以继续使用共享内存与其他客户端进程进行通信。
- 创建与销毁过程明确:在操作系统中,共享内存的生命周期是由进程通过系统调用进行控制的。一般通过
- 需要同步机制配合
- 数据一致性问题:由于多个进程可以同时访问共享内存区域,没有合适的同步机制时,很容易出现数据不一致的问题。例如,两个进程同时对共享内存中的一个变量进行写操作,可能会导致数据混乱。假设一个变量初始值为10,进程A读取这个值后准备加1并写回,同时进程B也读取这个值后准备加2并写回,如果没有同步机制,可能会出现进程A先写回11,然后进程B又写回13,而不是正确的13(先加2再加1的结果)。
- 常用同步工具:为了解决数据一致性问题,共享内存通常需要与同步机制配合使用。常用的同步机制包括信号量(Semaphore)和互斥锁(Mutex)。信号量可以用于控制对共享资源的访问数量,例如,设置一个信号量初始值为1,当一个进程访问共享内存时,先获取信号量,此时信号量值变为0,其他进程再试图获取信号量时就会被阻塞,直到持有信号量的进程释放信号量。互斥锁则更加简单直接,用于保证在同一时刻只有一个进程能够访问共享内存中的临界区(需要互斥访问的区域)。
- 内存管理特性
- 物理内存分配方式:操作系统在分配共享内存时,会从系统的物理内存中划出一块区域供进程共享。这块区域的物理内存分配方式与普通的进程内存分配有所不同,它是专门为多个进程能够同时访问而设计的。例如,在Linux系统中,共享内存的分配是通过内核的内存管理模块进行的,它会考虑到多个进程同时访问的情况,确保内存区域的安全性和可访问性。
- 虚拟地址映射特点:每个进程都有自己独立的虚拟地址空间,共享内存通过系统调用(如
shmat
)将物理共享内存区域映射到进程的虚拟地址空间。不同进程可以将同一块共享内存映射到自己虚拟地址空间的不同位置,但这些虚拟地址最终都指向同一块物理内存。这种映射方式使得进程可以方便地使用共享内存,就好像它是自己本地内存的一部分一样,同时也便于操作系统对共享内存的管理和控制。