0
点赞
收藏
分享

微信扫一扫

python画灰太狼

上次结束了基础IO:Linux:基础IO(三.软硬链接、动态库和静态库、动精态库的制作和加载)


文章目录


1.认识进程间通信

我们通过之前的知识知道,进程具有独立性。两个进程之间时不能进行数据的直接传递的

  • 为什么我们需要进程间通信?

    1. 数据传输:一个进程需要将自己的数据发送给另一个进程。这种通信方式可以实现进程之间的数据交换和共享,从而实现协作和协同工作。
    2. 资源共享:多个进程之间共享同样的资源,如共享内存、共享文件等。通过进程间通信,可以实现多个进程对同一资源的访问和操作,提高资源的利用率和效率。
    3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它们发生了某种事件,如进程终止、资源可用等。通过通知事件,进程可以及时响应和处理其他进程的状态变化,实现进程之间的协作和同步。
    4. 进程控制:有些进程希望完全控制另一个进程的执行,如调试进程需要拦截另一个进程的陷入和异常,并能够及时知道其状态改变。通过进程控制,可以实现对其他进程的监控、调试和管理,确保系统的稳定和安全运行。

    我们往往需要多个进程协同来完成一些任务

  • 进程间通信是什么?

  • 如何进行进程间通信

那么OS提供的样式有:

  1. 管道(匿名、命名)
  2. 共享内存
  3. 消息队列
  4. 信号量

2.管道

在这里插入图片描述

在这里插入图片描述

为了支持管道通信,OS提供了一个接口:pipe()

2.1匿名管道

匿名管道(Anonymous Pipe)Linux中提供的一种进程间通信(IPC)机制。匿名管道没有名字,它们仅存在于创建它们的进程及其子进程之间,并且一旦这些进程终止,管道也将随之消失。

匿名管道的主要特点如下:

  1. 单向通信:匿名管道是半双工的,这意味着数据只能在一个方向上流动。通常,一个进程向管道写入数据,而另一个进程从管道读取数据。如果需要双向通信,则需要创建两个管道,一个用于每个方向。
  2. 亲缘关系:匿名管道只能用于具有亲缘关系的进程之间,即一个进程和它的子进程之间。这是因为管道的文件描述符是通过fork()系统调用在父子进程之间复制的。
  3. 自动管理:当所有使用管道的文件描述符都被关闭时,管道将自动被删除。这意味着不需要像命名管道那样显式地打开和关闭它。
  4. 内存中的缓冲区管道实际上是一个在内核中维护的缓冲区,用于存储从写入端发送但尚未被读取端读取的数据。这个缓冲区的大小是有限的,如果写入的数据超过了缓冲区的大小,写操作可能会被阻塞,直到有空间可用。

在C语言中,可以使用pipe()函数来创建一个匿名管道。这个函数接受一个包含两个文件描述符的数组作为参数,并返回两个文件描述符:一个用于读操作,另一个用于写操作。然后,可以使用fork()创建一个子进程,并在父进程和子进程之间使用这些文件描述符进行通信。

2.2pipe()函数 —创建匿名管道

在这里插入图片描述

pipe函数用于创建管道,这是一种特殊的文件,用于连接一个程序的标准输出和另一个程序的标准输入,从而实现这两个程序之间的通信。在C语言中函数原型为:int pipe(int pipefd[2]);

参数

pipe函数接受一个整型数组作为参数(这是个输出型参数),即int pipefd[2]。这个数组用于存储管道的两个文件描述符:pipefd[0]表示管道的读端,而pipefd[1]表示管道的写端。

作用

调用pipe函数后,系统会创建一个匿名管道,并将这个管道的两个端点(一个用于读,一个用于写)的文件描述符分别赋值给pipefd[0]pipefd[1]。这样,一个进程就可以通过pipefd[1]向管道写入数据,而另一个进程则可以通过pipefd[0]从管道中读取数据。这种机制使得两个进程之间可以通过管道进行通信。

返回值

如果pipe函数成功创建了管道,则返回0。如果创建失败,则返回-1,并将错误原因存储在全局变量errno中。可能的错误原因包括:

  • EMFILE:进程已达到其文件描述符的最大数量。
  • ENFILE:系统已达到其文件描述符的最大数量。
  • EFAULT:传递给pipe函数的数组地址不合法。
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>

void writer(int wfd)//写端的操作
{
    const char *str = "hi father, I am child";
    char buffer[128];
    int cnt = 0;
    pid_t pid = getpid();
    while(1)
    {
        //先调用snprintf向buffer数组里写,然后在把buffer数组写到fd为wfd的文件里(这里就是管道的写端)
        snprintf(buffer, sizeof(buffer), "message: %s, pid: %d, count: %d\n", str, pid, cnt);
        write(wfd, buffer, strlen(buffer));
        cnt++;
        sleep(1);
    }
}

void reader(int rfd)//读端的操作
{
    char buffer[1024];
    while(1)
    {
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        (void)n;
        //没有使用这个 n 变量。如果编译器被配置为警告未使用的变量,那么它就会为 n 发出一个警告
        printf("father gets a message: %s", buffer);
    }
}


int main()
{
    //创建管道
    int pipefd[2];
    int n = pipe(pipefd);
    // pipefd[0]-->read   pipefd[1]-->write  0是写端,1是读端
    // 0-->嘴巴 读书       1-->钢笔 写字
    if(n < 0) return 1;

    //创建子进程
    pid_t id = fork();
    if(id == 0)
    {
        //child: w 我们让子进程来写
        close(pipefd[0]);//那么就要关闭读端

        writer(pipefd[1]);

        exit(0);
    }

    // father: r我们让父进程来读
    close(pipefd[1]);//那么就要关闭写端

    reader(pipefd[0]);
    wait(NULL);

    return 0;
}

在这里插入图片描述

2.3匿名管道的四种情况

  1. 管道内部没有数据而且子进程不关闭自己的写端文件fd, 读端(父)就要阻塞等待,直到pipe有数据

  2. 管道内部被写满而且读端(父进程)不关闭自己的fd,写端(子进程)写满之后,就要阻塞等待

    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    
    void writer(int wfd)//写端的操作
    {
        const char *str = "hi father, I am child";
        char buffer[128];
        int cnt = 0;
        pid_t pid = getpid();
        while(1)
        {
            char ch='A';
            write(wfd, &ch, 1);
            cnt++;
            printf("cnt=%d\n",cnt);
        }
    }
    
    void reader(int rfd)//读端的操作
    {
        char buffer[1024];
        while(1)
        {
            sleep(10);
            ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
            (void)n;
            //没有使用这个 n 变量。如果编译器被配置为警告未使用的变量,那么它就会为 n 发出一个警告
            printf("father gets a message: %s", buffer);
        }
    }
    
    
    int main()
    {
        //创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        // pipefd[0]-->read   pipefd[1]-->write  0是写端,1是读端
        // 0-->嘴巴 读书       1-->钢笔 写字
        if(n < 0) return 1;
    
        //创建子进程
        pid_t id = fork();
        if(id == 0)
        {
            //child: w 我们让子进程来写
            close(pipefd[0]);//那么就要关闭读端
    
            writer(pipefd[1]);
    
            exit(0);
        }
    
        // father: r我们让父进程来读
        close(pipefd[1]);//那么就要关闭写端
    
        reader(pipefd[0]);
        wait(NULL);
    
        return 0;
    }
    

    在这里插入图片描述

  3. 不再向管道写入数据并且关闭了写端(子进程)文件描述符时,读端(父进程)可以继续从管道中读取剩余的数据,直到管道中的数据全部被读取完毕。最后就会读到返回值为0,表示读结束,类似读到了文件的结尾

  4. 读端关闭其文件描述符并且不再读取数据时,如果写端继续向管道写入数据,操作系统会发送一个SIGPIPE信号给写端进程。默认情况下,这个信号会终止写端进程SIGPIPE信号是一个用于处理管道写端在写操作时无读端接收的情况的信号。

    #include<stdio.h>
    #include<string.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    
    void writer(int wfd)//写端的操作
    {
        int cnt = 0;
        while(1)
        {
            sleep(1);
    
            char ch='A';
            write(wfd, &ch, 1);
    
            cnt++;
            printf("cnt=%d\n",cnt);
        }//子进程一直写
    }
    
    void reader(int rfd)//读端的操作
    {
        int cnt=8;
        char buffer[1024];
        while(1)
        {
            sleep(1);
            ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
            if(n>0)
            {
                printf("father get a message: %s, n : %ld\n", buffer, n);
            }
            else if(n==0)
            {
                printf("reading has done: %s  %ld\n", buffer,n);
                break;
            }
            else 
            {
                break;
            }
            cnt--;
            if(cnt==0)
            {
                break;
            }
        }
        close(rfd);//8秒后,父进程不再读,直接关闭
        printf("end");
    }
    
    
    int main()
    {
        //创建管道
        int pipefd[2];
        int n = pipe(pipefd);
        // pipefd[0]-->read   pipefd[1]-->write  0是写端,1是读端
        // 0-->嘴巴 读书       1-->钢笔 写字
        if(n < 0) return 1;
    
        //创建子进程
        pid_t id = fork();
        if(id == 0)
        {
            //child: w 我们让子进程来写
            close(pipefd[0]);//那么就要关闭读端
    
            writer(pipefd[1]);
    
            exit(0);
        }
    
        // father: r我们让父进程来读
        close(pipefd[1]);//那么就要关闭写端
    
        reader(pipefd[0]);
    
        int status=0;
        pid_t rid=waitpid(id,&status,0);
        printf("exit code: %d, exit signal: %d\n",WEXITSTATUS(status),status&0x7f);
        return 0;
    }
    

    在这里插入图片描述

2.4管道的特征

  1. 匿名管道自带同步机制:在匿名管道中,写端在写数据且没有写完时,读端是不可能访问管道这块公共资源的。这种机制确保了数据的完整性和一致性,避免了数据冲突和错误

  2. 管道(Pipe)是一种常用于具有血缘关系进程间通信的机制,特别是在父子进程之间。这里的“血缘关系”指的是进程之间的创建关系,即一个进程创建了另一个进程,它们之间存在直接的父子关系

  3. 管道(pipe)是面向字节流的:这意味着管道在传输数据时,是以字节为单位进行处理的。无论是字符、整数还是其他类型的数据,都会被转换成字节序列进行传输。因此,管道不关心数据的具体格式或类型,只负责将数据以字节流的形式从一个进程传递到另一个进程

  4. 管道(pipe)是半双工的:它只能在一个方向上传输数据,属于单向通信的特殊概念。具体来说,一个管道有一个输入端和一个输出端,数据可以从输入端流入管道,并从输出端流出。但管道不允许数据在相反的方向上流动,即不能从输出端流回输入端

  5. 父子进程退出后,管道会自动释放。这是由操作系统的内存管理机制决定的。当进程结束时,操作系统会回收其占用的所有资源,包括打开的文件、管道、网络连接等

  6. 我们之前在命令行里使用的|其实就是匿名管道:在命令行中,当我们使用|来连接两个命令时,实际上是在这两个命令之间创建了一个匿名管道。这使得前一个命令的输出能够直接传输给后一个命令,实现了两个命令之间的数据共享和传输

3.基于管道的进程池设计


4.命名管道

4.1引入与性质

我们设想一个这样的情况:

  1. 当一个进程打开一个文件(比如log.txt),内核会为该进程创建一个struct file结构体,其中包含指向inode结构体、函数指针数组和缓冲区的指针。这个struct file结构体会指向已加载的inode结构体和缓冲区,用于表示文件在内核中的信息和缓存文件数据。
  2. 当另一个进程也打开同一个文件时,内核会为该进程创建另一个struct file结构体,其中也包含指向相同的inode结构体和缓冲区的指针。这意味着多个进程可以共享相同的inode结构体和缓冲区,而不会为每个进程创建一份完全一样的inode结构体和缓冲区。
  3. 由于inode结构体和缓冲区是在内核中维护的,因此多个进程可以共享相同的inode结构体和缓冲区,而不需要为每个进程复制一份。这种共享机制可以节省内存空间,并确保多个进程对同一文件的操作是一致的。

在这里插入图片描述

4.2命令行创建

命名管道(Named Pipe)是一种特殊的文件,用于进程间通信。它是一种半双工通信方式,允许一个或多个进程之间通过读写同一个文件来进行通信。

  1. 创建命名管道
    命名管道是通过调用mkfifo系统调用来创建的。命名管道在文件系统中以文件的形式存在,但实际上它是一个FIFO(First In First Out)的通信通道。创建命名管道的语法为:

    mkfifo <管道名称>
    

    在这里插入图片描述

  2. 打开和关闭命名管道
    命名管道可以像普通文件一样被打开和关闭。进程可以通过open系统调用打开一个命名管道文件,并通过close系统调用关闭它。在打开命名管道时,进程需要指定相应的读写权限。

  3. 读写数据
    进程可以通过打开的文件描述符对命名管道进行读写操作。一个进程往管道中写入数据,另一个进程从管道中读取数据。命名管道是阻塞的,如果写入进程写入数据时,没有进程读取数据,写入进程会被阻塞直到有进程读取数据

    在这里插入图片描述

  4. 进程间通信
    命名管道通常用于实现进程间通信,特别是在父子进程或者**不相关进程之间**。一个进程可以向命名管道写入数据,另一个进程可以从命名管道读取数据,实现了进程间的数据交换。

4.3程序中创建命名管道

在这里插入图片描述

mkfifo函数是一个UNIX系统中用于创建命名管道(named pipe)的函数。它的作用是在文件系统中创建一个特殊类型的文件,这个文件可以被多个进程用来进行进程间通信。

在C语言中,可以使用mkfifo函数来创建一个命名管道,其原型如下:

int mkfifo(const char *pathname, mode_t mode);
  • pathname参数是指定要创建的命名管道的路径和文件名。
  • mode参数是指定创建的管道的权限模式,通常以八进制表示(例如0666)。

使用mkfifo函数创建命名管道后,其他进程可以通过打开这个路径+文件名来访问这个管道,从而实现进程间的通信。一旦创建了命名管道,它就可以在文件系统中像普通文件一样被打开、读取和写入

写个小项目

项目规划

在这里插入图片描述

Cnmm.hpp
#ifndef __COMM_HPP__
#define __COMM_HPP__

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

using namespace std;

#define Mode 0666
#define Path "./fifo"

class Fifo
{
public:
    Fifo(const string &path) : _path(path)
    {
        umask(0);
        int n = mkfifo(_path.c_str(), Mode);
        if (n == 0)
        {
            cout << "mkfifo success" << endl;
        }
        else
        {
            cerr << "mkfifo failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
        }
    }
    
    ~Fifo()
    {
        int n = unlink(_path.c_str());
        if (n == 0)
        {
            cout << "remove fifo file " << _path << " success" << endl;
        }
        else
        {
            cerr << "remove failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
        }
    }

private:
    string _path; // 文件路径+文件名
};

#endif//条件编译结束
  1. cerr

    • cerr是C++标准库中的标准错误流,它用于输出错误信息到标准错误设备(通常是显示器)。
    • cout(标准输出流)类似,cerr也是一个对象,可以使用插入运算符<<来将数据插入到cerr中进行输出。
    • cout不同的是,cerr通常用于输出错误消息,而不是普通的程序输出。它是线程安全的,可以在多线程环境中使用。
  2. errno

    • errno是一个全局变量,通常定义在<cerrno>头文件中,用于存储函数调用发生错误时的错误码。
    • 当某个函数发生错误时,它会设置适当的错误码到errno中,以便程序能够检测和处理错误。
    • 错误码是整数类型,每个错误码对应于一种特定类型的错误。可以通过查看系统的错误码表来了解每个错误码的含义。
  3. strerror

    • strerror是一个C标准库函数,通常定义在<cstring><string.h>头文件中,用于将错误码转换为对应的错误消息字符串。
    • strerror接受一个错误码作为参数,并返回一个指向描述该错误的字符串的指针。
    • 通过调用strerror(errno),可以获取与当前errno值对应的错误消息字符串,以便程序输出或记录错误信息。
PipeClient.cpp
#include "Comm.hpp"

int main()
{
    // 打开管道,进行写入,最后关闭
    int wfd = open(Path, O_WRONLY | O_CREAT); // 以只写方式打开
    if (wfd < 0)
    {
        cerr << "open failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
        return 1;
    }

    string buffer; // 开始写入
    while (true)
    {
        cout << "please write your message:" << endl;
        getline(cin, buffer);

        ssize_t n = write(wfd, buffer.c_str(), buffer.size());
        if (n < 0)
        {
            cerr << "write failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
            break;
        }
    }

    close(wfd);
    return 0;
}
PipeServe.cpp
#include "Comm.hpp"
#include <unistd.h>

int main()
{
    Fifo fifo(Path);
    // 打开管道,进行读取,最后关闭
    int rfd = open(Path, O_RDONLY); // 以只读方式打开
    if (rfd < 0)
    {
        cerr << "open failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
        return 1;
    }

    char buffer[1024];//开始读取
    while (true)
    {
        ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
        if (n > 0)
        {
            buffer[n] = '\0';
            cout << "client say : " << buffer << endl;
        }
        else if (n == 0)
        {
            cout << "client quit, me too!!" << endl;
            break;
        }
        else
        {
            cerr << "read failed, errno: " << errno << ", errstring: " << strerror(errno) << endl;
            break;
        }
    }

    close(rfd);
    return 0;
}

在这里插入图片描述

文件描述符的阻塞模式和非阻塞模式指的是在进行I/O操作时的行为方式。

  1. 阻塞模式

    • 在阻塞模式下,当进行I/O操作时,如果数据尚未准备好或者操作无法立即完成,程序会被阻塞,也就是暂停执行,直到操作完成或者出现错误为止。
    • 例如,在阻塞模式下,如果调用read函数读取一个文件描述符,但是文件中没有数据可读,程序将会被阻塞,直到有数据到达为止。类似地,如果调用write函数写入数据到一个已满的管道中,程序也会被阻塞,直到有足够的空间写入数据。
  2. 非阻塞模式

    • 在非阻塞模式下,进行I/O操作时,如果操作无法立即完成,程序不会被阻塞,而是立即返回一个错误或者一个特定的状态码,提示当前操作无法立即完成。
    • 例如,在非阻塞模式下,如果调用read函数读取一个文件描述符,但是文件中没有数据可读,read函数将立即返回一个错误码,而不会等待数据到达。类似地,如果调用write函数写入数据到一个已满的管道中,write函数也会立即返回一个错误码,而不会等待空间可用。

5.System V共享内存

在这里插入图片描述

System V共享内存(Shared Memory)是一种Linux中用于进程间通信(IPC)的机制。它允许多个进程访问同一块物理内存区域,从而实现数据的快速共享和交换。

  1. 原理
    • 在物理内存中申请一块内存空间作为共享内存。
    • 将这块内存空间与各个进程的页表建立映射关系,使得这些进程在虚拟地址空间中可以看到并访问这块共享内存。
    • 通过这种方式,多个进程可以像访问自己的内存一样访问共享内存,从而实现数据的快速共享和交换。
  2. 使用方式
    • 创建:使用shmget()系统调用来创建共享内存。这个函数会分配一块指定大小的内存区域,并返回一个标识符,用于后续对这块共享内存的操作。
    • 关联:使用shmat()系统调用来将共享内存关联到进程的地址空间。这个函数会将共享内存的地址告诉进程,使得进程可以通过这个地址来访问共享内存。
    • 取消关联:当进程不再需要访问共享内存时,可以使用shmdt()系统调用来取消关联。这个函数会断开进程与共享内存之间的映射关系。
    • 释放:当所有进程都不再需要这块共享内存时,可以使用shmctl()系统调用来释放它。这个函数会回收这块内存区域,并释放相关的资源。

5.1相关函数介绍

ftok() 函数 Linux中用于生成一个唯一的键值(key)的系统调用,这个键值通常用于在进程间通信(IPC)中标识共享内存段、消息队列或信号量集。ftok() 函数基于一个已经存在的文件路径和一个非零的标识符(通常是一个小的正整数)来生成这个键值。

#include <sys/ipc.h>  
#include <sys/types.h>  
  
key_t ftok(const char *pathname, int proj_id);

参数:

  • pathname:指向一个已经存在的文件路径的指针。这个文件通常被用作生成键值的“种子”或“基础”。
  • proj_id:一个非零的标识符,通常是一个小的正整数。这个值将与文件路径一起被用于生成键值。返回值:

如果成功,ftok() 函数返回一个唯一的键值key_t 类型),该键值可以在后续的 IPC 调用(如 shmget(), msgget(), semget() 等)中用作参数。如果失败,则返回 (key_t) -1 并设置 errno 以指示错误。

  1. shmget():创建或获取共享内存

shmget() 系统调用用于创建一个新的共享内存对象,或者如果它已存在,则返回该对象的标识符。

函数原型

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

参数

  • key:一个键,用于唯一标识共享内存对象。通常使用ftok()函数生成。

  • size:共享内存的大小(以字节为单位)。

  • shmflg:权限标志和选项。通常设置为IPC_CREAT如果对象不存在则创建,存在的话直接获取)和权限(如0666)。

    若设置为IPC_CREAT|IPC_EXCL(如果对象不存在则创建,存在的话出错返回)

返回值:成功时返回共享内存对象的标识符;失败时返回-1并设置errno

  1. shmat():将共享内存关联到进程的地址空间

shmat() 系统调用用于将共享内存对象关联到调用进程的地址空间。

函数原型

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

参数

  • shmidshmget()返回的共享内存对象标识符
  • shmaddr:希望将共享内存附加到的进程的地址。如果设置为NULL,则系统选择地址。
  • shmflg:通常设置为0或SHM_RND(使附加地址向下舍入到最接近的SHMLBA边界)。

返回值:成功时返回共享内存附加到进程的地址;失败时返回(void *)-1并设置errno

  1. shmdt():取消共享内存的关联

shmdt() 系统调用用于取消之前通过shmat()附加到进程的共享内存的关联。

函数原型

int shmdt(const void *shmaddr);

参数

  • shmaddrshmat()返回的共享内存附加到进程的地址。

返回值:成功时返回0;失败时返回-1并设置errno

  1. shmctl():控制共享内存

shmctl() 系统调用用于获取或设置共享内存的属性,或者删除共享内存对象。

函数原型

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

参数

  • shmid:共享内存对象标识符。
  • cmd:要执行的操作。例如,IPC_RMID用于删除共享内存对象,IPC_STAT用于获取其状态。
  • buf:指向shmid_ds结构的指针,用于传递或接收共享内存的状态信息。

返回值:成功时返回0;失败时返回-1并设置errno


今天就到这里了,也是结束了期末周,现在就开始正常更新啦

举报

相关推荐

0 条评论