Linux进程间通信之命名管道及SystemV共享内存

在这里插入图片描述

命名管道

1. 什么是命名管道

命名管道是一种特殊类型的文件,也称为FIFO(First In, First Out)。它允许进程之间进行通信,就像使用管道一样,但不同之处在于它是存在于文件系统中的一种特殊文件

命名管道允许一个进程向管道中写入数据,而另一个进程可以从管道中读取相同的数据。这使得进程之间能够进行通信,无论它们是否在同一台计算机上,只要它们能够访问这个特殊文件。

你可以使用命令mkfifo在Linux中创建一个命名管道。这个特殊文件在文件系统中看起来像其他文件一样,但它被设计用来在进程之间传递数据。

当使用命名管道时,可以创建两个简单的脚本来展示进程间通信。在下面这个例子中,一个脚本会向命名管道写入消息,而另一个脚本则会从管道读取消息。

步骤:

  1. 创建命名管道

    mkfifo my_pipe
    
  2. 脚本 1:写入数据到命名管道 创建一个脚本 writer.sh,它向命名管道写入消息。

    #!/bin/bash
    
    PIPE=my_pipe
    
    echo "Sending message to the pipe..."
    echo "Hello from the writer script" > $PIPE
    
  3. 脚本 2:从命名管道读取数据 创建另一个脚本 reader.sh,它从命名管道读取消息。

    #!/bin/bash
    
    PIPE=my_pipe
    
    echo "Reading message from the pipe..."
    message=$(cat $PIPE)
    echo "Message received: $message"
    
  4. 运行脚本

    • 在一个终端中运行 ./reader.sh
    • 在另一个终端中运行 ./writer.sh

这样,你会看到写入脚本 writer.sh 向命名管道写入消息,而读取脚本 reader.sh 会从命名管道中读取这个消息并显示出来。这个过程展示了两个脚本之间通过命名管道进行的简单通信。

执行writer.sh向管道写入,没有读取就先阻塞

在这里插入图片描述

执行reader.sh读取管道并执行脚本命令输出发送过来的信息
在这里插入图片描述

当谈及命名管道,有几个要点值得深入探讨:

  1. 文件系统中的存在
    • 命名管道在文件系统中以文件的形式存在,它们有一个路径名。这使得多个进程能够通过这个路径名来访问同一个管道,从而实现进程间的通信。其他进程可以打开这个文件,并向其写入数据或从中读取数据。
  2. 持久性
    • 命名管道在文件系统中持久存在,即使通信的进程结束后,该管道文件仍然存在。除非显式地被删除,否则会一直保留在文件系统中。这使得它们适合用于长期的、不相关的进程间通信。
  3. 读写操作
    • 类似于匿名管道,命名管道也是一个先进先出(FIFO)的通道。一个进程可以向管道中写入数据,而另一个进程则可以从管道中读取相同的数据。这种读写操作是阻塞的,也就是说,如果没有数据可用,读取操作会等待,直到有数据可用为止。
  4. 权限和访问控制
    • 像其他文件一样,命名管道也受文件系统的权限控制。这意味着你可以使用权限位掩码(mode 参数)来设置谁可以对管道进行读取、写入以及执行操作。
  5. 多用途性
    • 由于其存在于文件系统中且具有路径名,命名管道在不同程序之间共享数据非常有用。它们可以被不相关的进程使用,只要这些进程可以访问相同的路径名。

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

int mkfifo(const char *filename, mode_t mode);
  • filename 参数是要创建的命名管道的路径名。
  • mode 参数是用于设置文件权限的模式。它类似于 chmod 命令中使用的权限位掩码,用于确定文件的访问权限。

这个函数会在指定路径创建一个命名管道,返回值为 0 表示成功创建,返回 -1 表示失败,并且会设置适当的错误代码,可以使用 errno 来获取具体的错误信息。
在这里插入图片描述

命名管道(Named Pipes)和匿名管道(Anonymous Pipes)是用于进程间通信的两种不同方式,它们有几个重要的区别

  1. 命名
    • 匿名管道:是一种临时的、单向的通道,只能在相关进程之间使用,无法用于无关的进程间通信。通常用于父子进程之间或者在一个进程内部创建的子进程之间。
    • 命名管道:以文件系统中的文件形式存在,可以被无关的进程访问。命名管道有一个路径名,允许不相关的进程通过这个路径名来访问同一个管道,从而实现进程间通信。
  2. 持久性
    • 匿名管道:进程间通信结束或管道被关闭后就消失,不留下痕迹。
    • 命名管道:作为文件存在于文件系统中,即使通信的进程结束,文件仍然存在,需要显式地被删除。
  3. 用途
    • 匿名管道:通常用于父子进程间或者在一个进程内部创建的子进程之间的通信。
    • 命名管道:适合不相关的进程间通信,比如在不同的程序之间共享数据。

总的来说,匿名管道更适合于有限的、有关的进程间通信,而命名管道更适合于长期、不相关的进程间通信,因为它们可以持久存在并被不同的进程访问。

2. 用命名管道实现server&client通信

Log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include <iostream>
#include <ctime>

#define Debug   0
#define Notice  1
#define Warning 2
#define Error   3


const std::string msg[] = {
    
    
    "Debug",
    "Notice",
    "Warning",
    "Error"
};

std::ostream &Log(std::string message, int level)
{
    
    
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    return std::cout;
}


#endif

这个头文件定义了一个简单的日志记录函数 Log 和日志级别常量。

  • #ifndef _LOG_H_#define _LOG_H_ 是头文件的保护宏,避免重复包含。
  • #include 部分引入了所需的标准库头文件。
  • Debug, Notice, Warning, Error 是日志级别的常量,代表不同的日志类型。
  • msg[] 是一个存储日志类型字符串的数组。
  • Log() 函数接受消息字符串和日志级别,然后将消息打印到标准输出,显示时间戳、日志级别和消息内容。

comm.hpp

#ifndef _COMM_H_
#define _COMM_H_

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"

using namespace std;

#define MODE 0666
#define SIZE 128

string ipcPath = "./fifo.ipc";


#endif

这个文件包含了一些常量和全局变量,以及必要的头文件。

  • #ifndef _COMM_H_#define _COMM_H_ 是头文件的保护宏。
  • #include 部分引入了所需的标准库头文件和其他自定义的头文件,包括 Log.hpp
  • using namespace std; 是为了方便使用 C++ 标准库中的函数和对象。
  • MODESIZE 是用于管道和缓冲区大小的常量。
  • ipcPath 是命名管道的路径。

server.cpp

#include "comm.hpp"
#include <sys/wait.h>

static void getMessage(int fd)
{
    
    
    char buffer[SIZE];
    while (true)
    {
    
    
        memset(buffer, '\0', sizeof(buffer));
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
    
    
            cout <<"["  << getpid() << "] "<< "client say> " << buffer << endl;
        }
        else if (s == 0)
        {
    
    
            // end of file
            cerr <<"["  << getpid() << "] " << "read end of file, clien quit, server quit too!" << endl;
            break;
        }
        else
        {
    
    
            // read error
            perror("read");
            break;
        }
    }
}

int main()
{
    
    
    // 1. 创建管道文件
    if (mkfifo(ipcPath.c_str(), MODE) < 0)
    {
    
    
        perror("mkfifo");
        exit(1);
    }

    Log("创建管道文件成功", Debug) << " step 1" << endl;

    // 2. 正常的文件操作
    int fd = open(ipcPath.c_str(), O_RDONLY);
    if (fd < 0)
    {
    
    
        perror("open");
        exit(2);
    }
    Log("打开管道文件成功", Debug) << " step 2" << endl;

    int nums = 3;
    for (int i = 0; i < nums; i++)
    {
    
    
        pid_t id = fork();
        if (id == 0)
        {
    
    
            // 3. 编写正常的通信代码了
            getMessage(fd);
            exit(1);
        }
    }
    for(int i = 0; i < nums; i++)
    {
    
    
        waitpid(-1, nullptr, 0);
    }
    // 4. 关闭文件
    close(fd);
    Log("关闭管道文件成功", Debug) << " step 3" << endl;

    unlink(ipcPath.c_str()); // 通信完毕,就删除文件
    Log("删除管道文件成功", Debug) << " step 4" << endl;

    return 0;
}

这个文件包含了从命名管道读取消息的服务端代码。

  • main() 函数中:
    • 使用 mkfifo() 创建一个命名管道。
    • open(ipcPath.c_str(), O_RDONLY) 打开命名管道以进行读取操作。
    • 使用 fork() 创建了多个子进程,每个子进程调用 getMessage() 从管道中读取消息并显示到控制台。
    • 等待所有子进程执行完毕后,关闭文件描述符。
    • 使用 unlink() 删除命名管道文件。

getMessage() 函数:

  • 不断循环读取管道中的数据,并将数据显示在控制台上。

client.cpp

#include "comm.hpp"

int main()
{
    
    
    // 1. 获取管道文件
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if(fd < 0)
    {
    
    
        perror("open");
        exit(1);
    }

    // 2. ipc过程
    string buffer;
    while(true)
    {
    
    
        cout << "Please Enter Message Line :> ";
        std::getline(std::cin, buffer);
        write(fd, buffer.c_str(), buffer.size());
    }

    // 3. 关闭
    close(fd);
    return 0;
}

client.cpp

这个文件包含了向命名管道写入消息的客户端代码。

  • main() 函数中:
    • open(ipcPath.c_str(), O_WRONLY) 打开命名管道以进行写入操作。
    • 使用 std::getline() 获取用户输入的消息,然后使用 write() 将消息写入到管道中。
    • close(fd) 关闭文件描述符。

Makefile编译

.PHONY:all
all:client mutiServer

client:client.cpp
	g++ -o $@ $^ -std=c++11
mutiServer:server.cpp
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f client mutiServer

编译完成后,我们打开终端,两个终端分别执行clientmutiServer两个程序

先执行mutiServer 等待client发送

在这里插入图片描述

在另一终端执行client,打开管道

在这里插入图片描述

我们在client端输入消息,mutiServer 端接收

在这里插入图片描述

client端终止程序,管道关闭并删除

在这里插入图片描述

system V共享内存

System V共享内存是一种用于在Linux系统中进程间共享内存的机制。它是System V IPC(Inter-Process Communication,进程间通信)机制的一部分,与信号量和消息队列一起组成了System V IPC

特点包括:

  1. 共享内存段: System V共享内存允许多个进程访问相同的逻辑内存部分。这些内存段通过一个唯一的标识符来标识,允许多个进程将同一个共享内存段映射到它们的地址空间中。
  2. 高效性: 与管道、消息队列等其他IPC机制相比,共享内存的效率更高。它可以用于需要频繁、大量数据交换的场景,因为进程可以直接读写共享内存而无需进行复制或通过内核来中转数据。
  3. 操作简单: System V共享内存提供了一组系统调用,如shmgetshmatshmdtshmctl等,用于创建、连接、断开和管理共享内存段。
  4. 需要显式清理: 与文件映射内存不同,System V共享内存在进程结束后并不会自动清理。因此,程序员需要确保适当地断开连接并删除共享内存段,以防止资源泄漏。

共享内存允许不同进程之间通过共享相同的内存区域来交换数据,适用于需要高性能和频繁数据交换的应用场景,但同时也需要程序员进行显式的内存管理和同步。

1. 共享内存示意图

在这里插入图片描述

2. 共享内存数据结构

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 */
};

System V 共享内存中用于维护共享内存段信息的结构体 shmid_ds。这个结构体中包含了关于共享内存段的多项信息,例如:

  • shm_perm: 是一个 struct ipc_perm 类型的结构体,包含了关于共享内存段权限的信息,比如操作权限等。
  • shm_segsz: 表示共享内存段的大小(以字节为单位)。
  • shm_atime, shm_dtime, shm_ctime: 分别代表最后一次附加(attach)、分离(detach)和更改(change)的时间。
  • shm_cpid: 是创建者进程的PID。
  • shm_lpid: 是最后操作共享内存的进程的PID。
  • shm_nattch: 表示当前附加到这个共享内存段的进程数。
  • 其他未使用的字段,如 shm_unused, shm_unused2, shm_unused3

这个结构体主要用于跟踪和管理共享内存段的属性和状态,例如谁创建了这个共享内存段、谁最后访问了它、它的大小等。这些信息对于维护和控制共享内存的生命周期和访问权限非常重要。

3. 共享内存函数

3.1 shmget函数

shmget 函数是用于创建或获取 System V 共享内存段的函数,它在Linux系统中的 System V IPC 机制中使用。

声明

#include <sys/ipc.h>
#include <sys/shm.h>

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

参数

  • key: 用于唯一标识共享内存段的键值。通常使用 ftok() 函数生成一个键。
  • size: 指定要创建或获取的共享内存段的大小(以字节为单位)。
  • shmflg: 是一个标志参数,用于指定一些附加的操作,比如权限标志和行为选项。可以与 | 操作符结合使用多个标志。

返回值

  • 若成功,返回一个标识共享内存段的标识符(非负整数),称为共享内存标识符(shmid)。
  • 若失败,返回 -1,并设置 errno 指示失败的具体原因。

工作原理

  • 如果传入的 key 对应的共享内存段已经存在,则 shmget 将返回其标识符(shmid),不会创建新的共享内存段。
  • 如果传入的 key 没有对应的共享内存段,且 shmflg 包含了 IPC_CREAT 标志,shmget 将会创建一个新的共享内存段,并返回其标识符。
  • shmflg 参数可以包含其他标志,如权限标志 IPC_CREAT | 0666 确定了共享内存段的权限。

这个函数是创建或获取共享内存段的第一步。创建成功后,接下来需要使用 shmat 将其连接到进程的地址空间,才能进行读写操作。

3.2 shmat函数

shmat 函数用于将共享内存附加到调用进程的地址空间,允许进程访问和操作共享内存段。

声明

#include <sys/types.h>
#include <sys/shm.h>

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

参数

  • shmid: 共享内存段的标识符,由 shmget 返回。
  • shmaddr: 期望共享内存段连接的地址。通常为 NULL,表示让操作系统选择合适的地址。
  • shmflg: 标志参数,可以包括一些特定的选项,比如 SHM_RDONLY 用于只读方式连接共享内存。

返回值

  • 若成功,返回共享内存段的起始地址;若失败,返回 (void *) -1,并设置 errno 指示失败的具体原因。

工作原理

  • shmat 将共享内存段连接到调用进程的地址空间。
  • 如果 shmaddr 为 NULL,操作系统将自动选择一个适当的地址将共享内存连接到调用进程的地址空间。
  • 连接成功后,进程可以使用返回的地址指针进行对共享内存的读写操作。

注意事项

  • 一旦共享内存连接到进程的地址空间,进程就可以直接读写共享内存。因此,需要小心处理共享内存中的数据,以避免数据损坏或不一致。
  • 当不再需要共享内存时,需要使用 shmdt 函数将其从进程的地址空间分离。

3.3 shmdt函数

shmdt 函数用于将共享内存从进程的地址空间中分离,使得进程无法再访问共享内存段。

声明

#include <sys/types.h>
#include <sys/shm.h>

int shmdt(const void *shmaddr);

参数

  • shmaddr: 指向共享内存段附加地址的指针。

返回值

  • 若成功,返回 0。
  • 若失败,返回 -1,并设置 errno 指示失败的具体原因。

工作原理

  • shmdt 用于将共享内存从调用进程的地址空间中分离,但不会删除共享内存段。
  • 进程使用这个函数将共享内存从其地址空间分离后,就无法再访问这段共享内存的内容。

注意事项

  • 分离共享内存后,其他仍然附加到共享内存的进程仍然可以访问和操作共享内存。
  • 分离共享内存后,通常不会释放共享内存,只是取消了进程对共享内存的访问权限。通常需要在不再需要共享内存时使用 shmctl 函数来删除共享内存段。

3.4 shmctl函数

shmctl 函数是用于控制 System V 共享内存的函数,可以用来执行各种对共享内存段的控制操作。

声明

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

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

参数

  • shmid: 共享内存段的标识符,由 shmget 返回。

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

    • IPC_STAT: 获取共享内存的状态,将信息写入 buf 参数指向的结构体中。

    • IPC_SET: 设置共享内存的状态,通过 buf 参数中提供的信息进行设置。

    • IPC_RMID: 从系统中删除共享内存段。
      在这里插入图片描述

  • buf: 一个指向 struct shmid_ds 结构体的指针,用于存储或传递共享内存段的信息。

返回值

  • 若成功,返回一个非负整数,具体返回值取决于执行的命令。
  • 若失败,返回 -1,并设置 errno 指示失败的具体原因。

工作原理

  • shmctl 函数用于执行与共享内存段相关的控制操作。
  • 通过 cmd 参数来指定需要执行的操作,如获取共享内存的状态、设置共享内存的状态或者删除共享内存段。

注意事项

  • 使用 IPC_RMID 命令可以删除共享内存段。删除后,所有附加到该共享内存段的进程将不再能够访问共享内存,但直到所有进程都断开连接,系统才会回收该共享内存。

3.5 ftok函数

声明

#include <sys/types.h>
#include <sys/ipc.h>

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

参数

  • pathname: 是一个指向路径名的字符串,通常是一个现有的文件路径。ftok 使用此路径名来生成键。
  • proj_id: 是一个用户指定的整数,用于生成唯一的键。

返回值

  • 若成功,返回一个由 pathnameproj_id 生成的唯一的键。
  • 若失败,返回 -1,并设置 errno 指示失败的具体原因。

工作原理

  • ftok 根据 pathnameproj_id 来创建一个键。
  • 它使用 st_devst_ino 两个文件属性(stat 结构体中的设备编号和节点编号)以及 proj_id 来创建唯一的键。

注意事项

  • 由于 ftok 使用文件属性来生成键值,因此传递给 ftok 的文件必须存在并且可访问,否则可能会导致生成的键不唯一或者失败。
  • 生成的键值通常用于标识 System V IPC 中的共享内存、消息队列或信号量等资源。

4. 共享内存示例

4.1 comm.hpp

#pragma once

#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <cassert>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"

using namespace std; 

#define PATH_NAME "/home/kingxzq"
#define PROJ_ID 0x66
#define SHM_SIZE 4096 //共享内存的大小,最好是页(PAGE: 4096)的整数倍

#define FIFO_NAME "./FIFO"

class Init
{
    
    
public:
    Init()
    {
    
    
        umask(0);
        int n = mkfifo(FIFO_NAME, 0666);
        assert(n == 0);
        (void)n;
        Log("create fifo success",Notice) << "\n";
    }
    ~Init()
    {
    
    
        unlink(FIFO_NAME);
        Log("remove fifo success",Notice) << "\n";
    }
};

#define READ O_RDONLY
#define WRITE O_WRONLY

int OpenFIFO(std::string pathname, int flags)
{
    
    
    int fd = open(pathname.c_str(), flags);
    assert(fd >= 0);
    return fd;
}

void Wait(int fd)
{
    
    
    Log("等待中....", Notice) << "\n";
    uint32_t temp = 0;
    ssize_t s = read(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
}

void Signal(int fd)
{
    
    
    uint32_t temp = 1;
    ssize_t s = write(fd, &temp, sizeof(uint32_t));
    assert(s == sizeof(uint32_t));
    (void)s;
    Log("唤醒中....", Notice) << "\n";
}

void CloseFifo(int fd)
{
    
    
    close(fd);
}
  • Init 类包含了创建和删除命名管道的方法。在构造函数中创建了一个命名管道,并在析构函数中删除了这个管道。
  • OpenFIFO 函数用于打开一个命名管道,并返回文件描述符。
  • Wait 函数在文件描述符中等待并接收信号。
  • Signal 函数用于向文件描述符中写入数据,发送信号。
  • CloseFifo 函数用于关闭文件描述符。

4.2 Log.hpp

#ifndef _LOG_H_
#define _LOG_H_

#include <iostream>
#include <ctime>

#define Debug   0
#define Notice  1
#define Warning 2
#define Error   3


const std::string msg[] = {
    
    
    "Debug",
    "Notice",
    "Warning",
    "Error"
};

std::ostream &Log(std::string message, int level)
{
    
    
    std::cout << " | " << (unsigned)time(nullptr) << " | " << msg[level] << " | " << message;
    return std::cout;
}


#endif
  • 定义了几个日志级别(Debug、Notice、Warning、Error),以及每个级别对应的描述字符串。
  • Log 函数用于接收消息和消息级别,然后在控制台上打印该消息,包括时间戳和级别。
  • 这个函数返回 std::ostream 对象,允许像使用 std::cout 一样使用这个函数打印日志。

4.3 shmServer.cpp

#include "comm.hpp"

Init init; 

string TransToHex(key_t k)
{
    
    
    char buffer[32];
    snprintf(buffer, sizeof buffer, "0x%x", k);
    return buffer;
}

int main()
{
    
    
    // 1. 创建公共的Key值
    key_t k = ftok(PATH_NAME, PROJ_ID);
    assert(k != -1);

    Log("create key done", Debug) << " server key : " << TransToHex(k) << endl;

    // 2. 创建共享内存
    int shmid = shmget(k, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0666); 
    if (shmid == -1)
    {
    
    
        perror("shmget");
        exit(1);
    }
    Log("create shm done", Debug) << " shmid : " << shmid << endl;

    // 3. 将指定的共享内存,挂接到自己的地址空间
    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    Log("attach shm done", Debug) << " shmid : " << shmid << endl;

    
    int fd = OpenFIFO(FIFO_NAME, READ);
    for(;;)
    {
    
    
        Wait(fd);

        printf("%s\n", shmaddr);
        if(strcmp(shmaddr, "quit") == 0) break;
    }
    // 4. 将指定的共享内存,从自己的地址空间中去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    (void)n;
    Log("detach shm done", Debug) << " shmid : " << shmid << endl;

    // 5. 删除共享内存,IPC_RMID即便是有进程和当下的shm挂接,依旧删除共享内存
    n = shmctl(shmid, IPC_RMID, nullptr);
    assert(n != -1);
    (void)n;
    Log("delete shm done", Debug) << " shmid : " << shmid << endl;

    CloseFifo(fd);
    return 0;
}

这段代码负责创建共享内存、连接共享内存、从共享内存中读取数据,并最终清理共享内存资源和命名管道。

  • Init init;:调用了 Init 类的构造函数,创建了一个命名管道。
  • TransToHex 函数:将 key_t 类型的键值转换为十六进制字符串。
  • main 函数中执行的关键步骤包括:
    1. 通过 ftok 函数创建共享内存的键值。
    2. 使用 shmget 创建一个共享内存段,设置了 IPC_CREAT 和 IPC_EXCL 标志以确保不存在同样的共享内存。
    3. 通过 shmat 连接到该共享内存,获得共享内存的地址。
    4. 使用 OpenFIFO 函数打开命名管道。
    5. 循环中使用 Wait 函数等待从命名管道接收到信号,然后从共享内存中读取数据并打印,直到读取到 “quit” 消息。
    6. 使用 shmdt 解除对共享内存的连接。
    7. 使用 shmctl 删除共享内存。
    8. 关闭命名管道。

整个过程实现了服务端的功能,通过共享内存和命名管道实现了与客户端的通信。

4.4 shmClient.cpp

#include "comm.hpp"

int main()
{
    
    
    Log("child pid is : ", Debug) << getpid() << endl;
    key_t k = ftok(PATH_NAME, PROJ_ID);
    if (k < 0)
    {
    
    
        Log("create key failed", Error) << " client key : " << k << endl;
        exit(1);
    }
    Log("create key done", Debug) << " client key : " << k << endl;

    // 获取共享内存
    int shmid = shmget(k, SHM_SIZE, 0);
    if(shmid < 0)
    {
    
    
        Log("create shm failed", Error) << " client key : " << k << endl;
        exit(2);
    }
    Log("create shm success", Error) << " client key : " << k << endl;


    char *shmaddr = (char *)shmat(shmid, nullptr, 0);
    if(shmaddr == nullptr)
    {
    
    
        Log("attach shm failed", Error) << " client key : " << k << endl;
        exit(3);
    }
    Log("attach shm success", Error) << " client key : " << k << endl;

    int fd = OpenFIFO(FIFO_NAME, WRITE);
    while(true)
    {
    
    
        ssize_t s = read(0, shmaddr, SHM_SIZE-1);
        if(s > 0)
        {
    
    
            shmaddr[s-1] = 0;
            Signal(fd);
            if(strcmp(shmaddr,"quit") == 0) break;
        }
    }

    CloseFifo(fd);
    // 去关联
    int n = shmdt(shmaddr);
    assert(n != -1);
    Log("detach shm success", Error) << " client key : " << k << endl;

    return 0;
}

这段代码实现了一个客户端程序,用于与服务端通过共享内存和命名管道进行通信。

  • 首先,程序打印出自己的进程 ID。
  • 接着使用 ftok 函数创建一个键值,用于获取共享内存。
  • 通过 shmget 获取共享内存段。
  • 使用 shmat 连接到共享内存,获得共享内存的地址。
  • 打开命名管道,并进入一个无限循环。
  • 在循环中,程序从标准输入中读取数据,将数据写入共享内存,并发送信号给服务端,以此实现数据的传输。
  • 如果输入 “quit”,则退出循环。
  • 关闭命名管道,解除对共享内存的连接并退出程序。

这个程序的作用是向服务端发送数据,并接收来自服务端的信号,在读取数据后将其写入共享内存,通过命名管道实现了与服务端的同步和通信。

Makefile编译

.PHONY:all
all:shmClient shmServer

shmClient:shmClient.cpp
	g++ -o $@ $^ -std=c++11
shmServer:shmServer.cpp
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f shmClient shmServer

编译完成后,我们打开终端,两个终端分别执行shmClientshmServer两个程序

先执行shmServer 等待shmClient发送,shmClient发送消息后,shmServer接收再次进行等待,直到客户端输入quit
在这里插入图片描述

如果我们在客户端造成非法退出,可能会导致再次开启服务端失败

在这里插入图片描述

再次启动服务端
在这里插入图片描述

这是因为ipc资源没有被删除,导致资源占用,无法初始化新的ipc资源

我们使用命令ipcs -m查看ipc资源

在这里插入图片描述

输入指令ipcrm -m [shmid号]删除shm ipc资源,需要注意的是

IPC资源必须删除,否则不会自动清除,除非重启,所以system V IPC资源的生命周期随内核

这里除了删除ipc资源还需要重新生成命名管道文件,这里命名管道的作用主要是实现进程间的同步和控制。在这个示例中,命名管道用于实现服务端和客户端之间的同步,以协调它们对共享内存的访问和操作。

具体来说:

  1. 服务端等待信号: 服务端通过命名管道等待来自客户端的信号,以指示何时从共享内存中读取数据。
  2. 客户端发送信号: 客户端在向共享内存写入数据后,通过命名管道向服务端发送信号,告知服务端可以读取共享内存中的数据。

这种设计允许两个进程以同步的方式共享数据,避免了竞争条件和数据不一致的问题。命名管道充当了同步的信号桥梁,协调了进程间的操作,使得共享内存能够在适当的时候被读取和写入。

共享内存本身并不包含进程同步和控制的机制。它只是提供了一个共享的内存区域,允许多个进程访问相同的内存空间。因此,需要其他机制来协调和控制进程对共享内存的访问以避免数据竞争和不一致性

通常,要确保共享内存的安全访问,需要结合其他形式的同步和控制机制,比如:

  • 信号量: 用于控制对共享资源的访问,确保同一时间只有一个进程可以访问共享内存。
  • 互斥锁: 可以确保同时只有一个进程能够执行临界区代码,避免多个进程同时写入共享内存。
  • 条件变量: 用于实现进程间的等待和通知机制,允许进程等待某个条件成立后才执行操作。

这些机制能够协调进程之间的行为,确保数据的一致性和安全访问。命名管道在这个示例中就是用来辅助实现了进程间的同步和控制。

system V消息队列

1. 什么是system V消息队列

System V 消息队列是 Linux 系统中一种 IPC(进程间通信)机制,它提供了一种进程间通信的方式,允许在不同进程之间传递数据。

特点包括:

  1. 消息队列结构: 消息队列是一组消息的集合,每个消息都有一个类型和一个正文。
  2. 独立于发送者和接收者: 发送者可以发送消息,接收者可以接收消息,彼此之间不需要直接连接。这使得进程可以异步地进行通信。
  3. 消息类型: 每个消息都有一个类型,接收者可以选择特定类型的消息进行接收。
  4. 持久性: 消息队列是持久的,它们会一直存在直到被显式地删除。
  5. 有限大小: 每个消息队列有限制大小,不同操作系统和配置可能有不同的限制。

System V 消息队列通过一系列函数来实现,比如 msgget(创建或获取一个消息队列)、msgsnd(发送消息到队列)、msgrcv(从队列接收消息)等。这些函数允许进程进行消息的发送、接收和控制消息队列的属性。

这种通信机制适用于需要异步通信、数据传递、以及进程间解耦的场景。它提供了一种可靠的进程间通信方式,使得不同的进程能够以较低的耦合度进行数据交换。

2. 伪代码示例

以下示例中的代码只是概念性的示范,并不是可直接运行的代码,因为 System V IPC 函数需要正确的参数和错误处理。另外,System V IPC 函数使用的是 C 的函数接口,不同操作系统之间可能会有细微的差异

// Sender Process

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

struct msgbuf {
    
    
    long mtype;
    char mtext[100];
};

int main() {
    
    
    key_t key = ftok("path", 'A');
    int msgid = msgget(key, IPC_CREAT | 0666);

    struct msgbuf message;
    message.mtype = 1; // 设置消息类型为1
    strcpy(message.mtext, "FileTransfer: file.txt, size: 1024");
    
    msgsnd(msgid, &message, sizeof(message), 0);

    // 继续其他任务或等待接收者的确认
    return 0;
}


// Receiver Process

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

struct msgbuf {
    
    
    long mtype;
    char mtext[100];
};

int main() {
    
    
    key_t key = ftok("path", 'A');
    int msgid = msgget(key, 0666);

    struct msgbuf message;
    msgrcv(msgid, &message, sizeof(message), 1, 0);

    printf("Received message: %s\n", message.mtext);

    // 提取文件名和大小
    // 创建文件
    // 发送确认消息

    return 0;
}

这段示例代码展示了如何使用 System V 消息队列在两个进程之间进行简单的通信。发送者发送一个包含文件信息的消息,接收者从消息中提取数据并执行相应操作,然后发送确认消息。

3. system V消息队列进程通信机制逐渐没落

System V 消息队列虽然是一种有效的 IPC(进程间通信)机制,但它逐渐被其他更现代化的进程间通信方式所取代,主要有以下几个原因:

  1. 复杂性: 使用 System V 消息队列需要处理 IPC API,需要调用诸如 msggetmsgsndmsgrcv 等函数,而这些操作对于开发者来说可能比较繁琐,容易出错,不够直观。
  2. 性能: 与其他 IPC 机制相比,消息队列的性能可能相对较低。比如,相对于共享内存,消息队列需要进行数据拷贝,这可能导致额外的开销。
  3. 限制: System V 消息队列具有固定的消息大小限制,这会限制消息的大小和传输的灵活性。
  4. 可移植性: 不同操作系统对 System V 消息队列的支持程度和细节可能不同,这可能导致不同系统上的兼容性问题。
  5. 替代技术: 随着时间的推移,出现了更现代化、更方便的进程间通信方式,比如 POSIX 消息队列、管道、套接字等,它们可能更易于使用,并提供了更好的性能和灵活性。

基于这些原因,开发者在选择 IPC 机制时可能更倾向于使用其他更简单、更高效的方式,这导致 System V 消息队列逐渐被替代或淘汰。

猜你喜欢

转载自blog.csdn.net/kingxzq/article/details/134387259