Linux_进程间通信_管道

发布于:2025-02-11 ⋅ 阅读:(35) ⋅ 点赞:(0)

为什么要有进程间通信?

原因有以下几点:

  1. 数据传输:一个进程需要将它的数据发送给另一个进程。
  2. 资源共享:多个进程之间需要共享同样的资源。
  3. 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  4. 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

进程间通信(IPC,Interprocess Communication)是指在不同进程之间传输数据和交换信息的一种机制。它允许多个进程在同一操作系统中同时运行,并实现彼此之间的协作。 

如何进行进程间通信?

我们知道,进程之间是具有独立性的。那么进行进程间通信的前提就是:让不同的进程,看到同一份资源。并且这个资源应是某种形式的内存空间,而提供资源的人只能是操作系统!

进程间通信的分类

进程间通信的方式共有两大类:本地通信和网络通信。今天我们主要介绍本地通信,网络通信我们后面再说。本地通信是同一台主机,同一个操作系统,不同进程之间通信。

管道

什么是管道

  1. 管道是Unix中最古老的进程间通信的形式。
  2. 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

who和wc是两个不同的命令,执行时成为两个进程,这两个进程为兄弟关系。

匿名管道

匿名管道的原理

管道只能进行单向通信

  • 父进程创建管道时需要将rw同时打开,然后在创建子进程,是为了让子进程也能看到这个管道,利用的是子进程会继承父进程相关资源的特性。如果先创建子进程,父进程在打开rw端,子进程就看不到这个管道了。如果父进程只将 r 或者 w 打开,子进程继承的就只是 r 或 w ,此时的父子进程要么都是读,要么都是写。就无法通信,管道就创建失败了。
  • 创建管道成功之后,需要将父子进程特定的文件描述符关闭。如果不关闭就容易造成 fd泄露 或者  误操作 等问题

我们创建的这个管道,没有路径,如果想创建直接在内核里创建就是了,没有名字,所以叫做匿名管道!

接口验证

pipe函数的参数是一个输出型参数,数组pipefd用于返回两个指向管道读端和写端的文件描述符:
一般是3和4

数组元素 含义
pipefd[0] 管道读端的文件描述符
pipefd[1] 管道写端的文件描述符
// 验证pipe接口
#include <iostream>
#include <unistd.h>
int main()
{
    int fds[2] ={0};
    int n = pipe(fds); //fds:输出型参数
    if(n == 0)
    {
        std::cout <<"fds[0]:"<< fds[0] << std::endl; // 3
        std::cout <<"fds[1]:"<< fds[1] << std::endl; // 4
    }
    return 0;
}

代码展示

// 子进程向父进程发送信息
using namespace std;
#include <iostream>
#include <string>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    // 1. 创建管道
    int fds[2] = {0};
    int n = pipe(fds);
    if(n != 0)
    {
        cerr << "pipe error" << endl;
        return 1;
    }
    // 2. 创建子进程
    pid_t id = fork();
    if(id < 0)
    {
        cerr << "fork error" << endl;
        return 2;
    }
    else if(id == 0)
    {
        // 子进程
        // 3. 关闭不需要的fd,关闭read
        close(fds[0]);
        int cnt = 0;
        while(true)
        {
            string message = "hello world, hello ";
            message += to_string(getpid());
            message += ",";
            message += to_string(cnt);

            // 直接通过系统调用write向fds[1]中写入
            ::write(fds[1], message.c_str(), message.size());
            cnt++;
            sleep(1);
        }
        exit(0);
    }
    else
    {
        // 父进程
        // 3. 关闭不需要的fd, 关闭write
        close(fds[1]);
        char buffer[1024];
        while(true)
        {
            // 通过系统调用read,从fds[0]中读取数据
            // 如果n等于0,代表读到了文件末尾
            ssize_t n = ::read(fds[0], buffer, 1024);
            if(n > 0)
            {
                buffer[1024] = 0;
                cout << "child -> father, message: " << buffer << endl;
            }
        }

        pid_t rid = waitpid(id, nullptr, 0);
        cout << "father wait child success:" << rid << endl;
    }
    return 0;
}
匿名管道的特性

1. 面向字节流!

2. 用来进行具有血缘关系的进程,进行IPC,常用于父子。

3. 文件的生命周期,随进程!管道也随进程!

4. 单向数据通信。

5. 管道自带同步互斥等保护机制(保护共享资源)!

命名管道 

命名管道是一种进程间通信(IPC)机制,运行不同进程之间进行可靠的、单向或双向的数据通信

命名管道的基本原理

  • 匿名管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。
  • 如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。
  • 命名管道是一种特殊类型的文件,两个进程通过命名管道的文件名(同一个文件系统)打开同一个管道文件,此时这两个进程也就看到了同一份资源,进而就可以进行通信了。

注意:

  • 普通文件是很难做到通信的,即便做到通信也无法解决一些安全问题。
  • FIFO是真实存在的。为什么?因为为我们需要使用这个文件名 + 路径来标识它资源的唯一性。
  • 命名管道和匿名管道一样,都是内存文件,只不过命名管道在磁盘有一个简单的占位符,但这个占位符的大小永远为0,因为命名管道和匿名管道都不会将通信数据刷新到磁盘当中,它们都只使用内核文件缓冲区。

命名管道的创建

$ mkfifo fifo//使用该命令来创建一个命名管道

接口验证

代码级创建有名管道-mkfifo,本质是创建文件! 

  • mkfifo函数的第一个参数是pathname,表示要创建的命名管道文件。    
    若pathname以路径的方式给出,则将命名管道文件创建在pathname路径下
    若pathname以文件名的方式给出,则将命名管道文件默认创建在当前路径下
  • mkfifo函数的第二个参数是mode,表示创建命名管道文件的默认权限。
  • mkfifo函数创建成功返回0,创建失败返回-1
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
    //将权限掩码设为0, 这样我们就可以自己设置我们所需要的权限
    umask(0);
    int n = ::mkfifo("new_Fifo", 0666);
    if(n < 0)
    {
        std::cerr << "mkfifo error" << std::endl;
        return 1;
    }
    std::cout << "mkfifo success" << std::endl;
    return 0;
}


#include <unistd.h>
// 删除上面代码中的管道文件new_Fifo
int main()
{
    int n = ::unlink("new_Fifo");
    if(n < 0)
    {
        std::cerr << "unlink error" << std::endl;
        return 1;
    }
    std::cerr << "unlink succeed" << std::endl;
    return 0;
}

代码展示

利用命名管道对server和client通信

// server-服务器代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define ERR_EXIT(m) \
do{\
        perror(m);\
        exit(EXIT_FAILURE);\
}while(0)

int main()
{
    umask(0);
    // 创建管道文件
    if(::mkfifo("mypipe", 0644) < 0)
        ERR_EXIT("mkfifo");
    
    // 以读打开管道文件
    int rfd = ::open("mypipe", O_RDONLY);
    if(rfd < 0)
        ERR_EXIT("open");
    
    char buf[1024];
    while(1)
    {
        buf[0] = 0;
        printf("Please wait...\n");
        // 从管道文件中读取sizeof(buf) - 1个数据到buf里
        ssize_t s = ::read(rfd, buf, sizeof(buf) - 1);
        if(s > 0 )
        {
            buf[s] = 0;
            printf("client say# %s\n", buf);
        }
        else if(s == 0)  // 表示读到了文件末尾,即客户端关闭
        {
            printf("client quit, exit now!\n");
            exit(EXIT_SUCCESS);
        }
        else
            ERR_EXIT("read");
    } 
    // 关闭管道文件
    ::close(rfd);
    return 0;
}
// client-客户端代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#define ERR_EXIT(m) \
do{\
    perror(m);\
    exit(EXIT_FAILURE);\
}while(0)

int main()
{
    // 以写方式打开管道文件
    int wfd = ::open("mypipe", O_WRONLY);
    if(wfd < 0)
        ERR_EXIT("open");

    char buf[1024];
    while(1)
    {
        buf[0] = 0;
        printf("Please Enter# ");
        fflush(stdout);
        // 从 标准输入 中读取sizeof(buf) - 1个数据到buf里
        ssize_t s = ::read(0, buf, sizeof(buf)-1);
        if(s > 0 )
        {
            buf[s] = 0;
            // 把从标准输入中读的数据写入管道文件
            ::write(wfd, buf, strlen(buf));
        }
        else if(s <= 0)
            ERR_EXIT("read");
    } 
    // 关闭管道文件
    ::close(wfd);
    return 0;
}

管道IO操作的四种情况

1. 管道为空 && 管道正常,read会阻塞[read是一个系统调用]

2. 管道为满 && 管道正常,write会阻塞[write也是一个系统调用]

3. 管道写端关闭 && 读端继续,读端读到0,表示读到文件结尾

4. 管道写端正常 && 读端关闭,OS会直接杀掉写入的进程!


网站公告

今日签到

点亮在社区的每一天
去签到