Linux操作系统进程间通信方式:共享内存

什么是共享内存

共享内存是内存上的一个区域,允许多个进程同时访问、写入数据,使用起来类似于使用malloc函数分配的内存,在写入数据时另外一个进程可以立刻获取到最新的数据。共享内存的生命周期和系统内核的生命周期是一致的,也可于命令行界面显式释放。

需要注意的是共享内存没有提供同步机制,也就是说,假设有多个进程同时写入数据,那么可能会造成其中的数据不是我们所预期的,所以需要我们自己定义其同步机制,例如信号量。


使用方法

共享内存的构造

Linux在头文件sys/shm.h定义了和共享内存相关的函数,首先是构造共享内存的函数shmget

int shmget (key_t key, size_t size, int shmflg) ;
  • 第一个参数key属于int类型,类似于信号量,程序需要提供一个非0整数作为该共享内存的命名(注意和下面函数中的shmid并不等价)

  • 第二个参数size表示共享内存的大小。

  • 第三个参数shmflg为共享内存的访问权限(前9位)、IPC_CREATEIPC_EXCL(第10~12位)。类似于Linux文件系统的访问权限,例如666644(八进制数),可以用来分别限制进程的读取、写入权限。此外还可以添加IPC_CREATEIPC_EXCL字段,IPC_CREAT表示若存在该shmid对应的共享内存,则返回,否则自动创建一个ID为shmid的共享内存。IP_EXCL一般不会单独使用,如果要使用一般来说传入的是IPC_CREAT | IPC_EXCL,保证返回的共享内存是新构造的。

函数执行完成后返回共享内存的ID,如果返回-1表示共享内存创建失败。

共享内存的访问

shmget函数仅仅是用来构造共享内存,真正用来进行访问的函数是shmat

void *shmat (int shmid, const void *shmaddr, int shmflg);
  • 第一个参数shmid就是需要访问的共享内存ID,对应于我们刚才调用shmget函数传入的key参数。
  • 第二个参数shmaddr用来指定共享内存映射到当前进程的内存空间中起始的地址,一般为NULL即可,表示由系统自动选择其映射的地址。
  • 第三个参数shmflg为标志位,一般为0

函数执行成功后,会返回void*指针,指向该共享内存的首地址,用户可以按照自己的定义的规则来访问该内存。


共享内存的分离

如果当前进程不需要再使用该共享内存,可以使用shmdt函数:

int shmdt(const void *shmaddr);

shmaddr需要传入目标共享内存的首地址。一般传入上述shmat返回的指针即可。


共享内存的操纵

可以调用shmctl函数来控制共享内存的一些基本属性:

int shmctl(int shm_id, int cmd, struct shmid_ds *buf);
  • 第一个参数shm_id为共享内存的ID
  • 第二个参数cmd为需要对该共享内存采取的操作,取值范围为:IPC_STATIPC_SETIPC_RMID
    IPC_STAT用来把第三个参数buf所指向的结构设为当前共享内存的基本属性,也就是获取共享内存的属性并赋值给buf
    IPC_SET表示如果进程有足够权限就把当前共享内存的基本属性设为buf
    IPC_RMID用来删除共享内存
  • 第三个参数表示需要设置的共享内存的基本属性。操作系统内核为每个共享内存都维护了一个特殊的数据结构,记录了共享内存的基本属性,这个数据结构就是结构体shmid_ds
struct shmid_ds 
{
    struct ipc_perm shm_perm;	/* 该共享内存的访问权限 */
    size_t shm_segsz;			/* 段大小,单位为字节 */
    __time_t shm_atime;			/* 最后一个进程调用shmat访问该共享内存的时间戳 */
#ifndef __x86_64__
    unsigned long int __unused1;
#endif
    __time_t shm_dtime;			/* time of last shmdt() */
#ifndef __x86_64__
    unsigned long int __unused2;
#endif
    __time_t shm_ctime;			/* 最后调用shmctl修改该共享内存的时间戳 */
#ifndef __x86_64__
    unsigned long int __unused3;
#endif
    __pid_t shm_cpid;			/* 创建该共享内存的进程PID */
    __pid_t shm_lpid;			/* pid of last shmop */
    shmatt_t shm_nattch;		/* number of current attaches */
    __syscall_ulong_t __unused4;
    __syscall_ulong_t __unused5;
};

共享内存的删除除了在程序中调用stmctl函数以外,还可以在命令行界面中使用ipcrm命令:

[root@bogon ~]# ipcrm -m <shmid>

此外,还可以通过执行ipm命令来查看当前操作系统所有的共享内存:

[root@bogon shm]# ipcs -m

------------ 共享内存段 --------------
键        shmid      拥有者  权限     字节     nattch     状态      
0x0000abcd 0          root       666        1048578    0         

程序示例

下面程序中我们利用共享内存来实现两个进程间的“聊天”。

首先我们需要定义一个结构体,用来定义共享内存的存储结构:

memdef.h

#pragma once
#include <sys/shm.h>
#include <unistd.h>
#include <stdbool.h>

#define BUFFER_SIZE (1024 * 512)  //512KB

typedef struct memory {
	bool buf0_can_read;   //buf0缓冲区是否可读
	bool buf1_can_read;   //buf1缓冲区
	char buf0[BUFFER_SIZE];  //buf0缓冲区
	char buf1[BUFFER_SIZE];  //buf1缓冲区
} *memory;

#define MEM_SIZE (sizeof(struct memory))  //上述结构所需共享内存总大小

为了保证并发安全,我们让创建共享内存的进程 A A 使用buf0缓冲区,另外一个进程 B B 使用buf1缓冲区。保证进程 A A 只能读取buf1缓冲区,进程 B B 读取buf0缓冲区

shmdemo.c

#include <stdio.h>
#include <string.h>
#include "memdef.h"


int main(int argc, char** argv) {
	const key_t key = 0xABCD;
	const pid_t pid = getpid();  //获取当前进程PID
	int shmid;
	//尝试创建共享内存并返回shmid,如果存在则直接返回对应的shmid
	if ((shmid = shmget(key, MEM_SIZE, IPC_CREAT | 0666)) == -1) {  
		fprintf(stderr, "Fail to create mem\n");
		return 1;
	}
	//根据shmid获取内存
	void* memptr = shmat(shmid, NULL, 0);
	if (memptr == (void*)-1) {
		fprintf(stderr, "Failure visit mem\n");
		return 1;
	}
	
	struct shmid_ds stat;
	shmctl(shmid, IPC_STAT, &stat); //获取共享内存基本属性,并赋给stat
	
	memory mem = (struct memory*)memptr;  //以我们定义的结构体来读取共享内存
	char *slf_buf, *other_buf; //slf_buf分别对应当前线程写入的缓冲区,other_buf对应读取的缓冲区
	bool *slf_can_read, *other_can_read; 
	
	if (stat.shm_cpid == pid) {  //如果该共享内存是当前进程创建的,则使用缓冲区buf0
		fprintf(stdout, "Using buf0\n");
		slf_buf = mem->buf0;
		other_buf = mem->buf1;
		slf_can_read = &(mem->buf0_can_read);
		other_can_read = &(mem->buf1_can_read);
	} else {
		fprintf(stdout, "Using buf1\n");
		slf_buf = mem->buf1;
		other_buf = mem->buf0;
		slf_can_read = &(mem->buf1_can_read);
		other_can_read = &(mem->buf0_can_read);
	}

	memset(slf_buf, 0, BUFFER_SIZE);  //初始化自己的缓冲区
	*slf_can_read = false;    //初始化自己的可读标记

	while (1) {
		if (*slf_can_read == false) {   //如果自己的缓冲区已经被另外一个进程读取了
			fprintf(stdout, "Enter message:");
			fflush(stdout);
			if (fscanf(stdin, "%s", slf_buf) == EOF) {  //从键盘读入一行字符串,写入到自己的缓冲区
				*slf_can_read = -1;
				break;
			}
			*slf_can_read = true;  //可读标记为true
		} else {
			usleep(50 * 1000); //如果另外一个进程还没有读取自己的缓冲区,睡眠50毫秒
		}
		
		if (*other_can_read == true) {  //如果其它进程发送的消息可读
			fprintf(stdout, "Receive message:%s\n", other_buf); 
			*other_can_read = false;  //将其它进程的可读标记为false
		} else if (*other_can_read == -1) {  //如果其它进程退出程序
			break;
		}
	}

	shmdt(memptr);
	if (stat.shm_cpid == pid) {  //由创建的进程回收共享内存
		shmctl(shmid, IPC_RMID, &stat);
	}

	return 0;
}

拷贝上述两个文件到同一目录,使用gcc编译上述程序为shm-demo

[root@bogon shm]# gcc shm-demo.c -o shm-demo

打开两个终端,同时运行shm-demo
终端1:

[root@bogon shm]# ./shm-demo
Using buf0
Enter message:abcdef
Receive message:asdasd
Enter message:asdasdasdasd
Receive message:adadasdaad
Enter message:123456
Receive message:123456
Enter message:

终端2:

[root@bogon shm]# ./shm-demo
Using buf1
Enter message:asdasd
Receive message:abcdef
Enter message:adadasdaad
Receive message:asdasdasdasd
Enter message:123456
Receive message:123456
Enter message:

当需要退出程序时按下Ctrl + D即可,共享内存会被回收。

发布了117 篇原创文章 · 获赞 96 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/abc123lzf/article/details/101440068
今日推荐