Linux进程间通信—管道

作者:泠沫
博客主页:泠沫的博客
专栏:Linux系统编程,文件认识与理解,Linux进程学习…
觉得博主写的不错的话,希望大家三连(✌关注,✌点赞,✌评论),多多支持一下!!
Linux进程间通信—管道_第1张图片

目录

  • 进程间通信
    • 通信目的
    • 通信发展
    • 通信分类
  • 匿名管道
    • 匿名管道原理
    • 匿名管道读写规则
    • 匿名管道特点
    • 基于匿名管道的简易版进程池的实现
  • 命名管道
    • 命名管道原理
    • 命令行创建命名管道
    • 命名与匿名管道区别
    • 命名管道实现两进程通信

进程间通信

本次介绍进程通信主要是讲解如何让两个进程看到同一份资源,而不是来进行通信的。

通信目的

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

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

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

  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另
    一个进程的所有陷入和异常,并能够及时知道它的状态改变。

总的来说,Linux进程进行通信的目的是为了协调多个进程之间的操作,共享资源和信息,提高应用程序的可靠性和强大性。

通信发展

  • 管道

  • System V进程间通信

  • POSIX进程间通信

通信分类

  • 管道
    匿名管道
    命名管道

  • System V IPC
    System V 消息队列
    System V 共享内存
    System V 信号量

  • POSIX IPC
    消息队列
    共享内存
    信号量
    互斥量
    条件变量
    读写锁

匿名管道

匿名管道原理

匿名管道是一种在进程间进行数据传递的机制,其本质基于操作系统内核提供的缓冲区来实现。通过使用类似于文件的方式进行读写,管道可以将一个进程生成的数据传递给另一个进程,而不需要将数据写入到实际的物理文件中,这样就避免了IO操作,从而提高效率。

  1. 匿名管道的创建是由操作系统来执行的,我们可以使用一个系统调用接口pipe,让操作系统创建一个匿名管道,当成功创建管道文件后,调用该接口的进程就会拿到管道的读写端文件描述符fds[2]。
  2. 然后再调用fork接口创建子进程,子进程是以父进程的进程控制块为模板进行创建的,所以子进程的进程控制块指向的文件描述符表中的文件描述符数组和父进程一样,都保存了管道的读写端。
  3. 最后,假设我们让父进程调用close系统接口关闭写端,子进程关闭读端。仅由子进程进行写操作,父进程进行读操作。那么匿名管道就可以实现两个具有血缘关系的进程进行通信。

Linux进程间通信—管道_第2张图片
下面介绍创建匿名管道的系统调用接口pipe():
在这里插入图片描述

该系统接口的参数是一个输出型参数,需要我们自己传入一个整型数组,如果创建匿名管道成功,该数组保存的是管道的读写端文件描述符。pipefd[0]存放的是读取端的文件描述符,pipefd[1]存放的是写入端的文件描述符。

该系统接口调用成功返回0,调用失败返回-1,且错误码被设置。
接下来用代码展示如何创建匿名管道,并且实现两个父子进程进行通信。

#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
int main()
{
    int fds[2];
    int n = pipe(fds);
    assert(n != -1);
    pid_t id = fork();
    assert(id >= 0);
    if(id == 0)
    {
        close(fds[0]);
        int cnt = 0;
        while(true)
        {
            char buffer[1024];
            snprintf(buffer, sizeof(buffer), "child -> parent say: 我是子进程[%d],pid:%d", ++cnt, getpid());
            write(fds[1], buffer, strlen(buffer));
            //sleep(100);
            sleep(1);
            if(cnt == 10)
                break;
        }   
        close(fds[1]);
        exit(0);
    }
    close(fds[1]);
    while(true)
    {
        //sleep(100);
        char buffer[1024];
        ssize_t num = read(fds[0], buffer, sizeof(buffer) - 1);
        if(num == -1)
        {
            perror("read failed");
            break;
        }
        if(num > 0)
        {
            buffer[num] = 0;
            cout << "Get messge: " << buffer << endl;
        }
        if(num == 0)
        {
            cout << "read over...." << endl;
            break;
        }
    }
    int status = 0;
    int ret = waitpid(id, &status, 0);
    cout << "exit code:" << ((status >> 8) & 0xff) << " exit signal:" << (status & 0x7f) << endl;

    return 0;
}

Linux进程间通信—管道_第3张图片

在上面的代码中,子进程负责往管道进行写入,父进程负责读出子进程写入管道中的内容,并将其打印到显示器上。当子进程往管道中进行10次数据写入之后,子进程退出,父进程等待回收已经推出的子进程,并打印出子进程的退出码和退出信号。

匿名管道读写规则

  1. 子进程写入慢,父进程读取快。那么父进程会进入阻塞状态,等待子进程将数据写入管道中再读取。

  2. 子进程写入快,父进程读取慢。那么父进程读取到的数据量取决于子进程的写入速度。

  3. 子进程关闭写端,父进程在读取的时候读到0,然后父进程回收已经退出的子进程。

  4. 父进程关闭读端,子进程会收到来自操作系统发出的终止进程的信号SIGPIPE。

匿名管道特点

  1. 管道的生命周期是随进程一起的,进程退出,管道的生命周期结束。

  2. 匿名管道只能用来具有血缘关系的两个进程的通信,一般用于父子进程。

  3. 管道是面向字节流的。

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

  5. 管道的互斥与同步机制是对共享资源进行保护的一种策略。

基于匿名管道的简易版进程池的实现

对于该进程池的简要描述:

  1. 首先我们要创建出一批子进程,然后让一个父进程来控制这些子进程,给这些子进程派发任务。
  2. 创建出一批管道,自己定义一个结构体,用来描述每一个管道和其对应的子进程。
  3. 创建出一批任务,可以用函数指针回调函数的方法让子进程完成任务。
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PROPOOLNUM 5
typedef void (*func)(int, int);
class proc
{
public:
    proc(int pid, int wfd)
        :pid_(pid), wfd_(wfd)
    {
        num++;
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "子进程%d, pid:%d", num, pid_);
        name_ = buffer;
    }
    static int num;
    string name_;
    int pid_; // 子进程pid
    int wfd_; // 子进程与父进程对应管道的写端
};
int proc::num = 0;
void Add(int a, int b)
{
    cout << "运算过程-> "<< a << " + " << b << " = " << a+b << endl << endl;
}
void Sub(int a, int b)
{
    cout << "运算过程-> "<< a << " - " << b << " = " << a-b << endl << endl;
}
void Mul(int a, int b)
{
    cout << "运算过程-> "<< a << " * " << b << " = " << a*b << endl << endl;
}
void Div(int a, int b)
{
    cout << "运算过程-> "<< a << " / " << b << " = " << a/b << endl << endl;
}
void creat_funcmap(vector<func>& fm)
{
    fm.push_back(Add);
    fm.push_back(Sub);
    fm.push_back(Mul);
    fm.push_back(Div);
}
void creat_procpool(vector<proc>& vp, vector<func>& fm)
{
    for(int i = 0; i < PROPOOLNUM; i++)
    {
        int fds[2];
        int n = pipe(fds);
        assert(n == 0);
        pid_t id = fork();
        assert(id >= 0);
        if(id == 0)
        {
            close(fds[1]);
            while(true)
            {
                int arr[3];
                ssize_t n = read(fds[0],(void*)arr,sizeof(arr));
                if(n > 0)   int ret = fm[arr[2]](arr[0], arr[1]);
                else if(n == 0)    exit(0);
                else    perror("read:");
            }
        }
        close(fds[0]);
        proc pb(id, fds[1]);
        vp.push_back(pb);
    }
}
void assign_tasks(vector<proc>& vp)
{
    int cnt  = 0;
    while(true)
    {
        if((cnt++) == 3)	break;
        usleep(500);
        srand((unsigned long)time(nullptr) ^ getpid() ^ 0x171237 ^ rand() % 1234);
        int proc_index = rand() % vp.size();
        int a = 0, b = 0, c = 0;
        cout << "#####请输入两个数:" ;
        cin >> a >> b;
        while(true)
        {
            cout << "******* 0.Add  1.Sub *******" << endl;
            cout << "******* 2.Mul  3.Div *******" << endl;
            cout << "#####请选择要实现的运算任务:";
            cin >> c;
            if (c > 3 || c < 0) cout << "选择错误,请重新选择!" << endl;
            else break;
        }
        int arr[3]={a,b,c};
        cout << "进程" << proc_index << "收到"<< c << "号任务,正在处理中......." << endl;
        ssize_t n = write(vp[proc_index].wfd_, (void*) arr, sizeof(arr));
        assert(n == sizeof(arr));
        
    }
    for(int i = 0; i < vp.size(); i++) close(vp[i].wfd_);
}
void wait_process(std::vector<proc>& processes)
{
    int processnum = processes.size();
    for(int i = 0; i < processnum; i++)
    {
        waitpid(processes[i].pid_, nullptr, 0);
        std::cout << "wait sub process success ...: " << processes[i].pid_ << std::endl;
    }
}
int main()
{
    vector<proc> vp;
    vector<func> funcmap;
    creat_funcmap(funcmap);
    creat_procpool(vp, funcmap);
    assign_tasks(vp);
    wait_process(vp);
    return 0;
}

命名管道

命名管道原理

命名管道是用于进程间通信的一种机制,其原理是在文件系统中建立一个特殊类型的文件节点,使得多个进程可以通过该文件节点进行数据传输。

命名管道与普通文件不同之处在于,其数据并不存储在磁盘中,而是存储在内核中的缓冲区中。当有进程向管道中写入数据时,数据会先被放入缓冲区中,当下一个进程读取数据时,就会从缓冲区中取出。因此,管道实际上就是一个临时存放数据的缓冲区,它允许多个进程共享数据,并且按照先进先出的原则进行数据传输。

命令行创建命名管道

在命令行中,我们可以使用mkfifo命令来创建一个管道文件(命名管道)。然后我们可以试着用三个会话窗口分别执行一下三个指令:

  1. 第一个会话窗口使用重定向的方式每隔一秒往管道文件中写入内容。

  2. 第二个会话窗口每隔一秒查看当前文件属性,主要关注管道文件的大小属性。

  3. 第三个会话窗口则是查看管道文件的内容。
    Linux进程间通信—管道_第4张图片

通过上述实验现象,我们可以发现当我们不断地往管道文件中进行写入的时候,管道文件的大小一直都是0,这是因为命名管道虽然是一个磁盘上的文件,但是往命名管道内写数据是会保存在内存缓冲区中的,命名管道是不会与磁盘外设进行IO的,所以命名管道内的数据不会加载到磁盘,所以命名管道的大小一直都是0。

我们观察第三个会话窗口可以发现,命名管道中的数据是可以被查看的,也就是说命名管道实现了两个毫无关系的进程进行通信。

命名与匿名管道区别

  • 首先,匿名管道用于有亲缘关系的进程之间进行通信,而无亲缘关系的进程则需要借助其他的IPC机制。而命名管道允许任何具有足够权限的进程通过指定同一个管道名称来进行通信,即使这些进程没有亲缘关系,也可以通过共享同一个命名管道完成协作。

  • 其次,匿名管道的生命周期受到创建它的进程的影响,如果该进程退出或者终止,那么所创建的匿名管道也会跟随被销毁。而命名管道则存在于磁盘上并拥有固定的路径,生命周期不受创建进程的影响。这意味着即使创建命名管道的进程已经退出,其他进程仍然可以使用这个命名管道来进行通信。

总的来说,匿名管道是一次性的、有亲缘关系的进程之间的通信方式,它只是内存上的一个缓冲区。而命名管道允许无亲缘关系的进程在较长时间内进行通信,它是一个实实在在存放在磁盘上的文件。

命名管道实现两进程通信

通过上面的学习我们知道命名管道可以让两个毫无血缘关系的进程进行通信。我们也在命令行上进行了实验。

接下来我们使用操作系统提供的系统调用接口来用代码创建一个命名管道,然后再编写两个小程序用来充当两个无亲情关系的进程,实现让这两个进程进行通信。

  1. 首先我们给两个进程分别取名client,server。

  2. server负责命名管道的创建和删除以及读取管道的数据。

  3. client负责往创建好的管道里写入数据。

在实现代码之前,我们还得先来学习创建命名管道的函数mkfifo():

在这里插入图片描述
第一个参数表示要创建的命名管道的文件路径和文件名。第二个参数表示要创建的命名管道文件的初始权限,该初始权限受umask影响。

如果该函数创建命名管道成功返回0,失败返回-1且错误码被设置。

当管道文件创建成功之后,我们就可以向一般文件一样,分别以只读和只写的方式打开文件,然后进行文件操作,从而达到两个毫不相关的进程进行通信。

  • server:
#include
#include
#include
#include
#include
#include
#include 
#include 
#include
#define NAME_PIPE "/tmp/name_pipe"
using namespace std;
bool creat_named_pipe(string& path)
{
    umask(0);
    int n = mkfifo(path.c_str(),0600);
    if(n == 0)
        return true;
    else if(n == -1)
    {
        perror("mkfifo");
        return false;
    }
}
void remove_named_pipe(string& path)
{
    int n = unlink(path.c_str());
    assert(n == 0);
}
int main()
{
    string s(NAME_PIPE);
    bool flag = creat_named_pipe(s);
    assert(flag);

    int rfd = open(s.c_str(), O_RDONLY);
    assert(rfd >= 0);

    while(true)
    {
        char buffer[1024];
        ssize_t n = read(rfd, buffer, sizeof(buffer)-1);
        if(n > 0)
        {
            buffer[n] = 0;
            cout << "client -> server say:" << buffer << endl;
        }
        else 
        {
            cout << "client quit, me too...." << endl;
            break;
        }
    }
    close(rfd);
    remove_named_pipe(s);
    return 0;
}
  • client:
#include
#include
#include
#include
#include
#include
#include 
#include 
#include
int main()
{
    int wfd = open(NAME_PIPE, O_WRONLY);
    assert(wfd >= 0);

    int cnt = 0;
    while(true)
    {
        char buffer[1024];
        // snprintf(buffer, sizeof(buffer)-1, "I am client, pid:%d cnt:%d", getpid(), cnt++);
        cout << "Please say:" ;
        cin >> buffer;
        ssize_t n = write(wfd, buffer, strlen(buffer));
        assert(n > 0);
    }
    close(wfd);
    return 0;
}

你可能感兴趣的:(Linux系统编程,Linux进程学习,linux,运维,服务器)