【IPC】管道通信【命名管道】

1.管道小总结

linux-manualshouce

在Linux中,manual手册的编号用于区分手册的不同部分。这些编号通常用于man命令中,以便用户可以指定要查看的手册部分。man命令的编号对应关系如下:

用户命令(User Commands):通常包括可执行程序或shell命令。
系统调用(System Calls):由内核提供的函数。
库调用(Library Calls):程序库中的函数。
特殊文件(Special Files):通常位于/dev目录下的设备文件。
文件格式和约定(File Formats and Conventions):例如/etc/passwd等配置文件的格式。
游戏(Games)。
杂项(Miscellaneous):包括宏包和约定等其他内容。
系统管理命令(System Administration Commands and Daemons)。
此外,还有第9个部分,通常用于其他内容,比如内核例行程序(Kernel Routines)。

管道读写规则

  1. 当没有数据可读时
    O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据为止。
    O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
  2. 当管道满的时候
    O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
    O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
  3. 如果所有管道写端对应的文件描述符被关闭,则read返回0
  4. 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出
  5. 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。
  6. 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

原子性

要么做要么不做。无中间状态。
多执行流下数据出现并发访问时考虑。

2.命名管道

2.1认识命名管道

在这里插入图片描述

  1. 进程A打卡了文件file,进程B再去打开file时,OS识别到file已打开,则不再创建对应的file结构体,而是直接指向已有的file结构体。【OS中一般都会存在n多个进程,且存在像许多进程打开同一个文件的情况,如果对于已经打开的文件,另外的进程再去打开他时区创建一个新的file结构体,这无疑是巨大的时空浪费】
  2. 命名管道无疑是这样的一个存在,首先他是一个文件,其次他不用将数据刷新到磁盘。
  3. 该文件一定在系统路径中(路径具有唯一性)。该文件有名字,可以被打开,但是不会将内存数据刷新到磁盘。
  4. 至此,两个毫不相干的进程通过命名管道文件的路径看到同一份资源。【匿名管道通常应用于有血缘关系的进程,这也是匿名管道的一个缺点】

2.2命名管道的应用小场景

创建命名管道

在这里插入图片描述
在这里插入图片描述

场景1

在这里插入图片描述

场景2:输出重定向

在这里插入图片描述

在这里插入图片描述

条件编译
#ifndef _COMMON_H_
#define _COMMON_H_
#endif

这三句代码是C或C++编程语言中常用的预处理器指令,它们通常用于头文件中以防止头文件内容的多次包含(多重定义问题)。我来详细解释每句代码的含义:

#ifndef COMMON_H
这是一个条件编译指令,它检查是否定义了名为_COMMON_H_的宏(macro)。如果_COMMON_H_没有被定义,那么紧随其后的代码(直到遇到#endif或另一个条件编译指令)会被编译器包含(或编译)。
2. #define COMMON_H

这行代码定义了一个名为_COMMON_H_的宏。一旦这个宏被定义,再次遇到#ifndef _COMMON_H_时,由于_COMMON_H_已经被定义,所以其后的代码不会被再次包含。
3. #endif

这是一个结束标记,表示#ifndef条件编译指令的结束。

将这三行代码放在头文件common.h的开始处,可以确保无论这个头文件被包含多少次,其内部的内容都只会被编译器编译一次。这在处理循环包含或者复杂项目结构时非常有用,因为你可以避免函数或变量被多次定义,从而避免编译错误。
这样,当common.h首次被包含时,其内容会被编译;如果再次被包含,由于_COMMON_H_已经被定义,其内容不会被再次编译。

  • #ifndef/#define/#endifC和C++语言标准支持的预处理指令组合,用于实现头文件保护(header guards)的功能。它们通过宏定义来防止头文件被重复包含。
  • #pragma once 则是编译器特定的指令,其工作原理依赖于编译器的实现。由于它是非标准的,因此在不同编译器之间的可移植性可能不如 #ifndef/#define/#endif 组合。

unlink()删除文件

在这里插入图片描述

rm和unlink的异同

在Linux中,rm和unlink都用于删除文件或目录,但它们在使用方式和功能上有一些重要的区别。

  1. 命令形式和使用方式:

rm 是一个更高级的、用户友好的命令,用于删除文件或目录。它可以处理多种复杂的情况,例如删除目录(使用 -r 或 -R 选项)或强制删除文件(使用 -f 选项)。
unlink 是一个较低级的系统调用,通常用于在编程中删除文件。在shell中,你可以使用 unlink 命令来删除文件,但它没有 rm 那么多的选项和功能。
2. 功能和行为:

在功能上,rm 和 unlink 基本上是一样的:它们都删除了指定的文件或目录。然而,由于 rm 提供了更多的选项,它可以更灵活地处理各种情况。
在行为上,rm 和 unlink 都减少了文件的链接计数。如果一个文件只有一个链接(即它是唯一被引用的),那么当这个链接被 rm 或 unlink 删除后,该文件占用的磁盘空间也会被释放。但是,如果一个文件有多个链接,那么删除其中一个链接只会减少链接计数,而不会释放磁盘空间。
3. 错误处理:

rm 在删除不存在的文件或目录时,会输出一个错误消息。然而,如果你使用 -f 选项,rm 会静默地忽略这些错误。
unlink 在遇到错误时,通常会返回一个错误代码,但具体的行为可能取决于你的shell和如何调用 unlink。
总的来说,rm 和 unlink 在功能上相似,但在使用方式和行为上有所不同。对于日常的文件删除任务,rm 通常是一个更好的选择,因为它提供了更多的选项和更友好的错误处理。而在编程中,你可能需要更直接地控制文件删除的过程,这时 unlink 可能更合适。
在Linux中,unlink 系统调用用于删除文件的链接。具体来说,unlink 可以对以下类型的文件进行操作:

拓展unlink可删除的文件

1. 普通文件

  • unlink 可以用于删除普通文件。当你调用 unlink 删除一个普通文件时,该文件的链接计数会减一,如果没有剩余的链接(即引用计数为零),其空间将被释放。

2. 符号链接(Symbolic Links)

  • unlink 也可以用于删除符号链接。对于符号链接,unlink 会删除链接本身,但不会影响它所指向的目标文件。

3. 管道文件

  • 对于命名管道(FIFO),unlink 可以用来删除这些特殊文件。

4. 套接字(Sockets)

  • unlink 也适用于Unix域套接字,可以用于删除套接字文件。

5. 目录

  • 虽然 unlink 可以用于目录项(移除目录的链接),但在大多数情况下,直接使用 rmdir 来删除空目录是更常见的做法。unlink 不能直接删除非空目录。

注意事项

  • 权限:在使用 unlink 删除文件时,必须具备对该文件的写权限以及对其父目录的执行权限。

  • 不适用于打开的文件:如果文件仍然被进程打开,则可以通过 unlink 删除文件,但文件内容在所有引用都关闭之前仍然存在,直到最后一个引用被关闭。

总结

unlink 可以操作的文件类型包括普通文件、符号链接、命名管道和套接字文件等。但是,它不适合用于删除非空目录,且需要注意文件的权限设置。

2.3模拟命名管道

1.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 tip[] ={
        "Debug",
        "Notice",
        "Warning",
        "Error"};

std::ostream &Log(std::string message, int option)
{
    // 获取时间戳 time_t timestamp; time(timestamp);
    time_t timestamp = time(nullptr);
    if (timestamp == std::time_t(-1))
    {
        std::cerr << "获取时间失败" << std::endl;
        exit(1);
    }

    // 获取格式化时间 tm *localtime(const time_t *__timer)
    tm *timeinfo = std::localtime(&timestamp);

    std::cout << " | "
              << 1900 + timeinfo->tm_year << "-"
              << 1 + timeinfo->tm_mon << "-"
              << timeinfo->tm_mday << " "
              << timeinfo->tm_hour << ":"
              << timeinfo->tm_min << ":"
              << timeinfo->tm_sec
              << " | "
              << tip[option]
              << " | "
              << message;

    return std::cout;
}

#endif

2.common.hpp

#ifndef _COMMON_H_
#define _COMMON_H_

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

using namespace std;

#define MODE 0666
#define SIZE 128

string ipcPath = "./fifo.ipc";

#endif

3.server.cxx

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

static void IpcWithClient(int fd)
{
    char buffer[SIZE];
    while (true)
    {
        memset(buffer, '\0', sizeof(buffer));
        // 【OS写的时候不写\0 这里读的时候自然没有\0 我们空一个以免有需要在末尾加0】
        // 当然 上面的memset已经对所有单元都加了\0 这里只是通用的默认规范
        ssize_t s = read(fd, buffer, sizeof(buffer) - 1);

        if (s > 0) // read success
        {
            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("IpcWithClient::read");
            break;
        }
    }
}

int main()
{
    // 1. 创建管道文件 int mkfifo(const char *__path, mode_t __mode)
    if (mkfifo(ipcPath.c_str(), MODE) < 0) //成功返回0 失败返回-1
    {
        perror("server::mkfifo");
        exit(1);
    }
    Log("创建管道文件成功", Debug) << " step 1" << endl;

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

    // 3. 开始进行IPC 服务端创建子进程去与客户端进行ipc
    int serverChildNum = 3;
    for (int i = 0; i < serverChildNum; i++)
    {
        pid_t id = fork();
        if (id == 0)
        {
            IpcWithClient(fd);
            exit(1);
        }
    }
    //阻塞式等待子进程 获取子进程退出状态 回收子进程
    for (int i = 0; i < serverChildNum; i++)
    {
        waitpid(-1, nullptr, 0);
    }

    // 4. 关闭文件
    close(fd);
    Log("关闭管道文件成功", Debug) << " step 3" << endl;

    unlink(ipcPath.c_str()); // int unlink(const char *__name)
    Log("删除管道文件成功", Debug) << " step 4" << endl;

    return 0;
}

4.client.cxx

#include "common.hpp"

int main()
{
    // 1. 获取管道文件 创建文件由server负责
    // 这里不加O_TRUNC 文件不存在client也不创建 失败是server的事情
    int fd = open(ipcPath.c_str(), O_WRONLY);
    if (fd < 0)
    {
        perror("client::open");
        exit(1);
    }

    // 2. 进行ipc
    string buffer;
    while (true)
    {
        cout << "Please input information:> ";
        //istream& getline<char, char_traits<char>, allocator<char>>(istream& __is, string& __str)
        std::getline(std::cin, buffer);
        write(fd, buffer.c_str(), buffer.size());
    }

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

3.管道代码总结

  1. 模拟匿名管道: 父进程调用pipe() 创建匿名管道 记录pipefd 父子进程各自关闭不用的pipefd 之后父进程写到写端 子进程从读端读
  2. 模拟进程池: 父进程创建N个子进程 对每一个子进程之开启读端 父进程只开启写端 父进程随机选择一个子进程去执行它写到写端的命令 子进程阻塞等待获取从读端的命令 直到获取成功 否则一直阻塞等待
  3. 模拟命名管道:服务端进程创建命名管道 让子进程从fd里读信息后输出到显示器 客户端打开彼管道文件 向该fd写信息

4.管道练习题

  1. 典型进程间通信方式:管道,共享内存,消息队列,信号量。 网络通信,文件等多种方式。若说内存,则太过宽泛,并没有特指某种技术,不考虑。
  2. 进程之间具有独立性,拥有自己的虚拟地址空间,无法通过各自的虚拟地址进行通信(A的地址经过B的页表映射不一定映射在什么位置)⇒ 进程之间不可以直接通过地址访问进行相互通信
  3. 进程间通信(1)通过内核中的缓冲区实现(2)文件以及网络通信
  4. 管道本质是内核中的一块缓冲区,多个进程通过访问同一块缓冲区实现通信。
    使用int pipe(int pipefd[2])接口创建匿名管道,pipefd[0]用于从管道读取数据,pipefd[1]用于向管道写入数据。
  5. 管道特性:半双工通信,自带同步与互斥,生命周期随进程,提供字节流传输服务。在同步的提现中,若管道所有写段关闭,则从管道中读取完所有数据后,继续read会返回0,不再阻塞;若所有读端关闭,则继续write写入会触发异常导致进程退出
  6. 匿名管道需要在创建子进程之前创建,因为只有这样才能复制到管道的操作句柄,与具有亲缘关系的进程实现访问同一个管道通信
  7. 管道是半双工通信,是可以选择方向的单向通信。不可以实现双向数据传输
  8. 管道的本质是内核中的缓冲区,通过内核缓冲区实现通信,命名管道的文件虽然可见于文件系统,但是只是标识符,并非通信介质。管道实际上是一种固定大小的缓冲区,它存在于内存中,而不是磁盘上。因此,管道的容量大小并不是由磁盘容量大小来限制的
  9. 管道自带同步(没有数据读阻塞,缓冲区写满写阻塞)与互斥⇒ 进程对管道进行读操作和写操作都可能被阻塞
  10. 多个进程只要能够访问同一管道就可以实现通信,不限于读写个数 ⇒ 一个管道只能有一个读进程或一个写进程对其操作⇒ 错误
  11. 管道的本质是内核中的缓冲区,命名管道文件是缓冲区的标识
  12. 管道在缓冲区写满后会写阻塞,跟磁盘空间并无关系
  13. 管道的生命周期随进程,本质是内核中的缓冲区,命名管道文件只是标识,用于让多个进程找到同一块缓冲区,删除管道文件后,之前已经打开管道的进程依然可以通信
  14. 管道的通信本质是通过内核中一块缓冲区(内存)实现数据传输,而命名管道的管道文件只是一个标识符,用于让多个进程能够访问同一块缓冲区
  15. 管道是半双工通信,是可以选择方向的单向通信
  16. 命名管道打开特性为,若以只读方式打开文件,则会阻塞,直到管道被以写的方式打开
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿猿收手吧!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值