Linux C语言共享内存基础与应用
2023-03-291.3k 阅读
共享内存概述
在Linux系统的进程间通信(IPC,Inter - Process Communication)机制中,共享内存是一种高效的数据共享方式。与其他IPC机制如管道(Pipe)、消息队列(Message Queue)相比,共享内存允许不同的进程直接访问同一块物理内存区域,这避免了数据在进程间复制的开销,大大提高了数据传输的效率。
从操作系统原理角度来看,每个进程在内存中都有自己独立的虚拟地址空间。共享内存的实现原理是,操作系统将同一块物理内存映射到多个进程的虚拟地址空间中。这样,不同进程通过访问各自虚拟地址空间中的这块映射区域,就可以实现数据的共享。
Linux下共享内存相关函数
shmget函数
- 函数原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
- 参数说明:
key
:是一个key_t
类型的键值,它是共享内存对象的标识。通常可以使用ftok
函数生成一个唯一的key
值。如果key
为IPC_PRIVATE
,则会创建一个新的、私有的共享内存对象,只有创建者及其子进程可以访问。size
:指定共享内存的大小,单位是字节。这个大小必须是系统页面大小的整数倍。shmflg
:是一组标志位,用于指定共享内存的创建和访问权限。常见的标志位有IPC_CREAT
(如果共享内存不存在则创建)、IPC_EXCL
(与IPC_CREAT
一起使用,确保共享内存是新建的,如果已存在则返回错误),以及文件访问权限标志(如0666
表示读写权限)。
- 返回值:
成功时返回共享内存标识符(一个非负整数),失败时返回 -1,并设置
errno
以指示错误原因。
shmat函数
- 函数原型:
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 参数说明:
shmid
:是由shmget
函数返回的共享内存标识符。shmaddr
:指定共享内存应映射到调用进程虚拟地址空间中的地址。如果为NULL
,系统会自动选择一个合适的地址进行映射。shmflg
:标志位,常见的有SHM_RDONLY
(以只读方式映射共享内存),如果为0则以读写方式映射。
- 返回值:
成功时返回指向共享内存映射区域的指针,失败时返回
(void *)-1
,并设置errno
。
shmdt函数
- 函数原型:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
- 参数说明:
shmaddr
是由shmat
函数返回的指向共享内存映射区域的指针。 - 返回值:
成功时返回0,失败时返回 -1,并设置
errno
。
shmctl函数
- 函数原型:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 参数说明:
shmid
:共享内存标识符。cmd
:是一个命令码,用于指定对共享内存对象执行的操作。常见的命令码有IPC_STAT
(获取共享内存对象的状态信息,存储在buf
指向的结构中)、IPC_SET
(设置共享内存对象的状态信息,从buf
指向的结构中获取)、IPC_RMID
(删除共享内存对象)。buf
:是一个指向struct shmid_ds
结构的指针,用于存储或传递共享内存对象的状态信息。
- 返回值:
成功时返回0,失败时返回 -1,并设置
errno
。
共享内存应用示例
下面通过一个简单的生产者 - 消费者模型示例来展示共享内存的使用。
代码示例
- 生产者代码(producer.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define SHM_SIZE 1024
int main() {
key_t key;
int shmid;
char *shm, *s;
// 创建一个唯一的key
if ((key = ftok(".", 'a')) == -1) {
perror("ftok");
exit(1);
}
// 获取共享内存标识符,如果不存在则创建
if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) == -1) {
perror("shmget");
exit(1);
}
// 将共享内存映射到进程的地址空间
if ((shm = (char *)shmat(shmid, NULL, 0)) == (void *)-1) {
perror("shmat");
exit(1);
}
s = shm;
// 向共享内存中写入数据
strcpy(s, "Hello, Consumer!");
// 等待消费者读取数据
while (*shm != '*') {
sleep(1);
}
// 分离共享内存
if (shmdt(shm) == -1) {
perror("shmdt");
exit(1);
}
// 删除共享内存对象
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(1);
}
return 0;
}
- 消费者代码(consumer.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#define SHM_SIZE 1024
int main() {
key_t key;
int shmid;
char *shm, *s;
// 创建与生产者相同的key
if ((key = ftok(".", 'a')) == -1) {
perror("ftok");
exit(1);
}
// 获取共享内存标识符
if ((shmid = shmget(key, SHM_SIZE, 0666)) == -1) {
perror("shmget");
exit(1);
}
// 将共享内存映射到进程的地址空间
if ((shm = (char *)shmat(shmid, NULL, 0)) == (void *)-1) {
perror("shmat");
exit(1);
}
s = shm;
// 等待生产者写入数据
while (*s == '\0') {
sleep(1);
}
// 读取共享内存中的数据
printf("Received: %s\n", s);
// 标记数据已读取
*shm = '*';
// 分离共享内存
if (shmdt(shm) == -1) {
perror("shmdt");
exit(1);
}
return 0;
}
代码说明
-
生产者部分:
- 使用
ftok
函数生成一个唯一的key
值,ftok
函数的第一个参数是一个存在的文件路径,第二个参数是一个字符,通过这两个参数生成一个唯一的key
。 - 调用
shmget
函数获取共享内存标识符,如果共享内存不存在则创建一个大小为SHM_SIZE
(这里定义为1024字节)、权限为0666
(读写权限)的共享内存对象。 - 使用
shmat
函数将共享内存映射到进程的虚拟地址空间,返回一个指向共享内存区域的指针shm
。 - 向共享内存中写入字符串
"Hello, Consumer!"
。 - 等待消费者标记数据已读取(通过共享内存中的一个字符
*
来标识)。 - 最后,调用
shmdt
函数分离共享内存,调用shmctl
函数删除共享内存对象。
- 使用
-
消费者部分:
- 生成与生产者相同的
key
值。 - 调用
shmget
函数获取已存在的共享内存标识符。 - 使用
shmat
函数将共享内存映射到进程的虚拟地址空间。 - 等待生产者写入数据(共享内存中第一个字符不为
\0
)。 - 读取共享内存中的数据并打印。
- 标记数据已读取,将共享内存中的第一个字符设为
*
。 - 调用
shmdt
函数分离共享内存。
- 生成与生产者相同的
共享内存使用中的同步问题
虽然共享内存提供了高效的数据共享方式,但在多进程并发访问共享内存时,同步问题变得至关重要。如果多个进程同时对共享内存进行读写操作,可能会导致数据不一致或竞争条件(Race Condition)。
为了解决这些问题,通常可以结合其他同步机制,如信号量(Semaphore)。信号量是一个计数器,它可以用来控制对共享资源的访问。例如,在上述生产者 - 消费者模型中,可以引入两个信号量:一个用于表示共享内存中有数据可供消费(empty
信号量),另一个用于表示共享内存中有空间可供生产(full
信号量)。
- 引入信号量后的生产者代码(producer_with_sem.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <semaphore.h>
#define SHM_SIZE 1024
int main() {
key_t key;
int shmid;
char *shm, *s;
sem_t *empty, *full;
// 创建一个唯一的key
if ((key = ftok(".", 'a')) == -1) {
perror("ftok");
exit(1);
}
// 获取共享内存标识符,如果不存在则创建
if ((shmid = shmget(key, SHM_SIZE, IPC_CREAT | 0666)) == -1) {
perror("shmget");
exit(1);
}
// 将共享内存映射到进程的地址空间
if ((shm = (char *)shmat(shmid, NULL, 0)) == (void *)-1) {
perror("shmat");
exit(1);
}
// 打开或创建信号量
if ((empty = sem_open("/empty", O_CREAT, 0666, 1)) == SEM_FAILED) {
perror("sem_open empty");
exit(1);
}
if ((full = sem_open("/full", O_CREAT, 0666, 0)) == SEM_FAILED) {
perror("sem_open full");
exit(1);
}
s = shm;
// 等待共享内存有空间可供生产
sem_wait(empty);
// 向共享内存中写入数据
strcpy(s, "Hello, Consumer!");
// 标记共享内存中有数据可供消费
sem_post(full);
// 等待消费者读取数据
sem_wait(empty);
// 分离共享内存
if (shmdt(shm) == -1) {
perror("shmdt");
exit(1);
}
// 删除共享内存对象
if (shmctl(shmid, IPC_RMID, NULL) == -1) {
perror("shmctl");
exit(1);
}
// 关闭并删除信号量
sem_close(empty);
sem_close(full);
sem_unlink("/empty");
sem_unlink("/full");
return 0;
}
- 引入信号量后的消费者代码(consumer_with_sem.c):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>
#include <semaphore.h>
#define SHM_SIZE 1024
int main() {
key_t key;
int shmid;
char *shm, *s;
sem_t *empty, *full;
// 创建与生产者相同的key
if ((key = ftok(".", 'a')) == -1) {
perror("ftok");
exit(1);
}
// 获取共享内存标识符
if ((shmid = shmget(key, SHM_SIZE, 0666)) == -1) {
perror("shmget");
exit(1);
}
// 将共享内存映射到进程的地址空间
if ((shm = (char *)shmat(shmid, NULL, 0)) == (void *)-1) {
perror("shmat");
exit(1);
}
// 打开信号量
if ((empty = sem_open("/empty", 0)) == SEM_FAILED) {
perror("sem_open empty");
exit(1);
}
if ((full = sem_open("/full", 0)) == SEM_FAILED) {
perror("sem_open full");
exit(1);
}
s = shm;
// 等待共享内存中有数据可供消费
sem_wait(full);
// 读取共享内存中的数据
printf("Received: %s\n", s);
// 标记共享内存有空间可供生产
sem_post(empty);
// 分离共享内存
if (shmdt(shm) == -1) {
perror("shmdt");
exit(1);
}
// 关闭信号量
sem_close(empty);
sem_close(full);
return 0;
}
共享内存的优缺点
优点
- 高效性:共享内存直接在进程间共享物理内存,避免了数据在进程间的复制,大大提高了数据传输的速度。这使得它在对性能要求较高的场景,如大数据量的实时处理中非常适用。
- 灵活性:共享内存的大小可以根据实际需求进行调整,并且可以被多个进程同时访问,适用于多种复杂的进程间通信场景。
缺点
- 同步复杂性:由于多个进程可以同时访问共享内存,必须使用额外的同步机制(如信号量、互斥锁等)来保证数据的一致性和避免竞争条件。这增加了编程的复杂性。
- 缺乏保护机制:共享内存本身没有提供对数据访问的保护机制。如果一个进程意外崩溃或错误地修改了共享内存中的数据,可能会影响到其他依赖该共享内存的进程,甚至导致系统不稳定。
共享内存的实际应用场景
- 数据库系统:数据库管理系统(DBMS)经常使用共享内存来缓存数据页和索引信息。多个数据库进程可以同时访问这些共享内存区域,提高数据查询和更新的效率。例如,在MySQL数据库中,InnoDB存储引擎使用共享内存来管理缓冲池(Buffer Pool),缓存经常访问的数据页,减少磁盘I/O操作。
- 多媒体处理:在视频流处理、音频编码等多媒体应用中,不同的处理模块(如视频解码模块、图像渲染模块)可能需要共享一些数据,如解码后的视频帧数据。共享内存可以高效地在这些模块之间传递数据,满足多媒体处理对实时性的要求。
- 分布式系统:在分布式计算环境中,不同节点上的进程可能需要共享一些配置信息或中间计算结果。通过共享内存和网络通信相结合的方式,可以实现分布式系统中进程间的数据共享和协同工作。例如,Hadoop分布式文件系统(HDFS)的一些组件可能会使用共享内存来提高数据处理的效率。
在实际应用中,开发人员需要根据具体的需求和场景,权衡共享内存的优缺点,合理地使用共享内存以及相关的同步机制,以实现高效、稳定的进程间通信。同时,要注意对共享内存的管理和保护,确保系统的可靠性和安全性。