【Linux】23.进程间通信(2)

文章目录

  • 3. 进程间通信
    • 3.1 进程间通信介绍
      • 3.1.1 进程间通信目的
      • 3.1.2 进程间通信发展
    • 3.2 什么是进程间通信
    • 3.3 管道
    • 3.4 匿名管道pipe()
      • 3.4.1 站在文件描述符角度-深度理解管道
      • 3.4.2 站在内核角度-管道本质
      • 3.4.3 用fork来共享管道原理
      • 3.4.5 管道相关知识
      • 3.4.6 代码一:实现父子进程间的单向通信
      • 3.4.7 代码二:验证杀掉正在写入进程的信号
      • 3.4.8 代码三:使用管道实现一个简易版本的进程池
    • 3.5 命名管道FIFO
      • 3.5.1 为什么命名管道可以在不相关的进程之间交换数据
      • 3.5.2 命名管道的打开规则
      • 3.5.3 一个简单的命名管道代码
      • 3.5.4 命名管道代码:一个简单的日志函数实现
      • 3.5.5 匿名管道与命名管道的区别


3. 进程间通信

3.1 进程间通信介绍

3.1.1 进程间通信目的

数据传输:一个进程需要将它的数据发送给另一个进程

资源共享:多个进程之间共享同样的资源。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另

一个进程的所有陷入和异常,并能够及时知道它的状态改变。


3.1.2 进程间通信发展

  • 管道

    • System V进程间通信

    • POSIX进程间通信

  • 进程间通信分类

    • 管道

    • 匿名管道pipe

    • 命名管道

  • System V IPC

    • System V 消息队列

    • System V 共享内存

    • System V 信号量

  • POSIX IPC

    • 消息队列

    • 共享内存

    • 信号量

    • 互斥量

    • 条件变量

    • 读写锁


3.2 什么是进程间通信

什么是进程间通信?

是两个或者多个进程实现数据层面的交互。

通信是有成本的,因为进程独立性的存在,导致进程通信的成本比较高

  1. 进程间通信的本质:必须让不同的进程看到同一份"资源

  2. 资源:特定形式的内存空间

  3. 这个"资源"谁提供?

    一般是操作系统(第三方空间)。

    为什么不是我们两个进程中的一个呢?

    假设一个进程提供,这个资源属于谁?

    如果是这个进程独有,就会破坏进程独立性。

  4. 我们进程访问这个空间,进行通信,本质就是访问操作系统。

    进程代表的就是用户,(一般而言)“资源”从创建,使用,释放。

    这整个过程通过系统调用接口。

    从底层设计,从接口设计,都要由操作系统独立设计。一般操作系统,会有一个独立的通信模块(隶属于文件系统),这个模块在系统里叫做:IPC通信模块。

    出现了很多的通信方案,所以要定制标准 – 进程间通信是有标准的:system Vposix两套标准。

  5. 基于文件级别的通信方式:管道

直接原理:

11bd99a8a31f23fec0238119c3bb6c0c

进程间通信本质前提是需要先让不同的进程,看到同一份资源。

管道就是文件。

多执行流共享的,难免出现访问冲突的问题(临界资源竞争的问题)。


3.3 管道

什么是管道

管道是Unix中最古老的进程间通信的形式。

我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

319a2238f4430afe4c5c679d78ee356d

管道读写规则:

  • 当没有数据可读时

    • O_NONBLOCK disableread调用阻塞,即进程暂停执行,一直等到有数据来到为止。

    • O_NONBLOCK enableread调用返回-1errno值为EAGAIN。(EAGAIN是一个错误码(errno),表示"再试一次"(Try again))

  • 当管道满的时候

    • O_NONBLOCK disablewrite调用阻塞,直到有进程读走数据

    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN

  • 如果所有管道写端对应的文件描述符被关闭,则read返回0

  • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

  • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

  • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

  • 管道提供流式服务

  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程

  • 一般而言,内核会对管道操作进行同步与互斥

  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道

4761637510e78523bf38edbbed3c556d


3.4 匿名管道pipe()

f9f834aa9620cb16f0dee53b45a078b0

#include <unistd.h>
功能:创建一无名管道
原型
	int pipe(int fd[2]);
参数
	fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
	返回值:成功返回0,失败返回错误代码

3.4.1 站在文件描述符角度-深度理解管道

7025236b4c30c1d05fd3c89db0c3faa0


3.4.2 站在内核角度-管道本质

51b85cc07816a663ff0d2b6615bf8948


3.4.3 用fork来共享管道原理

0424e04200696a6d142d083b1a74fd03


3.4.5 管道相关知识

如果我们想要双向通信,可以建立两个管道。

如果两个进程没有关系,就不能用这个原理进行通信了。

(因为管道创建后,文件描述符只在当前进程可见。无法将文件描述符传递给无关进程,fork()后子进程才会继承父进程的文件描述符。)

(但是可以使用FIFO(命名管道),消息队列,共享内存。这些IPC机制都可以通过某种标识符(如文件路径、key值)在无关进程间建立关联,从而实现通信。)

父子关系,兄弟关系,爷孙关系也可以用管道通信。

管道通信适用于:进程之间需要有血缘关系,常用于父子关系。

至此,我们父进程和子进程还没有通信,只是建立了通信的信道。

这里看起来很费劲,实际上是因为进程具有独立性,通信有成本导致的。

管道的特征:

  1. 具有血缘关系的进程进行进程间通信

  2. 管道只能单向通信

  3. 父子进程是会进程协同的,同步与互斥的 — 保护管道文件的数据安全

  4. 管道是面向字节流的

  5. 管道是基于文件的,而文件的生命周期是随进程的

管道的4种情况:

  1. 读写端正常,管道如果为空,读端就要阻塞(有数据就读,没数据就等)

  2. 读写端正常,管道如果被写满,写端就要阻寒(管道是有固定大小的,在不同内核里可能不一样,所以可以被写满,写满了之后就只能等读端读,才能继续写)

  3. 读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞

  4. 写端是正常写入,读端关闭了。操作系统就要杀掉正在写入的进程(因为操作系统是不会做低效,浪费等类似的工作的。如果做了,就是操作系统的bug)

    如何干掉?通过信号杀掉。这个信号是13号信号。


3.4.6 代码一:实现父子进程间的单向通信

子进程通过管道每隔1秒向父进程发送一个字符'c',总共发送5次,父进程负责接收并显示这些数据。

makefile

testPipe:testPipe.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f testPipe

testPipe.cc

// 必要的头文件包含
#include <iostream>     // C++输入输出
#include <cstdio>      // C标准输入输出
#include <string>      // C++字符串
#include <cstring>     // C字符串操作
#include <cstdlib>     // C标准库函数
#include <unistd.h>    // Unix标准函数定义
#include <sys/types.h> // 基本系统数据类型
#include <sys/wait.h>  // wait()函数

#define N 2            // 管道的两端:读端和写端
#define NUM 1024       // 缓冲区大小

using namespace std;

// 写数据的函数
void Writer(int wfd)   // wfd是管道的写端文件描述符
{
    string s = "hello, I am child";  // 待发送的消息
    pid_t self = getpid();           // 获取进程ID
    int number = 0;                  // 计数器

    char buffer[NUM];                // 数据缓冲区
    while (true)
    {
        sleep(1);    // 每次写入间隔1秒

        // 以下是发送/写入字符串的方式(被注释)
        //buffer[0] = 0;    // 清空缓冲区 
        //snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
        //write(wfd, buffer, strlen(buffer));

        // 当前使用的是发送单个字符的方式
        char c = 'c';
        write(wfd, &c, 1);  // 向管道写入一个字符
        number++;
        cout << number << endl;  // 显示写入次数

        if(number >= 5) break;   // 写入5次后退出
    }
}

// 读数据的函数
void Reader(int rfd)   // rfd是管道的读端文件描述符
{
    char buffer[NUM];  // 数据缓冲区

    while(true)
    {
        buffer[0] = 0;  // 清空缓冲区
        // 从管道读取数据
        ssize_t n = read(rfd, buffer, sizeof(buffer));
        if(n > 0)  // 读取成功
        {
            buffer[n] = 0;  // 添加字符串结束符
            cout << "father get a message[" << getpid() << "]# " << buffer << endl;
        }
        else if(n == 0)  // 读到EOF(管道写端关闭)
        {
            printf("father read file done!\n");
            break;
        }
        else break;      // 读取出错
    }
}

int main()
{
    // 创建管道
    int pipefd[N] = {0};  // pipefd[0]读端,pipefd[1]写端
    int n = pipe(pipefd); // 调用了 pipe() 函数,用于创建一个管道,并将管道的文件描述符存储在 pipefd 数组中。
    if (n < 0)           // 管道创建失败
        return 1;

    // 创建子进程
    pid_t id = fork();
    if (id < 0)          // 进程创建失败
        return 2;
        
    if (id == 0)         // 子进程
    {
        close(pipefd[0]); // 关闭子进程读端
        Writer(pipefd[1]); // 子进程写数据,写入5次字符'c',每次间隔1秒
        close(pipefd[1]); // 关闭子进程写端
        exit(0);         // 子进程退出
    }
    
    // 父进程
    close(pipefd[1]);    // 关闭父进程写端
    Reader(pipefd[0]);   // 父进程读数据

    // 等待子进程结束
    pid_t rid = waitpid(id, nullptr, 0);
    if(rid < 0) return 3;

    close(pipefd[0]);    // 关闭父进程读端

    sleep(5);            // 暂停5秒
    return 0;
}

运行结果:

ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson26/pipe$ ./testPipe
1
father get a message[276150]# c
2
father get a message[276150]# c
3
father get a message[276150]# c
4
father get a message[276150]# c
5
father get a message[276150]# c
father read file done!
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson26/pipe$ 

执行时序:

主进程创建管道
    ↓
fork()创建子进程
    ↓
子进程         父进程
关闭读端       关闭写端
    ↓            ↓
写入'c'        等待读取
sleep(1)       显示读取的数据
写入'c'        等待读取
sleep(1)       显示读取的数据
写入'c'        等待读取
sleep(1)       显示读取的数据
写入'c'        等待读取
sleep(1)       显示读取的数据
写入'c'        等待读取
    ↓            ↓
关闭写端       显示读取的数据
退出          等待子进程结束
              关闭读端
              sleep(5)
              退出

代码主要实现:

  1. 创建匿名管道用于父子进程通信
  2. fork创建子进程
  3. 子进程通过管道每秒向父进程发送一个字符'c',发送5次后退出
  4. 父进程持续从管道读取数据并显示,直到管道关闭
  5. 父进程等待子进程结束后退出

重要的系统调用:

  • pipe():创建管道
  • fork():创建子进程
  • write():写入数据
  • read():读取数据
  • waitpid():等待子进程结束

3.4.7 代码二:验证杀掉正在写入进程的信号

makefile

testPipe:testPipe.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f testPipe

testPipe.cc

#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib> // 提供exit()函数
#include <unistd.h> // 提供 pipe(), fork(), read(), write(), close()等系统调用
#include <sys/types.h> // 提供pid_t类型
#include <sys/wait.h> // 提供waitpid()函数

#define N 2    // pipe数组大小,0用于读端,1用于写端
#define NUM 1024  // 缓冲区大小 

using namespace std;

// 写数据的函数
void Writer(int wfd)
{
    string s = "hello, I am child";
    pid_t self = getpid();  // 获取当前进程ID
    int number = 0;         // 消息序号

    char buffer[NUM];       // 写缓冲区
    while (true)
    {
        sleep(1);          // 每秒写入一次数据
        // 构建发送字符串,格式为:"hello, I am child-进程ID-序号" 
        buffer[0] = 0;     // 清空缓冲区
        snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);
        
        // 通过管道写端写入数据给父进程
        write(wfd, buffer, strlen(buffer));  // 写入字符串长度的数据
    }
}

// 读数据的函数 
void Reader(int rfd)
{
    char buffer[NUM];     // 读缓冲区
    int cnt = 0;         // 读取次数计数
    while(true)
    {
        buffer[0] = 0;   // 清空缓冲区
        // 从管道读端读取数据
        ssize_t n = read(rfd, buffer, sizeof(buffer));
        if(n > 0)        // 读取成功
        {
            buffer[n] = 0;  // 添加字符串结束符
            cout << "father get a message[" << getpid() << "]# " << buffer << endl;
        }
        else if(n == 0)    // 管道写端关闭
        { 
            printf("father read file done!\n");
            break;
        }
        else break;       // 读取错误

        cnt++;
        if(cnt>5) break;  // 最多读取5次
    }
}

int main()
{
    int pipefd[N] = {0};  // 创建管道文件描述符数组
    int n = pipe(pipefd); // 创建管道,pipefd[0]为读端,pipefd[1]为写端
    if (n < 0)           // 创建管道失败
        return 1;

    // 创建子进程
    pid_t id = fork();
    if (id < 0)         // 创建子进程失败
        return 2;
    if (id == 0)       // 子进程
    {
        close(pipefd[0]);  // 关闭读端

        // 执行写操作
        Writer(pipefd[1]); 

        close(pipefd[1]);  // 关闭写端
        exit(0);          // 子进程退出
    }
    
    // 父进程
    close(pipefd[1]);    // 关闭写端

    // 执行读操作
    Reader(pipefd[0]);   // 读取5次数据
    close(pipefd[0]);    // 关闭读端
    cout << "father close read fd: " << pipefd[0] << endl;
    sleep(5);           // 等待5秒,此时子进程已经成为僵尸进程

    // 等待子进程退出并获取退出状态
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);    
    if(rid < 0) return 3;  // 等待失败

    // 打印子进程退出信息
    // status>>8 & 0xFF 获取退出码
    // status & 0x7F 获取信号值
    cout << "wait child success: " << rid << " exit code: " << ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) << endl;

    sleep(5); // 再等待5秒

    cout << "father quit" << endl;  // 父进程退出
    return 0;
}

运行结果:

ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson27/1.pipe$ ./testPipe
father get a message[277093]# hello, I am child-277094-0
father get a message[277093]# hello, I am child-277094-1
father get a message[277093]# hello, I am child-277094-2
father get a message[277093]# hello, I am child-277094-3
father get a message[277093]# hello, I am child-277094-4
father get a message[277093]# hello, I am child-277094-5
father close read fd: 3
wait child success: 277094 exit code: 0 exit signal: 13
father quit
ydk_108@iZuf68hz06p6s2809gl3i1Z:~/108/lesson27/1.pipe$ 

执行时序:

子进程                         父进程
close(pipefd[0])              close(pipefd[1])
    ↓                             ↓
Writer():                     Reader():
循环写入消息                    循环读取消息
格式:"hello,I am child-PID-序号"  最多读5次
    ↓                             ↓
close(pipefd[1])              close(pipefd[0])
exit(0)                       sleep(5)
                             waitpid()等待子进程
                             sleep(5)
                             退出

子进程会一直写入直到被父进程终止,而父进程只读取5次就结束读取。


3.4.8 代码三:使用管道实现一个简易版本的进程池

makefile

ProcessPool:ProcessPool.cc
	g++ -o $@ $^ -g -std=c++11
.PHONY:clean
clean:
	rm -f ProcessPool

Task.hpp

#pragma once

#include <iostream>
#include <vector>

typedef void (*task_t)(); //定义了一个函数指针类型task_t,它指向返回类型为void且不接受任何参数的函数。

void task1()
{
    std::cout << "lol 刷新日志" << std::endl;
}
void task2()
{
    std::cout << "lol 更新野区,刷新出来野怪" << std::endl;
}
void task3()
{
    std::cout << "lol 检测软件是否更新,如果需要,就提示用户" << std::endl;
}
void task4()
{
    std::cout << "lol 用户释放技能,更新用的血量和蓝量" << std::endl;
}

void LoadTask(std::vector<task_t> *tasks) // 该函数接受一个指向std::vector<task_t>的指针,并将其作为参数
{
    tasks->push_back(task1); //将task1函数的地址添加到向量中。
    tasks->push_back(task2);
    tasks->push_back(task3);
    tasks->push_back(task4);
}

ProcessPool.cc

#include "Task.hpp"  // 包含任务相关的头文件,定义了任务类型和函数
#include <string>    // 字符串操作
#include <vector>    // 使用vector容器
#include <cstdlib>   // 系统函数
#include <ctime>     // 时间函数
#include <cassert>   // 断言
#include <unistd.h>  // Unix标准函数
#include <sys/stat.h>    // 文件状态
#include <sys/wait.h>    // 进程等待

const int processnum = 10;      // 定义进程池中的进程数量
std::vector<task_t> tasks;      // 存储所有可执行的任务

// channel类:管理父子进程间的通信通道
class channel
{
public:
    // 构造函数:初始化通信管道
    channel(int cmdfd, int slaverid, const std::string &processname)
    :_cmdfd(cmdfd), _slaverid(slaverid), _processname(processname)
    {}
public:
    int _cmdfd;               // 命令管道的文件描述符(写端)
    pid_t _slaverid;          // 对应子进程的进程ID
    std::string _processname; // 进程的名称,用于显示和日志
};

// 子进程的主要执行函数
void slaver()
{
    while(true)
    {
        int cmdcode = 0;
        // 从标准输入读取命令(标准输入被重定向到了管道)
        int n = read(0, &cmdcode, sizeof(int)); 
        if(n == sizeof(int))
        {
            // 收到命令后打印信息并执行对应任务
            std::cout <<"slaver say@ get a command: "<< getpid() << " : cmdcode: " <<  cmdcode << std::endl;
            if(cmdcode >= 0 && cmdcode < tasks.size()) tasks[cmdcode]();
        }
        if(n == 0) break;  // 管道被关闭,退出循环
    }
}

// 初始化进程池,创建子进程和通信管道
void InitProcessPool(std::vector<channel> *channels)
{
    std::vector<int> oldfds;  // 存储已创建的管道文件描述符
    for(int i = 0; i < processnum; i++)
    {
        int pipefd[2];
        int n = pipe(pipefd);  // 创建管道,pipefd[0]读端,pipefd[1]写端
        assert(!n);
        (void)n;

        pid_t id = fork();     // 创建子进程
        if(id == 0) // 子进程执行的代码
        {
            // 关闭继承自父进程的所有历史文件描述符
            std::cout << "child: " << getpid() << " close history fd: ";
            for(auto fd : oldfds) {
                std::cout << fd << " ";
                close(fd);
            }
            std::cout << "\n";

            close(pipefd[1]);  // 关闭写端
            dup2(pipefd[0], 0);  // 将管道读端重定向到标准输入
            close(pipefd[0]);    // 关闭原读端
            slaver();            // 执行子进程的主要逻辑
            std::cout << "process : " << getpid() << " quit" << std::endl;
            exit(0);
        }
        // 父进程执行的代码
        close(pipefd[0]);  // 关闭读端

        // 创建新的channel对象并保存
        std::string name = "process-" + std::to_string(i);
        channels->push_back(channel(pipefd[1], id, name));
        oldfds.push_back(pipefd[1]);  // 保存文件描述符

        sleep(1);  // 等待1秒,确保进程创建的有序性
    }
}

// 打印所有channel的信息,用于调试
void Debug(const std::vector<channel> &channels)
{
    for(const auto &c :channels)
    {
        std::cout << c._cmdfd << " " << c._slaverid << " " << c._processname << std::endl;
    }
}

// 显示操作菜单
void Menu()
{
    std::cout << "################################################" << std::endl;
    std::cout << "# 1. 刷新日志             2. 刷新出来野怪        #" << std::endl;
    std::cout << "# 3. 检测软件是否更新      4. 更新用的血量和蓝量  #" << std::endl;
    std::cout << "#                         0. 退出               #" << std::endl;
    std::cout << "#################################################" << std::endl;
}

// 控制子进程执行任务的主函数
void ctrlSlaver(const std::vector<channel> &channels)
{
    int which = 0;  // 当前选择的进程索引
    while(true)
    {
        int select = 0;
        Menu();
        std::cout << "Please Enter@ ";
        std::cin >> select;

        if(select <= 0 || select >= 5) break;  // 退出条件
        
        int cmdcode = select - 1;  // 将选项转换为命令代码

        // 向选中的子进程发送任务,并打印信息
        std::cout << "father say: " << " cmdcode: " <<
            cmdcode << " already sendto " << channels[which]._slaverid << " process name: " 
                << channels[which]._processname << std::endl;
        
        write(channels[which]._cmdfd, &cmdcode, sizeof(cmdcode));

        which++;  // 轮询选择下一个进程
        which %= channels.size();
    }
}

// 清理进程池,关闭所有管道和进程
void QuitProcess(const std::vector<channel> &channels)
{
    for(const auto &c : channels){
        close(c._cmdfd);  // 关闭管道
        waitpid(c._slaverid, nullptr, 0);  // 等待子进程结束
    }
}

int main()
{
    LoadTask(&tasks);  // 加载任务列表
            
    // 初始化随机数种子
    srand(time(nullptr)^getpid()^1023);
    
    std::vector<channel> channels;
    InitProcessPool(&channels);  // 初始化进程池
    
    ctrlSlaver(channels);  // 运行任务分发循环
    
    QuitProcess(channels);  // 清理资源
    return 0;
}

这个程序的详细解释我单独放在了一篇博客里进行讲解,有兴趣的可以看看:

使用管道实现一个简易版本的进程池

这个程序实现了一个简单的进程池系统,主要功能如下:

  1. 核心功能:
  • 创建一个包含10个子进程的进程池
  • 通过管道实现父子进程间的通信
  • 父进程可以向子进程分配不同的任务
  • 使用轮询方式分配任务
  1. 工作流程:
  • 程序启动后创建10个子进程
  • 每个子进程都有独立的管道用于接收命令
  • 父进程通过菜单界面接收用户输入
  • 根据用户选择的任务,轮询分配给子进程执行
  • 子进程接收到命令后执行对应的任务

3.5 命名管道FIFO

  • 管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。

  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。

  • 命名管道是一种特殊类型的文件

mkfifo(FIFO_FILE, MODE) 是创建一个命名管道(FIFO)的系统调用

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

参数含义:

  1. FIFO_FILE:命名管道的路径名(字符串)
    • 例如:"/tmp/myfifo"
  2. MODE:设置管道的访问权限(八进制)
    • 常见值:0666 (rw-rw-rw-)
    • 0644 (rw-r--r--)
    • 0777 (rwxrwxrwx)

返回值:

  • 成功返回0
  • 失败返回-1

3.5.1 为什么命名管道可以在不相关的进程之间交换数据

进程间通信的前提:先让不同进程看到同一份资源

一般进程通信,我们只想要使用它的内存级缓冲区,不想要把数据写入磁盘文件(刷盘)。但是打开普通文件的话就会把数据写进去,所以就出现了一个新的文件类型:管道文件。

管道文件是一个内存级文件,不需要把数据写进磁盘去(刷盘)。

理解:

  1. 如果两个不同的进程,打开同一个文件的时候,在内核中,操作系统会打开几个文件?

    打开一个文件。

  2. 你怎么知道你们两个打开的是同一个文件?为什么要打开同一个文件?

    同路径下同一个文件名 = 路径 +文件名


3.5.2 命名管道的打开规则

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

  • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO

  • O_NONBLOCK enable:立刻返回成功

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

  • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO

  • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO


3.5.3 一个简单的命名管道代码

写进程 (writer.cpp):

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string>

int main() {
    const char* FIFO = "/tmp/myfifo";    // 管道文件路径
    
    // 创建命名管道
    mkfifo(FIFO, 0666);
    
    // 打开管道的写端
    int fd = open(FIFO, O_WRONLY);
    if(fd == -1) {
        std::cout << "打开管道失败" << std::endl;
        return 1;
    }
    
    // 写入数据
    std::string msg = "Hello from writer!";
    write(fd, msg.c_str(), msg.size());
    
    // 关闭管道
    close(fd);
    return 0;
}

读进程 (reader.cpp):

#include <iostream>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main() {
    const char* FIFO = "/tmp/myfifo";    // 管道文件路径
    char buf[100] = {0};                 // 读取缓冲区
    
    // 打开管道的读端
    int fd = open(FIFO, O_RDONLY);
    if(fd == -1) {
        std::cout << "打开管道失败" << std::endl;
        return 1;
    }
    
    // 读取数据
    read(fd, buf, sizeof(buf));
    std::cout << "收到消息: " << buf << std::endl;
    
    // 关闭管道
    close(fd);
    return 0;
}

使用方法:

  1. 编译两个程序:
g++ writer.cpp -o writer
g++ reader.cpp -o reader
  1. 在两个终端中分别运行:
# 终端1
./reader

# 终端2
./writer

运行结果:

reader将显示:“收到消息: Hello from writer!”

注意事项:

  1. 需要先运行reader,再运行writer
  2. 管道文件会在/tmp目录下创建
  3. 使用完可以删除管道文件:rm /tmp/myfifo

3.5.4 命名管道代码:一个简单的日志函数实现

makefile

.PHONY:all
all:server client

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

.PHONY:clean
clean:
	rm -f server client

comm.hpp

#pragma once  // 防止头文件重复包含

// 包含必要的系统头文件
#include <iostream>
#include <string>
#include <cerrno>      // 错误号定义
#include <cstring>     // 字符串操作
#include <cstdlib>     // 标准库函数
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h>  // 文件状态
#include <unistd.h>    // UNIX标准函数
#include <fcntl.h>     // 文件控制

// 定义命名管道文件名和权限
#define FIFO_FILE "./myfifo"
#define MODE 0664  // 用户读写,组读写,其他读

// 错误码枚举
enum
{
    FIFO_CREATE_ERR = 1,  // 创建管道失败
    FIFO_DELETE_ERR,      // 删除管道失败
    FIFO_OPEN_ERR         // 打开管道失败
};

// 初始化类,用于管理命名管道的创建和删除
class Init
{
public:
    Init()
    {
        // 创建命名管道
        int n = mkfifo(FIFO_FILE, MODE);
        if (n == -1)
        {
            perror("mkfifo");
            exit(FIFO_CREATE_ERR);
        }
    }
    ~Init()
    {
        // 删除命名管道
        int m = unlink(FIFO_FILE);
        if (m == -1)
        {
            perror("unlink");
            exit(FIFO_DELETE_ERR);
        }
    }
};

log.hpp

#pragma once

// 包含必要的系统头文件
#include <iostream>
#include <time.h>      // 时间相关
#include <stdarg.h>    // 变参函数
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024  // 缓冲区大小

// 日志级别定义
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

// 日志输出方式
#define Screen 1      // 输出到屏幕
#define Onefile 2     // 输出到单个文件
#define Classfile 3   // 根据日志级别输出到不同文件

#define LogFile "log.txt"  // 日志文件名

class Log
{
public:
    Log()
    {
        printMethod = Screen;  // 默认输出到屏幕
        path = "./log/";      // 日志文件路径
    }

    // 设置日志输出方式
    void Enable(int method)
    {
        printMethod = method;
    }

    // 将日志级别转换为字符串
    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    



    // void logmessage(int level, const char *format, ...)
    // {
    //     time_t t = time(nullptr);
    //     struct tm *ctime = localtime(&t);
    //     char leftbuffer[SIZE];
    //     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    //              ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
    //              ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

    //     // va_list s;
    //     // va_start(s, format);
    //     char rightbuffer[SIZE];
    //     vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
    //     // va_end(s);

    //     // 格式:默认部分+自定义部分
    //     char logtxt[SIZE * 2];
    //     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

    //     // printf("%s", logtxt); // 暂时打印
    //     printLog(level, logtxt);
    // }
    // 打印日志的具体实现
    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }

    // 输出到单个文件
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }

    // 根据日志级别输出到不同文件
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level);
        printOneFile(filename, logtxt);
    }

    // 重载函数调用运算符,支持格式化输出
    void operator()(int level, const char *format, ...)
    {
        // 获取当前时间
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        
        // 构造日志前缀(时间和级别信息)
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", 
                levelToString(level).c_str(),
                ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        // 处理变参部分
        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 组合完整日志信息
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

        // 输出日志
        printLog(level, logtxt);
    }

    ~Log()
    {
    }
    

private:
    int printMethod;        // 日志输出方式
    std::string path;       // 日志文件路径
};

// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);

//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
//         n--;
//     }

//     va_end(s); //s = NULL
//     return sum;
// }

server.cc

#include "comm.hpp"
#include "log.hpp"

using namespace std;

int main()
{
    Init init;  // 创建命名管道
    Log log;    // 创建日志对象
    log.Enable(Onefile);  // 设置日志输出到文件

    // 以只读方式打开管道
    int fd = open(FIFO_FILE, O_RDONLY);  // 阻塞等待写入方打开
    if (fd < 0)
    {
        log(Fatal, "error string: %s, error code: %d", strerror(errno), errno);
        exit(FIFO_OPEN_ERR);
    }

    // 记录不同级别的日志
    log(Info, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Warning, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Fatal, "server open file done, error string: %s, error code: %d", strerror(errno), errno);
    log(Debug, "server open file done, error string: %s, error code: %d", strerror(errno), errno);

    // 循环读取客户端发送的消息
    while (true)
    {
        char buffer[1024] = {0};
        // read函数从管道(fd)中读取数据
        // sizeof(buffer)指定最多读取的字节数
        // x 存储实际读取的字节数
        int x = read(fd, buffer, sizeof(buffer));
        if (x > 0)  // 读取成功
        {
            buffer[x] = 0; // 在读取到的数据末尾添加字符串结束符'\0'
            cout << "client say# " << buffer << endl; // 打印客户端发送的消息
        }
        else if (x == 0)  // x == 0 表示客户端关闭了连接(发送了EOF)
        {
            log(Debug, "client quit, me too!, error string: %s, error code: %d", strerror(errno), errno); // 记录日志,包含错误信息和错误码
            break;
        }
        else  // 读取错误
            break;
    }

    close(fd);  // 关闭管道
    return 0;
}

client.cc

#include <iostream>
#include "comm.hpp"

using namespace std;

int main()
{
    // 以只写方式打开命名管道
    int fd = open(FIFO_FILE, O_WRONLY);
    if(fd < 0) // 打开失败时fd返回-1
    {
        perror("open");
        exit(FIFO_OPEN_ERR);
    }

    cout << "client open file done" << endl;

    // 循环读取用户输入并发送到服务端
    string line;
    while(true)
    {
        cout << "Please Enter@ ";
        getline(cin, line);  // 读取一行输入

        // 将输入写入管道
        write(fd, line.c_str(), line.size());
    }

    close(fd);  // 关闭管道
    return 0;
}

实现client.ccserver.cc的通信

运行结果:

20c61708ab31ac5bb52c6036f5827e5e

日志:

65de8ad203a3dee663be3de95e72c4c3


3.5.5 匿名管道与命名管道的区别

匿名管道由pipe函数创建并打开。

命名管道由mkfifo函数创建,打开用open

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/965127.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AI大模型开发原理篇-8:Transformer模型

近几年人工智能之所以能迅猛发展&#xff0c;主要是靠2个核心思想&#xff1a;注意力机制Attention Mechanism 和 Transformer模型。本次来浅谈下Transformer模型。 重要性 Transformer模型在自然语言处理领域具有极其重要的地位&#xff0c;为NLP带来了革命性的突破‌。可以…

html2canvas绘制页面并生成图像 下载

1. 简介 html2canvas是一个开源的JavaScript库&#xff0c;它允许开发者在用户的浏览器中直接将HTML元素渲染为画布&#xff08;Canvas&#xff09;&#xff0c;并生成图像。以下是对html2canvas的详细介绍&#xff1a; 2. 主要功能 html2canvas的主要功能是将网页中的HTML元…

基于RK3588/RK3576+MCU STM32+AI的储能电站电池簇管理系统设计与实现

伴随近年来新型储能技术的高质量规模化发展&#xff0c;储能电站作为新能源领域的重要载体&#xff0c; 旨在配合逐步迈进智能电网时代&#xff0c;满足电力系统能源结构与分布的创新升级&#xff0c;给予相应规模 电池管理系统的设计与实现以新的挑战。同时&#xff0c;电子系…

机器学习-线性回归(参数估计之结构风险最小化)

前面我们已经了解过关于机器学习中的结构风险最小化准则&#xff0c;包括L1 正则化&#xff08;Lasso&#xff09;、L2 正则化&#xff08;Ridge&#xff09;、Elastic Net&#xff0c;现在我们结合线性回归的场景&#xff0c;来了解一下线性回归的结构风险最小化&#xff0c;通…

【数据分析】豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask)

豆瓣电影Top250的数据分析与Web网页可视化(numpy+pandas+matplotlib+flask) 豆瓣电影Top250官网:https://movie.douban.com/top250写在前面 实验目的:实现豆瓣电影Top250详情的数据分析与Web网页可视化。电脑系统:Windows使用软件:PyCharm、NavicatPython版本:Python 3.…

备考蓝桥杯8——EEPROM读写

目录 看手册时间 关于IIC 附录 IIC代码 看手册时间 我们主要是搞编程&#xff0c;所以&#xff0c;我们一般会非常关心我们如何对EEPROM进行编程。特别的&#xff0c;EEPROM要做读写&#xff0c;首先是看它的IIC设备地址。 有趣的是——我们的EEPROM的IIC地址是根据地址进行…

深入浅出:旋转变位编码(RoPE)在现代大语言模型中的应用

在现代大语言模型&#xff08;LLMs&#xff09;中&#xff0c;位置编码是一个至关重要的组件。无论是 Meta 的 LLaMA 还是 Google 的 PaLM&#xff0c;这些模型都依赖于位置编码来捕捉序列中元素的顺序信息。而旋转变位编码&#xff08;RoPE&#xff09; 作为一种创新的位置编码…

“message“: “类型注释只能在 TypeScript 文件中使用

VScode中使用CtrlShiftP打开搜素框&#xff0c;输入Preferences: Open User Settings或Preferences: Open Workspace Settings。 找到settings.json文件 "typescript.validate.enable": false

VSCode中使用EmmyLua插件对Unity的tolua断点调试

一.VSCode中搜索安装EmmyLua插件 二.创建和编辑launch.json文件 初始的launch.json是这样的 手动编辑加上一段内容如下图所示&#xff1a; 三.启动调试模式&#xff0c;并选择附加的进程

SQL 秒变三线表 sql导出三线表

&#x1f3af;SQL 秒变三线表&#xff0c;校园小助手超神啦 宝子们&#xff0c;搞数据分析、写论文的时候&#xff0c;从 SQL 里导出数据做成三线表是不是特别让人头疼&#x1f629; 手动调整格式&#xff0c;不仅繁琐&#xff0c;还容易出错&#xff0c;分分钟把人逼疯&#…

学习threejs,pvr格式图片文件贴图

&#x1f468;‍⚕️ 主页&#xff1a; gis分享者 &#x1f468;‍⚕️ 感谢各位大佬 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍⚕️ 收录于专栏&#xff1a;threejs gis工程师 文章目录 一、&#x1f340;前言1.1 ☘️PVR贴图1.2 ☘️THREE.Mesh…

力扣1022. 从根到叶的二进制数之和(二叉树的遍历思想解决)

Problem: 1022. 从根到叶的二进制数之和 文章目录 题目描述思路复杂度Code 题目描述 思路 遍历思想(利用二叉树的先序遍历) 1.在先序遍历的过程中&#xff0c;用一个变量path记录并更新其经过的路径上的值&#xff0c;当遇到根节点时再将其加到结果值res上&#xff1b; 2.该题…

.NET 中实现生产者-消费者模型,BlockingCollection<T> 和 Channel<T>使用示例

一、方案对比&#xff1a;不同线程安全集合的适用场景 二、推荐方案及示例代码 方案 1&#xff1a;使用 BlockingCollection&#xff08;同步模型&#xff09; public class QueueDemo {private readonly BlockingCollection<int> _blockingCollection new BlockingCo…

C_位运算符及其在单片机寄存器的操作

C语言的位运算符用于直接操作二进制位&#xff0c;本篇简单结束各个位运算符的作业及其在操作寄存器的应用场景。 一、位运算符的简单说明 1、按位与运算符&#xff08;&&#xff09; 功能&#xff1a;按位与运算符对两个操作数的每一位执行与操作。如果两个对应的二进制…

Redis入门概述

1.1、Redis是什么 Redis&#xff1a;官网 高性能带有数据结构的Key-Value内存数据库 Remote Dictionary Server&#xff08;远程字典服务器&#xff09;是完全开源的&#xff0c;使用ANSIC语言编写遵守BSD协议&#xff0c;例如String、Hash、List、Set、SortedSet等等。数据…

个人毕业设计--基于HarmonyOS的旅行助手APP的设计与实现(挖坑)

在行业混了短短几年&#xff0c;却总感觉越混越迷茫&#xff0c;趁着还有心情学习&#xff0c;把当初API9 的毕业设计项目改成API13的项目。先占个坑&#xff0c;把当初毕业设计的文案搬过来 摘要&#xff1a;HarmonyOS&#xff08;鸿蒙系统&#xff09;是华为公司推出的面向全…

C++11详解(二) -- 引用折叠和完美转发

文章目录 2. 右值引用和移动语义2.6 类型分类&#xff08;实践中没什么用&#xff09;2.7 引用折叠2.8 完美转发2.9 引用折叠和完美转发的实例 2. 右值引用和移动语义 2.6 类型分类&#xff08;实践中没什么用&#xff09; C11以后&#xff0c;进一步对类型进行了划分&#x…

车载以太网__传输层

车载以太网中&#xff0c;传输层和实际用的互联网相差无几。本篇文章对传输层中的IP进行介绍 目录 什么是IP&#xff1f; IP和MAC的关系 IP地址分类 私有IP NAT DHCP 为什么要防火墙穿透&#xff1f; 广播 本地广播 直接广播 本地广播VS直接广播 组播 …

大数据学习之Spark分布式计算框架RDD、内核进阶

一.RDD 28.RDD_为什么需要RDD 29.RDD_定义 30.RDD_五大特性总述 31.RDD_五大特性1 32.RDD_五大特性2 33.RDD_五大特性3 34.RDD_五大特性4 35.RDD_五大特性5 36.RDD_五大特性总结 37.RDD_创建概述 38.RDD_并行化创建 演示代码&#xff1a; // 获取当前 RDD 的分区数 Since ( …

第一性原理:游戏开发成本的思考

利润 营收-成本 营收定价x销量x分成比例 销量 曝光量x 点击率x &#xff08;购买率- 退款率&#xff09; 分成比例 100%- 平台抽成- 税- 引擎费- 发行抽成 成本开发成本运营成本 开发成本 人工外包办公地点租金水电设备折旧 人工成本设计成本开发成本迭代修改成本后续内容…