目录
本文在最后给出了基于共享内存对象的共享内存代码示例,可以对应代码去学习相关函数的使用。
共享内存概述
共享内存在 IPC 通信中是最快的。当一块内存映射到共享他的进程的地址上之后,所有的进程之间进行通信的时候就不需要涉及内核了。
下面举一个在其他 IPC 方式 下,两个进程间进行通信的例子。
- 进程A先从输入文件中读取一些内容,这个过程就会涉及将文件内容从内核区拷贝到进程(用户区)的一个过程。
- 进程A想要通过管道、消息队列这些 IPC 将内容发送给进程B,就会涉及到将内容写入 IPC 通道中,这个过程是将内容从进程拷贝到内核区。
- 进程B想要接收这些消息,就会将内容从 IPC 通道中读取出来,就会将内容从内核区拷贝到用户区。
- 最后,进程B通过 write() 函数将内容写如指定文件中,又将内容从用户区拷贝到内核区。
整个过程涉及到了4次内核和进程间的数据的复制,这种开销比单纯在进程或内核中复制数据要大很多的。所以这些 IPC 通信的方式,主要就是因为数据要经过内核区才导致开销非常大。如果通过共享内存的方式通信的话,就可以绕过内核直接进行用户层的消息传递。
下面举一个共享内存下,两个进程通信的例子。
- 两个进程共享了一块内存区域。进程间通过信号量进行进程间的同步。
- 进程A从输入文件读取内容到共享内存区,涉及到将数据从内核区拷贝到进程。然后使用信号量通知进程B处理数据。
- 进程B通过共享内存拿到数据,然后写入到指定文件中,涉及到数据从进程拷贝到内核区。
本次过程只涉及到了两次复制,所以相比其他的进程通信方式,共享内存可以大幅减小开销。
mmap 函数
mmap() 用于把一个文件或一个共享内存区对象映射到调用进程的地址空间。
#include <sys/mman.h>
void *mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset);
/* 成功返回被映射区的起始地址,失败返回MAP_FAILED */
addr 用于指定被映射进程空间的起始地址。通常被指定为一个空指针,告诉内核自己去选择起始地址。返回值是 fd 被映射到的内存区域的起始地址。
offset 是指被映射文件的起始地址,通常被指定为0。
len 用于指定要映射空间的字节数。
prot 用于指定映射区域的保护权限。常用 PROT_READ | PROT_WRITE。
权限类型 | 说明 |
---|---|
PROT_READ | 数据可读 |
PROT_WRITE | 数据可写 |
PROT_EXEC | 数据可执行 |
PROT_NONE | 数据不可访问 |
flag 标志位必须指定 MAP_SHARED 或 MAP_PRIVATE 其中一个。MAP_PRIVATE 是进程的修改只对当前进程有效,不会影响底层的共享内存对象。MAP_SHARED 是进程的修改会去修改底层的共享内存对象,修改对所有共享内存的进程可见。考虑到移植性,MAX_FIXED 标志位一般情况不会去指定。
标志类型 | 说明 |
---|---|
MAP_SHARED | 变动是共享的 |
MAP_PRIVATE | 变动是私有的 |
MAP_FIXED | 准确地解释addr参数 |
munmap 函数
munmap() 用于从映射地址中删除映射关系。
#include <sys/mman.h>
int munmap(void *addr, size_t len);
/* 成功返回0,失败返回-1 */
调用 munmap() 删除映射关系后,我们再去访问 addr 这个地址,会产生 SIGSEGV(段错误)信号。
如果进程对共享内存对象的操作时 MAX_PRIVATE,那调用 munmap() 之后,进程之前所有对该对象的变动都会被删除。
msync 函数
msync() 用于控制文件映射内存区域的同步。当进程对一个通过 mmap 系统调用映射到内存中的文件进行写操作后,这些修改可能首先只存在于进程的私有地址空间中。为了确保这些更改被写道实际的文件中,可以使用 msync() 函数。有助于提高数据的一致性和持久性。
msync() 函数主要作用:
- 数据同步:将映射内存中的脏页(已被修改的页)刷写到文件中,确保数据的持久化。
- 控制写操作:允许进程控制何时将更改写入文件,优化性能并减少不必要的磁盘I/O操作。
- 内存清洗:将映射的内存区域标记为无效,后续访问时操作系统会从文件重新读取数据,避免使用过时的内存副本。
#include <sys/mman.h>
int msync(void *addr, size_t len, int flags);
/* 成功返回0,出错返回-1 */
addr 和 len 参数通常指代内存中的整个内存映射区,不过也可以指定该内存区的一个子集。flags 参数如下表。
常量名称 | 说明 |
---|---|
MS_ASYNC | 执行异步写 |
MS_SYNC | 执行同步写 |
MS_INVALIDATE | 使高速缓存的数据失效 |
MS_ASYNC 和 MS_SYNC 这两个常值中必须指定一个,但不能都指定。它们的差别是,一旦写操作已由内核排入队列,MS_ASYNC 即返回,而 MS_SYNC 则要等到写操作完成后才返回。如果还指定了MS_INVALIDATE,h使内存中与文件不一致的数据副本失效。后续对这部分内存的访问会直接从文件重新加载数据,确保内存内容与文件一致。
Posix 共享内存区
Posix.1 提供了两种在无亲缘关系进程间共享内存的方法。
内存映射文件(memory-mapped file):由 open() 函数打开,由 mmap() 把得到的文件描述符映射到当前进程地址中的一个文件。
共享内存区对象(shared-memory object):由 shm_open() 打开一个 Posix.1 IPC 名字(也许是在文件系统中的一个路径名),所返回的描述符由 mmap() 函数映射到当前进程的地址空间。
open() 函数用于打开一个指定文件,返回文件描述符,这里就不多阐述了。主要来学习一下 shm_open() 怎么使用。
shm_open 和 shm_unlink 函数
Posix 共享内存涉及以下两个步骤。
- 使用 shm_open() 指定一个名字参数,创建一个共享内存对象或者打开一个已经存在的共享内存对象。
- 使用 mmap() 把共享内存对象映射到多个进程的地址空间。
#include <sys/mman.h>
int shm_open(const char *name, int oflag, mode_t mode);
/* 成功返回非负描述符,失败返回-1 */
int shm_unlink(const char *name);
/* 成功返回0,失败返回-1 */
oflag 参数必须包含 O_RDONLY(只读)标志,或者函数 O_RDWR(读写)标志,还可以指定如下标志,O_CREAT、O_EXCL 或 O_TRUNC。如果随 O_RDWR 指定 O_TRUNC 标志,而且所需的共享内存区对象已经存在,那么它将被截短成0长度。
mode 参数用于指定权限位,在指定了 O_CREAT 的前提下使用。shm_open 的 mode 参数必须指定,如果没有使用 O_CREAT 的情况下,我们就将其指定为 0。
shm_open() 返回一个整数的文件描述符,用于后续 mmap() 的第五个参数。
shum_unlink() 用于删除一个共享内存对象的名字,删除名字后,并没有真正的删除对象引用。已经打开了共享内存对象名字的仍然可以使用名字,新的进程不能再通过名字区访问这个共享内存对象。直到所有的进程全部删除名字。
ftruncate 函数
普通文件和共享内存对象的大小都可以调用 ftruncate() 函数进行修改。
#include <unistd.h>
int ftruncate(int fd, off_t length);
ftruncate() 是一个用于调整文件大小的系统调用函数,主要用于将文件截断或扩展到指定长度。
- 若当前文件大小 >
length
,超出部分的数据会被丢弃,文件从 length 处截断。 - 若当前文件大小 <
length
,文件会扩展至 length,新增部分填充空字节(\0
)。
fstat 函数
fstat() 用于获取共享内存对象的信息。
#include <sys/types.h>
#include <sys/stat.h>
int fstat(int fd, struct stat *buf);
/* 成功返回0,失败返回-1 */
标准 stat 结构包含12个或以上的成员,当文件描述符 (fd) 指向共享内存区对象时,仅4个成员会包含有效信息。
struct stat
{
mode_t st_mode; // 文件模式(权限位:S_I{RW}{USR,GRP,OTH})
uid_t st_uid; // 所有者的用户ID
gid_t st_gid; // 所有者的组ID
off_t st_size; // 文件大小(字节单位)
};
共享内存代码示例
代码定义了一个大小为 10 的环形队列,生产者和消费者共享了一个信号量和任务队列的结构体,任务队列满了,消费者就去消费。并且生产者和消费者进行的操作通过另一个信号量进行互斥。
公共头文件
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <semaphore.h>
#define QUEUE_SIZE 10
#define SHM_KEY "/shm_ring_queue"
typedef struct
{
int data[QUEUE_SIZE]; /*环形队列*/
int head; /*消费者读取位置*/
int tail; /*生产者写入位置*/
} RingQueue;
typedef struct
{
RingQueue queue;
sem_t empty; /*空闲槽位信号量*/
sem_t full; /*已填充槽位信号量*/
sem_t mutex; /*互斥锁*/
} SharedData;
生产者代码
#include "common.h"
int main() {
/* 创建或打开共享内存 */
int fd = shm_open(SHM_KEY, O_CREAT | O_RDWR, 0666);
if (fd == -1)
{
perror("打开共享内存失败");
exit(1);
}
ftruncate(fd, sizeof(SharedData));
/* 映射共享内存 */
SharedData *shared = mmap(NULL, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared == MAP_FAILED)
{
perror("映射共享内存失败");
exit(1);
}
/* 初始化信号量(仅在第一个进程创建) */
sem_init(&shared->empty, 1, QUEUE_SIZE); /*初始空闲槽位=队列容量*/
sem_init(&shared->full, 1, 0); /*初始已填充槽位=0*/
sem_init(&shared->mutex, 1, 1); /*互斥锁初始=1*/
int item = 0;
while (1)
{
sem_wait(&shared->empty); /*等待空闲槽位*/
sem_wait(&shared->mutex); /*进入临界区*/
/*生产数据到环形队列*/
shared->queue.data[shared->queue.tail] = item++;
shared->queue.tail = (shared->queue.tail + 1) % QUEUE_SIZE;
printf("生产了: %d\n", item - 1);
sem_post(&shared->mutex); /*离开临界区*/
sem_post(&shared->full); /*增加已填充槽位*/
sleep(1); /*模拟生产耗时*/
}
/*实际应用中需添加退出逻辑*/
munmap(shared, sizeof(SharedData));
shm_unlink(SHM_KEY);
return 0;
}
消费者代码
#include "common.h"
int main() {
/* 打开共享内存 */
int fd = shm_open(SHM_KEY, O_RDWR, 0666);
if (fd == -1)
{
perror("打开共享内存失败");
exit(1);
}
/* 映射共享内存 */
SharedData *shared = mmap(NULL, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shared == MAP_FAILED)
{
perror("映射共享内存失败");
exit(1);
}
while (1)
{
sem_wait(&shared->full); /*等待已填充槽位*/
sem_wait(&shared->mutex); /*进入临界区*/
/*从环形队列消费数据*/
int item = shared->queue.data[shared->queue.head];
shared->queue.head = (shared->queue.head + 1) % QUEUE_SIZE;
printf("Consumed: %d\n", item);
sem_post(&shared->mutex); /*离开临界区*/
sem_post(&shared->empty); /*增加空闲槽位*/
sleep(2); /*模拟消费耗时*/
}
munmap(shared, sizeof(SharedData));
return 0;
}