1、定义
System V共享内存是一种在Unix和类Unix操作系统上用于进程间通信的机制。它允许多个进程共享同一块物理内存区域,从而可以在这些进程之间传递数据。
应用场景:
- 数据共享:多个进程需要共享大量数据,如数据库缓存、图像处理等。
- 通信效率:共享内存是一种高效的通信方式,适用于需要快速传递大量数据的场景。
优点:
- 高效:共享内存是一种高效的通信方式,因为进程可以直接访问共享的内存区域。
- 灵活性:共享内存允许多个进程共享数据,提供了一种灵活的通信方式。
缺点:
- 同步问题:由于多个进程可以同时访问共享内存,需要额外的同步机制来避免数据竞争和一致性问题。
- 安全性:共享内存需要额外的安全机制来保护数据,防止其他进程非法访问。
- 编程复杂性:使用共享内存需要处理同步和数据一致性等复杂问题,编程复杂度较高。
2、常用接口介绍
2.1 编程常用接口和数据结构
2.1.1 ftok函数
ftok函数用于生成一个System V IPC对象(如消息队列、共享内存等)的key。它将pathname和proj_id组合起来,生成一个唯一的key,用于标识一个System V IPC对象。
key_t ftok(const char *pathname, int proj_id);
- 入参:pathname是一个路径名,proj_id是一个用户指定的整数。
- 返回值:返回一个基于pathname和proj_id生成的key。
2.1.2 shmget函数
创建一个新的共享内存或获取一个已存在的共享内存的标识符。
int shmget(key_t key, size_t size, int shmflg);
- 入参:key(用于标识共享内存)、size(共享内存的大小)、flags(用于指定共享内存的访问权限和创建方式)。
- 返回值:返回共享内存的标识符(即共享内存的ID),出错时返回-1。
shmflg
选项:
IPC_CREAT
:如果共享内存不存在,则创建一个新的共享内存段。IPC_EXCL
:与IPC_CREAT
一起使用时,如果共享内存已经存在,则返回错误。- 权限标志:比如
IPC_PRIVATE
或者IPC_CREAT | 0666
,用于指定共享内存的访问权限
2.1.3 shmat函数
将共享内存连接到当前进程的地址空间,使得进程可以访问共享内存中的数据。
void *shmat(int shmid, const void *shmaddr, int shmflg);
- 入参:shmid(共享内存的标识符)、shmaddr(指定共享内存连接到进程的地址)、shmflg(用于指定连接方式)。
- 返回值:返回共享内存连接的地址,出错时返回-1。
shmflg
选项:
SHM_RDONLY
:将共享内存连接为只读模式,进程无法对共享内存进行写操作。0
:通常使用0,表示默认的连接方式。
2.1.4 shmdt函数
将共享内存从当前进程的地址空间分离,使得进程不再能够访问共享内存中的数据。
int shmdt(const void *shmaddr);
- 入参:shmaddr(指定要分离的共享内存地址)。
- 返回值:返回0表示成功,返回-1表示失败。
2.1.5 shmctl函数
对共享内存执行各种控制操作,比如删除共享内存、获取共享内存状态等。
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 入参:shmid(共享内存的标识符)、cmd(用于指定要执行的操作)、buf(用于传递或接收信息的结构体指针)。
- 返回值:返回操作相关的信息,出错时返回-1。
cmd
选项:
IPC_STAT
:获取共享内存的状态信息。IPC_SET
:设置共享内存的状态信息。IPC_RMID
:删除共享内存。IPC_INFO
:获取系统关于共享内存的信息。
2.1.6 struct 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; // 创建者的进程ID
pid_t shm_lpid; // 最后一次调用shmat(2)/shmdt(2)的进程ID
unsigned short shm_nattch; // 当前连接的进程数
};
struct ipc_perm {
key_t __key; // 键值
uid_t uid; // 所有者的用户ID
gid_t gid; // 所有者的组ID
uid_t cuid; // 创建者的用户ID
gid_t cgid; // 创建者的组ID
unsigned short mode; // 权限
unsigned short __seq; // 序列号
};
2.2 控制台常用命令
2.2.1 ipcs
ipcs命令用于显示系统中的IPC资源信息,包括消息队列、共享内存和信号量。-m选项表示只显示共享内存的信息:
ipcs -m
2.2.2 ipcrm
ipcrm命令用于删除指定的 IPC 对象,包括共享内存:
ipcrm -m <semid>
3、编程示例
3.1 共享内存实现进程间通信
测试代码如下:
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#define MEM_FILE_PATH "/home/mem"
#define MEM_SIZE 1024
// 打印时分秒的宏
#define PRINT_MIN_SEC() do { \
time_t t = time(NULL); \
struct tm *tm_ptr = localtime(&t); \
printf("%02d:%02d:%02d:", tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec); \
} while (0)
int main(int argc, char *argv[])
{
key_t key;
int shmid;
char *data = NULL;
int SendNum = 0;
char DataTemp[16] = {0};
// 文件不存在则创建文件
if (-1 == access(MEM_FILE_PATH, F_OK))
{
system("touch "MEM_FILE_PATH);
}
// 获取key
if((key = ftok(MEM_FILE_PATH, 'a')) < 0)
{
return 0;
}
shmid = shmget(key, MEM_SIZE, IPC_CREAT | 0666);
// 命令行参数
// 第一个参数 W表示每2秒写入一次数据 R表示每1秒读取数据
if (argc != 2)
{
printf("Usage: %s W|R|D\n", argv[0]);
return 0;
}
data = (char*)shmat(shmid, (void*)0, 0);
if (!strcmp(argv[1], "W"))
{
while(1)
{
bzero(DataTemp, sizeof(DataTemp));
bzero(data, MEM_SIZE);
sprintf(DataTemp, "Data-%d", SendNum);
SendNum++;
strcpy(data, DataTemp);
PRINT_MIN_SEC();
printf("Write:%s\n",DataTemp);
if(SendNum == 5)
{
break;
}
sleep(2);
}
shmdt(data);
}
else if (!strcmp(argv[1], "R"))
{
while(1)
{
bzero(DataTemp, sizeof(DataTemp));
SendNum++;
strcpy(DataTemp, data);
PRINT_MIN_SEC();
printf("Read:%s\n",DataTemp);
if(SendNum == 10)
{
break;
}
sleep(1);
}
shmdt(data);
}
else if (!strcmp(argv[1], "D"))
{
if(!shmctl(shmid, IPC_RMID, NULL))
{
printf("Delete OK\n");
}
}
else
{
printf("Usage: %s W|R|D\n", argv[0]);
return 0;
}
}
运行程序指定不同参数实现向共享内存进行读写:
测试删除功能:
3.2 共享内存和信号量实现进程间通信
上述3.1的示例中程序通过读取频率超多写入频率的方式保证获取到所有数据,此方法在正常编程过程中不会使用,需要通过信号量等方式实现进程间读写的同步,信号量编程可参考之前的文章:linux应用 进程间通信之信号量(System V)。
编写测试用例,使用信号量实现读写同步,测试代码如下:
#include <sys/shm.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#define MEM_FILE_PATH "/home/mem"
#define SEM_FILE_PATH "/home/sem1"
#define MEM_SIZE 1024
// 打印时分秒的宏
#define PRINT_MIN_SEC() do { \
time_t t = time(NULL); \
struct tm *tm_ptr = localtime(&t); \
printf("%02d:%02d:%02d:", tm_ptr->tm_hour, tm_ptr->tm_min, tm_ptr->tm_sec); \
} while (0)
int main(int argc, char *argv[])
{
key_t key, key_sem;
int shmid, semid;
char *data = NULL;
int SendNum = 0;
char DataTemp[16] = {0};
struct sembuf sops = {0};
// 文件不存在则创建文件
if (-1 == access(MEM_FILE_PATH, F_OK))
{
system("touch "MEM_FILE_PATH);
}
// 文件不存在则创建文件
if (-1 == access(SEM_FILE_PATH, F_OK))
{
system("touch "SEM_FILE_PATH);
}
// 获取key
if((key = ftok(MEM_FILE_PATH, 'a')) < 0)
{
return 0;
}
// 获取key
if((key_sem = ftok(SEM_FILE_PATH, 'a')) < 0)
{
return 0;
}
shmid = shmget(key, MEM_SIZE, IPC_CREAT | 0666);
// 信号量[0]用于写P操作 读V操作
// 信号量[1]用于写V操作 读P操作
semid = semget(key_sem, 2, IPC_CREAT | 0666);
if(semid < 0)
{
perror("Failed to create semaphore");
}
// 命令行参数
// 第一个参数 W表示写入 R表示读取
if (argc != 2)
{
printf("Usage: %s W|R|D\n", argv[0]);
return 0;
}
data = (char*)shmat(shmid, (void*)0, 0);
// 信号量值初始化
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
} arg;
arg.val = 0;
semctl(semid, 0, SETVAL, arg);
semctl(semid, 1, SETVAL, arg);
if (!strcmp(argv[1], "W"))
{
// 设置信号量[1]的值为0
arg.val = 0;
semctl(semid, 1, SETVAL, arg);
while(1)
{
// 写前执行信号量[0]P操作 等读操作发起后将信号量设置为1 然后进行写操作
sops.sem_flg = 0;
sops.sem_op = -1;
sops.sem_num = 0;
semop(semid, &sops, 1);
// 向共享内存写入数据
bzero(DataTemp, sizeof(DataTemp));
bzero(data, MEM_SIZE);
sprintf(DataTemp, "Data-%d", SendNum);
SendNum++;
strcpy(data, DataTemp);
// 写完后执行信号量[1]V操作 通知读进程可以读取
sops.sem_flg = 0;
sops.sem_op = 1;
sops.sem_num = 1;
semop(semid, &sops, 1);
PRINT_MIN_SEC();
printf("Write:%s\n",DataTemp);
if(SendNum == 5)
{
break;
}
}
shmdt(data);
}
else if (!strcmp(argv[1], "R"))
{
// 设置信号量[0]的值为1 通知写进程可以写入
arg.val = 1;
semctl(semid, 0, SETVAL, arg);
while(1)
{
// 读前执行信号量[1]P操作 等待写进程写完数据
sops.sem_flg = 0;
sops.sem_op = -1;
sops.sem_num = 1;
semop(semid, &sops, 1);
// 从共享内存读取数据
bzero(DataTemp, sizeof(DataTemp));
SendNum++;
strcpy(DataTemp, data);
// 读后执行信号量[0]V操作 让写进程进行下一次写入
sops.sem_flg = 0;
sops.sem_op = 1;
sops.sem_num = 0;
semop(semid, &sops, 1);
PRINT_MIN_SEC();
printf("Read:%s\n",DataTemp);
if(SendNum == 5)
{
break;
}
}
shmdt(data);
}
else if (!strcmp(argv[1], "D"))
{
if(!shmctl(shmid, IPC_RMID, NULL))
{
printf("Delete OK\n");
}
}
else
{
printf("Usage: %s W|R|D\n", argv[0]);
return 0;
}
}
使用两个信号量实现读写同步,不再需要时间函数,测试结果如下:
4、总结
本文阐述了进程间通信之共享内存(System V)的定义,列举了编程中使用的接口和linux命令,编写了测试用例测试相关功能。