linux进程间通信之共享内存

Posted by Dandan on May 16, 2021

前言

前几天整理了消息队列,那就不得不提一下共享内存,说起来他, 那可真是好处多多,一般应用我都会首先考虑它, 共享内存 + 信号灯 应用。

简介

共享内存

共享内存:是一种最为高效的进程间通信方式,进程可以直接读写内存,而不需要任何数据的拷贝.

  • ipcs -m: 列出当前系统上的所有共享内存段的详细信息,包括共享内存段的标识符(ID)、权限、大小等
  • ipcrm -m ID: 将具有指定ID的共享内存段从系统中删除。这可以用于清理不再需要的共享内存段,以释放系统资源。

共享内存是最快的进程间通信形式,允许两个或多个进程同时访问一块内存。
共享内存就是有一块内存被多个进程共享,就是在物理内存中开辟一块内存,让两个或两个以上的进程分别将新开辟的物理内存映射到自己的地址空间

什么是映射?

在物理内存中开辟一块空间,然后两个进程将各自的页表进行修改,然后映射到各自的进程地址空间上,进而把地址空间上的虚拟地址返回给用户,往后就可以使用这个虚拟地址。当执行一条命令的时候,在CPU上访问的地址其实是虚拟地址,只有正在执行这条命令时,它才会把虚拟地址转换为物理地址。
所以共享内存的步骤:

  • 创建共享内存区,通过shmget实现。在物理内存中开辟一块共享内存区。

  • 把这块共享内存区挂接映射到两个进程的地址空间上,通过shmat实现。

  • 完成通信之后,撤销内存映射关系,通过shmdt进行脱离。

  • 删除共享内存区,通过shmctl实现。

1、int shmget(key_t key, size_t size, int shmflg);

  • 功能:创建并打开共享内存
  • 参数:size共享内存的大小
  • 返回:成功共享内存id;失败-1

2、void *shmat(int shmid, const void *shmaddr, int shmflg);

  • 功能:将共享内存映射到当前进程中,或的访问的地址
  • 参数:shmaddr为NULL由系统选择一个合适的地址,shmflg为0可读可写,SHM_RDONLY只读
  • 返回:共享内存的映射地址

3、int shmdt(const void *shmaddr);

  • 功能:解除映射
  • 参数:shmaddr:shmat的返回值
  • 返回:成功为0,失败-1

4、int shmctl(int shmid, int cmd, struct shmid_ds *buf);

  • 功能:删除共享内存、获得共享内存段的属性信息、设置属性
  • 参数:cmd:IPC_RMID IPC_STAT IPC_SET

信号灯

由若干个信号量组成。

1、int semget(key_t key, int nsems, int semflg);

  • 功能:创建信号灯
  • 参数:nsems指定信号灯中有多少个信号量
  • 返回:成功信号灯ID,失败-1

2、int semctl(int semid, int semnum, int cmd);
int semctl(int semid, int semnum, int cmd, union semun);

  • 功能:得到一个信号量集标识符或创建一个信号量集对象
  • 参数:
    • cmd :IPC_RMID IPC_STAT IPC_SET SETVAL GETVAL
    • semun :使用SETVAL使用第四个参数(IPC_STAT,IPC_SET也需要)
    • semnum:信号灯中信号量的编码(从0开始)
  • 返回:cmd为GETVAL时返回某一个信号量的值。失败-1

3、int semop(int semid, struct sembuf *sops, unsigned nsops);

  • 功能:PV操作
  • 参数:
    • nsops:同时操作信号量的个数
    • sops :unsigned short sem_num; //信号量的编码
      short sem_op; //正数V操作, 负数P操作
      short sem_flg; //0阻塞等待模式, IPC_NOWAIT非阻塞模式
  • 返回:成功返回0,失败-1

例子

写数据

/*共享内存:*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>

#define N 32

union semun{
	int val;
};

int main(int argc, const char *argv[])
{
	//获取key值
	key_t key = ftok(".", 1);

	/**************** 共享内存 *********************/
	//创建并打开
	int shmid = shmget(key, N, IPC_CREAT|0664);
	if(shmid == -1)
	{
		perror("shmget error");
		exit(1);
	}
	//映射
	char *p = (char*)shmat(shmid, NULL, 0);

	/****************  信号灯 ***********************/
	//创建并打开
	int semid = semget(key, 1, IPC_CREAT|0664);
	if(semid == -1)
	{
		perror("semget error");
		exit(1);
	}
	//设置信号量初值
	union semun semun;
	semun.val = 0;
	semctl(semid, 0, SETVAL, semun);
	//设置加操作(V)
	struct sembuf sembuf;
	sembuf.sem_num = 0; 					//信号量的编码
	sembuf.sem_op  = 1; 					//执行加一操作
	sembuf.sem_flg = 0; 					//阻塞


	/****************  通信 **************************/
	while(1)
	{
		fgets(p, N, stdin); 				//从终端获取数据写到共享内存
		semop(semid, &sembuf, 1); 			//执行加一操作

		if(strncmp(p, "quit\n", 5) == 0)
		{
			break;
		}
	}
	

	/****************  删除 **************************/
	//解除映射 
	shmdt(p);

	
	return 0;
}

读数据

/*共享内存:*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>


#define N 32

int main(int argc, const char *argv[])
{
	//获取key值
	key_t key = ftok(".", 1);

	/**************** 共享内存 *********************/
	//创建并打开共享内存
	int shmid = shmget(key, N, IPC_CREAT|0664);
	if(shmid == -1)
	{
		perror("shmget error");
		exit(1);
	}
	//映射
	char *p = (char*)shmat(shmid, NULL, 0);

	/****************  信号灯 ***********************/
	//创建并打开
	int semid = semget(key, 1, IPC_CREAT|0664);
	if(semid == -1)
	{
		perror("semget error");
		exit(1);
	}
	//设置减操作(P)
	struct sembuf sembuf;
	sembuf.sem_num = 0; 					//信号量的编码
	sembuf.sem_op  = -1; 					//执行减一操作
	sembuf.sem_flg = 0; 					//阻塞

	/*****************  通信  *************************/
	while(1)
	{
		semop(semid, &sembuf, 1); 			//执行减一操作

		if(strncmp(p, "quit\n", 5) == 0)
		{
			break;
		}

		fputs(p, stdout); 					//将共享内存的数据打印到终端
	}

	/***************** 删除 ***************************/
	//解除映射 
	shmdt(p);

	//删除共享内存
	shmctl(shmid, IPC_RMID, NULL);
	system("ipcs -m");

	//删除信号灯
	semctl(semid, 0, IPC_RMID);
	system("ipcs -s");
	
	return 0;
}

上述是通过共享内存+信号灯的方式读写数据。

共享内存与信号灯结合使用的区别

共享内存的单独使用

  • 区别:在共享内存的单独使用中,多个进程可以访问相同的内存段,但没有任何内置机制来控制进程之间的并发访问。这可能导致竞争条件和数据一致性问题。

  • 好处:

    • 高性能:由于没有额外的同步机制,共享内存的访问通常比其他IPC机制(如管道或消息队列)更快。
    • 灵活性:可以在共享内存中存储大量数据,这对于需要高吞吐量和低延迟的应用程序非常有用。

共享内存与信号灯结合使用

  • 区别:与单独使用共享内存相比,共享内存与信号灯结合使用引入了信号灯(或信号量)来控制对共享内存的并发访问。信号灯允许进程协调和同步它们的操作。

  • 好处:

    • 同步:通过信号灯,你可以确保多个进程以协调的方式访问共享内存,避免竞争条件和数据损坏。
    • 互斥:信号灯可用于实现互斥,即一次只允许一个进程访问共享内存。这对于防止多个进程同时写入或读取共享数据非常重要。
    • 灵活性:你可以根据需要灵活地控制进程之间的访问,允许某些进程进行读取,而其他进程进行写入,等等。

综上所述,共享内存的单独使用适用于不需要考虑并发和同步的情况,而共享内存与信号灯结合使用适用于需要确保多个进程之间有序访问共享数据的情况。信号灯提供了更多的控制和安全性,但也需要更多的编程和维护工作。