自学内容网 自学内容网

【linux深入剖析】命名管道 | 匿名管道与命名管道的区别 | system V共享内存

🍁你好,我是 RO-BERRY
📗 致力于C、C++、数据结构、TCP/IP、数据库等等一系列知识
🎄感谢你的陪伴与支持 ,故事既有了开头,就要画上一个完美的句号,让我们一起加油

请添加图片描述



1. 命名管道

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件

命名管道(Named Pipe),也称为命名管道文件或FIFO(First-In-First-Out,先进先出队列),是一种在操作系统级别创建的特殊类型的文件,它允许不同进程间通过文件系统共享数据或通信。在Windows环境中,它们通常用于两个进程之间的临时、无服务器的数据交换,比如应用程序间的同步或者是服务端向客户端发送信息。

命名管道有以下几个关键特点:

  • 双向通信:既可以从一端读取数据,也可以从另一端写入数据。
  • 非阻塞模式:支持非阻塞I/O操作,提高程序的并发性能。
  • 安全隔离:默认情况下,只有拥有足够权限的进程才能访问命名管道。

2. 创建命名管道

  1. 命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

mkfifo filename

  • mkfifo是Unix/Linux系统中的一个命令行工具,全称“Make FIFO”,用于创建一个名为“FIFO”(Fifo File)或“命名管道”(Named Pipe)。这是一个特殊的文件类型,类似于普通文件,但它提供了一种半双工的通信机制,即数据只能从一端写入,另一端读取,不能同时双向传输。

  • 当你创建一个FIFO文件时,需要指定其路径,例如mkfifo /path/to/myfifo。一旦创建,其他进程就可以通过这个文件名来读写数据。因为它是基于文件的操作,所以权限管理也是按照常规文件来进行的。

  • FIFOs常用于在同一台机器上不同进程之间的异步通信,特别是当通信需求简单且不需要网络连接的情况。它们在守护进程、系统启动脚本或其他需要进程间通信的应用场景中十分常见。

  1. 命名管道也可以从程序里创建,相关函数有:

int mkfifo(const char *filename,mode_t mode);

  • 在C语言中,它是一个库函数,通常在头文件unistd.h中声明。这个函数的主要作用是在当前工作目录下创建一个名为filename的命名管道(FIFO),并设置其权限模式mode。

  • filename参数是你要创建的FIFO文件的路径和名称,比如 “my_fifo”。mode参数是一个整数,包含了文件的访问权限信息,如读写权限(S_IRWXU、S_IWUSR等位标志组合)以及文件所有者和组的权限。这个参数可以根据应用需求调整。

  • 该函数返回一个整数值,如果操作成功,返回0;若失败则返回负值,可以检查errno变量获取错误原因。常见的错误包括权限不足、文件已存在或者无法在指定路径创建等。

🐥在Linux下创建一个命名管道名称为fifo如下:

在这里插入图片描述

🐥当我们向管道文件输入信息的时候,命令行会锁住不动,这就是证明我们的信息输入在管道里,需要我们进行处理了

在这里插入图片描述

🐥我们复制会话,在另一个窗口将管道信息打印

在这里插入图片描述

🐥可以发现左边的命令行恢复正常,并且信息被打印在了右边,证明信息被输出

🐥我们再采取循环的方式进行打印输出

在这里插入图片描述

🐥在这里可以一直输出信息了。

在使用管道的时候,我们可以发现,管道里无论写入什么,其大小一直都是0。

当你向命名管道写入数据时,确实会观察到写入的字节数总是显示为0,但实际上你已经成功地将数据放入了管道中。这是因为Linux在内部实现了这种特殊的数据结构,当你查看标准的write()操作返回值时,它并不反映实际的数据长度,而是表示是否成功写入管道。

当write()返回0时,通常意味着管道已满或者出错,而不是真的没有写入任何东西。正确的做法是在读取之前检查返回值和管道的状态,比如通过fcntl()或ioctl()函数获取管道的读写指针位置等信息。如果你确认已经写入数据,而显示的字节数为0,可能需要检查管道的权限、文件描述符状态或是否有其他进程阻塞在读取端。


3.匿名管道与命名管道的区别

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义

匿名管道只能对具有血缘关系的进程进行进程间通信,但是命名管道可以让两个毫不相干的进程进行通信,这是为什么?

匿名管道(也叫无名管道)是基于内核机制的,它的创建是隐式的,通常由操作系统自动为其分配一个唯一的ID。因为它们是在父进程和子进程之间创建的,所以只能用于父子进程之间的通信,这就是所谓的“血缘关系”限制。一旦子进程结束,匿名管道也随之消失,因此无法支持跨进程的持久化通信。

相比之下,命名管道(fifo,即first-in-first-out队列)是预创建的,并有一个明确的路径名。当一个进程打开这个命名管道时,它就是通过这个名字而不是关联关系来找到管道的。这就使得任何一个拥有权限并知道名称的进程都可以尝试去读取或写入管道,即使这些进程彼此没有任何血缘关系。这种设计使得命名管道非常适合那些需要在任意进程间共享信息的应用场景。

因为路径具有唯一性,所以,我们可以使用路径+文件名,来唯一的让不同进程看到同一份资源


4. 命名管道的打开规则

如果当前打开操作是为读而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO
O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
O_NONBLOCK enable:立刻返回失败,错误码为ENXIO


5. system V 共享内存

  • System V是Unix操作系统的一个版本系列,它起源于AT&T贝尔实验室对早期Unix系统V的改进和发展。System V最初由System I(第一代大型机Unix)发展而来,经历了多个版本,包括System V Release 1 (SVR1) 到System V Release 4 (SVR4),后者也被称为Solaris 2.x。

  • System V的主要特点是稳定性、可靠性以及兼容性。它引入了许多重要的改进,比如内核模块化设计、网络功能增强、标准工具集和系统管理工具的提升等。SVR4尤其显著,它是许多现代Unix和Linux发行版的基础,如Red Hat Enterprise Linux和SUSE Linux等。

  • System V也与POSIX标准紧密关联,它定义了一套通用的应用程序接口(API),使得不同厂商的Unix系统之间能够更好地交互。

共享内存区是最快的IPC形式。一旦这样的内存映射到共享它的进程的地址空间,这些进程间数据传递不再涉及到内核,换句话说是进程不再通过执行进入内核的系统调用来传递彼此的数据

共享内存示意图

在这里插入图片描述


6. 共享内存的理解

System V共享内存是一种进程间通信(IPC,Inter-Process Communication)机制,它允许不同的进程之间直接交换数据,而无需通过文件系统或者网络。在Unix-like系统如Linux上,System V模型源于System V Interprocess Communication Specification,其核心思想是通过一段预分配的内存区域(称为段或段映射),让多个进程可以同时访问。

共享的原理和动态库的共享原理一致,共享内存的申请主要分为以下三步:

  • 操作系统在物理内存上申请一块空间。
  • 将申请到的空间,通过页表挂接到进程地址空间的共享区。
  • 返回起始虚拟地址,供程序中使用。

我们想释放共享内存时,就只需要去除掉我们在页表上的关联信息,然后再将物理内存空间释放即可

在系统中,一般都会有有多组进程都需要通信,因此系统中一般会存在多个共享内存
所以操作系统要把这多个共享内存管理起来。

  • 先描述,再组织,操作系统中一定有一个内核结构体是用来描述共享内存的。

7. 共享内存数据结构:

struct shmid_ds {
struct ipc_perm shm_perm; /* operation perms */
int shm_segsz; /* size of segment (bytes) */
__kernel_time_t shm_atime; /* last attach time */
__kernel_time_t shm_dtime; /* last detach time */
__kernel_time_t shm_ctime; /* last change time */
__kernel_ipc_pid_t shm_cpid; /* pid of creator */
__kernel_ipc_pid_t shm_lpid; /* pid of last operator */
unsigned short shm_nattch; /* no. of current attaches */
unsigned short shm_unused; /* compatibility */
void *shm_unused2; /* ditto - used by DIPC */
void *shm_unused3; /* unused */
};

struct shmid_ds 是 System V 共享内存的状态信息结构体,在 Linux 系统中用来描述共享内存段的属性和状态。下面是 struct shmid_ds 结构体中各个成员的含义:

成员含义
struct ipc_perm shm_perm用于描述共享内存的操作权限。
int shm_segsz共享内存段的大小,以字节为单位。
__kernel_time_t shm_atime最后一次附加(attach)该共享内存的时间。
__kernel_time_t shm_dtime最后一次分离(detach)该共享内存的时间。
__kernel_time_t shm_ctime最后一次修改该共享内存的时间。
__kernel_ipc_pid_t shm_cpid创建者进程的进程号(PID)。
__kernel_ipc_pid_t shm_lpid最后一次操作该共享内存的进程号(PID)。
unsigned short shm_nattch当前附加(attach)该共享内存的进程数。
unsigned short shm_unused未使用的字段,用于兼容性。
void* shm_unused2未使用的字段,由 DIPC(Distributed Inter-Process Communication)使用。
void* shm_unused3未使用的字段。

通过这些状态信息,我们可以获取共享内存段的大小、权限、最后访问时间、创建者信息以及当前附加进程的数量等相关信息。这些信息对于管理和监控共享内存非常有用,能够帮助我们更好地理解和控制共享内存的状态。


8. 共享内存函数

shmget函数—创建共享内存

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

在这里插入图片描述

shmget()是Unix/Linux系统提供的一种System V共享内存管理函数,它的作用是在内核级为应用程序动态创建一块预分配的共享内存区域。这个函数需要以下几个关键参数:

  • key:这是一个整数,用于标识共享内存区。相同的key值会产生相同的共享内存资源,方便程序找到并链接起来。
  • size:想要创建的共享内存块的大小,以字节为单位。
  • flag:标志位,通常包括以下几个选项:
    SHM_RDONLY: 创建只读共享内存
    SHMReadWrite 或 0: 创建可读写共享内存
    SHM创造出错时保留旧的内容: 如果先前存在同key的共享内存,就保留它

调用shmget()返回一个进程ID,如果成功则指向新的或已存在的共享内存区域的首地址;若失败则返回-1,并设置errno错误码。

使用完毕后,通过shmat()函数将这个地址映射到进程的地址空间,然后就可以像普通数组那样访问共享内存了。当不再需要时,应调用shmdt()解除内存映射,最后使用shmctl()函数关闭共享内存区域。


shmat函数—将共享内存段连接到进程地址空间

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

在这里插入图片描述

这个函数是一个C语言中的函数原型,用于进程间的共享内存操作。它属于系统调用的一部分,常在头文件sys/shm.h中找到。函数接收三个参数:

  • shmid (整型):这是由之前调用 shmget() 函数获取的共享内存标识符,代表了要连接到的共享内存块。
  • shmaddr (void*):这是一个可选参数,如果提供,表示希望映射到特定的物理地址。如果没有指定,就会从已创建的共享内存的第一个字节开始映射。
  • shmflg (标志位):这是操作标志,可以包含以下几个标志之一(例如 SHM_RDONLY 或 SHM_WRITECOPY 等)来控制共享内存的行为,如只读、写入副本等。

函数的返回值是 void * 类型,代表了映射到进程地址空间内的共享内存的起始位置。如果操作成功,返回非NULL地址;失败则返回 NULL。使用完共享内存后,需要调用 shmdt() 函数解除映射。

shmaddr为NULL,核心自动选择一个地址
shmaddr不为NULL且shmflg无SHM_RND标记,则以shmaddr为连接地址。
shmaddr不为NULL且shmflg设置了SHM_RND标记,则连接的地址会自动向下调整为SHMLBA的整数倍。公式:shmaddr -
(shmaddr % SHMLBA)
shmflg=SHM_RDONLY,表示连接操作用来只读共享内存


shmdt函数—将共享内存段与当前进程脱离

int shmdt(const void *shmaddr);

在这里插入图片描述
Unix/Linux 系统下的 System V 共享内存 API 中的一个函数,用于将共享内存段从进程的地址空间中分离出来。这个函数接收一个指向共享内存区域(通常是从 shmat() 或者 shm_open() 创建的)的指针作为参数。

  • shmaddr: 由shmat所返回的指针
  • 返回值:成功返回0;失败返回-1

当你调用 shmdt(shmaddr) 时,它会让当前进程不再直接映射该内存块到它的虚拟地址空间中,但仍保留对这块内存的所有权。这通常发生在你想结束对共享内存的访问,但并不立即删除它,因为其他进程可能还在使用这个内存。随后,你可以通过 shmctl() 来正式地取消共享内存(SHM_RMID 命令),或者直到所有关联进程都终止才会自动清除。

注意: 将共享内存段与当前进程脱离不等于删除共享内存段


shmctl函数 —控制共享内存

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

在这里插入图片描述
Linux 系统提供的 System V 共享内存管理函数。此函数的作用是控制已存在的共享内存段(由 shmat() 或 shmopen() 创建并分配给特定 ID (shmid) 的那段内存)。它接受三个参数:

  • shmid:是一个整数,表示之前通过 shmget() 函数创建的共享内存标识符。
  • cmd:是一个操作命令,例如:

SHM_INFO:获取关于指定共享内存的信息,如大小、创建时间等,并存储在 buf 结构中。
SHM_SETATTR:修改共享内存属性,比如设置权限等。
SHM_RENAME:改变共享内存名称。
SHM_LOCK 和 SHM_UNLOCK:锁定或解锁共享内存区域。
SHM.removeItem 或 SHM_RMID:删除共享内存。

  • buf:如果 cmd 包含信息请求或需要传递数据,则此指针指向一个 struct shmid_ds 结构,用于存储响应信息。

通过这个函数,进程可以管理和维护其使用的共享内存资源。


9. 共享内存通信

processa:

#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 创建共享内存
    int shmid = CreatMem();
  
    // 挂接共享内存
    char *shamem = (char*)shmat(shmid, nullptr, 0);

    // ipc-cod 通信代码
    while(true)
    {
        cout << "client asy@ " << shamem << endl; // 直接访问共享内存
        sleep(1);
    }

    // 去关联
    shmdt(shamem);
    
    // 释放共享内存
    int ret = shmctl(shmid, IPC_RMID, NULL);
    return 0;
}

processb:

#include "comm.hpp"
#include <unistd.h>

int main()
{
    // 获取共享内存
    int shmid = GetMem();

    // 挂接
    char *shmaddr = (char*)shmat(shmid, nullptr, 0);

    // ipc-code 通信代码
    while(true)
    {
        cout << "Please enter:";
        fgets(shmaddr, size, stdin);
    }

    // 去关联
    shmdt(shmaddr);

    return 0;
}

一旦有了共享内存,并且挂接到当前进程的地址空间上了,在程序中就把它当做该进程自己的内存空间来使用即可,无需再调用系统调用。一旦有人把数据写入到共享内存,其实我们立马就能看到,不需要经过系统调用,就能直接看到数据。可以把共享内存就当做用户自己 malloc 出来的一块空间。

10. 共享内存的特点

  1. 共享内存的共享内存是所有的进程间通信中,速度最快的。

数据存储在一个可以被多个进程直接访问的物理内存区域。这种方式的优点在于速度极快,因为不需要经过网络传输或系统调用,进程可以直接读写内存中的数据,无需中间环节。它特别适合于在同一台机器上、对性能有极高要求的应用程序之间,比如实时系统和高性能计算。进程在使用的时候,需要将内容从写端拷贝到管道,然后再将内容拷贝到读端,就会有两次拷贝,而共享内存则只需要拷贝一次,因为我们写是拷贝到共享内存中,从共享内存中读取就相当于在文件中读取数据,这就不算拷贝

在共享内存中,通常通过映射文件或内存段使得进程能够映射相同的地址空间,然后就可以直接操作这块内存了。它的缺点是需要所有涉及的进程都能正确地管理这部分内存,并同步对它的访问,防止数据冲突。同时,由于涉及硬件层面,如果进程崩溃未正常结束,可能会导致其他进程的内存访问混乱。

  1. 共享内存可以提供较大的空间

共享内存允许进程之间共享非常大的数据块,因为它基于物理内存而不是受限的系统消息队列。这种特性使得它可以支持跨越核心甚至整个计算机系统的大型数据结构,例如大规模数组、复杂的算法数据结构等。由于它是直接在内存中交换数据,因此不存在像文件I/O那样的限制,也不受文件大小的约束。

然而,尽管空间大,但共享内存也有其局限性。首先,内存资源是有限的,如果分配的空间过大超过了可用内存,可能会导致系统不稳定。其次,当涉及到跨机器的共享内存时,这通常通过特殊的网络技术和集群配置才能实现,而且会带来额外的复杂性和开销。此外,管理和同步共享的大空间数据也是一个挑战,需要考虑数据的一致性和并发控制问题。



原文地址:https://blog.csdn.net/weixin_60521256/article/details/140395339

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!