进程间通信(管道与共享内存)

本质!不同的进程看到同一份东西

前言知识点

半双工通信机制

半双工通信允许数据在通信双方之间单向传输,但不能同时进行双向传输。这意味着在半双工通信中,通信的两个实体可以轮流发送和接收数据,但不能同时进行发送和接收操作。

在半双工通信中,数据的流动只能在一个方向上,而不能同时进行双向的数据传输。这是因为在通信系统中,数据传输需要使用共享的通信通道,如管道、电缆等。半双工通信机制通过在时间上分割发送和接收操作来实现单向的数据传输。

半双工通信通常用于一些场景,其中只有一个实体可以发送数据,而另一个实体则负责接收数据。典型的半双工通信场景包括:

  1. 对讲机:在对讲机中,用户通过按下按钮启动发送模式,然后放开按钮切换到接收模式,从而实现在不同时间点的发送和接收操作。

  2. 管道通信:管道是一种半双工通信的方式,其中数据只能沿一个方向进行传输。一个进程可以将数据写入管道,而另一个进程则可以从管道中读取数据。

  3. 消息队列:消息队列是一种半双工的通信方式,允许一个实体将消息放入队列,并由另一个实体按顺序接收这些消息。

在半双工通信中,需要注意的是发送方和接收方在时间上的协调和同步,以确保数据的完整和正确性。因为半双工通信只能单向传输,所以需要设计合适的机制来处理发送者和接收者之间的交互和协作。

全双工通信机制

全双工通信机制是一种允许双方同时进行双向通信的通信方式。在全双工通信中,发送方可以同时发送数据给接收方,同时接收方也可以发送数据给发送方,实现了双方之间的并行通信。

全双工通信可以在同一物理通道上进行,如一根双绞线、光纤或无线信道。发送方和接收方使用不同的频率、时间槽或编码方案来避免冲突,从而同时进行双向通信。

常见的全双工通信机制包括:

  1. 网络中的TCP套接字:在网络通信中,使用TCP协议的套接字可以实现全双工通信。一个套接字可以同时接收和发送数据,在连接建立后,双方可以同时进行双向的数据传输。

  2. 电话通信:在传统的电话通信中,通信双方可以同时听到对方的声音,实现了全双工通信。通过双方独立的语音信道,每个人可以同时说话和听对方说话。

  3. 双向无线电通信:无线电通信中的全双工通信可以通过不同的频段进行,允许发射器和接收器在不同的频率上同时发送和接收数据。

全双工通信提供了更高的通信容量和吞吐量,允许双方同时进行双向数据传输。这对于需要实时交互、高效数据传输和多方协作的应用非常重要。

内存级文件

内存级文件(Memory-mapped files)是一种将磁盘文件映射到进程的虚拟内存空间中的技术。通过内存映射文件,可以将文件的内容直接映射到内存中,并通过内存访问操作来读写文件。

内存映射文件的过程如下:

  1. 打开文件:首先需要打开待映射的文件,这通常使用操作系统提供的文件I/O函数来完成。

  2. 创建映射:进程通过调用操作系统提供的函数(如mmap())来创建一个映射,并将文件映射到进程的虚拟内存空间中。

  3. 访问文件:一旦映射创建成功,进程可以像访问内存一样访问文件。进程可以直接使用指针进行文件的读写操作,而无需使用繁琐的读写函数。

  4. 同步数据:进程对映射的文件进行读写操作时,对内存中的数据的更改也会反映到磁盘文件上。此外,进程还可以通过msync()函数将内存中的数据同步回磁盘,以保证数据的持久化。

内存映射文件的优点包括:

  • 性能:通过内存映射文件可以避免频繁的读写操作,提高文件的访问性能。

  • 简便性:使用内存映射文件可以直接在内存中操作文件,无需繁琐的读写函数。

  • 共享和协作:多个进程可以同时映射同一个文件,并共享文件的内容,方便实现进程间的协作和共享数据。

然而,需要注意以下一些问题:

  • 内存限制:内存映射文件会消耗系统的内存资源,因此需要注意文件大小和系统的内存限制。

  • 文件同步:对映射文件的更改需要进行适时的同步操作,以确保数据的持久性和一致性。

  • 安全性:对内存映射文件的读写操作需要谨慎,需要注意数据的正确处理和保护。

内存级文件是一种高效的文件访问方式,特别适用于大文件读写、共享和并发访问的场景。它在许多应用领域中得到了广泛的应用,如数据库系统、高性能计算和缓存系统等。

ftok()

介绍

在Linux中,ftok函数用于根据文件的路径名和项目标识生成一个键值(key),用于创建或访问共享内存、消息队列和信号量等IPC资源。

ftok函数的原型如下:

#include 
#include 
​
key_t ftok(const char *pathname, int proj_id);

pathname参数是一个指向文件的路径名的字符串,用于确定文件的唯一性。通常情况下,可以使用一个已存在的文件作为路径名,如/tmp/file.txt

proj_id参数是一个项目标识符的整数值,用于区分不同的IPC资源。在不同的应用程序或进程间,使用不同的项目标识来保证生成的键值不同。

ftok函数将pathname的最后一个字符与proj_id相结合,并通过一定的算法生成一个32位的键值。该键值将被用于创建或访问共享内存、消息队列和信号量等IPC资源。

需要注意的是,proj_id的范围是0到255之间的整数,因此选择合适的项目标识符很重要,以避免重复的键值。

另外,使用ftok函数生成的键值,还可以通过IPC_PRIVATE与权限标志进行或运算,生成私有的键值,用于在同一个进程内部使用。

用例
#include 
#include 
#include 
​
int main() {
    const char *pathname = "/tmp/file.txt";  // 文件路径名
    int proj_id = 1;  // 项目标识
​
    key_t key = ftok(pathname, proj_id);
    if (key == -1) {
        perror("ftok");
        return 1;
    }
​
    // 使用生成的键值进行共享内存、消息队列或信号量的操作
    // ...
​
    return 0;
}

为什么

  1. 资源共享:现代计算机系统中,多个进程通常需要访问相同的资源,如文件、数据库、网络连接等。进程间通信使得这些资源可以被有效地共享和管理,避免了每个进程都有自己独立副本的冗余。

  2. 分工合作:复杂的应用程序通常由多个进程组成,每个进程负责不同的任务。通过IPC(进程间通信),这些进程可以协调工作,交换数据,共同完成任务。例如,一个进程可能负责数据处理,而另一个进程负责显示结果。

  3. 模块化设计:在软件开发中,将系统划分为多个进程模块可以提高可维护性和可扩展性。每个模块可以独立运行,通过IPC与其他模块通信,这样修改一个模块不会影响到整个系统。

  4. 响应性和性能:在交互式系统中,如图形用户界面,需要快速响应用户操作。通过IPC,用户界面进程可以与后台处理进程通信,确保用户操作能够得到及时处理,同时避免了用户界面冻结的情况。

  5. 分布式系统:在分布式计算中,多个进程可能运行在不同的机器上。IPC技术如套接字和消息队列使得这些进程能够跨越网络进行通信,共同完成计算任务。

  6. 进程控制:IPC允许一个进程请求另一个进程的服务,或者通知另一个进程某个事件已经发生。这种控制流的管理对于复杂的系统来说是必不可少的。

  7. 错误处理和恢复:当一个进程发生错误或者需要停止时,通过IPC可以通知其他进程进行相应的错误处理或恢复操作。

是什么

进程间通信(Inter-process Communication,IPC)是计算机程序之间进行通信的方法和技术。它允许不同的进程在计算机系统中的不同地址空间进行交互。首先进程是具有独立性的,所以通信的本质是让不同的进程看到同一份资源。

当涉及到进程间通信时,以下是一些常见的进程间通信方法:

  1. 管道(Pipe):管道是一种单向的进程间通信机制,将一个进程的输出连接到另一个进程的输入。有两种类型的管道:匿名管道和命名管道(FIFO)(文件系统中的特殊文件)。

  • 管道是一种最基本的IPC机制,它允许一个进程将数据发送到另一个进程。

    • 管道可以是匿名管道(无需预先创建)或命名管道(需要在系统中预先创建)。

    • 管道是一种单向通信机制,数据只能从一个进程流向另一个进程。

  1. 共享内存(Shared Memory):共享内存允许多个进程共享同一块内存区域。通过在进程之间共享共享内存块,进程可以直接读写共享数据,从而实现高效的进程间通信。

    • 共享内存允许多个进程访问同一块内存区域,这意味着它们可以读写相同的内存地址。

    • 这是一种高速的IPC方式,因为数据不需要在客户端和服务器之间复制。

    • 共享内存需要同步机制(如互斥锁)来避免并发时的数据竞争。

  1. 消息队列(Message Queue):消息队列是一个消息的列表,可以在不同进程之间传递。每个消息都具有一个特定的类型,接收方可以选择接收相应类型的消息。

  • 消息队列允许进程以消息为单位进行通信,这些消息存储在队列中。

    • 发送进程将消息添加到队列中,而接收进程从队列中读取消息。

    • 消息队列提供了更复杂的数据结构和访问控制。

  1. .信号量(Semaphore):信号量是一种用于同步进程和控制进程访问共享资源的方法。它可以用来解决进程间的互斥和同步问题。

    • 信号量是一种用于同步的IPC机制,它可以用来控制对共享资源的访问。

    • 信号量可以是二进制的(只有0和1两个状态),也可以是计数信号量,允许一定数量的进程访问资源。

    • 信号量通常与互斥锁一起使用,以避免死锁和资源竞争。

  1. 信号(Signals)

  • 信号是一种简单的IPC机制,用于通知接收进程某个事件已经发生。

  • 发送进程通过发送信号来通知接收进程,而接收进程可以处理这个信号。

  • 信号处理可以是非阻塞的,也可以是阻塞的,取决于信号的性质。

  1. 套接字(Socket):套接字是一种在网络上实现进程间通信的方法。通过套接字,不同主机上的进程可以通过网络进行通信。

    • 套接字是一种通用的IPC机制,它支持网络通信,也支持同一台机器上的进程间通信。

    • 套接字使用TCP或UDP协议进行通信,提供了面向连接和无连接的通信方式。

    • 套接字允许进程通过网络进行交互,也可以用于本地进程间通信。

  2. 远程过程调用 RPC(Remote Procedure Call):RPC允许在网络上的不同计算机上的进程之间进行通信,使其感觉像是在本地执行过程调用一样。

管道

命令

在 Linux 中,管道相关的命令主要涉及创建、操作和管理管道文件。以下是一些常用的管道相关命令:

  1. mknod 命令:

    • 用于创建一个命名管道FIFO)或设备文件。

    • 例如,mknod mypipe p 创建一个命名管道文件 mypipe

  2. mkfifo 命令:

    • 用于创建一个命名管道文件。

    • 例如,mkfifo /path/to/myfifo 创建一个名为 myfifo 的命名管道文件。

  3. rmdir 命令:

    • 用于删除空的目录。

    • 例如,rmdir mypipe 删除一个名为 mypipe 的空目录。

  4. rm 命令:

    • 用于删除文件和目录。

    • 例如,rm -rf /path/to/myfifo 删除一个名为 myfifo 的命名管道文件。

  5. ln 命令:

    • 用于创建文件的链接。

    • 例如,ln -s /path/to/myfifo /path/to/alink 创建一个指向 myfifo 的符号链接。

  6. cat 命令:

    • 用于查看文件内容。

    • 例如,cat /path/to/myfifo 查看命名管道文件 myfifo 的内容。

  7. headtail 命令:

    • 用于查看文件的开头几行(head)或结尾几行(tail)。

    • 例如,head -n 10 /path/to/myfifo 查看 myfifo 的前 10 行。

  8. echo 命令:

    • 用于输出文本到标准输出。

    • 例如,echo "Hello, World!" > /path/to/myfifo 将文本写入命名管道文件。

  9. truncate 命令:

    • 用于截断文件,使其大小变为指定的字节数。

    • 例如,truncate -s 1024 /path/to/myfifomyfifo 的大小截断为 1024 字节。

  10. fcntl 命令:

    • 用于控制文件描述符。

    • 例如,fcntl(myfifo, F_GETFL) 获取 myfifo 的文件状态标志。

这些命令涵盖了命名管道的创建、查看、删除和操作的基本需求。

mknod

在 Linux 中,mknod 命令用于创建一个新的文件节点,可以用于创建设备文件,管道文件等。

mknod 的基本语法如下:

mknod [options] 文件名 类型 [主设备号 次设备号]
  • [主设备号 次设备号(major minor)]:这两个参数用于指定设备的主次设备号。对于字符设备(例如串行端口),主设备号通常为 100 以下的数字,对于块设备(例如硬盘),主设备号通常为 80 以上的数字。

  • 文件名:这是你想要创建的特殊文件的名称。

常见的选项包括:

  • -m:设置文件的权限模式。例如,mknod -m 660 /dev/mydevice c 234 0 表示创建一个名为 /dev/mydevice 的字符设备文件,并为其设置读写权限为 660。

  • -p:自动选择合适的权限模式和主次设备号。例如,mknod -p /tmp/mypipe p 表示创建一个名为 /tmp/mypipe 的命名管道文件,并自动选择权限模式和主次设备号。

  • -t--type:指定要创建的文件类型。例如mknod -t fifo /tmp/mypipe 将创建一个命名管道文件,mknod -t c /dev/mydevice c 10 1/dev/mydevice 设置为字符设备。

  • -s--socket:用于创建套接字文件。

  • -n:在创建文件时跳过已存在的文件。例如,mknod -n /dev/mydevice c 10 1 将不会创建已存在的 /dev/mydevice 文件。

  • -f--force:强制创建文件,即使文件已经存在。

    1.创建一个名为 /dev/mydevice 的字符设备文件,你可以在命令行中输入:

mknod /dev/mydevice c 234 0

这里的 c 表示这是一个字符设备,234 是主设备号,而 0 是次设备号。

  1. 创建一个名为 /tmp/mypipe 的命名管道文件,你可以输入以下命令:

mknod /tmp/mypipe p

这将在 /tmp 目录下创建一个名为 mypipe 的命名管道文件。

然后,使用以下命令启动 udevadm 并查看设备属性:

udevadm test --builtin=/dev --name=/tmp/mypipe --subsystem-list=fifo

这将显示 /tmp/mypipe 命名管道的设备属性信息,包括主设备号、次设备号等。

  1. 如果你想要创建一个名为/dev/myblock的块设备文件,你可以输入以下命令:

sudo mknod /dev/myblock b 8 0

mkfifo

在 Linux 中,mkfifo 命令用于创建一个命名管道(Named Pipe),也被称为 FIFO(First-In-First-Out)。命名管道是一种特殊的文件类型,它允许不相关的进程通过文件进行通信。

mkfifo 的基本语法如下:

mkfifo [options] 文件名
  • 文件名:指定要创建的命名管道的名称。

  • [options]:包含一些可选的标志,例如:

    • -m:文件的权限模式。

常用的选项包括:

  • -m:设置文件的权限模式。例如,mkfifo -m 660 mypipe 表示创建一个名为 mypipe 的命名管道,并为其设置读写权限为 660。

  • -p:为管道指定主设备号和次设备号。例如,mkfifo -p 80 90 mypipe 表示创建一个名为 mypipe 的命名管道,并为其指定主设备号为 80,次设备号为 90。

  • -m:设置文件的权限模式。例如,mkfifo - 660 mypipe 表示创建一个名为mypipe` 的命名管道,并为其设置读写权限为 660。

  • 有一些特殊的选项可用于特定的目的,如创建带区号的命名管道(-b)或限制命名管道的大小(-s)。

举个例子,如果你想要创建一个命名管道文件,你可以在命令行中输入:

mkfifo mypipe

这将在当前目录下创建一个名为 mypipe 的命名管道文件。

创建命名管道后,你可以在不同的进程之间使用该文件进行通信。一个进程可以将数据写入命名管道,而另一个进程可以从管道中读取这些数据。

需要注意的是,默认情况下,管道是阻塞的。这意味着当管道为空时,读取进程将被阻塞,直到有数据可供读取。同样,当管道已满时,写入进程将被阻塞,直到有空间可供写入。

使用命名管道时,你需要确保读取和写入进程以正确的顺序进行操作,以避免死锁或其他问题。

函数

  1. pipe() 函数:

    • int pipe(int pipefd[2]);

    • 用于创建一个匿名管道,并返回两个文件描述符,pipefd[0] 用于从管道读取数据,pipefd[1] 用于向管道写入数据。

  2. mkfifo() 函数:

    • int mkfifo(const char *pathname, mode_t mode);

    • 用于创建一个命名管道,指定路径名和权限模式。

  3. open() 函数:

    • int open(const char *pathname, int flags);

    • 在读写操作之前,使用此函数打开已存在的命名管道。

  4. read()write() 函数:

    • ssize_t read(int fd, void *buf, size_t count);

    • ssize_t write(int fd, const void *buf, size_t count);

    • 用于从管道读取数据和向管道写入数据。

  5. close() 函数:

    • int close(int fd);

    • 用于关闭管道的文件描述符。

pipe()

pipe函数是一个系统调用,用于创建一个管道(pipe)。它的原型如下:

#include 
int pipe(int pipefd[2]);

pipe函数接受一个整型数组pipefd作为参数,用于存储管道的两个文件描述符。

  • pipefd[0]用于从管道读取数据。通常情况下,它被关联到管道的读取端。

  • pipefd[1]用于向管道写入数据。通常情况下,它被关联到管道的写入端。

pipe函数成功创建管道时返回0,如果发生错误,则返回-1并设置errno错误代码。

下面是一个简单的示例,展示了如何使用pipe函数创建管道:

#include 
#include 
#include 
​
int main() {
    int pipefd[2];
​
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
​
    printf("Read end of pipe: %d\n", pipefd[0]);
    printf("Write end of pipe: %d\n", pipefd[1]);
​
    return 0;
}

在上述示例中,我们调用pipe函数创建了一个管道,并将其文件描述符存储在pipefd数组中。然后,我们打印出管道的读取端和写入端的文件描述符。

注意,pipe函数创建的管道是双向的,即可以在两个方向上进行读写。但通常情况下,一个进程只使用其中一个方向来进行读写操作。另外,由于管道是内核中的一个缓冲区,当管道被读取完毕后,再次读取将会阻塞,直到有新的数据写入管道。

mkfifo()

mkfifo()函数是一个系统调用,用于根据指定的路径名创建一个FIFO(First-In-First-Out)文件,也称为命名管道文件。

mkfifo()函数的原型如下:

#include 
#include 
​
int mkfifo(const char *pathname, mode_t mode);

参数说明:

  • pathname:要创建的FIFO文件的路径名。

  • mode:FIFO文件的权限模式。在创建FIFO文件时,它的权限默认是根据进程的umask值和文件创建者的权限来确定的。通常情况下,应该使用合适的mode参数对FIFO文件的权限进行显式设置。

函数返回值:

  • 如果成功创建FIFO文件,则返回0。

  • 如果发生错误,则返回-1,并设置相应的错误码。

mkfifo()函数的工作原理如下:

  1. mkfifo()函数尝试在文件系统中创建一个具有指定路径名的FIFO文件。

  2. 如果路径名已存在,并且不是一个FIFO文件,则创建失败,返回错误。

  3. 如果路径名已存在并且已经是一个FIFO文件,则创建成功,返回0。

  4. 如果路径名不存在,则根据指定的权限模式创建一个FIFO文件,并返回0。

一旦使用mkfifo()函数成功创建了一个FIFO文件,应用程序就可以使用该文件进行进程间通信,通过读写FIFO文件进行数据交换。需要注意的是,FIFO文件是一个特殊类型的文件,不同于普通的文本文件或二进制文件,要正确地使用FIFO文件进行通信,需要遵循FIFO文件的读写规则。


是什么

在进程间通信中,管道(Pipe)是一种常用的通信方式之一。管道是一种半双工通信机制,通常用于具有亲缘关系的进程之间进行通信,如父子进程。这种半双工是支持数据的反向流动,但是这种反向流动是通过创建另一个管道来实现的。

在操作系统中,管道可以分为两种类型:匿名管道和命名管道。

  1. 匿名管道:匿名管道是一种无名的管道,只能在具有亲缘关系的进程之间使用(如父子进程,兄弟进程)。它在创建时会自动分配一个文件描述符,并存储在进程表中。匿名管道是内存中的一个缓冲区,其大小是固定的。其中一个进程将数据写入管道,另一个进程则从管道中读取数据。(’|‘操作是一种匿名管道)

  2. 命名管道(FIFO):命名管道是一种有名字的管道,可以在任何两个进程之间进行通信。与匿名管道不同,命名管道是通过文件系统中的特殊文件进行通信。命名管道使用mkfifo命令或mkfifo()系统调用创建,它允许多个进程以类似于读写文件的方式进行管道通信。

管道在进程间通信中有许多应用场景。例如,在一个父进程和多个子进程之间,父进程可以使用管道向子进程发送命令或数据,子进程则可以通过管道将处理结果返回给父进程。另一个例子是在一个生产者-消费者模型中,生产者进程可以将数据写入管道,而消费者进程则从管道中读取数据进行处理。

函数:pipe() read() write() mkfifo() mknod()

命令:mknod mkfifo


匿名管道

原理(匿名管道)
管道文件

在操作系统中,管道文件通常指的是命名管道的文件表示。命名管道是一种特殊的文件,它允许不同进程通过文件系统中的这个文件进行通信。这种文件类型通常用于实现进程间的数据传输。

当一个进程向命名管道写入数据时,它实际上是向这个特殊文件写入数据。另一个进程可以从命名管道读取数据,它是通过读取这个特殊文件来获取数据的。因此,命名管道文件是管道通信机制在文件系统中的一个表现形式。

需要注意的是,管道文件本身并不是内存中的一个结构,而是文件系统中的一个实体。它提供了进程间通信的接口,但不同于普通的文件,它的目的是为了支持进程间的数据传输,而不是为了持久化存储数据,可以说管道文件类似一种内存级文件,但是绝对不等于,管道文件是操作系统在程序运行时候创建的,不是从磁盘上加载的,他在磁盘中没有实体

虽然管道本身没有对应的实体文件,但是它可以与其他文件系统实体(如命名管道或消息队列)进行组合,以实现更复杂的持久化通信机制。


管道文件的文件描述符

管道文件通过文件描述符来进行操作。文件描述符(fd)是一个非负整数,它是由系统调用分配给每个打开的文件(或管道)的。当一个进程打开一个管道时,它会获得两个文件描述符,一个用于读取(通常是0或标准输入),另一个用于写入(通常是1或标准输出)。


原理(不同进程看到同一份资源)

两者看到的是相同的文件描述符,也就是相同的内存缓冲区。

管道文件(Pipe file)是一种特殊的文件类型,它在文件系统中有对应的路径名,并且可以通过文件描述符来进行访问和操作。

pipe()函数会在进程中新增两个文件描述符,在实验一中我们可以明白。

当父进程创建管道时,操作系统会在内核中创建对应的管道数据结构并在文件系统中为其分配路径名,从而创建了管道文件。父进程获得的文件描述符可以用来访问和操作该管道文件。这个管道文件实际上是一个普通的文件,但对于读写管道的操作,它会通过特殊的文件系统操作来处理。

不会创建新的页表项。

创建子进程,子进程会获得这个文件描述符表的拷贝,通过开关父子进程的管道文件的读写接口,实现读取和写入。


问题:
0.管道文件会在进程中创建页表项嘛?

不会。管道的缓冲区在内核中,由操作系统内核维护,而不是在物理内存中。内核负责管理缓冲区的分配、释放和读写操作,以确保数据的安全传输和正确处理。进程中的文件描述符可以找到这片缓冲区,通过系统调用完成读写操作。

页表是进程模块和物理内存模块的联系。与内核无关。

1.管道也是文件,他有FCB嘛?

管道文件(Pipe)并没有对应的FCB(文件控制块)。管道是一种特殊的文件类型,用于实现进程间通信。它通过在内核中创建一个缓冲区,将一个进程的输出直接连接到另一个进程的输入,从而实现两个进程之间的数据传输。

管道文件属于进程通信模块,不属于文件系统。文件系统主要负责文件的存储、检索和管理,进程通信模块负责处理管道文件的创建、读写、关闭等操作。管道文件直接由操作系统管理,不需要FCB,不需要文件系统参与,操作系统会维护一些专门的数据结构来管理管道文件的读取和写入操作,如文件描述符、读写位置等。

与普通文件不同,匿名管道文件没有对应的磁盘上的存储空间,而是存在于内存中。因此,管道的创建和销毁都是在内核中进行的,并不需要对应的FCB。

值得注意的是,虽然管道文件没有FCB,但操作系统在内核中可能会为管道维护一些其他相关信息,例如读取和写入的位置等。但这些信息并不被称为FCB,而是保存在其他数据结构中(文件描述符表,文件表)

2.创建管道时候生成的管道数据结构是什么?

创建管道时会在内核中一个管道数据结构,它通常使用一个特殊的结构体 pipe_inode_info 来表示。(也就是struct_file)

pipe_inode_info 结构体包含了以下关键属性:

  1. 读取端(Reader End):记录了读取端相关的信息,如读取偏移量、等待读取的进程列表等。

  2. 写入端(Writer End):记录了写入端相关的信息,如写入偏移量、等待写入的进程列表等。

  3. 缓冲区(Buffer):用于存放从写入端到读取端传输的数据的中间缓冲区。通常是一个环形缓冲区,数据从写入端进入缓冲区,然后通过读取端被取出。

  4. 管道状态(Pipe Status):记录了管道的状态,如是否为阻塞模式、是否已关闭等。

这个管道数据结构是在内核中使用的,用户程序无法直接访问。用户程序通过使用管道的文件描述符,从而与管道数据结构进行交互和进行进程间通信。当一个进程向管道写入数据时,数据被写入到管道的缓冲区;而另一个进程从管道读取数据时,数据则从缓冲区读取。这个缓冲区一个特殊的环形缓冲区。

进程创建匿名管道,会把读写端口分开成两个文件描述符,实现读写分离。

3.管道文件会在进程的页表中形成嘛,这个过程的细节?

管道文件在父进程的页表中并没有直接的映射,因为它不是存储在进程的虚拟地址空间中的,而是位于内核地址空间。相反,通过文件描述符(file struct)和内核提供的系统调用(如read、write)、接口来进行对管道文件的访问和操作。

这与一般的文件不同,对于普通的文件,当进程打开文件时,操作系统会将文件映射到进程的虚拟地址空间中,以便进程可以通过虚拟地址来访问文件数据。这个过程涉及到页表(page table)的更新,。当一个进程向管道写入数据时,数据会被暂存在内核缓冲区中,然后从缓冲区传递给等待读取的进程。读写操作不是直接在虚拟地址空间中进行的,而是在内核缓冲区中进行的

4.两个文件描述符映射的是同一个struct file嘛?

文件描述符中有一个fd_type成员,指示了文件描述符的类型。

一个管道文件的两个文件描述符会映射到同一个 struct file 实例。

当一个进程通过调用 pipe() 系统调用创建一个管道时,会返回两个文件描述符,一个用于读取数据,一个用于写入数据。

这两个文件描述符共享同一个 struct file 数据结构,而这个数据结构表示整个管道。这意味着对于同一个管道,一个进程使用一个文件描述符写入数据到管道,另一个进程使用另一个文件描述符从管道读取数据。

5.进程怎么区分两个描述符的?他们中的struct file是相同的不是吗?

当一个进程通过 pipe 系统调用创建一个管道时,它会收到两个文件符,通常称为 pipefd[0]pipefd[1]。这两个文件描述符分别对应于管道的读端和写端。进程可以通过这两个文件描述符来区分管道的两个方向,进行相应的读写操作。

6.读写操作是怎么进行的呢?

管道映射的两个文件描述符分别对应着管道读端和写端,可以通过不同的偏移量(属于文件描述符的属性而非struct_file的属性)来区分。由于管道缓冲区是环形的,因此可以通过偏移量来区分读端和写端。写端偏移量从缓冲区起始地址开始,而读端偏移量从缓冲区末尾地址开始。

最开始,两个指针地址相差一个单位(不一定是一个字节),这时候可以开始写操作,写指针向前运动。写入完毕后,如果进行读操作,读指针会向前运动进行读取,直到读取到写指针的位置的前一位。这时候两个指针地址还是相差一个单位,准备接受新一轮的读写。形成了一个闭环。

7.如果读端正常读取,写段却提前关闭了
  1. 读取操作返回0:当写入端关闭时,读取操作将读取到缓冲区中的已存在的数据,并当没有更多数据可供读取时,返回0。这表示已经读取到了管道的末尾。

  2. 后续的读取操作将返回0:一旦读取端读取了管道中的所有数据,后续的读取操作将返回0,表示已经读取到了管道的末尾。

  3. 读取端的进程可以继续执行:读取端的进程可以继续执行其余的操作,而不会受到写入端的关闭影响。

8.如果写端正常写入,读端却提前关闭了

操作系统通过信号杀死这个写入的进程

  1. 读取端读取操作返回0:当读取端关闭时,读取操作将返回0,表示已到达文件结束或管道结束。写入端仍然可以继续写入数据,但没有读取端可以接收它们。

  2. 未读取的数据被丢弃:操作系统会丢弃写入端已经写入但尚未被读取端读取的数据。没有读取端时,写入端的数据将无法传递给任何进程使用,并且会被废弃。

  3. 管道文件描述符关闭:当读取端关闭时,操作系统将关闭管道的文件描述符。这意味着无法再使用这些文件描述符进行读取或写入操作。

  4. 管道的资源被释放:一旦读取端关闭,操作系统会释放与管道相关的资源,包括内存和其他相关的系统资源。

9.环形缓存中 的一些问题:

原本没有数据的读取会发生什么?

原本没有数据的读取,读指针检测到下一位就是写指针,发生读取堵塞。直到写入了数据。

一部分数据的读取会发生什么?

一部分数据的读取,读指针向前运动正常读取,直到将缓存全部读取完毕(读取到写指针的位置的前一位)。读取到的数据放到用户定义的缓冲区域中。一般是数组buffer。

原本有数据的写入会发生什么?

如果管道中已经有数据,写指针会尝试将数据写入到缓冲区中。如果缓冲区已满,这时候会发生写入阻塞,直到有足够的空间可供写入。因此,如果没有读取操作,写入操作将无法继续,直到有其他进程准备好读取管道中的数据。

环形的缓存区可不可能发生覆盖问题

对于匿名管道,它们通常具有固定大小的缓冲区,因此管道空间不会扩充。写满后会进入阻塞状态。

进程可以通过以下几种方式解决阻塞问题:

  1. 等待一段时间后再次尝试写入数据。在阻塞期间,进程可以等待一段时间,例如几秒钟,然后再次尝试写入数据。

  2. 使用其他机制来分批次写入数据。如果进程需要频繁地与管道进行通信,可以使用其他机制来分批次写入数据,例如将数据分为多个批次写入,或者使用循环写入的策略。

  3. 使用临时文件作为缓冲区。如果进程需要频繁地与管道进行通信,并且管道空间有限,可以考虑使用临时文件作为缓冲区。进程可以将数据写入临时文件中,然后在管道中有可用空间时再将其写入管道中。

有的操作系统会扩充

当写指针在写入时候转了一圈遇到了读指针(这里的缓存区是内核中的缓存区),这时候会重新分配一个更大的空间,发生拷贝,将两个指针放到合适的位置。(拷贝:两个指针在同一起点,运动其中一个指针遍历整个缓冲区,将整个缓冲区复制到更大的缓冲区,同时在新的缓冲区中建立两个指针变量,指向起始位置,其中一个指针随着数据的拷贝在运动)

匿名管道的创建和使用

在实验一中介绍

特点(匿名管道)
  1. 亲缘关系通信:匿名管道通常用于具有亲缘关系的进程之间,如父子进程。命名管道(也称为FIFO)可以用于没有亲缘关系的进程之间,但它们必须以某种方式知道对方的管道名。

  2. 数据流管道是半双工的,管道是一种单向通信机制,数据只能从一个进程流向另一个进程。为了实现双向通信,可以创建两个管道,一个用于读,一个用于写。一旦确定方向就不能改变方向。

  3. 创建和删除:管道在创建时需要指定是创建匿名管道还是命名管道。匿名管道在进程终止时会自动删除,而管道则会一直存在,直到显式删除。

  4. 阻塞操作:进程可以在管道文件上进行阻塞操作。如果读取进程没有可用数据,它会阻塞直到有数据可读;如果写入进程的管道已满,它会阻塞直到有空间可写。

  5. 同步和互斥管道是不安全的:如果多个进程同时读写管道,会导致数据竞争和不确定的结果。因此,在多进程场景中,需要适当的同步机制(如互斥锁、信号量)来保证数据的一致性和正确性。

  6. 限制:管道的大小有限(我的系统大小是1MB),因此传送大数据量时可能会受到限制。此外,管道通信不支持复杂的通信协议,如错误检查和流控制。可以使用操作系统提供的sysctl变量(例如/proc/sys/fs/pipe-max-size)来查看或修改管道缓冲区的大小。

    [root@MYCAT fs]# cat pipe-max-size 
    1048576
    ​
  7. 管道是基于字节流的,没有边界,因此需要适当的协议来解析和处理数据。

  8. 后边的几点在后续学习中会理解。

实验 (匿名管道)
1.创建匿名管道

展示了如何在父子进程中使用管道文件描述符:

#include 
#include 
#include 
#include 
#include 
​
int main() {
    int pipefd[2];
    //创建匿名管道
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    if (pid == 0) {
        // 在子进程中写入数据到管道
        close(pipefd[0]); // 关闭读取描述符
        
        char message[] = "Hello, parent!";
        write(pipefd[1], message, strlen(message));
        
        close(pipefd[1]); // 关闭写入描述符
        
        exit(EXIT_SUCCESS);
    } else {
        // 在父进程中读取管道中的数据
        close(pipefd[1]); // 关闭写入描述符
        
        char buffer[20];
        read(pipefd[0], buffer, sizeof(buffer) - 1);
        buffer[sizeof(buffer)-1] = '\0';
        
        close(pipefd[0]); // 关闭读取描述符
        
        printf("Message from child: %s\n", buffer);
        
        int status;
        waitpid(pid, &status, 0);
        
        exit(EXIT_SUCCESS);
    }
}
​

输出:

[lzh@MYCAT pipe]$ ./myexe 
Message from child: Hello, parent!
​

2.管道缓冲区为空读取,读取阻塞
#include 
#include 
#include 
#include 
#include 
​
int main() {
    int pipefd[2];
    
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    if (pid == 0) {
        // 在子进程中写入数据到管道
        close(pipefd[0]); // 关闭读取描述符
        int cnt =0;
        //多次写入
        while (cnt!=500000)
        {
        char message[] = "Hello, parent!";
        write(pipefd[1], message, strlen(message));
                sleep(10);
        }
        
        close(pipefd[1]); // 关闭写入描述符
        
        exit(EXIT_SUCCESS);
    } else {
        // 在父进程中读取管道中的数据
        int cnt =50;
        close(pipefd[1]); // 关闭写入描述符
        while(cnt--)
        {
         char buffer[1024];
        read(pipefd[0], buffer, sizeof(buffer) - 1);
        buffer[sizeof(buffer)-1] = '\0';
        printf("Message from child: %s\n", buffer);
                printf("%d\n",cnt);
        }
​
                close(pipefd[0]); // 关闭读取描述符
        int status;
        waitpid(pid, &status, 0);
        
        exit(EXIT_SUCCESS);
    }
}
​

输出

[lzh@MYCAT pipe]$ ./myexe 
Message from child: Hello, parent!
49
Message from child: Hello, parent!
48
Message from child: Hello, parent!
47
Message from child: Hello, parent!
46
Message from child: Hello, parent!
45
Message from child: Hello, parent!
44
Message from child: Hello, parent!
43
Message from child: Hello, parent!
42
Message from child: Hello, parent!
41
Message from child: Hello, parent!
40
Message from child: Hello, parent!
39

只要有了数据就会马上读取,没有数据发生读取阻塞。

3.管道缓冲区是有大小的,写入阻塞:
#include 
#include 
#include 
#include 
#include 
​
int main() {
    int pipefd[2];
    
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    if (pid == 0) {
        // 在子进程中写入数据到管道
        close(pipefd[0]); // 关闭读取描述符
        int cnt =0;
        //多次写入
        while (cnt!=500000)
        {
            cnt++;
        char message[] = "Hello, parent!";
        printf("%d\n",cnt);
        write(pipefd[1], message, strlen(message));
        }
        
        close(pipefd[1]); // 关闭写入描述符
        
        exit(EXIT_SUCCESS);
    } else {
        // 在父进程中读取管道中的数据
        int cnt =50;
        close(pipefd[1]); // 关闭写入描述符
        while(cnt--)
        {
         char buffer[1024];
        read(pipefd[0], buffer, sizeof(buffer) - 1);
        buffer[sizeof(buffer)-1] = '\0';
        printf("Message from child: %s\n", buffer);
        sleep(10);
        }
​
                close(pipefd[0]); // 关闭读取描述符
        int status;
        waitpid(pid, &status, 0);
        
        exit(EXIT_SUCCESS);
    }
}

输出

[lzh@MYCAT pipe]$ ./myexe 
1
2
3
4
5
6
7
........
........
46
47
Message from child: Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!
48
49
.......
.......
4702
4703
4704
///在这里卡了将近十秒钟
Message from child: Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!Hello, parent!H
​
4.如果写端正常写入,读端却提前关闭了
#include 
#include 
#include 
#include 
#include 
​
int main() {
    int pipefd[2];
    
    if (pipe(pipefd) == -1) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    
    pid_t pid = fork();
    
    if (pid == -1) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    
    if (pid == 0) {
        // 在子进程中写入数据到管道
        close(pipefd[0]); // 关闭读取描述符
        int cnt =0;
        //多次写入
        while (cnt!=500000)
        {
        char message[] = "Hello, parent!";
        write(pipefd[1], message, strlen(message));
        cnt++;
        printf("child write: %d\n",cnt);
        sleep(1);
​
        }
        
        close(pipefd[1]); // 关闭写入描述符
        
        exit(EXIT_SUCCESS);
    } else {
        // 在父进程中读取管道中的数据
​
        int cnt =5;
        close(pipefd[1]); // 关闭写入描述符
        while(cnt--)
        {
         char buffer[1024];
        read(pipefd[0], buffer, sizeof(buffer) - 1);
        buffer[sizeof(buffer)-1] = '\0';
        printf("Message from child: %s\n", buffer);
            sleep(1);
        }
                int status;
            close(pipefd[0]); // 关闭读取描述符
            printf("father close read,waite 5 seconds\n");
            sleep(5);
        pid_t ret = waitpid(pid,&status,0);
           if(ret==pid){
            printf("father kill child,exit code:%d, exit signal:%d\n"
            ,status&(0xff),status&(0x7f));}
            
​
        
        exit(EXIT_SUCCESS);
    }
}
​

输出

[lzh@MYCAT pipe]$ ./myexe 
child write: 1
Message from child: Hello, parent!
child write: 2
Message from child: Hello, parent!
child write: 3
Message from child: Hello, parent!
child write: 4
Message from child: Hello, parent!
child write: 5
Message from child: Hello, parent!
child write: 6
father close read,waite 5 seconds
father kill child,exit code:13, exit signal:13
[lzh@MYCAT pipe]$ kill -l | grep "13)"
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
​

发现:

子进程是被信号杀死的,是操作系统杀死了它。

命名管道

原理(命名管道)

命名管道(Named Pipe)是一种特殊类型的管道,用于进程间通信。与匿名管道不同,命名管道在文件系统中有一个唯一的名称,适用于不共享亲缘关系的进程之间的通信。有了命名管道,进程可以通过共享一个指定的名称进行通信,而不需要直接使用网络套接字或其他复杂的通信机制。

命名管道在Unix和类Unix系统中作为文件存在于文件系统中,而在Windows中,它们作为对象存在于命名管道命名空间中。

命名管道的原理可以总结如下:

  1. 创建命名管道:首先,要创建一个命名管道,应用程序需要调用系统调用或API来创建一个具有唯一名称的FIFO文件(在Unix中)或命名管道对象(在Windows中)。)(路径具有唯一性

  2. 打开管道:一旦创建了命名管道,应用程序就可以使用系统调用或API打开管道并获取一个文件描述符(在Unix中)或一个句柄(在Windows中)。

  3. 读/写数据:通过文件描述符或句柄,应用程序可以使用类似读取和写入文件的操作来从管道中读取数据或向管道中写入数据。读取和写入的操作可以是阻塞的或非阻塞的,取决于应用程序的设计。

  4. 进程间通信:通过共享管道的名称,不同的进程可以打开同一个命名管道,并通过读取和写入数据来进行进程间通信。数据会从一个进程的写入端流到另一个进程的读取端。

  5. 关闭管道:当应用程序完成对命名管道的使用时,应该调用适当的系统调用或API来关闭文件描述符或句柄,以释放相关资源。同时,对于FIFO文件,还应该删除该文件。

FIFO命名管道文件:
  1. 唯一名称:FIFO命名管道文件在文件系统中有一个唯一的名称。它可以通过文件路径来标识,通常以一个特定的文件名出现在文件系统的某个位置。

  2. 半双工通信:FIFO文件是一种半双工通信方式,只允许单向数据流。因此,要实现双向通信,需要创建两个FIFO文件,每个文件在不同的方向上进行数据传输。

  3. 阻塞与非阻塞操作:在对FIFO文件进行读取和写入操作时,可以选择是阻塞操作还是非阻塞操作。阻塞操作会使进程在没有数据可读或没有空间可写入时暂停等待,而非阻塞操作会立即返回,并返回适当的错误码。

  4. 读写规则:FIFO文件的读写操作有一些特定的规则。当一个进程打开一个FIFO文件进行读取时,它会等待另一个进程打开同一FIFO文件进行写入。当一个进程写入FIFO文件时,它会等待另一个进程打开同一FIFO文件进行读取。这确保了在读取和写入之间的同步。

原理(不同进程看到同一份资源)

两者看到的是相同的文件,不同进程打开同一个文件,操作系统只会打开同一个文件。它们所获得的文件描述符指向的是同一个文件。

在匿名管道中,磁盘中不会生成文件,匿名管道文件只会在内存中存在,随着进程的销毁而销毁,没有FCB,没有inode,在文件描述符中指示了他的类型是匿名管道,将他的读写端口分成两个fd。

同样的,在命名管道中,操作系统会在磁盘上创建这个特殊的文件(为了实现持久化),但是不会创建FCB(不涉及文件管理,这个文件由操作系统管理),会创建出文件描述符,然后直接通过文件描述符(通过特殊读写机制(锁)实现了他的读写端口)进行读写操作(在内核缓冲区中)。直接使用read write等系统调用接口来管理。

注意:这里创建或者打开文件,但是不会创建页表项,这点和匿名管道是相同的。因为命名管道的数据传输是在内核缓冲区中进行的,并不涉及虚拟地址空间和内存的映射。

问题
1.如果两个进程同时打开一个普通文件,对其进行写入会发生什么

当两个进程同时打开一个普通文件并对其进行写入操作时,会发生以下情况之一:

  1. 写冲突:如果两个进程同时试图向文件中的相同位置写入数据,那么可能会发生数据的覆盖和混乱。结果取决于操作系统和文件系统的实现方式,可能会导致数据的丢失或不一致。

  2. 同步写入:某些操作系统和文件系统可能会提供同步写入的机制,确保多个进程按顺序写入数据。这意味着第一个进程完成写入操作后,第二个进程才能开始写入。通过这种机制,可以避免数据的覆盖和混乱,但会导致效率低下,因为进程需要等待其他进程完成写入操作。

  3. 并发写入:某些操作系统和文件系统允许多个进程同时写入文件,但在内部会使用锁机制来处理冲突。锁机制确保每个进程只能同时访问文件的某个部分,从而避免数据的冲突和损坏。这种并发写入可能会提高效率,但需要操作系统和文件系统提供相应的支持。

综上所述,对于普通文件的并发写入,具体的行为取决于操作系统和文件系统的实现方式。在多个进程同时写入同一个文件时,可能会发生数据冲突、同步写入或并发写入等情况。为避免数据冲突,可以使用锁机制或其他同步机制来协调进程间的写入操作。

2.管道文件的文件描述符中实现了特殊的锁机制,保证写入和读取不会冲突?

管道文件的文件描述符中通常会实现一种特殊的锁机制,以避免写入和读取的冲突。这种机制通常被称为互斥锁或文件锁,它允许一个进程在写入管道文件时阻止其他进程同时读取或写入该文件,从而确保数据的一致性和完整性。

当一个进程向管道写入数据时,它会将数据写入到管道文件的写入端,并将数据保留在文件的缓冲区中。此时,如果另一个进程尝试从管道文件的读取端读取数据,它会阻塞,直到写入进程释放了对管道文件的锁定。

这样,多个进程可以同时打开同一个管道文件进行读写操作,但是它们之间的访问是互斥的,即一个进程在写入数据时,其他进程必须等待直到写入进程释放了对管道文件的锁定。这种机制可以确保数据在多个进程之间的正确传输和同步,避免数据冲突和竞争条件的发生。

3.命名管道文件的缓冲区

命名管道的通信机制在底层是通过操作系统的文件系统来实现的。当一个进程向命名管道写入数据时,数据首先被写入到操作系统的内核缓冲区(而不是内存缓冲区中)中,然后根据管道的状态和另一个进程的读取请求,数据会被从内核缓冲区传输到另一个进程的缓冲区中。注意不会再磁盘中写,因为没有FCB。

读取进程的工作方式类似,它通过文件描述符向操作系统请求数据。操作系统从管道的读取端(即另一个进程写入数据的端口)读取数据,并将其放入内核缓冲区。然后,数据会被传输到读取进程的缓冲区中,供进程使用。

这个过程是同步的,也就是说,当一个进程正在写入数据时,其他进程必须等待,直到写入操作完成(通过锁机制实现)。同样,当一个进程正在读取数据时,其他进程必须等待,直到读取操作完成。这种同步机制确保了数据的完整性和一致性。

4.命名管道也有缓冲区,他和匿名管道一样吗?

是的,命名管道也有自己的缓冲区,也是环形的。

5.既然如此,他的读写的阻塞规则和匿名管道一样吗?

是的,特点与她一样,你可以看看匿名管道中的“问题模块”。

命名管道的创建和使用

我们以在命令行中创建文件为例:

命名管道的创建

可以使用 mknod 命令或者mkfifo命令创建一个命名管道。例如:

mknod mypipe p
或者
mkfifo mypipe

这将在当前目录下创建一个名为 mypipe 的命名管道。

命名管道的使用

要使用命名管道,必须在管道两端的进程之间进行适当的安排。以下是一个简单的使用示例:

# 假设我们有两个进程,一个生产者(producer)和一个消费者(consumer)
​
# 生产者进程
echo "Hello, World!" > mypipe
​
# 消费者进程
cat mypipe

在这段代码中,生产者进程将文本 Hello, World! 写入命名管道 mypipe,而消费者进程从同一管道中读取该文本。

命名管道的删除

当不再需要命名管道时,可以使用 unlink 命令删除它:

unlink mypipe
注意事项
  • 命名管道的路径必须是绝对路径,否则可能会导致进程无法找到对方。

  • 命名管道的使用受限于进程之间的文件系统访问权限。

  • 如果在使用命名管道的过程中,任何一方进程终止,另一方可能会遇到问题,除非有适当的错误处理机制。

特点(命名管道)
  • 持久性:命名管道在创建后将持续存在,直到进程使用 unlink 删除它。

  • 跨进程通信:命名管道允许不同进程间的通信,只要它们能够访问同一个文件系统路径。

  • 同步机制:命名管道提供了基本的同步机制,确保数据在管道中的正确传递。

  • 文本和二进制数据:命名管道可以传输文本数据和二进制数据。

  • 缓冲区大小固定:命名管道中的缓冲区大小是固定的,无法动态调整。这意味着管道中的数据量是有限的。如果数据量超过了缓冲区大小,数据将会丢失或被丢弃。

实验(命名管道)
1.使用命名管道文件在两个进程间实现通信
[lzh@MYCAT pipe]$ cat named\ pipe.cpp 
#include 
#include 
#include 
#include 
#include 
#include 
​
int main() {
    const char* fifoFile = "myfifo"; // 定义命名管道文件的名称
​
    // 创建命名管道文件
    mkfifo(fifoFile, 0666);
​
   int fd = open(fifoFile, O_WRONLY); // 打开命名管道文件
​
    // 写入数据到命名管道文件
    const char* data = "Hello, named pipe!";
    write(fd, data, strlen(data) + 1);
​
    return 0;
}
​
​
​
​
[lzh@MYCAT pipe]$ cat named\ pipe1.cpp 
#include 
#include 
#include 
#include 
#include 
#include 
​
int main() {
    const char* fifoFile = "myfifo"; // 定义命名管道文件的名称
​
    int fd = open(fifoFile, O_RDONLY); // 打开命名管道文件
​
    char buffer[25];
​
    // 从命名管道文件中读取数据
    read(fd, buffer, sizeof(buffer));
​
    std::cout <<  buffer << std::endl;
​
​
    close(fd); // 关闭命名管道文件
​
    // 删除命名管道文件
    // unlink(fifoFile);
​
    return 0;
}
​
​
[lzh@MYCAT pipe]$ g++ named\ pipe.cpp -o myexewrite
[lzh@MYCAT pipe]$ g++ named\ pipe1.cpp -o myexeread
​
    //接下来是分开的终端窗口
    window1
[lzh@MYCAT pipe]$ ./myexewrite
    
    //发现页面卡住,发生阻塞。
    
    window2
[lzh@MYCAT pipe]$ ./myexeread
    //此时window1突然输出语句
[lzh@MYCAT pipe]$ ./myexewrite
Read from named pipe: Hello, named pipe!
​
​

我们首先定义了一个命名管道文件的名称("myfifo")。然后使用mkfifo函数创建命名管道文件。接下来,使用open函数打开命名管道文件,并获得文件描述符。我们将一段数据写入命名管道文件,然后使用read函数从命名管道文件中读取数据到缓冲区。最后,通过close函数关闭文件描述符,并使用unlink函数删除命名管道文件。

我们可以创建管道文件的类,在析构函数中Unlink掉它,这样可以实现更简便的操作(直接在进程中创建一个类就行,不用管理自动析构)。

2.读写的阻塞?细节。

mywrite.cpp

#include 
#include 
#include 
#include 
#include 
#include 
​
int main() {
    const char* fifoFile = "myfifo"; // 定义命名管道文件的名称
​
    // 创建命名管道文件
    // mkfifo(fifoFile, 0666);
​
   int fd = open(fifoFile, O_WRONLY); // 打开命名管道文件
    std::cout<< "write open file"<

myread.cpp

#include 
#include 
#include 
#include 
#include 
#include 
​
int main()
{
    const char *fifoFile = "myfifo"; // 定义命名管道文件的名称
​
    int fd = open(fifoFile, O_RDONLY); // 打开命名管道文件
    std::cout<< "read open file"<

以下按照时间顺序进行

窗口一(read) 窗口二(write)
[lzh@MYCAT pipe]$ ./myread read open file
(阻塞) [lzh@MYCAT pipe]$ ./mywrite write open file
Hello, named pipe!
[lzh@MYCAT pipe]$ ./mywritewrite open file
[lzh@MYCAT pipe]$ ./mywritewrite open file
[lzh@MYCAT pipe]$ ./mywritewrite open file
[lzh@MYCAT pipe]$ ./myread read open file Hello, named pipe!
[lzh@MYCAT pipe]$ ./myread read open file named pipe!
[lzh@MYCAT pipe]$ ./myread read open file pipe!
[lzh@MYCAT pipe]$ ./myread read open file
(阻塞) [lzh@MYCAT pipe]$ ./mywrite write open file
Hello, named pipe!
//原因:?

这是因为你的read中的缓冲区大小问题,’Hello, named pipe!‘ 的长度是19,你每次读取25个字节,向缓冲区写了3遍’Hello, named pipe!‘ .

但是:write默认情况下不是覆盖写文件嘛?这里就体现出了文件的特殊性质,这里的写是向文件内核缓冲区写的,指针一直在前进。

read指针没有运动,所以在读取 时候,每次读取25个字符。第一次读到buffer中的其实是

’Hello, named pipe!\0Hello,'

第二次读的是

' named pipe!\0Hello, named'

第三次

‘ pipe!\0’

所以第二次第三次前都有一个空格。

共享内存

命令

在Linux中,共享内存相关的命令包括:

  1. ipcs:用于列出系统当前的共享内存、消息队列及信号量的状态。

  2. ipcrm:用于删除系统中的共享内存、消息队列及信号量。

  3. shmget:用于创建共享内存段。

  4. shmat:将共享内存段连接到当前进程的地址空间。

  5. shmdt:断开当前进程与共享内存段的连接。

  6. shmctl:用于对共享内存段进行控制操作,如获取、设置或删除共享内存段的权限和属性。

  7. ipcs -m:**与ipcs命令一起使用,仅列出共享内存的状态信息。

  8. ipcrm -m:删除指定的共享内存,注意-m后要加shmid而不是key。

这些命令可以帮助创建、连接、控制和删除共享内存段,以及获取共享内存的状态信息。

ipcs -m 中的几个名词
[root@MYCAT ~]# ipcs  -m
​
------ Shared Memory Segments --------
key      shmid   owner   perms    bytes    nattch   status     
0x00000000 2      gdm     777     16384      1       dest     
0x00000000 5      gdm     777     14417920   2       dest   

这些值的含义?

  • key:共享内存的键值,用于唯一标识共享内存段。

  • shmid:共享内存的标识符,由shmget函数返回。它是共享内存段的一个唯一标识。

  • owner:共享内存段的所有者,即创建共享内存段的进程的用户ID。

  • perms:共享内存段的权限,用八进制表示。它定义了共享内存段的访问权限,包括读、写和执行的权限。

  • bytes:共享内存段的大小,以字节为单位。

  • nattch当前挂接(或连接)到共享内存段的进程数。

  • status:共享内存段的状态信息,包括一些附加的标志和元数据。

这些术语用于描述和了解共享内存段的基本信息,可以通过使用shmctl函数中的IPC_STAT命令来获取这些信息。使用shmctl函数的IPC_STAT命令,将共享内存的状态信息填充到一个struct shmid_ds结构体中,然后可以通过访问结构体成员来获取这些信息。

函数

当涉及到共享内存的操作,还有一些其他的函数可用:

  1. shmget():用于获取一个共享内存段的标识符。如果共享内存段不存在,则创建一个新的共享内存段。

  2. shmat():将指定的共享内存段连接到当前进程的地址空间中,并返回该共享内存段的内存地址。

  3. shmdt():将指定的共享内存段与当前进程断开连接。

  4. shmctl():用于对共享内存段进行控制操作,如获取、修改或删除共享内存段。

  5. semget():用于获取一个信号量的标识符。如果信号量不存在,则创建一个新的信号量。

  6. semop():对指定的信号量进行操作,如锁定、解锁或修改信号量的值。

  7. semctl():用于对信号量进行控制操作,如获取、修改或删除信号量。

  8. mmap():用于将文件或设备映射到进程的地址空间中,包括共享内存段。

  9. shm_open():用于打开或创建一个共享内存对象。

  10. shm_map():将共享内存段的内存映射到当前进程的地址空间中。

  11. shm_unlink():用于删除指定的共享内存对象。

这些函数一起提供了在Linux中进行共享内存操作所需的完整功能。

shmget()

在Linux中,shmget函数是用于创建或打开共享内存段的系统调用函数。

函数原型:
#include 
#include 
​
int shmget(key_t key, size_t size, int shmflg);
参数:
  • key:共享内存的键值,是一个整数值,用于标识共享内存段。可以使用ftok函数生成键值,也可以使用事先约定的键值。

  • size:需要创建的共享内存段的大小,以字节为单位。

  • shmflg:用于指定共享内存段的权限和状态标志,可以使用IPC_CREAT与权限值的或操作,表示创建共享内存段并设置权限。还可以使用其他标志,如IPC_EXCL表示如果已存在相同的键值则创建失败,IPC_NOWAIT表示以非阻塞方式创建。

返回值:

shmget函数的返回值为共享内存段的标识符(shmid),如果创建或打开失败,则返回-1,并设置相应的错误码。

key参数:

在Linux中的shmget函数中,key参数是共享内存的键值。它是一个整数值,用于唯一标识共享内存段。多个进程可以使用相同的键值来访问同一个共享内存段。

1.ftok函数

使用不同的key值,可以创建或访问不同的共享内存段。常见的获取key值的方法是使用ftok函数,其原型如下:

#include 
#include 
​
key_t ftok(const char *pathname, int proj_id);

ftok函数根据文件路径名和项目标识生成一个key值。路径名用于确定文件,项目标识用于区分不同的共享内存段。通常,多个进程通过使用相同的路径名和项目标识来获得相同的key值,从而可以访问到同一个共享内存段。

注意,key值有一定的限制,在32位系统中通常为32位,而在64位系统中通常为64位。因此,key值的选择应该在范围内合理,避免冲突。

除了使用ftok函数生成key值外,也可以直接使用预定义的key值或通过其他方式协商得到。在多进程间进行共享内存通信时,确保使用相同的key值非常重要。

2.IPC_PRIVATE宏

在Linux中,IPC_PRIVATE是一个宏定义,用于生成私有的键值(key)用于创建或访问IPC资源,如共享内存、消息队列和信号量等。

IPC_PRIVATE宏在头文件中定义如下:

#define IPC_PRIVATE ((key_t) 0)  //私有键值

IPC_PRIVATE宏的值为0。使用IPC_PRIVATE作为键值时,shmgetmsggetsemget等函数将会创建一个新的标识符,用于唯一标识相应的IPC资源,并将它们设为私有。这意味着只有创建该IPC资源的进程可以访问和操作该资源,其他进程无法直接访问。

需要注意的是,使用IPC_PRIVATE创建的IPC资源,像共享内存段、消息队列或信号量,在不同的进程之间是隔离和独立的。即使两个进程都使用相同的IPC_PRIVATE值调用相同的创建函数,它们也会分别创建各自的私有资源,并且这些资源是互相独立的。

  1. 使用getpid函数:

对于某些情况,可以使用当前进程的进程ID(PID)作为键值的一部分。这通常用于创建与当前进程相关的IP资源。

key_t key = getpid(); // 使用当前进程ID作为键值的一部分

shmflg参数:

在Linux的shmget函数中,shmflg参数是用于指定共享内存段的权限和状态标志的参数。

shmflg参数可以是以下几种组合方式的标志之一或多个:

  • IPC_CREAT如果指定的共享内存不存在,则创建一个新的共享内存段。如果共享内存已存在,则改变访问权限为指定的权限。如果省略了此标志,则表示仅打开现有的共享内存段。

  • IPC_EXCL:与IPC_CREAT标志一起使用(不单独使用),用于保证只有一个进程能够创建共享内存段,如果指定的key已经存在,则返回错误。

  • IPC_NOWAIT:指定以非阻塞方式运行,如果没有可用的共享内存段,则立即返回错误。

  • 权限标志:可以使用IPC_PRIVATE与权限值的或操作,表示使用私有的key值,并使用权限值对共享内存进行权限设置。

shmflg参数可以根据需要任意组合,使用IPC_CREAT | IPC_EXCL表示创建一个新的共享内存段并确保唯一性。

shmflg参数在创建或打开共享内存段时起关键作用,因此在使用时需仔细考虑各个标志位的组合,以确保正确的操作。

使用例子:
    int shmid;
    char *shmaddr;
​
    // 创建共享内存段
if ((shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666)) < 0) {
        perror("shmget");
        return 1;
    }

shmat()

在Linux中,shmat函数用于将共享内存映射到进程的地址空间,使得进程可以访问共享内存中的数据。

函数原型:
#include 
​
void *shmat(int shmid, const void *shmaddr, int shmflg);
参数:
  • shmid:共享内存标识符,由shmget函数返回。

  • shmaddr:可选参数,指向进程中的内存地址,用于指定共享内存映射。如果省略该参数或将其设为NULL(nullptr),则系统将选择一个合适的地址进行映射。

  • shmflg:可选参数,用于控制共享内存的访问权限和内存保护等。常见的包括:

  • SHM_R:读取共享内存的权限。

    • SHM_W:写入共享内存的权限。

    • SHM_X:执行共享内存的权限。

    • SHM_U:允许进程访问共享内存,即使该进程没有写入权限。

    • SHM_P:禁止其他进程访问共享内存,即使它们拥有相应的权限。

    • 0:默认原本的权限。

返回值:

shmat函数的返回值是一个指向共享内存中数据类型的指针,通过该指针可以访问共享内存中的数据。如果返回值为NULL,则表示映射失败,可能是因为共享内存已被删除或访问权限不足等原因。

需要注意的是,在使用完共享内存后,应使用shmdt函数将其从进程的地址空间中解除映射,以释放系统资源。同时,如果共享内存中的数据已被修改,则需要将其同步回原始存储介质,以数据的完整性和一致性。

使用例子:
    int shmid;
    char *shmaddr;
    // 创建共享内存段
    if ((shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666)) < 0) {
        perror("shmget");
        return 1;
    }
​
    // 将共享内存段连接到当前进程的地址空间
    shmaddr = (char *) shmat(shmid, NULL, 0);
    if (shmaddr == (char *) -1) {
        perror("shmat");
        return 1;
    }

shmdt()

在Linux中,shmdt函数是用于断开进程与共享内存段之间的连接的函数。它的作用是将共享内存从当前进程的地址空间中分离,使得进程无法再访问共享内存中的数据。

函数原型:
#include 
​
int shmdt(const void *shmaddr);
shmaddr参数:

shmaddr参数是一个共享内存段的映射地址,即之前使用shmat函数返回的指针。该参数指定要断开连接的共享内存段。

返回值:

shmdt函数将共享内存段从进程的地址空间中分离,并返回成功与否的状态。如果断开连接成功,则返回0;如果失败,则返回-1,并设置相应的错误码。

需要注意的是,断开共享内存段的连接并不会导致共享内存段本身被删除。共享内存段仍然存在于系统中,其他连接到该共享内存段的进程仍然可以访问和操作共享内存。

在使用完共享内存后,为了释放资源,确保调用shmdt函数断开与共享内存段的连接。同时,还需要注意及时删除共享内存段,使用shmctl函数进行清理和释放。

使用例子:
  int shmid;
    char *shmaddr;
​
    // 创建共享内存段
    if ((shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666)) < 0) {
        perror("shmget");
        return 1;
    }
​
    // 将共享内存段连接到当前进程的地址空间
    shmaddr = (char *) shmat(shmid, NULL, 0);
    if (shmaddr == (char *) -1) {
        perror("shmat");
        return 1;
    }
​
    // 在共享内存中写入数据
    *shmaddr = 'H';
    *(shmaddr + 1) = 'i';
​
    // 从共享内存中读取数据并输出
    printf("Shared memory content: %c%c\n", shmaddr[0], shmaddr[1]);
​
    // 断开共享内存连接并释放资源
    if (shmdt(shmaddr) == -1) {
        perror("shmdt");
        return 1;
    }
​

shmctl()

在Linux中,shmctl函数用于对共享内存段进行控制操作,比如获取/设置共享内存段的状态信息、修改共享内存段的权限、删除共享内存段等。

函数原型:
#include 
​
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
参数:
  • shmid:共享内存段标识符,由shmget函数返回。

  • cmd:控制命令,用于指定要执行的操作。常用的控制命令包括:

    • IPC_STAT:获取共享内存段的状态信息,该信息将被存储在buf参数指向的结构体中。

    • IPC_SET:修改共享内存段的权限和其他属性,修改的信息为buf参数指向的结构体中的内容。

    • IPC_RMID:删除共享内存段,释放系统资源。

  • buf:指向shmid_ds结构体的指针,用于存储获取到的状态信息或修改的属性信息。一般置为NULL(在删除共享空间时)

返回值:

shmctl函数的返回值取决于执行的控制命令。一般来说,如果操作成功,则返回0;如果发生错误,则返回-1,并设置相应的错误码。

需要注意的是,使用shmctl函数修改共享内存段的属性、权限等操作可能需要管理员权限。同时,删除共享内存段后,其他进程无法再访问此共享内存段。使用shmctl函数时要小心谨慎,确保操作的正确性和安全性。

cmd参数:

cmd参数用于指定要执行的操作类型。cmd的取值如下:

  • IPC_STAT获取共享内存的状态信息,将共享内存的信息存储在struct shmid_ds结构体中。通过该结构体可以获取共享内存的大小、创建时间、最后连接时间等信息。

  • IPC_SET:修改共享内存的属性,需要提供一个struct shmid_ds结构体的指针作为buf参数,通过修改该结构体的成员来改变共享内存的权限、最后连接时间等属性。

  • IPC_RMID:删除共享内存,将释放占用的系统资源,并使其他进程无法再访问该共享内存。

例如,可以使用以下代码删除一个共享内存段:

#include 
#include 
​
int main() {
    int shmid = shmget(key, size, IPC_CREAT | 0666);  // 假设已创建一个共享内存段
    if (shmid == -1) {
        perror("shmget");
        return 1;
    }
​
    if (shmctl(shmid, IPC_RMID, NULL) == -1) {
        perror("shmctl");
        return 1;
    }
​
    printf("Shared memory removed successfully.\n");
    return 0;
}

在上述示例中,首先通过shmget函数获取共享内存段的标识符,然后使用shmctl函数以IPC_RMID命令删除该共享内存。如果删除成功,将输出"Shared memory removed successfully."。 需要注意的是,删除共享内存只是断开了与该共享内存的连接,并释放了共享内存使用的系统资源,但实际的共享内存段可能在文件系统中保留。

buf参数:

buf参数是一个指向struct shmid_ds类型的指针,通过该指针传递共享内存段的信息和修改共享内存段属性的值。struct shmid_ds结构体定义如:

struct shmid_ds {
    uid_t shm_perm.uid;    // 所有者ID
    gid_t shm_perm.gid;    // 组ID
    mode_t shm_perm.mode;  // 访问权限
    int shm_perm.__key;    // IPC键值
    struct ipc_pid shm_perm.cuid;
    struct ipc_pid shm_perm.uid;
    unsigned short shm_perm.mode;
    unsigned short shm_perm._seq;
    time_t shm_ctime;      //创建时间
    time_t shm_atime;      //最后连接时间
    time_t shm_dtime;      //最后断开连接时间
    size_t shm_segsz;      //内存段大小
    pid_t shm_cpid; //创建进程号
    pid_t shm_lpid;        //最后连接进程号
    short shm_nattch;      //当前挂接进程数
    ...  // 其他成员
}

下面是使用shmctl函数获取共享内存状态信息的示例代码:

#include 
#include 
​
int main() {
    int shmid = shmget(key, size,\
     IPC_CREAT | 0666); // 假设已创建一个共享内存
    if (shmid == -1) {
        perror("shmget");
        return 1;
    }
​
    struct shmid_ds buf;
    if (shmctl(shmid, IPC_STAT, &buf) == -1) {
        perror("shmctl");
        return 1;
    }
​
    printf("Shared memory size: %lu bytes
", buf.shm_segsz);
    printf("Shared memory owner UID: %d
", buf.shm_perm.uid);
    printf("Shared memory owner GID: %d
", buf.shm_perm.gid);
​
    return 0;
}

在该示例代码中,使用shmctl函数获取共享内存段的状态信息,并将信息存储在struct shmid_ds结构体中。然后,通过结构体成员访问共享内的大小、所有者UID、所有者GID等信息。类似地,可以使用shmctl函数来修改共享内存段的属性信息。

使用例子:
    // 删除共享内存段
    if (shmctl(shmid, IPC_RMID, NULL) < 0) {
        perror("shmctl");
        return 1;
    }
​

是什么

共享内存是在多进程或多线程环境下,不同进程或线程之间共享相同的一段内存区域的技术。共享内存是一种高效的IPC(进程间通信)方式,因为它可以避免不必要的数据复制和序列化,从而提高通信效率。

在Linux中,可以使用shmget()shmat()shmdt()shmctl()等系统调用创建和管理共享内存。其中,shmget()用于创建共享内存对象并分配内存空间,shmat()用于将共享内存映射到进程的地址空间中,shmdt()用于将共享内存从进程的地址空间中解除映射,shmctl()用于对共享内存控制和管理。

原理(不同的进程看到同一份资源)

通俗来说:系统在物理内存中创建一片共享区域,这个区域(结构体)由系统维护,通过系统的接口,可以让进程获得这片区域的物理地址,从而在页表中形成映射。

共享内存实现的原理主要涉及以下几个方面:

  1. 内核操作:共享内存由内核维护和管理,它通过系统调用(例如shmget,shmat,shmdt,shmctl)来提供对共享内存的创建、映射、解除映射和控制等操作。

  2. 分配和映射内存:在创建共享内存时,内核会分配一块连续的内存空间,并为该内存块生成一个唯一的标识符(即共享内存id)。通过映射操作,将这块共享内存映射到进程的地址空间中,使得不同进程之间可以通过相同的地址来访问共享内存。

  3. 共享内存访问:不同进程可以通过相同的地址访问共享内存中的数据,因为它们映射的是同一块物理内存。读取和写入共享内存的操作是直接的,无需涉及数据的拷贝或序列化,因此具有高效的性能。

  4. 同步机制:由于共享内存可以被多个进程同时访问,因此需要使用适当的同步机制来保证数据的一致性和完整性。常用的同步机制包括信号量和互斥锁,用于防止数据竞争和访问冲突。

  5. 内存管理:共享内存的生命周期由创建它的进程控制。当所有使用共享内存的进程都解除了映射关系(使用shmdt)并且不再需要这块共享内存时,可以通过shmctl系统调用来删除共享内存。

总的来说,共享内存是一种多进程间共享内存区域的机制,通过内核提供的系统调用实现分配、映射和控制等操作。不同进程通过映射共享内存到自己的地址空间,实现共享数据的读写操作,并通过适当的同步机制保证数据的一致性。

问题:

1.注意点

使用共享内存需要注意以下几点:

  • 共享内存中的数据对所有共享该内存的进程都是可见的,因此需要采取同步措施来避免数据竞争和一致性问题。

  • 共享内存的大小在创建时需要进行配置,如果创建时未指定大小,系统将使用默认值。

  • 共享内存中数据的访问速度比其他IPC方式更快,因此适用于需要快速通信的场景。

  • 共享内存适用于多个进程之间需要频繁通信的场景,因为它避免了不必要的进程间通信的开销

2.共享内存读写两个指针,是不是也是环形缓冲区啊?

不一定

当设计共享内存缓冲区时,可以考虑以下几个方面:

  1. 缓冲区的结构:确定缓冲区的数据组织方式,可以是线性缓冲区、环形缓冲区或其他自定义结构。根据需求选择合适的数据结构,考虑到数据读写的方式和效率。

  2. 数据同步:多个进程或线程同时读写共享内存缓冲区时,需要有效地进行数据同步以避免数据冲突。可以使用互斥锁或其他同步机制来确保并发访问的正确性。

  3. 读写指针:在环形缓冲区中,可以使用读写指针来标记当前读取和写入的位置。需要确保正确地更新读写指针,并处理边界条件,以避免指针越界或数据覆盖问题。

  4. 缓冲区大小:确定缓冲区的大小,以容纳期望的数据量。根据应用程序的需求和性能要求,选择适当的缓冲区大小。

  5. 锁粒度:考虑并发访问时锁的粒度,即锁定整个缓冲区还是仅保护特定部分的读写操作。需要权衡锁的开销和并发性能,合理选择锁的粒度。

共享内存缓冲区的设计和实现是复杂的,需要仔细考虑并发访问和数据同步等问题。在使用共享内存缓冲区时,应该注意保证数据的一致性、避免死锁和数据竞争等并发问题,并进行充分的测试和验证。

3.共享内存访问好像没有锁机制啊,不会产生空读(读到空)的情况吗?

确实可能出现空读(读到空)的情况,因为其他进程可能已经更新了共享内存中的数据。

为了避免这种情况,使用锁机制来确保只有一个进程在共享内存中写入数据,其他进程在读取数据之前必须先获取锁,以确保读取到最新的数据。 常见的锁机制包括互斥锁和读写锁。(在以后学习中逐步介绍) 总之,共享内存访问需要使用适当的锁机制来确保数据的一致性和完整性,避免空读和其他数据竞争问题。

4.共享内存和管道的优势点结合起来不是更好吗?

是滴!!

共享内存和管道结合可以实现高效的并发处理和数据传输。共享内存可以提供快速的数据交换,而管道可以提供进程间通信的机制。

共享内存可以被多个进程访问,因此可以用来实现多个进程之间的数据交换。

管道可以被用来实现进程间通信,将数据从发送进程发送到接收进程。

将共享内存和管道结合使用,可以将数据快速地从发送进程传输到接收进程,从而实现高效的并发处理。

在实验二中我们模拟了一个实例

特点

主要特点如下:(可以结合原理和注意点来看待

  1. 不需要亲缘关系;

  2. 没有同步互斥之类的保护机制

  3. 易产生死锁:共享内存区允许多个进程同时访问,但是如果对内存区的访问控制没有做好同步,就可能会产生死锁问题。

  4. 速度最快:共享内存是最快的共享数据方法,在进程间复制数据时效率很高。

  5. 有效减少内存消耗:因为所有的内存管理都由操作系统内核负责,所以应用进程间无需再交换数据。

  6. 可直接访问:通过共享内存,进程可以直接访问其他进程的数据,无需通过交换区,这样可以提高数据访问效率。

实验

1.基础实验实现共享内存
comm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__
#include 
#include 
#include 
#include 
#include 
using namespace std;
const string pathname = "/home/lzh/tmp";
const int proj_id = 1;
// 大小最好分配4096整数倍
#define SHM_SIZE 4096
// 获得key
key_t Get_key()
{
    key_t k = ftok(pathname.c_str(), proj_id);
    if (k == -1)
    {
        perror("ftok");
        // 后续中可以把自己的log管理加进来
        exit(1);
    }
    return k;
}
// 创建共享内存段的接口
int Get_share_mem(int flag)
{
    key_t k = Get_key();
    int shmid = shmget(k, SHM_SIZE, flag);
    // int shmid = shmget(k,SHM_SIZE,IPC_CREAT|0666);
    if (shmid == -1)
    {
        perror("shmget");
        exit(2);
    }
    return shmid;
}
// 创建
int Creat_shm()
{
    return Get_share_mem(IPC_CREAT | IPC_EXCL | 0666);
}
// 获取
int Get_shm()
{
    return Get_share_mem(IPC_CREAT);
}
​
// 将共享内存段连接到当前进程的地址空间
char *Set_share_mem(int shmid)
{
    char *shmaddr;
    shmaddr = (char *)shmat(shmid, nullptr, 0);
    if (shmaddr == nullptr)
    {
        perror("shmat");
        exit(3);
    }
    cout<<"get shmaddr: " << shmaddr < 
  

pa.cpp
#include "comm.hpp"
#include
​
int main()
{
​
    int shmid = Creat_shm();
    char *shmaddr = Set_share_mem(shmid);
    int cnt=5;
    while (cnt--)
    {
        cout<<"pa:"< 
  

pb.cpp
#include "comm.hpp"
​
#include
#include
#include
int main()
{
    int shmid = Get_shm();
    char *shmaddr = Set_share_mem(shmid);
    int cnt=5;
    while (cnt--)
    {
        char buffer[1024];
        cout << "pb write:" ; fgets(buffer,sizeof(buffer),stdin); memcpy(shmaddr,buffer,strlen(buffer));
    }
    Disconnect_shm(shmid, shmaddr);
​
    return 0;
}

输出::

在A窗口中

[lzh@MYCAT shared_mem]$ ./pa
get shmaddr,shmid: 7
pa:
pa:12345
​
pa:123456789
​
pa:adafsdfdv1
​
pa:1234565555
​
disconnect shmid: 7
delete shmid: 7
​

在B窗口中:

[lzh@MYCAT shared_mem]$ ./pb
get shmaddr,shmid: 7
pb write:12345
pb write:123456789
pb write:adafsdfdv1
pb write:1234565555
pb write:你好
disconnect shmid: 7

注意:
  1. 分配内存大小最好分配4096整数倍。(好理解)

  2. pb直接用得到的共享区就可以啦

      while (cnt--)
        {
          cout << "pb write:" ;
     fgets(shmaddr,SHM_SIZE,stdin);
        }

2.共享内存和管道结合

当将共享内存与管道结合使用时,可以实现进程间的高效数据传输和并发处理。实现方式:利用管道的特点做了一个类似命令发送的过程,如果写端没有发送读取的命令,那么读取端就会一直阻塞下去。

以下是一个示例:

假设有两个进程,一个是发送进程,一个是接收进程:

  • 发送进程:将数据写入共享内存中,并通过管道通知接收进程。

  • 接收进程:从管道接收通知,然后从共享内存中读取数据进行处理。

发送进程的伪代码示例:
#include 
#include 
#include 
#include 
​
#define SHM_SIZE 1024
​
int main() {
    int shmid;
    char *shmaddr;
    int pipefd[2];
​
    // 创建共享内存段
    shmid = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);
    shmaddr = (char *)shmat(shmid, NULL, 0);
​
    // 创建管道
    pipe(pipefd);
​
    // 写入数据到共享内存
    strcpy(shmaddr, "Hello, shared memory!");
​
    // 通过管道发送消息通知接收进程
    write(pipefd[1], "1", 1);
​
    // 等待接收进程处理完毕
    char buf;
    read(pipefd[0], &buf, 1);
​
    // 断开共享内存连接
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, NULL);
​
    return 0;
}
接收进程的伪代码示例:
#include 
#include 
#include 
#include 
​
#define SHM_SIZE 1024
​
int main() {
    int shmid;
    char *shmaddr;
    int pipefd[2];
​
    // 创建共享内存段
    shmid = shmget(IPC_PRIVATE, SHM_SIZE, IPC_CREAT | 0666);
    shmaddr = (char *)shmat(shmid, NULL, 0);
​
    // 创建管道
    pipe(pipefd);
​
    // 等待发送进程的通知
    char buf;
    read(pipefd[0], &buf, 1);
​
    // 读取共享内存中的数据进行处理
    printf("Received data: %s\n", shmaddr);
​
    // 通知发送进程数据处理完毕
    write(pipefd[1], "1", 1);
​
    // 断开共享内存连接
    shmdt(shmaddr);
    shmctl(shmid, IPC_RMID, NULL);
​
    return 0;
}

这只是一个示例,实际使用时还需要考虑数据同步、错误处理等方面的实现。

3.获取共享内存数据块的属性shmctl()

#include 
#include 
​
int main() {
    int shmid = shmget(key, size,\
     IPC_CREAT | 0666); // 假设已创建一个共享内存
    if (shmid == -1) {
        perror("shmget");
        return 1;
    }
    //创建结构体接受
    struct shmid_ds buf;
    if (shmctl(shmid, IPC_STAT, &buf) == -1) {
        //IPVC_STAT获取共享内存的状态信息
        perror("shmctl");
        return 1;
    }
​
    printf("Shared memory size: %lu bytes
", buf.shm_segsz);
    printf("Shared memory owner UID: %d
", buf.shm_perm.uid);
    printf("Shared memory owner GID: %d
", buf.shm_perm.gid);
​
    return 0;
}

可能的输出结果:
Shared memory size: 4096 bytes
Shared memory owner UID: 
Shared memory owner GID: 

你可能感兴趣的:(模块知识,php,开发语言,c++,linux)