进程间通信总结 && IPC主题三之 共享内存

本文详细介绍了共享内存的概念、特点及其实现方法,包括如何通过Linux系统提供的接口函数创建、挂接和销毁共享内存,以及与其他进程间通信方式的对比。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

之前我们已经了解了system V IPC的其他两种通信方式,今天我们来了解第三种—–共享内存

一:什么是共享内存

顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存映射到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

这里写图片描述

那么可能我们就有些疑惑了,之前我们已经学过了其他两种进程间通信了,为什么还需要共享内存呢?他的特点和优缺点分别是什么?

1.共享内存是将同一份物理内存映射到不同进程的地址空间中去;

2.共享内存是不带任何互斥和同步机制的进程间通信;

3.共享内存是所有进程间通信中最快的;

4.共享内存不需要数据的拷贝;

5.共享内存的生命周期随内核;

6.共享内存可以实现进程间的双向通信。

接下来还是一样我们了解下共享内存的接口函数用它代码来模拟它实现下进程间通信。

接口函数: 声明在 sys/shm.h中

1.shmget 函数
功能:用来创建共享内存
函数原型:int shmget(key_t key, size_t size, int shmflg);
参数
key:ftok(const char* pathname,int proj_id);与信号量的key类似;
size : 共享的内存容量(以字节为单位);
shmflg : 权限标志
(1)为 IPC_CREAT | IPC_EXCL, 则表示共享内存存在则错误返回,若不存在则创建后返回新建的共享内存标识符。保证返回的是一个新建的共享内存;
(2)为 IPC_CREAT, 则表示共享内存存在则打开,若不存则创建。

返回值
创建成功:返回该共享内存的标识符;
创建失败:返回 -1。

2.shmat 函数
功能:挂接即将物理内存映射到进程的地址空间。
函数原型:void *shmat(int shmid, const void *shmaddr, int shmflg);
参数
shmid:需要挂接的共享内存标识符;
shmaddr:通常为NULL,表示让系统自己选择共享内存的地址;
shmflg:标志位,通常为0。
返回值
挂接成功:返回所挂接的共享内存的地址;
挂接失败:返回 -1;

3.shmdt 函数
功能:去挂接,即将共享内存从当前进程中分离。
函数原型:int shmdt(const void *shmaddr);
参数
shmaddr:为shmat的返回值(即为所挂接的共享内存的地址);
返回值
去挂接成功:返回 0;
去挂接失败:返回 -1;

4.shmctl 函数
功能:属性函数(控制共享内存)
函数原型:int shmctl(int semid,int cmd,struct shmid_ds* buf);
参数
semid:共享内存标识符;
cmd:可取三种值
(1)IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。

(2)IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值

(3) IPC_RMID:删除共享内存段.
buf:是一个结构指针,它是指向共享内存模式和访问权限的结构。当共享内存释放时为NULL。

shmid_ds结构体成员:
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};

返回值
成功:返回 0;
失败:返回-1;

下面我们来直接用代码来实现共享内存方式的进程间通信。

ipcs -m 查看系统中的共享存储段;

ipcrm -m shmid 删除系统中的共享存储段;

comm.h

  1 #ifndef _COMM_H_
  2 #define _COMM_H_
  3 #include <stdio.h>
  4 #include <sys/types.h>
  5 #include <sys/ipc.h>
  6 #include <sys/shm.h>
  7 #define PATHNAME "."
  8 #define PROJ_ID 0
  9 
 10 int CreatShm(int size);
 11 int GetShm(int size);
 12 int DestoryShm(int shmid);
 13 
 14 #endif

comm.c

  1 #include "comm.h"
  2 static  int CommShm(int size, int flags)
  3 {
  4     key_t _key = ftok(PATHNAME, PROJ_ID);
  5     if(_key < 0)
  6     {
  7         perror("ftok");
  8         return -1;
  9     }
 10 
 11     int shmid = shmget(_key, size, flags);
 12     if(shmid < 0)
 13     {
 14         perror("shmid");
 15         return -2;
 16     }
 17     return shmid;
 18 }
 19 
 20 int CreatShm(int size)
 21 {
 22     return CommShm(size, IPC_CREAT | IPC_EXCL | 0666);
 23 }
 24 
 25 int GetShm(int size)
 26 {
 27     return CommShm(size, IPC_CREAT);
 28 }
 29 
 30 int DestoryShm(int shmid)
 31 {
 32     if(shmctl(shmid, IPC_RMID, NULL) < 0)
 33     {
 34         perror("destory");
 35         return -1;
 36     }
 37     return 0;
 38 }

server.c

  1 #include "comm.h"
  2 int main()
  3 {
  4     //创建共享内存
  5     int shmid = CreatShm(4095);
  6     char *buf;
  7     //挂接
  8     buf = shmat(shmid, NULL, 0);
  9     sleep(5);
 10     int count = 0;
 11     while(count < 20)
 12     {
 13         buf[count++] = 'a' + count;
 14         buf[count] = '\0';
 15         sleep(1);
 16     }
 17     shmdt(buf);//去关联
 18     DestoryShm(shmid);//删除共享内存
 19     return 0;
 20 }

client.c

  1 #include "comm.h"
  2 
  3 int main()
  4 {
  5     //创建共享内存
  6     int shmid = GetShm(0);
  7     char* buf;
  8     //挂接
  9     buf = shmat(shmid, NULL, 0);
 10 
 11     int count = 0;
 12     while(count++ < 15)
 13     {
 14         printf("client %s\n", buf);
 15         sleep(1);
 16     }
 17     shmdt(buf);//去关联
 18     return 0;
 19 }

代码运行结果:

先运行server 然后查看共享内存;
这里写图片描述

这里写图片描述

我们让进程server先创建共享存储段,然后client获取共享存储段的标识符与之挂接,然后进程server将数据写入共享内存,进程client就可以读取进程server写的数据了。通信完成后,进程server和进程client都要去关联共享存储段,并由进程server释放共享内存。

总结:在linux下的多个进程间的通信机制叫做IPC(Inter-Process Communication),它是多个进程之间相互沟通的一种方法。我们了解了匿名管道(PIPE),命名管道(FIFO),消息队列,信号量,共享内存。


(一)匿名管道

(1)半双工,也就是数据只能单向流动。双向通信需要建立两个管道;
(2)只能作用与有血缘关系的进程之间。(相同的文件描述符);
(3)管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
(4)管道中的数据读写必须是按顺序读写。
实现机制:管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管道可以被循环利用。当管道中没有信息的话,从管道中读取的进程会等待,直到另一端的进程放入信息。当管道被放满信息的时候,尝试放入信息的进程会等待,直到另一端的进程取出信息。当两个进程都终结的时候,管道也自动消失。

注意:管道只能在本地计算机中使用,而不可用于网络间的通信。


(二)命名管道

命名管道是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了管道的弊端,他可以允许没有亲缘关系的进程间通信。

管道和命名管道的区别
对于命名管道FIFO来说,IO操作和普通管道IO操作基本一样,但是两者有一个主要的区别,在命名管道中,管道可以是事先已经创建好的,比如我们在命令行下执行mkfifo;就是创建一个命名通道,我们必须用open函数来显示地建立连接到管道的通道,而在匿名管道中,管道已经在主进程里创建好了,然后在fork时直接复制相关数据或者是用exec创建的新进程时把管道的文件描述符当参数传递进去。

一般来说FIFO和PIPE一样总是处于阻塞状态。也就是说如果命名管道FIFO打开时设置了读权限,则读进程将一直阻塞,一直到其他进程打开该FIFO并向管道写入数据。这个阻塞动作反过来也是成立的。如果不希望命名管道操作的时候发生阻塞,可以在open的时候使用O_NONBLOCK标志,以关闭默认的阻塞操作。


(三)消息队列

消息队列是内核地址空间中的内部链表,通过linux内核在各个进程直接传递内容,消息顺序地发送到消息队列中,并以几种不同的方式从队列中获得,每个消息队列可以用IPC标识符唯一地进行识别。内核中的消息队列是通过IPC的标识符来区别,不同的消息队列直接是相互独立的。每个消息队列中的消息,又构成一个独立的链表。

注意:消息队列克服了信号承载信息量少,管道只能承载无格式字符流。Linux的消息队列(queue)实质上是一个链表,它有消息队列标识符(queue ID)。 msgget创建一个新队列或打开一个存在的队列;msgsnd向队列末端添加一条新消息;msgrcv从队列中取消息, 取消息是不一定遵循先进先出的, 也可以按消息的类型字段取消息。

消息队列与命名管道的比较

消息队列跟命名管道有不少的相同之处,通过与命名管道一样,消息队列进行通信的进程可以是不相关的进程,同时它们都是通过发送和接收的方式来传递数据的。在命名管道中,发送数据用write,接收数据用read,则在消息队列中,发送数据用msgsnd,接收数据用msgrcv。而且它们对每个数据都有一个最大长度的限制。

与命名管道相比,消息队列的优势在于
(1) 消息队列也可以独立于发送和接收进程而存在,从而消除了在同步命名管道的打开和关闭时可能产生的困难。
(2)同时通过发送消息还可以避免命名管道的同步和阻塞问题,不需要由进程自己来提供同步方法。
(3)接收程序可以通过消息类型有选择地接收数据,而不是像命名管道中那样,只能默认地接收。


(四)信号量

信号量是一种计数器,用于控制对多个进程共享的资源进行的访问。它们常常被用作一个锁机制,在某个进程正在对特定的资源进行操作时,信号量可以防止另一个进程去访问它。
信号量是特殊的变量,它只取正整数值并且只允许对这个值进行两种操作:等待(wait)和信号(signal)。(P、V操作,P用于等待,V用于信号)
p(sv):如果sv的值大于0,就给它减1;如果它的值等于0,就挂起该进程的执行

V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行;如果没有其他进程因等待sv而挂起,则给它加1

简单理解就是P相当于申请资源,V相当于释放资源 。


(五)共享内存

共享内存是在多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建的一个特殊地址范围,它将出现在该进程的地址空间中。其他进程可以将同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存中的地址,就好像它们是malloc分配的一样。如果一个进程向共享内存中写入了数据,所做的改动将立刻被其他进程看到。

共享内存是IPC最快捷的方式,因为共享内存方式的通信没有中间过程,而管道、消息队列等方式则是需要将数据通过中间机制进行转换。共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。

注意:共享内存本身并没有同步机制,需要程序员自己控制。

消息队列、信号量以及共享内存的相似之处

它们被统称为XSI IPC,它们在内核中有相似的IPC结构(消息队列的msgid_ds,信号量的semid_ds,共享内存的shmid_ds),而且都用一个非负整数的标识符加以引用(消息队列的msg_id,信号量的sem_id,共享内存的shm_id,分别通过msgget、semget以及shmget获得),标志符是IPC对象的内部名,每个IPC对象都有一个键(key_t key)相关联,将这个键作为该对象的外部名。

XSI IPC和PIPE、FIFO的区别

(1)XSI IPC的IPC结构是在系统范围内起作用,没用使用引用计数。如果一个进程创建一个消息队列,并在消息队列中放入几个消息,进程终止后,即使现在已经没有程序使用该消息队列,消息队列及其内容依然保留。而PIPE在最后一个引用管道的进程终止时,管道就被完全删除了。对于FIFO最后一个引用FIFO的进程终止时,虽然FIFO还在系统,但是其中的内容会被删除。

(2)和PIPE、FIFO不一样,XSI IPC不使用文件描述符,所以不能用ls查看IPC对象,不能用rm命令删除,不能用chmod命令删除它们的访问权限。只能使用ipcs和ipcrm来查看可以删除它们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值