Linux高性能服务器编程 进程间通信方式 共享内存 System V IPC 和POSIX
进程间通信方式—共享内存(System V IPC 和POSIX)
System V IPC共享内存
共享内存的基本原理
- 共享内存是一种进程间通信(IPC)机制。当多个进程需要共享数据时,可以创建一块共享内存区域。通过
shmget
函数创建共享内存段后,这块内存区域就存在于系统的内存空间中。 - 然后,各个进程可以通过
shmat
函数将这块共享内存连接到自己的进程地址空间。连接后,进程就可以像访问自己的本地内存一样访问共享内存中的数据。
共享内存是最高效的IPC机制,因为它不涉及进程之间的任何数据传输,这种高效率带来的问题是,我们必须用其他辅助手段来同步进程对共享内存的访问,否则会产生竞态条件,因此,共享内存通常和其他进程间通信方式一起使用。
Linux共享内存的API都定义在sys/shm.h
头文件中,包括4个系统调用shmget
、shmat
、shmdt
、shmctl
。
1.shmget
系统调用
shmget
系统调用创建一段新的共享内存,或者获取一段已经存在的共享内存。
1 |
|
参数详解
key_t key
:- 这是一个键值,用于标识共享内存段。它可以是由
ftok
函数生成的值,也可以是IPC_PRIVATE
(用于创建私有共享内存段,通常在父子进程间使用)。ftok
函数根据一个文件路径和一个项目标识符生成一个唯一的key_t
值。
- 这是一个键值,用于标识共享内存段。它可以是由
size_t size
:- 这个参数指定了要创建或获取的共享内存段的大小,单位是字节。如果是创建新的共享内存段,这个大小必须大于 0。如果
shmget
函数用于获取已存在的共享内存段,这个参数通常可以被忽略(但仍需提供一个合理的值)。
- 这个参数指定了要创建或获取的共享内存段的大小,单位是字节。如果是创建新的共享内存段,这个大小必须大于 0。如果
int shmflg
:- 这个参数用于控制共享内存段的创建和访问权限,它由以下几种标志组成:
IPC_CREAT
:如果共享内存段不存在,则创建它。如果和IPC_EXCL
一起使用(IPC_CREAT | IPC_EXCL
),则只有在共享内存段不存在时才创建,若已存在则shmget
函数返回 - 1 并设置errno
为EEXIST
。IPC_EXCL
:与IPC_CREAT
一起使用,用于确保创建的是一个新的、唯一的共享内存段。- 权限标志:如
0666
等,用于指定共享内存段的访问权限,格式与文件权限相同(用户、组、其他用户的读、写、执行权限)。 SHM_HUGETLB
标志:当在shmflg
参数中使用SHM_HUGETLB
标志时,表示请求使用大页(huge pages)来分配共享内存。大页是一种内存管理技术,它使用比标准内存页更大的页面大小(通常为 2MB 或 1GB,取决于系统配置)。SHM_NORESERVE
标志:SHM_NORESERVE
标志用于在创建共享内存时,不预留交换空间(swap space)。通常情况下,当创建共享内存段时,系统会为其预留相应的交换空间,以确保在内存不足时可以将部分数据交换到磁盘上。使用SHM_NORESERVE
可以避免这种交换空间的预留。这样,当物理内存不足时,对该共享内存执行写操作将触发SIGESV信号。
- 这个参数用于控制共享内存段的创建和访问权限,它由以下几种标志组成:
返回值
成功时,
shmget
函数返回一个非负整数,即共享内存段的标识符(shmid
)。这个标识符可以用于后续的shmat
(连接共享内存段到进程地址空间)、shmdt
(从进程地址空间分离共享内存段)和shmctl
(控制共享内存段的操作,如删除、获取状态等)操作。失败时,函数返回 - 1,并设置errno
变量来指示错误原因。常见的错误原因包括:
EINVAL
:size
小于SHMMIN
(最小共享内存大小)或者key
无效。EEXIST
:当使用IPC_CREAT | IPC_EXCL
标志且共享内存段已经存在时。ENOENT
:当key
对应的共享内存段不存在且没有使用IPC_CREAT
标志时。
如果 shmget 用于创建共享内存,则这段共享内存的所有字节都被初始化为 0,与之关联的内核数据结构 shmid_ds 将被创建并初始化。shmid_ds 结构体的定义如下:
1 | struct shmid_ds |
shmget 对 shmid_ds 结构体的初始化包括:
- 将 shm_perm.cuid 和 shm_perm.uid 设置为调用进程的有效用户 ID。
- 将 shm_perm.cgid 和 shm_perm.gid 设置为调用进程的有效组 ID。
- 将 shm_perm.mode 的最低 9 位设置为 shmflg 参数的最低 9 位。
- 将 shm_segsz 设置为 size。
- 将 shm_lpid、shm_nattach、shm_atime、shm_dtime 设置为 0。
- 将 shm_ctime 设置为当前的时间。
2.shmat
和 shmdt
系统调用
共享内存被创建/获取后,我们不能立即访问它,而是需要先将它关联到进程的地址空间中,使用完共享内存后,我们也需要将它从进程地址空间中分离,这两项任务分别由以下两个系统调用实现:
1 |
|
1.shamt
1 | void* shmat(int shm_id, const void* shm_addr, int shmflg); |
功能:
- 将由
shm_id
标识的共享内存段连接到调用进程的地址空间。连接成功后,进程可以像访问普通内存一样访问共享内存中的数据。
- 将由
参数:
shm_id
:- 这是共享内存段的标识符,由
shmget
函数成功创建或获取共享内存段时返回。它唯一标识了一个共享内存段。
- 这是共享内存段的标识符,由
shm_addr
:- 用于指定连接共享内存段的地址。
- 如果
shm_addr
为NULL
,则由系统选择合适的连接地址。 - 如果
shm_addr
不为NULL
,并且shmflg
中没有设置SHM_RND
标志,那么共享内存段将被连接到shm_addr
所指定的地址。 - 如果
shm_addr
不为NULL
,并且shmflg
中设置了SHM_RND
标志,那么连接的地址将是shm_addr
向下舍入到系统页面大小(page - size)的整数倍的地址。
shmflg
:- 包含一些控制连接操作的标志。
SHM_RND
:如上述,用于对连接地址进行舍入操作。SHM_RDONLY
:如果设置了这个标志,那么共享内存段将以只读方式连接到进程地址空间。如果不设置此标志,共享内存段将以读写方式连接。
返回值:
成功时,返回指向连接后的共享内存段在进程地址空间中的起始地址的指针。并修改shmid_ds中的字段
- 将 shm_nattach 加 1。
- 将 shm_lpid 设置为调用进程的 PID。
- 将 shm_atime 设置为当前时间。
失败时,返回(void *)-1,并且会设置errno
来指示错误原因。常见的错误原因包括:
EINVAL
:shm_id
无效,或者shm_addr
和shmflg
的组合无效。ENOMEM
:没有足够的内存来连接共享内存段。
shm_id
参数是由shmget
函数返回的共享内存标识符。shm_addr
参数指定将共享内存关联到进程的哪块地址空间,最终效果还受到shmflg
参数的可选标志SHM_RND
的影响。
如果 shm_addr 为 NULL,则被关联的地址由操作系统选择。这是推荐的做法,以确保代码的可移植性。
如果 shm_addr 非空,并且 SHM_RND 标志未被设置,则共享内存被关联到 addr 指定的地址处。
如果 shm_addr 非空,并且设置了 SHM_RND 标志,则被关联的地址是 [shm_addr-(shm_addr % SHMLBA)]。SHMLBA 的含义是 “段低端边界地址倍数”(Segment Low Boundary Address Multiple),它必须是内存页面大小 (PAGE_SIZE) 的整数倍。现在的 Linux 内核中,它等于一个内存页大小。SHM_RND 的含义是圆整 (round),即将共享内存被关联的地址向下圆整到离 shm_addr 最近的 SHMLBA 的整数倍地址处。
除了 SHM_RND 标志外,shmflg 参数还支持如下标志:
SHM_RDONLY。进程仅能读取共享内存中的内容。若没有指定该标志,则进程可同时对共享内存进行读写操作(当然,这需要在创建共享内存的时候指定其读写权限)。
SHM_REMAP。如果 shmaddr 已经被关联到一段共享内存上,则重新关联。
SHM_EXEC。它指定对共享内存段的执行权限。对于共享内存的执行权限,执行权限和读权限是一样的。
2. shmdt
1 | int shmdt(const void* shm_addr); |
功能:
将之前由
shmat
连接到进程地址空间的共享内存段分离。分离操作并不会删除共享内存段本身,只是将该共享内存段从调用进程的地址空间中移除。shm_addr
:- 这是共享内存段在进程地址空间中的起始地址,即
shmat
函数成功连接共享内存段时返回的地址。
- 这是共享内存段在进程地址空间中的起始地址,即
返回值:
成功时,返回
0
。失败时,返回-1,并且会设置errno来指示错误原因。成功调用会修改内核数据结构 shmid_ds 的部分字段,如下:
将 shm_nattach 减 1。
将 shm_lpid 设置为调用进程的 PID。
将 shm_dtime 设置为当前时间。
常见的错误原因包括:
EINVAL
:shm_addr
不是有效的共享内存段地址。
shmdt
函数将关联到shm_addr
参数处的共享内存从进程中分离,它成功时返回0,失败则返回-1并设置errno。
调用该函数使得它从进程空间分离的含义
- 不删除本身:
- 共享内存段在系统内存中是独立存在的。即使一个进程不再需要使用它,这个共享内存段本身并不会消失。这是因为其他进程可能还在使用这块共享内存进行数据交互。例如,有进程 A、B、C 都连接到了同一块共享内存。如果进程 A 调用
shmdt
,只是进程 A 不再能访问这块共享内存,但进程 B 和 C 仍然可以正常访问,共享内存段本身依然存在于系统内存中。
- 共享内存段在系统内存中是独立存在的。即使一个进程不再需要使用它,这个共享内存段本身并不会消失。这是因为其他进程可能还在使用这块共享内存进行数据交互。例如,有进程 A、B、C 都连接到了同一块共享内存。如果进程 A 调用
- 从进程地址空间移除:
- 在进程调用
shmdt
之前,共享内存段是映射到该进程的地址空间中的。这意味着进程可以直接通过指针访问共享内存中的数据。当调用shmdt
后,这个映射关系就被解除了。就好像在进程的 “视野” 中,这块共享内存消失了。虽然共享内存还在系统中,但该进程已经无法再通过之前的指针去访问其中的数据了。这就是所谓的从进程地址空间移除。
- 在进程调用
3.shmctl 系统调用
shmctl
系统调用控制共享内存的某些属性。
1 |
|
函数功能
shmctl
函数用于对共享内存段进行控制操作,如获取共享内存段的状态信息、设置共享内存段的属性、删除共享内存段等。
参数详解
int shm_id
:- 这是共享内存段的标识符,由
shmget
函数成功创建或获取共享内存段时返回。它唯一标识了一个共享内存段,shmctl
函数将对这个标识符所对应的共享内存段进行操作。
- 这是共享内存段的标识符,由
int command
:(见下表)- 这是一个控制命令,用于指定对共享内存段进行何种操作,常见的命令有:
IPC_STAT
:获取共享内存段的状态信息,并将其存储到buf
所指向的struct shmid_ds
结构体中。这个结构体包含了共享内存段的各种属性,如操作权限、大小、最后访问时间等。IPC_SET
:根据buf
所指向的struct shmid_ds
结构体中的信息来设置共享内存段的属性。例如,可以修改共享内存段的操作权限等。IPC_RMID
:删除由shm_id
标识的共享内存段。这是一个非常重要且具有危险性的操作,一旦执行,共享内存段及其所包含的数据将被永久删除,所有关联到该共享内存段的进程将无法再访问它。
- 除了上述常见命令外,还有一些其他命令,如
SHM_LOCK
(锁定共享内存段到物理内存,防止被交换到磁盘)和SHM_UNLOCK
(解锁共享内存段,允许其被交换到磁盘)等,但这些命令的可用性可能取决于操作系统的支持。
- 这是一个控制命令,用于指定对共享内存段进行何种操作,常见的命令有:
struct shmid_ds* buf
:这是一个指向struct shmid_ds结构体的指针,其作用取决于command
参数的值:
- 当
command
为IPC_STAT
时,buf
用于存储获取到的共享内存段的状态信息。 - 当
command
为IPC_SET
时,buf
指向的结构体中的信息将被用于设置共享内存段的属性。 - 如果
command
不涉及IPC_STAT
或IPC_SET
操作(如IPC_RMID
),buf
通常可以设置为NULL
。
- 当
返回值(见下表)
成功时:
- 如果
command
是IPC_STAT
或IPC_SET
,返回0
表示操作成功。 - 如果
command
是IPC_RMID
,返回0
表示共享内存段已成功删除。
- 如果
失败时:
返回-1,并设置errno
变量来指示错误原因。常见的错误原因包括:
EINVAL
:shm_id
无效,或者command
参数无效,或者buf
指向的结构体无效(在IPC_STAT
或IPC_SET
操作时)。EPERM
:调用进程没有足够的权限来执行请求的操作(例如,没有权限删除共享内存段或修改其属性)。
4.函数接口使用
1 |
|
5.共享内存实例–实现生产者消费者模型
使用 System V IPC 的共享内存实现生产者 - 消费者模型
- 以下是使用 System V IPC 的
shmget
、shmat
、shmdt
和shmctl
函数实现生产者 - 消费者模型的 C 代码:
1 |
|
整体代码解释:
- 共享内存结构体定义:
- 定义了
SharedMemory
结构体,包含一个整数数组作为缓冲区,以及in
和out
索引,用于生产者和消费者操作。
- 定义了
- 信号量操作函数:
semaphore_op
函数用于对信号量进行操作,实现P
操作(sem_op
为负,获取资源)和V
操作(sem_op
为正,释放资源)。
- 生产者函数:
- 通过
shmat
将共享内存连接到进程地址空间。 - 使用
semaphore_op
等待空闲缓冲区信号量(semid
的第 0 个信号量),生产一个随机数放入缓冲区,更新in
索引,然后使用semaphore_op
释放产品信号量(semid
的第 1 个信号量)。
- 通过
- 消费者函数:
- 同样通过
shmat
连接共享内存。 - 使用
semaphore_op
等待产品信号量,从缓冲区取出数据,更新out
索引,然后使用semaphore_op
释放空闲缓冲区信号量。
- 同样通过
- 主函数:
- 使用
shmget
创建共享内存,并使用semget
创建信号量集。 - 初始化信号量,一个表示空闲缓冲区数量,一个表示产品数量。
- 使用
fork
创建子进程,子进程作为消费者,父进程作为生产者。 - 最后等待子进程结束,并使用
shmctl
和semctl
删除共享内存和信号量集。
- 使用
运行结果:
6.无血缘关系的进程之间通信
ftok
函数的定义和功能- 函数原型:
key_t ftok(const char *pathname, int proj_id);
- 功能:
ftok
函数用于生成一个唯一的key
(键值),这个key
通常用于 System V IPC(进程间通信)机制中,如创建共享内存、消息队列和信号量集等。它将一个文件路径名(pathname
)和一个项目标识符(proj_id
)组合起来,生成一个适合作为 System V IPC 资源标识符的key
值。
- 函数原型:
- 参数解释
const char *pathname
:- 这是一个指向文件路径名的指针。这个文件路径必须是一个已经存在的文件的有效路径,通常使用当前目录(
."
)或者一个程序相关的配置文件路径等。ftok
函数会使用文件的inode
(索引节点)信息作为生成key
的一部分。 - 注意,如果文件被删除然后重新创建,即使文件名相同,
inode
可能会改变,这会导致ftok
生成不同的key
值。
- 这是一个指向文件路径名的指针。这个文件路径必须是一个已经存在的文件的有效路径,通常使用当前目录(
int proj_id
:- 这是一个
0 - 255
之间的整数,作为项目标识符。它和文件路径的inode
信息一起组合生成key
。不同的项目可以使用不同的proj_id
来区分,这样即使基于同一个文件路径,不同的项目也能生成不同的key
值用于各自的 IPC 资源。例如,一个程序中有两个不同的模块需要使用消息队列进行通信,它们可以使用相同的文件路径但不同的proj_id
来生成不同的key
,以创建两个独立的消息队列。
- 这是一个
- 返回值
- 成功时,
ftok
函数返回一个key_t
类型的非负整数,这个整数可以作为shmget
、msgget
、semget
等 System V IPC 函数的key
参数来创建或获取对应的 IPC 资源。 - 失败时,返回-1,并且会设置errno来指示错误原因。常见的错误原因包括:
EACCESS
:没有权限访问pathname
指定的文件。ENOENT
:pathname
指定的文件不存在。
- 成功时,
发送端
1 |
|
接收端
1 |
|
运行结果:
共享内存的 POSIX 方法
mmap函数和munmap函数利用mmap
函数的MAP_ANONYMOUS
标志可以实现父、子进程间的匿名内存共享。通过打开同一个文件,mmap
也可以实现无关进程之间的内存共享。
Linux提供了另一种在无关进程间共享内存的方式,这种方式无须任何文件的支持,但它需要先用shm_open
函数来创建或打开一个POSIX共享内存对象。
1.shm_open
1 |
|
shm_open
函数的使用方法与open
系统调用完全相同。
- 功能:
- 用于创建或打开一个共享内存对象。这个函数是基于 POSIX 标准的,提供了一种可移植的方式来操作共享内存。
- 参数:
const char* name
:- 这是共享内存对象的名字。从可移植性的角度考虑,名字应该遵循特定格式:以
/
开始,后面跟着多个非/
字符,并且以\0
结尾,长度通常不超过NAME_MAX
(一般为 255)。例如"/my_shared_memory"
。
- 这是共享内存对象的名字。从可移植性的角度考虑,名字应该遵循特定格式:以
int oflag
:- 用于指定打开共享内存对象的方式,它是以下标志的按位或组合:
O_RDONLY
:以只读方式打开共享内存对象。如果只指定了这个标志,那么进程只能从共享内存中读取数据。O_RDWR
:以可读可写方式打开共享内存对象,这是最常用的方式之一,允许进程对共享内存进行读写操作。O_CREAT
:如果共享内存对象不存在,则创建它。当使用这个标志时,mode
参数用于指定新创建的共享内存对象的访问权限(类似文件权限)。O_EXCL
:通常与O_CREAT
一起使用。如果name
指定的共享内存对象已经存在,shm_open
调用将返回错误(-1
);如果不存在,则创建一个新的共享内存对象。这可以用于确保创建的是一个全新的、未被使用过的共享内存对象。O_TRUNC
:如果共享内存对象已经存在,将其截断(长度变为 0)。这个操作会丢失共享内存对象中原先存储的数据。
- 用于指定打开共享内存对象的方式,它是以下标志的按位或组合:
mode_t mode
:- 当
oflag
中包含O_CREAT
标志时,mode
参数用于指定共享内存对象的访问权限。它类似于文件权限,例如0666
表示用户、组和其他用户都有读写权限。权限的低 9 位用于设置实际的权限。
- 当
返回值:
- 返回值:
- 成功时,返回一个非负整数,即共享内存对象的文件描述符。这个文件描述符可以用于后续的操作,如
mmap
(将共享内存映射到进程地址空间)等操作。 - 失败时,返回-1,并且会设置errno来指示错误原因。常见的错误原因包括:
EEXIST
:当O_CREAT
和O_EXCL
一起使用且共享内存对象已经存在时。EINVAL
:name
参数不符合格式要求,或者oflag
参数无效。EACCES
:没有足够的权限按照oflag
和mode
指定的方式创建或打开共享内存对象。
- 成功时,返回一个非负整数,即共享内存对象的文件描述符。这个文件描述符可以用于后续的操作,如
2.shm_unlink
和打开的文件最后需要关闭一样,由shm_open
函数创建的共享内存对象用完后也需要删除,可通过shm_unlink
函数实现。
1 |
|
功能:
- 用于删除一个共享内存对象。当一个进程调用
shm_unlink
后,共享内存对象会被标记为删除,但实际的删除操作会在所有进程都关闭了对该共享内存对象的引用(通过shm_open
打开得到的文件描述符都被关闭)后才会执行。
- 用于删除一个共享内存对象。当一个进程调用
参数:
shm_unlink
函数将name
参数指定的共享内存对象标记为等待删除,当所有使用该共享内存对象的进程都使用munmap
函数将它从进程中分离后,系统将销毁这个共享内存对象所占据的资源。
返回值:
成功时,返回
0
。失败时,返回-1,并且会设置errno来指示错误原因。常见的错误原因包括:
EINVAL
:name
参数不符合格式要求。ENOENT
:指定的共享内存对象不存在。
如果代码中使用了以上POSIX共享内存函数,则编译时需要指定链接选项-lrt
。
3.使用案例
1 |
|
代码解释
- 首先,使用
shm_open
创建或打开一个名为/my_shared_memory
的共享内存对象,权限为0666
,并以读写方式打开。 - 然后,使用
ftruncate
设置共享内存对象的大小为MEMORY_SIZE
(这里定义为 4096 字节)。 - 接着,使用
mmap
将共享内存映射到进程的地址空间,以便可以像访问普通内存一样访问共享内存。 - 之后,向共享内存中写入了一个字符串
Hello, shared memory!
。 - 再然后,使用
munmap
解除共享内存的映射。 - 接着,使用
close
关闭共享内存对象的文件描述符。 - 最后,使用
shm_unlink
删除共享内存对象。
4.实现生产者消费者模型
使用共享内存实现生产者 - 消费者模型(基于shm_open
和shm_unlink
函数)
- 以下是使用
shm_open
和shm_unlink
函数实现的生产者 - 消费者模型的 C++ 代码示例:
1 |
|
整体代码解释:
- 共享内存结构体定义:
- 定义了
SharedMemory
结构体,包含一个整数数组作为缓冲区,以及in
和out
索引,用于生产者和消费者操作。
- 定义了
- 生产者函数:
- 通过
mmap
将共享内存映射到进程地址空间。 - 使用
sem_wait
等待empty_sem
信号量(表示缓冲区有空闲位置),生产一个随机数放入缓冲区,更新in
索引,然后使用sem_post
释放full_sem
信号量(表示缓冲区有数据可供消费)。
- 通过
- 消费者函数:
- 同样通过
mmap
映射共享内存。 - 使用
sem_wait
等待full_sem
信号量,从缓冲区取出数据,更新out
索引,然后使用sem_post
释放empty_sem
信号量。
- 同样通过
- 主函数:
- 使用
shm_open
创建共享内存对象,并设置大小。 - 创建
empty_sem
和full_sem
两个信号量,分别用于控制缓冲区的空闲和满状态。 - 映射共享内存后初始化
in
和out
索引。 - 创建生产者和消费者线程并等待它们结束。
- 最后关闭并删除信号量,解除共享内存映射,关闭并删除共享内存对象。
- 使用
运行结果:
5.无血缘关系的进程之间通信
写入端
1 |
|
读出端
1 |
|
运行结果: