多进程通信之共享内存
目录
共享内存的POSIX方法
POSIX方法的共享内存基本原理:
详解5个核心函数的各个参数:
示例代码:test_A创建共享内存,test_B读取共享内存
共享内存的系统调用方法
系统调用方法的共享内存基本原理:
详解4个核心函数的各个参数:
示例代码:test_A创建共享内存,test_B读取共享内存 test_A
额外说明:
关于共享内存的讲解,我们在写代码举例的时候,往往还需要考虑一些其它事情,为了更加轻松和更加清晰的认识共享内存,我们先忽略了这些“其它事情”,在后续的代码中我会做出说明。
共享内存的POSIX方法
首先总览一下要用到的5个关键函数:大家先浏览一下即可。
- int shm_open(const char *name, int oflag, mode_t mode);创建一个新的 POSIX 共享内存对象或打开一个已存在的共享内存对象。成功时返回共享内存对象的文件描述符,失败时返回
-1
并设置errno
以指示错误原因。 - int ftruncate(int fd, off_t length);设置与文件描述符
fd
关联的共享内存对象的大小为length
字节。创建共享内存对象时只是创建了一个 “外壳”,需要通过此函数来分配实际的内存空间。成功时返回0
,失败时返回-1
并设置errno
。 - void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);将共享内存对象映射到调用进程的地址空间,使得进程可以像访问普通内存一样访问共享内存。成功时返回映射区域的起始地址,失败时返回
MAP_FAILED
(即(void *)-1
)并设置errno
。 - int munmap(void *addr, size_t length);解除之前通过
mmap
映射的内存区域,断开进程与共享内存对象的映射关系,但不删除共享内存对象本身。成功时返回0
,失败时返回-1
并设置errno
。 - int shm_unlink(const char *name);删除指定名称的 POSIX 共享内存对象。只有当所有映射该共享内存对象的进程都解除映射(通过
munmap
)后,该共享内存对象才会真正从系统中删除。成功时返回0
,失败时返回-1
并设置errno
。
POSIX方法的共享内存基本原理:
详解5个核心函数的各个参数:
nt shm_open(const char *name, int oflag, mode_t mode);
name
:一个以/
开头的字符串,用于唯一标识共享内存对象。不同进程可以通过相同的name
来访问同一个共享内存对象,实现进程间通信。例如"/my_shared_memory"
。oflag
:打开标志,常见取值有:O_CREAT
:如果共享内存对象不存在,则创建它。若对象已存在,该标志通常不会导致错误,而是直接打开已存在的对象。O_RDONLY
:以只读方式打开共享内存对象。O_RDWR
:以读写方式打开共享内存对象。
mode
:当使用O_CREAT
创建共享内存对象时,mode
用于设置其权限,类似于文件权限设置。例如,0666
表示所有者、组和其他用户都有读写权限;0600
表示只有所有者有读写权限。这个参数仅在使用O_CREAT
时有效。
int ftruncate(int fd, off_t length);
fd
:由shm_open
函数返回的共享内存对象的文件描述符。length
:要设置的共享内存对象的大小,以字节为单位。这个大小必须是系统页大小(通常为 4096 字节)的整数倍。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr
:通常设为NULL
,表示由系统自动选择合适的地址进行映射。如果不为NULL
,系统会尽量使用该地址作为映射起始地址,但并不保证一定使用。length
:要映射的共享内存区域的长度,以字节为单位,应与之前ftruncate
设置的大小一致。prot
:指定映射区域的保护权限,常见取值有:PROT_READ
:映射区域可读。PROT_WRITE
:映射区域可写。PROT_EXEC
:映射区域可执行(用于映射共享库等可执行代码)。PROT_NONE
:映射区域不可访问。- 这些标志可以通过逻辑或(
|
)组合使用,比如PROT_READ | PROT_WRITE
表示映射区域可读可写。
flags
:指定映射的类型和其他一些选项,常见取值有:MAP_SHARED
:对映射区域的写入会反映到共享内存对象中,同时其他映射该对象的进程也能看到这些修改,实现进程间共享数据。这是进程间通信的关键。MAP_PRIVATE
:对映射区域的写入不会反映到共享内存对象中,而是产生一个写时复制(Copy - On - Write)的效果,即当进程对映射区域进行写入操作时,系统会为该进程分配新的物理页面,而原始共享内存对象不受影响。
fd
:由shm_open
返回的共享内存对象的文件描述符。offset
:通常设为0
,表示从共享内存对象的起始位置开始映射。必须是系统页大小的整数倍。
int munmap(void *addr, size_t length);
addr
:由mmap
返回的映射区域起始地址。length
:映射区域的长度,应与mmap
时的长度一致。
int shm_unlink(const char *name);
name
:要删除的共享内存对象的名称,即shm_open
函数中使用的名称。
示例代码:test_A创建共享内存,test_B读取共享内存
test_A
#include <iostream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
// test_A用于创建共享内存,让另一个程序test_B来读。int main()
{// 创建共享内存// int shm_open(const char *name, int oflag, mode_t mode);int shm_fd = shm_open("/my_share_memory",O_CREAT | O_RDWR,0666);// 设置共享内存大小// int ftruncate(int fd, off_t length);ftruncate(shm_fd,4096);// 将共享内存映射到进程地址空间,并且设置MAP_SHARED,对映射区域的修改会映射到共享内存中// void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);void *ptr = mmap(NULL,4096,PROT_READ | PROT_WRITE,MAP_SHARED,shm_fd,0);// 在映射区域中输入"I am test_A"char message[] = "I am test_A";strcpy((char *)ptr, message);sleep(30);munmap(ptr, 4096);close(shm_fd);return 0;
}
test_B
#include <iostream>
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
using namespace std;
// test_B用于读取共享内存中的数据
int main()
{// 获取共享内存// int shm_open(const char *name, int oflag, mode_t mode);int shm_fd = shm_open("/my_share_memory",O_RDWR,0666);// 无需设置共享内存大小// 将共享内存映射到进程地址空间,并且设置MAP_SHARED,对映射区域的修改会映射到共享内存中// void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);void *ptr = mmap(NULL,4096,PROT_READ | PROT_WRITE,MAP_SHARED,shm_fd,0);// 读取共享内存中的数据cout <<"test_B see: " << (char*)ptr << endl;munmap(ptr, 4096);close(shm_fd);shm_unlink("/my_share_memory");return 0;
}
注意编译的时候要指定链接选项“-lrt"
输出结果为:
上述代码示例足以说明共享内存的逻辑,只不过稍有一些与共享内存原理无关的小问题;例如:test_A万一还没把数据写入共享内存,test_B就开始读了。例如:假如双方同时往共享内存中写数据,不就乱套了。但是这些问题是另一些话题上的了。
共享内存的系统调用方法
首先总览一下要用到的4个关键函数:大家先浏览一下即可。
- int shmget(key_t key, size_t size, int shmflg);创建一个新的共享内存段或获取一个已存在的共享内存段的标识符。成功时返回共享内存段的标识符(一个非负整数),失败时返回
-1
并设置errno
以指示错误原因。 - void *shmat(int shmid, const void *shmaddr, int shmflg);将共享内存段连接到调用进程的地址空间,使得进程可以访问共享内存。成功时返回指向共享内存段的指针,失败时返回
(void *)-1
并设置errno
。 - int shmdt(const void *shmaddr);将共享内存段从调用进程的地址空间分离。注意,这并不删除共享内存段,只是断开进程与共享内存段的连接。成功时返回指向共享内存段的指针,失败时返回
(void *)-1
并设置errno
。 - int shmctl(int shmid, int cmd, struct shmid_ds *buf);对共享内存段执行各种控制操作。成功时返回
0
,失败时返回-1
并设置errno
。
系统调用方法的共享内存基本原理:
详解4个核心函数的各个参数:
int shmget(key_t key, size_t size, int shmflg);
key
:一个key_t
类型的值,用于唯一标识共享内存段。通常可以使用ftok
函数生成一个key
。不同进程使用相同的key
来获取同一个共享内存段,实现进程间通信。size
:指定共享内存段的大小,以字节为单位。这个大小必须是系统页大小的整数倍。shmflg
:一组标志位,用于控制共享内存段的创建和访问权限。常见的标志有:IPC_CREAT
:如果共享内存段不存在,则创建它。IPC_EXCL
:与IPC_CREAT
一起使用,如果共享内存段已存在,则shmget
调用失败。- 权限标志:如
0666
表示所有者、组和其他用户都有读写权限。
void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid
:由shmget
函数返回的共享内存段的标识符。shmaddr
:通常设为NULL
,表示让系统自动选择合适的地址进行连接。如果指定非NULL
地址,系统会尽量使用该地址,但不保证一定使用。shmflg
:标志位,常见取值有SHM_RDONLY
(以只读方式连接),若设为0
则以读写方式连接。
int shmdt(const void *shmaddr);
shmaddr
:由shmat
函数返回的指向共享内存段的指针。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid
:共享内存段的标识符。cmd
:要执行的命令,常见的命令有:IPC_RMID
:删除共享内存段。IPC_STAT
:获取共享内存段的状态信息,填充到buf
指向的结构体中。IPC_SET
:设置共享内存段的属性,如权限等,属性信息来自buf
指向的结构体。
buf
:一个指向struct shmid_ds
结构体的指针,用于传递或接收共享内存段的相关信息,具体取决于cmd
的值。当cmd
为IPC_RMID
时,该参数可以为NULL
。
示例代码:test_A创建共享内存,test_B读取共享内存
test_A
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>
// test_A用于创建共享内存,让另一个程序test_B来读。int main()
{// 创建共享内存int shmid = shmget(1234, 4096, IPC_CREAT | 0666);// 将共享内存连接到进程地址空间char *shm = (char *)shmat(shmid, NULL, 0);// 在映射区域中输入"I am test_A"char message[] = "I am test_A";strcpy(shm, message);sleep(30);// 分离共享内存shmdt(shm);return 0;
}
test_B
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <unistd.h>
#include <string.h>
#include <iostream>
using namespace std;
// test_B用于读取共享内存中的数据
int main()
{// 获取共享内存int shmid = shmget(1234, 4096,0666);// 将共享内存连接到进程地址空间char *shm = (char *)shmat(shmid, NULL, 0);// 接收数据cout << "test_B see: " << shm << endl;// 分离共享内存shmdt(shm);// 删除共享内存段shmctl(shmid, IPC_RMID, NULL);return 0;
}
额外说明:
如果shmget用于创建共享内存,则这段共享内存的所有字节都被初始化为0,与之关联的内核数据结构shmid_ds将被创建并初始化。shmid_ds结构体的定义如下:
struct shmid_ds {struct ipc_perm shm_perm; /* 共享内存段的权限结构 */size_t shm_segsz; /* 共享内存段的大小(字节数) */time_t shm_atime; /* 上次连接到共享内存段的时间 */time_t shm_dtime; /* 上次从共享内存段分离的时间 */time_t shm_ctime; /* 共享内存段的创建时间或上次修改时间 */pid_t shm_cpid; /* 创建共享内存段的进程的 PID */pid_t shm_lpid; /* 上次对共享内存段执行操作的进程的 PID */shmatt_t shm_nattch; /* 当前连接到共享内存段的进程数 *//* 以下字段是系统保留字段,应用程序一般不应直接访问 */void *shm_unused1; void *shm_unused2; void *shm_unused3;
};
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设置为当前的时间。
shmid_ds结构体与shmctl()一起用于对共享内存段信息的获取。
对于共享内存更加完善的应用,在后续文章中会进行说明。