文章目录
- 一、引言
- 二、System V IPC的基本概念
- 1、IPC结构的引入
- 2、IPC标识符(IPC ID)
- 3、S ystem V的优缺点
- 三、共享内存(Shared Memory)
- 1、共享内存的基本概念
- 2、共享内存的创建(shmget)
- 3、共享内存的附加(shmat)和分离(shmdt)
- 4、共享内存的控制(shmctl)
- 5、使用案例
- 四、消息队列(Message Queues)
- 1、消息队列的基本概念
- 2、消息队列的创建(msgget)
- 3、消息的发送(msgsnd)和接收(msgrcv)
- 4、消息队列的控制(msgctl)
- 五、信号量(Semaphores)
- 1、信号量的基本概念
- 2、信号量的创建(semget)
- 3、信号量的初始化(semctl)
- 4、信号量的P操作(semop)和V操作
一、引言
System V IPC是Linux中的一种进程间通信机制。它主要包括消息队列、信号量和共享内存三种形式。这些机制都通过内核中的IPC设施来实现,允许进程之间进行高效的数据交换和同步。
- 消息队列:消息队列允许一个进程向另一个进程发送一个具有特定类型(或称为“消息类型”)的消息。发送者将消息放入队列的尾部,而接收者则从队列的头部取出消息。消息队列对于需要异步通信的场景特别有用。
- 信号量:信号量是一个整数变量,主要用于控制对共享资源的访问。通过操作信号量(如P操作和V操作),进程可以实现对共享资源的互斥访问或同步操作。信号量在防止死锁和保证系统稳定性方面起着重要作用。
- 共享内存:共享内存允许两个或多个进程共享一块内存区域。通过映射同一块物理内存到不同进程的地址空间,这些进程可以直接访问该内存区域中的数据,从而实现高速的数据交换。共享内存是IPC机制中效率最高的一种。
二、System V IPC的基本概念
1、IPC结构的引入
System V IPC的基本概念中,IPC结构的引入是为了解决本地间不同进程之间的通信问题。在Linux系统中,多个进程可能需要相互协作、共享数据或资源,以及协调各自的工作。为了实现这些目标,System V IPC引入了几种类型的IPC结构,包括消息队列、共享内存和信号量。
这些IPC结构存在于内核中,而不是文件系统中,这意味着它们由内核直接管理,并且可以通过特定的系统调用来访问和操作。与管道等其他通信机制不同,IPC结构的释放不是由内核自动控制的,而是由用户显式控制。
System V IPC中的IPC结构并不为按名字为文件系统所知。因此我们不能使用ls
命令看到他们,不能使用rm
命令删掉它们。因此我们在查看时使用ipcs
命令,删除时使用ipcrm
命令。
例如,我们可以使用while :; do ipcs -m ; sleep 1 ; done
循环查看当前进程中的共享内存。
ipcrm
命令用于删除IPC设施。它可以根据设施的标识符或键来删除消息队列、信号量集或共享内存段。命令的基本语法是:
ipcrm [-M key | -m id | -Q key | -q id | -S key | -s id]
每个IPC结构都有一个唯一的标识符(ID),这是由系统在创建对象时分配的。此外,每个IPC结构还有一个关联的key
值,用于在不同进程之间唯一标识和访问同一个IPC结构。通过KEY值,不同的进程可以打开或找到同一个IPC结构,从而实现进程间的通信。
总的来说,System V IPC结构的引入使得不同进程之间可以更加高效、灵活地进行通信和协作。这些IPC结构的存在使得进程间的数据交换、资源共享和同步变得更加简单和可靠。
2、IPC标识符(IPC ID)
每个内核中的 IPC 结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符加以引用。
在System V IPC中,IPC标识符(IPC ID)是一个非负整数,用于唯一地标识一个IPC结构。每个IPC结构(如消息队列、信号量或共享内存段)在创建时都会被分配一个唯一的IPC ID。
当创建一个消息队列、信号量或共享内存段时,系统调用(如 msgget()
, semget()
, shmget()
)会返回一个 IPC ID,如果成功的话。这个 IPC ID 是用来在后续的 IPC 操作中引用该 IPC 对象的。
这个IPC ID是操作系统范围内的全局变量,只要具有相应的权限,任何进程都可以通过这个ID来访问和操作相应的IPC结构。这种机制使得进程间的通信更加灵活和高效,因为进程可以直接通过IPC ID来引用和操作IPC结构,而无需通过文件路径或其他标识符。
无论何时创建IPC结构,都应指定一个关键字,关键字的数据类型由系统规定为key_t
,通常在头文件<sys/types.h>
中被规定为长整型。关键字由内核变换成标识符。
在System V IPC中,获取IPC ID的通常方式是调用相应的get函数(如msgget
、semget
或shmget
,分别是创-建消息队列,信号量和共享内存的系统调用函数),并传递一个key
值作为参数。这个key值是通过ftok
函数从文件路径和ID生成的唯一键。系统会根据这个key
值来查找或创建对应的IPC结构,并返回其IPC ID。man 3 ftok
:
ftok函数的意义在于为System V IPC 提供唯一的键值(key)。在创建共享内存、消息队列等进程间通信的标识符时,通常需要指定一个ID值。这个ID值通常是通过ftok函数得到的。
ftok
函数是Linux系统中提供的一种比较重要的进程间通信机制,它可以将一个已经存在的文件的路径名和一个子序号(通常为非负整数)作为输入,然后返回一个唯一的key_t
类型的键值。这个键值在系统中是全局唯一的,可以用于标识和访问特定的IPC结构(如共享内存、消息队列)。
使用ftok
函数时,需要确保指定的文件路径下存在一个有效的文件,并且该文件在程序运行期间不会被删除或移动。否则,ftok函数可能会返回错误或生成不同的键值,导致进程间通信失败。
在System V IPC机制中,两个或多个进程可以通过约定形成同样的key
,然后使用这个key
来找到和访问同一个共享内存或消息队列。
需要注意的是,在System V IPC机制中,IPC ID是系统分配的,并且在系统重启之前都是有效的。即使创建IPC结构的进程已经退出,只要没有执行删除操作或系统重启,其他进程仍然可以通过IPC ID来访问和操作该IPC结构。
当系统重启时,由于内核会重新加载,所有的IPC对象都会被销毁,因为它们的生命周期并不是永久性的,而是依赖于内核的运行状态。
IPC标识符(IPC ID)是System V IPC中用于唯一标识IPC结构的非负整数。通过IPC ID,进程可以高效地访问和操作IPC结构,实现进程间的通信和协作。
key与IPC ID的关系?
key
是在内核角度用于区分共享内存的唯一性。我们以shmid
为例,shmid
是共享内存的ID。此处不明白,可先到后文共享内存处。
首先,key
是长整型的,用于在进程间共享内存、信号量和消息队列等系统资源之间进行标识和访问。这个key
值通常是通过ftok()
函数根据给定的路径名和标识符生成的。在shmget
函数的调用中,key
被用于指定要创建或访问的共享内存段,也就是将key
和shmid
关联起来。
而shmid
是共享内存段的用户级标识符,它是一个非负整数,用于唯一地标识一个共享内存段。当通过shmget
函数成功创建或打开一个共享内存段时,系统会返回一个shmid
,进程可以使用这个shmid
来进行后续的共享内存操作,如shmat
(将共享内存附加到进程的地址空间)和shmdt
(将共享内存从进程的地址空间中分离)。
因此,可以说key
是创建或访问共享内存段的“钥匙”,而shmid
则是成功创建或打开共享内存段后获得的“通行证”。在共享内存的管理中,key
和shmid
共同确保了进程能够正确地访问和操作共享内存段。
类似文件inode
和文件fd
的关系
在文件系统中,inode
(索引节点)和文件描述符(fd
)各自扮演了不同的角色,而在共享内存管理中,key
和shmid
也有类似的关系。
- 文件
inode
:在Linux系统中,inode
是文件系统用于存储文件元数据(如权限、所有者、大小、创建时间等)的数据结构。每个文件(或目录)在文件系统中都有一个唯一的inode
与之关联。这个inode
是从内核角度区分文件的唯一性标识。 - 文件描述符(
fd
):文件描述符是一个非负整数,用于在用户空间程序中引用一个打开的文件。当进程打开一个文件时,内核会分配一个文件描述符给该进程,进程通过这个文件描述符来进行文件的读写等操作。
类似地,在共享内存管理中:
key
:key
是用于在内核角度区分共享内存的唯一性标识。它通常通过ftok
函数生成,或者由程序员直接指定。在创建共享内存段时,key
被用来确定要创建或访问的是哪个共享内存段。shmid
:shmid
(共享内存标识符)是一个非负整数,用于在用户空间程序中引用一个已创建的共享内存段。当成功创建一个共享内存段后,系统会返回一个shmid
给调用进程。进程通过这个shmid
来进行后续的共享内存操作,如附加(shmat
)、分离(shmdt
)和删除(shmctl
)。
因此,key
和inode
都是从内核角度区分资源(文件或共享内存)的唯一性标识,而shmid
和文件描述符(fd
)则是从用户空间角度引用这些资源的标识符。这样的设计使得内核可以高效地管理资源,同时允许用户空间程序以更加灵活和直观的方式使用这些资源。
系统为每一个IPC结构设置一个了ipc_perm
结构。
struct ipc_perm
{__key_t __key; /* Key. */__uid_t uid; /* Owner's user ID. */__gid_t gid; /* Owner's group ID. */__uid_t cuid; /* Creator's user ID. */__gid_t cgid; /* Creator's group ID. */__mode_t mode; /* Read/write permission. */unsigned short int __seq; /* Sequence number. */
};
在创建IPC对象(如通过msgget()
, semget()
, shmget()
等系统调用)时,内核会为该对象分配一个ipc_perm
结构,并初始化其中的字段。除了__seq
字段(它通常由内核管理以跟踪对象的创建和删除),其他字段都由创建者或具有适当权限的进程来设置。
为什么要设计该结构体呢?
系统为每一个IPC(进程间通信)结构设置一个ipc_perm
结构,是因为这个结构用于描述IPC对象的权限和所有权信息:
- 权限管理:
ipc_perm
结构中的uid
(用户ID)、gid
(组ID)和mode
(访问模式)字段用于定义哪些用户可以访问、修改或删除IPC对象。这种权限管理机制确保了系统的安全性和稳定性,防止未授权的进程访问或篡改IPC对象。 - 所有权跟踪:
ipc_perm
结构中的uid
和gid
字段还用于跟踪IPC对象的所有者。这有助于系统管理员识别和管理IPC对象,例如查找和删除不再需要的IPC对象。 - 统一接口:在Linux系统中,多种IPC机制(如消息队列、信号量和共享内存)都使用类似的接口和数据结构。为每种IPC结构都设置一个
ipc_perm
结构,可以确保这些IPC机制在权限管理和所有权跟踪方面具有一致的接口和行为。 - 内核管理:IPC对象是在内核中创建的,因此内核需要一种方式来跟踪和管理这些对象的权限和所有权。
ipc_perm
结构为内核提供了一种方便的方式来存储和检索这些信息。当系统创建一个新的IPC对象(如一个消息队列或信号量集)时,它会在内核中为该对象分配内存,并初始化一个ipc_perm
结构来保存该对象的权限和所有权信息。这个ipc_perm
结构通常作为IPC对象特定数据结构(如msgid_ds
、semid_ds
或shmid_ds
)的一部分。内核中,所有IPC对象的ipc_perm
结构被组织成一个数组,以便内核能够快速地根据IPC对象的标识符(如消息队列的ID)找到对应的ipc_perm
结构。
3、S ystem V的优缺点
- 访问计数和垃圾回收:System V IPC结构(如消息队列、信号量和共享内存)在系统范围内起作用,但它们没有访问计数机制。这意味着,即使不再有任何进程引用这些IPC结构,它们也不会被自动删除。这可能导致系统资源的浪费,因此需要我们显式地删除不再需要的IPC结构。
- 文件系统不可见:System V IPC结构并不通过文件系统来管理,因此它们对于传统的文件操作命令(如ls、
rm
和chmod
)是不可见的。这增加了管理和调试的复杂性,因为需要使用专门的命令(如ipcs
和ipcrm
)来列出、删除和修改IPC结构的属性。 - 编程接口复杂性:System V IPC提供了丰富的功能,但也引入了复杂的编程接口。与基于文件的IPC机制(如管道和
FIFO
)相比,使用System V IPC需要更多的系统调用和更复杂的编程技术。 - 不支持文件描述符:由于System V IPC结构不是通过文件系统来管理的,因此它们没有文件描述符。这限制了使用基于文件描述符的I/O函数(如
select
和poll
)来监控多个IPC结构的能力。 - 标识符的动态分配:System V IPC结构的标识符是在系统启动时动态分配的,并且与创建时的系统状态有关。这增加了在多个进程之间共享IPC结构标识符的难度,因为需要某种形式的通信或配置文件来传递这些标识符。
此外,随着Unix和类Unix系统的发展,出现了其他IPC机制,如POSIX IPC(包括消息队列、信号量和共享内存),它们在某些方面提供了更好的抽象和更简单的编程接口。这些新机制可能更适合某些应用场景,并减少了System V IPC的一些限制。
三、共享内存(Shared Memory)
1、共享内存的基本概念
共享存储允许两个或多个进程共享一给定的内存区。由于数据不需要在客户机和服务器之 间复制,所以这是最快的一种通信方式。
**共享内存无进程间协调机制。**这意味着,当多个进程或线程访问和修改同一块共享内存区域时,它们必须自己管理对这块内存的访问,以防止数据冲突和不一致。
具体来说,当一个进程(我们称之为写入方)正在向共享内存写入数据时,另一个进程(我们称之为读取方)可能同时尝试读取这块内存。由于共享内存没有内置的同步机制,读取方可能会读取到写入方还未完全写入的数据,或者读取到写入方写入过程中的中间状态,从而导致数据的不一致性和错误。
内核为每个共享存储段设置了一个shmid_ds
结构:
shmid_ds
结构体用于描述一个共享内存段的属性。当系统创建一个共享内存段时,内核会为该段分配一个shmid_ds
结构体来保存其相关信息。这个结构体包含了共享内存段的权限、大小、时间戳、附加到该段的进程数等信息。
2、共享内存的创建(shmget)
我们通常使用此函数来创建或者获取当前key对应的共享内存,当新建一个共享内存时,我们会初始化shmid_ds
结构体的部分成员:
参数:
key
: 这是一个键值,用于唯一地标识一个共享内存段。多个进程可以通过这个键值来访问同一个共享内存段。size
: 这是要分配的共享内存段的大小(以字节为单位)。即,我们可以通过该参数来设置共享内存的大小。Linux中,共享内存的大小以4KB为基本单位。若size
的值为4097,共享内存实际大小为8KB。我们只能用4097B。如果正在存访一个现存的共享内存,则将size
指定为0。shmflg
: 这个标志位用于控制shmget
的行为。它可以是以下值的组合:IPC_CREAT
: 如果指定的共享内存段不存在,则创建它。IPC_EXCL
: 与IPC_CREAT
一起使用时,如果指定的共享内存段已经存在,则调用失败。- 权限位(如
0666
):这些位指定了新创建的共享内存段的权限。这些权限位与文件系统的权限类似,但它们的解释略有不同(特别是对于组和其他用户)。
返回值:
- 如果成功,
shmget
返回一个非负整数,这个整数是共享内存段的标识符(也称为“键”或“句柄”)。 - 如果失败,
shmget
返回-1
,并设置全局变量errno
以指示错误原因。
使用案例:shmid = shmget(key, size, IPC_EXCL | IPC_CREAT | 0666 );
3、共享内存的附加(shmat)和分离(shmdt)
在System V IPC机制中,shmat
和shmdt
是用于附加和分离共享内存段的函数。一旦创建了一个共享存储段,进程就可调用shmat
将其连接到它的地址空间中。
shmat(附加共享内存)
shmat
是 “shared memory attach” 的缩写,它的功能是将共享内存区域附加到指定的进程地址空间中。一旦附加成功,进程就可以像访问自己的内存一样访问共享内存。
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数说明:
shmid
:由shmget
返回的共享内存标识符。shmaddr
:这是一个可选参数,指定了共享内存附加到进程地址空间的地址。如果设置为NULL,则由系统选择地址。shmflg
:一组标志,用于控制附加操作的行为。通常设置为0。
如果成功,shmat
返回一个指向共享内存段的指针。如果失败,返回-1并设置errno
。
shmdt(分离共享内存)
shmdt
函数的功能是将之前附加到进程的共享内存段从进程地址空间中分离。一旦分离,进程就不能再访问这块共享内存了。
当对共享存储段的操作已经结束时,则调用shmdt
脱接该段。注意,这并不从系统中删除 其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务器)调用 shmctl
(带命令IPC_RMID
)特地删除它。
函数的原型如下:
#include <sys/types.h>
#include <sys/shm.h>
int shmdt(const void *shmaddr);
参数shmaddr
是由shmat
返回的指向共享内存段的指针。如果成功,shmdt
返回0。如果失败,返回-1并设置errno
。
4、共享内存的控制(shmctl)
共享内存的控制通常使用shmctl函数来实现。这个函数的全称是"shared memory control",用于控制共享内存段的属性、状态以及执行一些管理操作。
参数说明:
shmid
:共享内存标识符,即要控制的共享内存段的标识符。cmd
:控制命令,指定了要执行的操作。常见的命令包括:IPC_STAT
:获取共享内存段的状态信息,并将其保存在buf中。IPC_SET
:设置共享内存段的状态信息为buf中的值。IPC_RMID
:删除共享内存段。因为每个共享存储段有一个连接计数 (shm_nattch
在shmid_ds
结构中),所以除非使用该段的最后一个进程终止或与该段脱接,否则 不会实际上删除该存储段。不管此段是否仍在使用,该段标识符立即被删除 ,所以不能再用shmat
与该段连接。此命令只能由下列两种进程执行 :一种是其有效用户I D等于shm_perm.cuid
或shm_perm.uid
的进程;另一种是具有超级用户特权的进程。
buf
:一个指向shmid_ds
结构体的指针,用于传递或接收共享内存段的状态信息。这个结构体包含了共享内存的大小、拥有者ID和组ID、权限设置、最后访问和修改的时间等信息。
调用shmctl
函数并不会直接清除共享内存中的数据,它只是控制共享内存的属性和状态。例如,你可以使用IPC_SET
命令来修改共享内存的权限,或者使用IPC_RMID
命令来删除不再需要的共享内存段。
进程使用共享内存时是如何知道共享内存大小呢
System V共享内存中,当进程想要使用共享内存时,它们会先通过shmget
函数来获取共享内存的标识符(shmid
),然后再通过该标识符以及其他相关信息来操作共享内存。
具体来说,shmget
函数在创建或获取共享内存时,会返回一个共享内存的标识符(shmid
)。这个标识符是唯一的,并且可以用来引用特定的共享内存段。同时,shmget
函数还接受一个size
参数,用于指定共享内存的大小(以字节为单位)。当创建新的共享内存段时,这个size
参数就是新段的大小;而当获取已经存在的共享内存段时,这个参数实际上是被忽略的,因为段的大小已经由之前创建它的进程确定了。
一旦进程获取了共享内存的标识符(shmid
),它就可以使用其他相关的函数来操作这个共享内存段了。但是,如何知道这个共享内存段的大小呢?
使用shmctl
函数和IPC_STAT
命令来获取共享内存的状态信息:shmctl
函数是一个通用的控制函数,可以用于执行各种与共享内存相关的操作。当使用IPC_STAT
命令调用shmctl
时,它会返回一个shmid_ds
结构体,其中包含了共享内存的各种状态信息,包括大小、权限、连接数等。进程可以通过这种方式来获取共享内存的大小。
5、使用案例
假设我们有一个客户端一个服务端,我们需要客户端向服务端输入内容。
下面代码,我们使用FIFO做辅助,来进行读取控制。
Comm.hpp
#pragma once
#include "Fifo.hpp"
#include <unistd.h>
#include <iostream>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/types.h>
#include <cstring>
#include <string>using namespace std;const char *pathname = "/home/zyb/study_code";
const int proj_id = 0x66;
const int defaultsize = 4097; // 单位是字节
// 我们可以通过size大小,来设置共享内存的值。
// OS中,共享内存的大小以4KB为基本单位。若size为4097,共享内存实际大小为8KB。std::string ToHex(key_t k)
{char buffer[1024];snprintf(buffer, sizeof(buffer), "0x%x", k);return buffer;
}
key_t GetShmKeyOrDie()
{key_t k = ftok(pathname, proj_id);if (k < 0){std::cerr << "ftok error, errno : " << errno << " , error string : " << strerror(errno) << std::endl;exit(1);}return k;
}
int CreateShmOrDie(key_t key, int size, int flag)
{int shmid = shmget(key, size, flag);if (shmid < 0){std::cerr << "shmget error, errno : " << errno << " , error string : " << strerror(errno) << std::endl;exit(2);}return shmid;
}// IPC_CREAT : 不存在就创建,存在就获取
// IPC_EXCL: 存在就出错返回
// IPC_EXCL|IPC_CREAT: 不存在就创建,存在就出错返回
int CreateShm(key_t key, int size)
{return CreateShmOrDie(key, size, IPC_EXCL | IPC_CREAT | 0666 /*指定共享内存的默认权限*/);
}
int GetShm(key_t key, int size)
{return CreateShmOrDie(key, size, IPC_EXCL);
}void DeleteShm(int shmid)
{int n = shmctl(shmid, IPC_RMID, nullptr);if (n < 0){std::cerr << "shmctl error, errno : " << errno << " , error string : " << strerror(errno) << std::endl;}else{std::cout << "shmctl delete shm success, shmid: " << shmid << std::endl;}
}void ShmDebug(int shmid)
{struct shmid_ds shmds;int n = shmctl(shmid, IPC_STAT, &shmds);if (n < 0){std::cerr << "shmctl error, errno : " << errno << " , error string : " << strerror(errno) << std::endl;return;}// 共享内存大小std::cout << "shmds.shm_segsz: " << shmds.shm_segsz << std::endl;// 当前附加到该共享内存段的进程数std::cout << "shmds.shm_nattch:" << shmds.shm_nattch << std::endl;// 表示共享内存段的“更改时间” 通常以时间戳(从1970年1月1日开始的秒数)的形式存储std::cout << "shmds.shm_ctime:" << shmds.shm_ctime << std::endl;// 共享内存的key值std::cout << "shmds.shm_perm.__key:" << ToHex(shmds.shm_perm.__key) << std::endl;
}void *ShmAttach(int shmid)
{void *addr = shmat(shmid, nullptr, 0);if ((long long int)addr == -1){std::cerr << "shmat error" << std::endl;return nullptr;}return addr;
}void ShmDetach(void *addr)
{int n = shmdt(addr);if (n < 0){std::cerr << "shmdt error" << std::endl;}
}
Fifo.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <cassert>using namespace std;#define Mode 0666
#define Path "./fifo"class Fifo
{
public:Fifo(const string &path = Path) : _path(path){umask(0);int n = mkfifo(_path.c_str(), Mode);if (n == 0){cout << "mkfifo success" << endl;}else{cerr << "mkfifo failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;}}~Fifo(){int n = unlink(_path.c_str());if (n == 0){cout << "remove fifo file " << _path << " success" << endl;}else{cerr << "remove failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;}}private:string _path; // 文件路径+文件名
};class Sync
{
public:Sync() : rfd(-1), wfd(-1){}void OpenReadOrDie(){rfd = open(Path, O_RDONLY);if (rfd < 0)exit(1);}void OpenWriteOrDie(){wfd = open(Path, O_WRONLY);if (wfd < 0)exit(1);}bool Wait(){bool ret = true;uint32_t c = 0;ssize_t n = read(rfd, &c, sizeof(uint32_t));if (n == sizeof(uint32_t)){std::cout << "server wakeup, begin read shm..." << std::endl;}else if (n == 0){ret = false;}else{return false;}return ret;}void Wakeup(){uint32_t c = 0;ssize_t n = write(wfd, &c, sizeof(c));assert(n == sizeof(uint32_t));std::cout << "wakeup server..." << std::endl;}~Sync() {}private:int rfd;int wfd;
};#endif
ShmServer.cc
#include "Comm.hpp"int main()
{// 1. 获取keykey_t key = GetShmKeyOrDie();std::cout << "key: " << ToHex(key) << std::endl;// sleep(2);// 2. 创建共享内存int shmid = CreateShm(key, defaultsize);// ShmDebug(shmid);// 3. 将共享内存和进程进行挂接(关联)char *addr = (char *)ShmAttach(shmid);std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;// sleep(2);// 4. 引入管道Fifo fifo;Sync syn;syn.OpenReadOrDie();// 通信for (;;){if (!syn.Wait())break;cout << " shm content : " << addr << endl;}ShmDetach(addr);std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;sleep(2);// 3. 删除共享内存DeleteShm(shmid);return 0;
}
ShmClient.cc
#include "Comm.hpp"int main()
{key_t key = GetShmKeyOrDie();std::cout << "key: " << ToHex(key) << std::endl;// sleep(2);int shmid = GetShm(key, defaultsize);std::cout << "shmid: " << shmid << std::endl;// sleep(2);char *addr = (char *)ShmAttach(shmid);std::cout << "Attach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;// sleep(5);memset(addr, 0, defaultsize);Sync syn;syn.OpenWriteOrDie();// 通信for (char c = 'A'; c <= 'Z'; c++){addr[c - 'A'] = c;sleep(1);syn.Wakeup();}ShmDetach(addr);std::cout << "Detach shm success, addr: " << ToHex((uint64_t)addr) << std::endl;sleep(2);return 0;
}
最后我们思考:共享内存是所有进程间通信中速度最快的。为什么呢
在共享内存模型中,进程A和进程B都可以直接访问同一块物理内存区域(即“共享区”)。这意味着数据不需要通过系统调用或其他中间层进行复制或传输,从而减少了数据传输的开销。
共享内存通过允许进程直接访问同一块物理内存区域,减少了数据传输和I/O操作的开销,降低了延迟,从而提高了进程间通信的效率。
当A进程需要与B进程通信时,只需要把共享区的虚拟地址与物理地址的映射写入两进程的页表中。因此,进程A可以对该物理地址直接进行写入;而B进程则是通过页表的映射关系,从该物理地址直接进行读取。
传统的进程间通信(IPC)机制,如管道、消息队列等,通常涉及到内核空间和用户空间之间的数据拷贝,这会产生大量的I/O操作。而共享内存允许进程直接访问内存中的数据,从而避免了这种I/O开销。
上图中的管道,我们在使用时,虽然是使用内核缓冲区来进行操作,并没有将数据写入到磁盘中,但进行读写的两个进程,当发送方将数据写入管道时,数据会被拷贝到内核缓冲区中。然后,当接收方从管道中读取数据时,数据会从内核缓冲区被拷贝到接收方的用户空间。这个过程中,数据只被拷贝了两次:一次是从发送方的用户空间到内核缓冲区,另一次是从内核缓冲区到接收方的用户空间。
四、消息队列(Message Queues)
1、消息队列的基本概念
消息队列本质上是一个队列,队列中存放的是一个个消息。而队列是一个数据结构,具有先进先出的特点,它存放在内核中并由消息队列标识符标识。我们称消息队列标识符为“”队列ID”。msgget
用于创建一个新队列或打开一个现存的队列。 msgsnd
用于将新消息添加到队列尾端。每个消息包含一个正长整型类型字段,一个非负长度以及实际 数据字节(对应于长度),所有这些都在将消息添加到队列时,传送给msgsnd
。msgrcv
用于从队列中取消息。我们并不一定要以先进先出次序取消息,也可以按消息的类型字段取消息。
每个队列中都有一个msqid_ds
结构与其相关:
上述结构规定了消息队列的当前状态。
2、消息队列的创建(msgget)
我们使用消息队列首先就需要mssget
函数,用来打开一个消息队列或创建一个新的队列。
该函数的参数与共享内存相似,我们不再赘述。
若执行成功,则返回非负队列ID。此后,此值就可被用于消息队列的其他函数。
3、消息的发送(msgsnd)和接收(msgrcv)
这两个函数都需要消息队列ID(msqid)以及特定的结构体struct msgbuf
作为参数。
msgsnd
函数用于将一个新的消息写入队列。为了发送消息,调用进程对消息队列进行写入时必须有写权能。
msgp
:指向要发送消息的指针,该消息应该是msgbuf
结构体的实例。msgsz
:消息的大小(不包括mtype
字段)。
msgrcv
函数用于从消息队列中读取消息。接收消息时必须有读权能。
-
msgp
:指向用于存储接收到的消息的缓冲区的指针,该缓冲区应该是msgbuf
结构体的实例。 -
msgsz
:缓冲区中mtext
字段的最大大小。 -
msgtyp
:要接收的消息的类型。如果msgtyp
为0,则接收队列中的第一个消息。如果msgtyp
大于0,则接收具有相同类型的第一个消息。如果msgtyp
小于0,则接收类型小于或等于msgtyp
绝对值的最低类型消息。
其中msgp
结构体定义如下:
struct msgbuf {long mtype; /* 消息类型,必须大于0 */char mtext[1]; /* 消息数据,实际大小由msgsz指定 */
};
注意,该结构当中的第二个成员mtext
即为待发送的信息,当我们定义该结构时,mtext
的大小可以自己指定。
4、消息队列的控制(msgctl)
msgctl
函数类似共享内存的shmctl
,也可以取出消息队列的结构,设置消息队列结构,删除消息队列。参数同shmctl
,第一个参数表示对哪个消息队列进行操作:
五、信号量(Semaphores)
1、信号量的基本概念
信号量是一个计数器,用于多进程对共享数据对象的存取,其值表示某个共享资源的可用数量。当一个进程或线程需要访问这个共享资源时,它必须先请求信号量,并等待信号量变为可用。
为了获得共享资源,进程需要执行下列操作:
- 测试控制该资源的信号量。
- 若此信号量的值为正,则进程可以使用该资源。进程将信号量值减 1,表示它使用了一 个资源单位。
- 若此信号量的值为 0,则进程进入睡眠状态,直至信号量值大于 0。若进程被唤醒后, 它返回至(第( 1 )步)。
当进程不再使用由一个信息量控制的共享资源时,该信号量值增 1。如果有进程正在睡眠等待此信号量,则唤醒它们。 为了正确地实现信息量,信号量值的测试及减 1操作应当是原子操作。为此,信号量通常是在内核中实现的。
内核为每个信号量设置了一个semid_ds
结构体:
2、信号量的创建(semget)
返回值是信号量集ID:
3、信号量的初始化(semctl)
该函数包含了多种信号量操作:
semctl
函数常用于对信号量集进行各种操作,包括设置信号量的初始值(即初始化)。
4、信号量的P操作(semop)和V操作
- P操作:也称为“等待”操作。它用于请求访问共享资源。如果信号量的值大于0,则将其减1并允许进程继续执行;如果信号量的值为0,则进程将被阻塞,直到信号量的值变为大于0。这通常通过
semop
函数实现,并设置信号量的值semval为0(如果semval不为0,则阻塞或报错),然后将其值加1(即semval为0时可以立即通过,否则等待)。 - V操作:也称为“信号”或“发布”操作。它用于释放共享资源。当进程完成共享资源的使用后,它会将信号量的值加1,以表示该资源现在可用。这同样通过
semop
函数实现,并设置信号量的值减1(但只有在信号量值大于或等于要减去的值时才能立即返回,否则进程需要等待)。
在Linux系统中,使用信号量通常涉及以上提到的四个步骤:创建信号量、初始化信号量、进行P/V操作以及(在不再需要时)删除信号量。